diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 450b6e0ad9..82674e5141 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -455,6 +455,10 @@ void AudioMixer::run() { } } + if (!hasRatioChanged) { + ++framesSinceCutoffEvent; + } + foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { if (node->getType() == NodeType::Agent && node->getActiveSocket() && node->getLinkedData() && ((AudioMixerClientData*) node->getLinkedData())->getAvatarAudioRingBuffer()) { diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 0ec7c3e15e..65e0acd4a6 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -11,7 +11,7 @@ // nodes, and broadcasts that data back to them, every BROADCAST_INTERVAL ms. #include -#include +#include #include #include @@ -31,10 +31,13 @@ const unsigned int AVATAR_DATA_SEND_INTERVAL_USECS = (1 / 60.0) * 1000 * 1000; AvatarMixer::AvatarMixer(const QByteArray& packet) : ThreadedAssignment(packet), + _lastFrameTimestamp(QDateTime::currentMSecsSinceEpoch()), _trailingSleepRatio(1.0f), _performanceThrottlingRatio(0.0f), _sumListeners(0), - _numStatFrames(0) + _numStatFrames(0), + _sumBillboardPackets(0), + _sumIdentityPackets(0) { // make sure we hear about node kills so we can tell the other nodes connect(NodeList::getInstance(), &NodeList::nodeKilled, this, &AvatarMixer::nodeKilled); @@ -46,6 +49,8 @@ void attachAvatarDataToNode(Node* newNode) { } } +const float BILLBOARD_AND_IDENTITY_SEND_PROBABILITY = 1.0f / 300.0f; + // NOTE: some additional optimizations to consider. // 1) use the view frustum to cull those avatars that are out of view. Since avatar data doesn't need to be present // if the avatar is not in view or in the keyhole. @@ -99,6 +104,41 @@ void AvatarMixer::broadcastAvatarData() { // copy the avatar into the mixedAvatarByteArray packet mixedAvatarByteArray.append(avatarByteArray); + + // if the receiving avatar has just connected make sure we send out the mesh and billboard + // for this avatar (assuming they exist) + bool forceSend = !myData->checkAndSetHasReceivedFirstPackets(); + + // we will also force a send of billboard or identity packet + // if either has changed in the last frame + + if (otherNodeData->getBillboardChangeTimestamp() > 0 + && (forceSend + || otherNodeData->getBillboardChangeTimestamp() > _lastFrameTimestamp + || randFloat() < BILLBOARD_AND_IDENTITY_SEND_PROBABILITY)) { + QByteArray billboardPacket = byteArrayWithPopulatedHeader(PacketTypeAvatarBillboard); + billboardPacket.append(otherNode->getUUID().toRfc4122()); + billboardPacket.append(otherNodeData->getAvatar().getBillboard()); + nodeList->writeDatagram(billboardPacket, node); + + ++_sumBillboardPackets; + } + + if (otherNodeData->getIdentityChangeTimestamp() > 0 + && (forceSend + || otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp + || randFloat() < BILLBOARD_AND_IDENTITY_SEND_PROBABILITY)) { + + QByteArray identityPacket = byteArrayWithPopulatedHeader(PacketTypeAvatarIdentity); + + QByteArray individualData = otherNodeData->getAvatar().identityByteArray(); + individualData.replace(0, NUM_BYTES_RFC4122_UUID, otherNode->getUUID().toRfc4122()); + identityPacket.append(individualData); + + nodeList->writeDatagram(identityPacket, node); + + ++_sumIdentityPackets; + } } } } @@ -106,66 +146,8 @@ void AvatarMixer::broadcastAvatarData() { nodeList->writeDatagram(mixedAvatarByteArray, node); } } -} - -void broadcastIdentityPacket() { - - NodeList* nodeList = NodeList::getInstance(); - QByteArray avatarIdentityPacket = byteArrayWithPopulatedHeader(PacketTypeAvatarIdentity); - int numPacketHeaderBytes = avatarIdentityPacket.size(); - - foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { - if (node->getLinkedData() && node->getType() == NodeType::Agent) { - - AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); - AvatarData& avatar = nodeData->getAvatar(); - QByteArray individualData = avatar.identityByteArray(); - individualData.replace(0, NUM_BYTES_RFC4122_UUID, node->getUUID().toRfc4122()); - - if (avatarIdentityPacket.size() + individualData.size() > MAX_PACKET_SIZE) { - // we've hit MTU, send out the current packet before appending - nodeList->broadcastToNodes(avatarIdentityPacket, NodeSet() << NodeType::Agent); - avatarIdentityPacket.resize(numPacketHeaderBytes); - } - - // append the individual data to the current the avatarIdentityPacket - avatarIdentityPacket.append(individualData); - - // re-set the bool in AvatarMixerClientData so a change between key frames gets sent out - nodeData->setHasSentIdentityBetweenKeyFrames(false); - } - } - - // send out the final packet - if (avatarIdentityPacket.size() > numPacketHeaderBytes) { - nodeList->broadcastToNodes(avatarIdentityPacket, NodeSet() << NodeType::Agent); - } -} - -void broadcastBillboardPacket(const SharedNodePointer& sendingNode) { - AvatarMixerClientData* nodeData = static_cast(sendingNode->getLinkedData()); - AvatarData& avatar = nodeData->getAvatar(); - QByteArray packet = byteArrayWithPopulatedHeader(PacketTypeAvatarBillboard); - packet.append(sendingNode->getUUID().toRfc4122()); - packet.append(avatar.getBillboard()); - - NodeList* nodeList = NodeList::getInstance(); - foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { - if (node->getType() == NodeType::Agent && node != sendingNode) { - nodeList->writeDatagram(packet, node); - } - } -} - -void broadcastBillboardPackets() { - foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { - if (node->getLinkedData() && node->getType() == NodeType::Agent) { - AvatarMixerClientData* nodeData = static_cast(node->getLinkedData()); - broadcastBillboardPacket(node); - nodeData->setHasSentBillboardBetweenKeyFrames(false); - } - } + _lastFrameTimestamp = QDateTime::currentMSecsSinceEpoch(); } void AvatarMixer::nodeKilled(SharedNodePointer killedNode) { @@ -202,18 +184,10 @@ void AvatarMixer::readPendingDatagrams() { if (avatarNode && avatarNode->getLinkedData()) { AvatarMixerClientData* nodeData = reinterpret_cast(avatarNode->getLinkedData()); AvatarData& avatar = nodeData->getAvatar(); - if (avatar.hasIdentityChangedAfterParsing(receivedPacket) - && !nodeData->hasSentIdentityBetweenKeyFrames()) { - // this avatar changed their identity in some way and we haven't sent a packet in this keyframe - QByteArray identityPacket = byteArrayWithPopulatedHeader(PacketTypeAvatarIdentity); - - QByteArray individualByteArray = avatar.identityByteArray(); - individualByteArray.replace(0, NUM_BYTES_RFC4122_UUID, avatarNode->getUUID().toRfc4122()); - - identityPacket.append(individualByteArray); - - nodeData->setHasSentIdentityBetweenKeyFrames(true); - nodeList->broadcastToNodes(identityPacket, NodeSet() << NodeType::Agent); + + // parse the identity packet and update the change timestamp if appropriate + if (avatar.hasIdentityChangedAfterParsing(receivedPacket)) { + nodeData->setIdentityChangeTimestamp(QDateTime::currentMSecsSinceEpoch()); } } break; @@ -226,12 +200,12 @@ void AvatarMixer::readPendingDatagrams() { if (avatarNode && avatarNode->getLinkedData()) { AvatarMixerClientData* nodeData = static_cast(avatarNode->getLinkedData()); AvatarData& avatar = nodeData->getAvatar(); - if (avatar.hasBillboardChangedAfterParsing(receivedPacket) - && !nodeData->hasSentBillboardBetweenKeyFrames()) { - // this avatar changed their billboard and we haven't sent a packet in this keyframe - broadcastBillboardPacket(avatarNode); - nodeData->setHasSentBillboardBetweenKeyFrames(true); + + // parse the billboard packet and update the change timestamp if appropriate + if (avatar.hasBillboardChangedAfterParsing(receivedPacket)) { + nodeData->setBillboardChangeTimestamp(QDateTime::currentMSecsSinceEpoch()); } + } break; } @@ -252,18 +226,20 @@ void AvatarMixer::sendStatsPacket() { QJsonObject statsObject; statsObject["average_listeners_last_second"] = (float) _sumListeners / (float) _numStatFrames; + statsObject["average_billboard_packets_per_frame"] = (float) _sumBillboardPackets / (float) _numStatFrames; + statsObject["average_identity_packets_per_frame"] = (float) _sumIdentityPackets / (float) _numStatFrames; + statsObject["trailing_sleep_percentage"] = _trailingSleepRatio * 100; statsObject["performance_throttling_ratio"] = _performanceThrottlingRatio; ThreadedAssignment::addPacketStatsAndSendStatsPacket(statsObject); _sumListeners = 0; + _sumBillboardPackets = 0; + _sumIdentityPackets = 0; _numStatFrames = 0; } -const qint64 AVATAR_IDENTITY_KEYFRAME_MSECS = 5000; -const qint64 AVATAR_BILLBOARD_KEYFRAME_MSECS = 5000; - void AvatarMixer::run() { ThreadedAssignment::commonInit(AVATAR_MIXER_LOGGING_NAME, NodeType::AvatarMixer); @@ -277,12 +253,6 @@ void AvatarMixer::run() { gettimeofday(&startTime, NULL); - QElapsedTimer identityTimer; - identityTimer.start(); - - QElapsedTimer billboardTimer; - billboardTimer.start(); - int usecToSleep = AVATAR_DATA_SEND_INTERVAL_USECS; const int TRAILING_AVERAGE_FRAMES = 100; @@ -336,20 +306,11 @@ void AvatarMixer::run() { } } - broadcastAvatarData(); + if (!hasRatioChanged) { + ++framesSinceCutoffEvent; + } - if (identityTimer.elapsed() >= AVATAR_IDENTITY_KEYFRAME_MSECS) { - // it's time to broadcast the keyframe identity packets - broadcastIdentityPacket(); - - // restart the timer so we do it again in AVATAR_IDENTITY_KEYFRAME_MSECS - identityTimer.restart(); - } - - if (billboardTimer.elapsed() >= AVATAR_BILLBOARD_KEYFRAME_MSECS) { - broadcastBillboardPackets(); - billboardTimer.restart(); - } + broadcastAvatarData(); QCoreApplication::processEvents(); @@ -361,8 +322,6 @@ void AvatarMixer::run() { if (usecToSleep > 0) { usleep(usecToSleep); - } else { - qDebug() << "AvatarMixer loop took too" << -usecToSleep << "of extra time. Won't sleep."; } } } diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 4d54b715f8..4171df49af 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -30,11 +30,15 @@ public slots: private: void broadcastAvatarData(); + quint64 _lastFrameTimestamp; + float _trailingSleepRatio; float _performanceThrottlingRatio; int _sumListeners; int _numStatFrames; + int _sumBillboardPackets; + int _sumIdentityPackets; }; #endif /* defined(__hifi__AvatarMixer__) */ diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 90088173a9..d1449e956e 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -10,8 +10,9 @@ AvatarMixerClientData::AvatarMixerClientData() : NodeData(), - _hasSentIdentityBetweenKeyFrames(false), - _hasSentBillboardBetweenKeyFrames(false) + _hasReceivedFirstPackets(false), + _billboardChangeTimestamp(0), + _identityChangeTimestamp(0) { } @@ -21,3 +22,9 @@ int AvatarMixerClientData::parseData(const QByteArray& packet) { int offset = numBytesForPacketHeader(packet); return _avatar.parseDataAtOffset(packet, offset); } + +bool AvatarMixerClientData::checkAndSetHasReceivedFirstPackets() { + bool oldValue = _hasReceivedFirstPackets; + _hasReceivedFirstPackets = true; + return oldValue; +} diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 854e8172d3..bc0a54f06b 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -20,22 +20,21 @@ public: AvatarMixerClientData(); int parseData(const QByteArray& packet); - - bool hasSentIdentityBetweenKeyFrames() const { return _hasSentIdentityBetweenKeyFrames; } - void setHasSentIdentityBetweenKeyFrames(bool hasSentIdentityBetweenKeyFrames) - { _hasSentIdentityBetweenKeyFrames = hasSentIdentityBetweenKeyFrames; } - - bool hasSentBillboardBetweenKeyFrames() const { return _hasSentBillboardBetweenKeyFrames; } - void setHasSentBillboardBetweenKeyFrames(bool hasSentBillboardBetweenKeyFrames) - { _hasSentBillboardBetweenKeyFrames = hasSentBillboardBetweenKeyFrames; } - AvatarData& getAvatar() { return _avatar; } - + + bool checkAndSetHasReceivedFirstPackets(); + + quint64 getBillboardChangeTimestamp() const { return _billboardChangeTimestamp; } + void setBillboardChangeTimestamp(quint64 billboardChangeTimestamp) { _billboardChangeTimestamp = billboardChangeTimestamp; } + + quint64 getIdentityChangeTimestamp() const { return _identityChangeTimestamp; } + void setIdentityChangeTimestamp(quint64 identityChangeTimestamp) { _identityChangeTimestamp = identityChangeTimestamp; } + private: - - bool _hasSentIdentityBetweenKeyFrames; - bool _hasSentBillboardBetweenKeyFrames; AvatarData _avatar; + bool _hasReceivedFirstPackets; + quint64 _billboardChangeTimestamp; + quint64 _identityChangeTimestamp; }; #endif /* defined(__hifi__AvatarMixerClientData__) */ diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index fb295cffc3..ff33cc206b 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -40,6 +40,10 @@ span.port { color: #666666; } +.stats-key { + width: 400px; +} + .stale { color: red; } \ No newline at end of file diff --git a/domain-server/resources/web/stats/js/stats.js b/domain-server/resources/web/stats/js/stats.js index a7b0aecfcf..74d58c72c0 100644 --- a/domain-server/resources/web/stats/js/stats.js +++ b/domain-server/resources/web/stats/js/stats.js @@ -21,8 +21,9 @@ $(document).ready(function(){ $.each(json, function(key, value) { statsTableBody += ""; - statsTableBody += "" + key + ""; - statsTableBody += "" + value + ""; + statsTableBody += "" + key + ""; + var formattedValue = (typeof value == 'number' ? value.toLocaleString() : value); + statsTableBody += "" + formattedValue + ""; statsTableBody += ""; }); diff --git a/examples/defaultScripts.js b/examples/defaultScripts.js index 4b228492c7..eae3c2eb8c 100644 --- a/examples/defaultScripts.js +++ b/examples/defaultScripts.js @@ -3,3 +3,4 @@ Script.include("lookWithTouch.js"); Script.include("editVoxels.js"); Script.include("selectAudioDevice.js"); +Script.include("hydraMove.js"); \ No newline at end of file diff --git a/examples/editVoxels.js b/examples/editVoxels.js index cb2553a8e3..5e4b77a10f 100644 --- a/examples/editVoxels.js +++ b/examples/editVoxels.js @@ -400,9 +400,9 @@ function calcScaleFromThumb(newThumbX) { } function setAudioPosition() { - var camera = Camera.getPosition(); + var position = MyAvatar.position; var forwardVector = Quat.getFront(MyAvatar.orientation); - audioOptions.position = Vec3.sum(camera, forwardVector); + audioOptions.position = Vec3.sum(position, forwardVector); } function getNewPasteVoxel(pickRay) { @@ -735,6 +735,7 @@ function trackKeyReleaseEvent(event) { if (event.text == "TAB") { editToolsOn = !editToolsOn; moveTools(); + setAudioPosition(); // make sure we set the audio position before playing sounds showPreviewGuides(); Audio.playSound(clickSound, audioOptions); } diff --git a/examples/includeExample.js b/examples/includeExample.js new file mode 100644 index 0000000000..489928d759 --- /dev/null +++ b/examples/includeExample.js @@ -0,0 +1,16 @@ +// +// includeExample.js +// hifi +// +// Created by Brad Hefta-Gaub on 3/24/14 +// Copyright (c) 2013 HighFidelity, Inc. All rights reserved. +// +// This is an example script that demonstrates use of the Script.include() feature +// + +// You can include scripts from URLs +Script.include("http://public.highfidelity.io/scripts/lookWithTouch.js"); + +// You can also include scripts that are relative to the current script +Script.include("editVoxels.js"); +Script.include("../examples/selectAudioDevice.js"); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 2ca4ef74cd..13c05fa702 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -250,7 +251,7 @@ Application::Application(int& argc, char** argv, timeval &startup_time) : QMetaObject::invokeMethod(&accountManager, "checkAndSignalForAccessToken", Qt::QueuedConnection); _settings = new QSettings(this); - + // Check to see if the user passed in a command line option for loading a local // Voxel File. _voxelsFilename = getCmdOption(argc, constArgv, "-i"); @@ -329,9 +330,20 @@ Application::Application(int& argc, char** argv, timeval &startup_time) : LocalVoxelsList::getInstance()->addPersistantTree(DOMAIN_TREE_NAME, _voxels.getTree()); LocalVoxelsList::getInstance()->addPersistantTree(CLIPBOARD_TREE_NAME, &_clipboard); - - // do this as late as possible so that all required subsystems are inialized - loadScripts(); + + // check first run... + QVariant firstRunValue = _settings->value("firstRun",QVariant(true)); + if (firstRunValue.isValid() && firstRunValue.toBool()) { + qDebug() << "This is a first run..."; + // clear the scripts, and set out script to our default scripts + clearScriptsBeforeRunning(); + loadScript("http://public.highfidelity.io/scripts/defaultScripts.js"); + + _settings->setValue("firstRun",QVariant(false)); + } else { + // do this as late as possible so that all required subsystems are inialized + loadScripts(); + } } Application::~Application() { @@ -3453,6 +3465,13 @@ void Application::loadScripts() { settings->endArray(); } +void Application::clearScriptsBeforeRunning() { + // clears all scripts from the settings + QSettings* settings = new QSettings(this); + settings->beginWriteArray("Settings"); + settings->endArray(); +} + void Application::saveScripts() { // saves all current running scripts QSettings* settings = new QSettings(this); @@ -3508,35 +3527,17 @@ void Application::cleanupScriptMenuItem(const QString& scriptMenuName) { Menu::getInstance()->removeAction(Menu::getInstance()->getActiveScriptsMenu(), scriptMenuName); } -void Application::loadScript(const QString& fileNameString) { - QByteArray fileNameAscii = fileNameString.toLocal8Bit(); - const char* fileName = fileNameAscii.data(); - - std::ifstream file(fileName, std::ios::in|std::ios::binary|std::ios::ate); - if(!file.is_open()) { - qDebug("Error loading file %s", fileName); - return; - } - qDebug("Loading file %s...", fileName); - _activeScripts.append(fileNameString); - - // get file length.... - unsigned long fileLength = file.tellg(); - file.seekg( 0, std::ios::beg ); - - // read the entire file into a buffer, WHAT!? Why not. - char* entireFile = new char[fileLength+1]; - file.read((char*)entireFile, fileLength); - file.close(); - - entireFile[fileLength] = 0;// null terminate - QString script(entireFile); - delete[] entireFile; +void Application::loadScript(const QString& scriptName) { // start the script on a new thread... bool wantMenuItems = true; // tells the ScriptEngine object to add menu items for itself + ScriptEngine* scriptEngine = new ScriptEngine(QUrl(scriptName), wantMenuItems, &_controllerScriptingInterface); - ScriptEngine* scriptEngine = new ScriptEngine(script, wantMenuItems, fileName, &_controllerScriptingInterface); + if (!scriptEngine->hasScript()) { + qDebug() << "Application::loadScript(), script failed to load..."; + return; + } + _activeScripts.append(scriptName); // add a stop menu item Menu::getInstance()->addActionToQMenuAndActionHash(Menu::getInstance()->getActiveScriptsMenu(), @@ -3599,6 +3600,31 @@ void Application::loadDialog() { loadScript(fileNameString); } +void Application::loadScriptURLDialog() { + + QInputDialog scriptURLDialog(Application::getInstance()->getWindow()); + scriptURLDialog.setWindowTitle("Open and Run Script URL"); + scriptURLDialog.setLabelText("Script:"); + scriptURLDialog.setWindowFlags(Qt::Sheet); + const float DIALOG_RATIO_OF_WINDOW = 0.30f; + scriptURLDialog.resize(scriptURLDialog.parentWidget()->size().width() * DIALOG_RATIO_OF_WINDOW, + scriptURLDialog.size().height()); + + int dialogReturn = scriptURLDialog.exec(); + QString newScript; + if (dialogReturn == QDialog::Accepted) { + if (scriptURLDialog.textValue().size() > 0) { + // the user input a new hostname, use that + newScript = scriptURLDialog.textValue(); + } + loadScript(newScript); + } + + sendFakeEnterEvent(); +} + + + void Application::toggleLogDialog() { if (! _logDialog) { _logDialog = new LogDialog(_glWidget, getLogger()); diff --git a/interface/src/Application.h b/interface/src/Application.h index caeea529af..15778f2a17 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -115,6 +115,7 @@ public: void loadScript(const QString& fileNameString); void loadScripts(); void storeSizeAndPosition(); + void clearScriptsBeforeRunning(); void saveScripts(); void initializeGL(); void paintGL(); @@ -254,6 +255,7 @@ public slots: void setRenderVoxels(bool renderVoxels); void doKillLocalVoxels(); void loadDialog(); + void loadScriptURLDialog(); void toggleLogDialog(); void initAvatarAndViewFrustum(); void stopAllScripts(); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 71500c12d0..30be26ee96 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -110,6 +110,8 @@ Menu::Menu() : addDisabledActionAndSeparator(fileMenu, "Scripts"); addActionToQMenuAndActionHash(fileMenu, MenuOption::LoadScript, Qt::CTRL | Qt::Key_O, appInstance, SLOT(loadDialog())); + addActionToQMenuAndActionHash(fileMenu, MenuOption::LoadScriptURL, + Qt::CTRL | Qt::SHIFT | Qt::Key_O, appInstance, SLOT(loadScriptURLDialog())); addActionToQMenuAndActionHash(fileMenu, MenuOption::StopAllScripts, 0, appInstance, SLOT(stopAllScripts())); addActionToQMenuAndActionHash(fileMenu, MenuOption::ReloadAllScripts, 0, appInstance, SLOT(reloadAllScripts())); _activeScriptsMenu = fileMenu->addMenu("Running Scripts"); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 9cd00db8e9..9452ba220d 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -274,7 +274,8 @@ namespace MenuOption { const QString OffAxisProjection = "Off-Axis Projection"; const QString OldVoxelCullingMode = "Old Voxel Culling Mode"; const QString TurnWithHead = "Turn using Head"; - const QString LoadScript = "Open and Run Script..."; + const QString LoadScript = "Open and Run Script File..."; + const QString LoadScriptURL = "Open and Run Script from URL..."; const QString Oscilloscope = "Audio Oscilloscope"; const QString Pair = "Pair"; const QString Particles = "Particles"; @@ -306,4 +307,6 @@ namespace MenuOption { const QString VoxelTextures = "Voxel Textures"; } +void sendFakeEnterEvent(); + #endif /* defined(__hifi__Menu__) */ diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 9147a08dbd..c2ba28ac7b 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -48,15 +48,12 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { AvatarHash::iterator avatarIterator = _avatarHash.begin(); while (avatarIterator != _avatarHash.end()) { Avatar* avatar = static_cast(avatarIterator.value().data()); - if (avatar == static_cast(_myAvatar.data())) { + if (avatar == static_cast(_myAvatar.data()) || !avatar->isInitialized()) { // DO NOT update _myAvatar! Its update has already been done earlier in the main loop. - //updateMyAvatar(deltaTime); + // DO NOT update uninitialized Avatars ++avatarIterator; continue; } - if (!avatar->isInitialized()) { - avatar->init(); - } if (avatar->getOwningAvatarMixer()) { // this avatar's mixer is still around, go ahead and simulate it avatar->simulate(deltaTime); @@ -120,22 +117,40 @@ void AvatarManager::renderAvatarFades(const glm::vec3& cameraPosition, Avatar::R foreach(const AvatarSharedPointer& fadingAvatar, _avatarFades) { Avatar* avatar = static_cast(fadingAvatar.data()); - if (avatar != static_cast(_myAvatar.data())) { + if (avatar != static_cast(_myAvatar.data()) && avatar->isInitialized()) { avatar->render(cameraPosition, renderMode); } } } +AvatarSharedPointer AvatarManager::matchingOrNewAvatar(const QUuid& nodeUUID, const QWeakPointer& mixerWeakPointer) { + AvatarSharedPointer matchingAvatar = _avatarHash.value(nodeUUID); + + if (!matchingAvatar) { + // construct a new Avatar for this node + Avatar* avatar = new Avatar(); + avatar->setOwningAvatarMixer(mixerWeakPointer); + + // insert the new avatar into our hash + matchingAvatar = AvatarSharedPointer(avatar); + _avatarHash.insert(nodeUUID, matchingAvatar); + + qDebug() << "Adding avatar with UUID" << nodeUUID << "to AvatarManager hash."; + } + + return matchingAvatar; +} + void AvatarManager::processAvatarMixerDatagram(const QByteArray& datagram, const QWeakPointer& mixerWeakPointer) { switch (packetTypeForPacket(datagram)) { case PacketTypeBulkAvatarData: processAvatarDataPacket(datagram, mixerWeakPointer); break; case PacketTypeAvatarIdentity: - processAvatarIdentityPacket(datagram); + processAvatarIdentityPacket(datagram, mixerWeakPointer); break; case PacketTypeAvatarBillboard: - processAvatarBillboardPacket(datagram); + processAvatarBillboardPacket(datagram, mixerWeakPointer); break; case PacketTypeKillAvatar: processKillAvatar(datagram); @@ -154,26 +169,21 @@ void AvatarManager::processAvatarDataPacket(const QByteArray &datagram, const QW QUuid nodeUUID = QUuid::fromRfc4122(datagram.mid(bytesRead, NUM_BYTES_RFC4122_UUID)); bytesRead += NUM_BYTES_RFC4122_UUID; - AvatarSharedPointer matchingAvatar = _avatarHash.value(nodeUUID); - - if (!matchingAvatar) { - // construct a new Avatar for this node - Avatar* avatar = new Avatar(); - avatar->setOwningAvatarMixer(mixerWeakPointer); - - // insert the new avatar into our hash - matchingAvatar = AvatarSharedPointer(avatar); - _avatarHash.insert(nodeUUID, matchingAvatar); - - qDebug() << "Adding avatar with UUID" << nodeUUID << "to AvatarManager hash."; - } + AvatarSharedPointer matchingAvatarData = matchingOrNewAvatar(nodeUUID, mixerWeakPointer); // have the matching (or new) avatar parse the data from the packet - bytesRead += matchingAvatar->parseDataAtOffset(datagram, bytesRead); + bytesRead += matchingAvatarData->parseDataAtOffset(datagram, bytesRead); + + Avatar* matchingAvatar = reinterpret_cast(matchingAvatarData.data()); + + if (!matchingAvatar->isInitialized()) { + // now that we have AvatarData for this Avatar we are go for init + matchingAvatar->init(); + } } } -void AvatarManager::processAvatarIdentityPacket(const QByteArray &packet) { +void AvatarManager::processAvatarIdentityPacket(const QByteArray &packet, const QWeakPointer& mixerWeakPointer) { // setup a data stream to parse the packet QDataStream identityStream(packet); identityStream.skipRawData(numBytesForPacketHeader(packet)); @@ -187,7 +197,7 @@ void AvatarManager::processAvatarIdentityPacket(const QByteArray &packet) { identityStream >> nodeUUID >> faceMeshURL >> skeletonURL >> displayName; // mesh URL for a UUID, find avatar in our list - AvatarSharedPointer matchingAvatar = _avatarHash.value(nodeUUID); + AvatarSharedPointer matchingAvatar = matchingOrNewAvatar(nodeUUID, mixerWeakPointer); if (matchingAvatar) { Avatar* avatar = static_cast(matchingAvatar.data()); @@ -206,11 +216,11 @@ void AvatarManager::processAvatarIdentityPacket(const QByteArray &packet) { } } -void AvatarManager::processAvatarBillboardPacket(const QByteArray& packet) { +void AvatarManager::processAvatarBillboardPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer) { int headerSize = numBytesForPacketHeader(packet); QUuid nodeUUID = QUuid::fromRfc4122(QByteArray::fromRawData(packet.constData() + headerSize, NUM_BYTES_RFC4122_UUID)); - AvatarSharedPointer matchingAvatar = _avatarHash.value(nodeUUID); + AvatarSharedPointer matchingAvatar = matchingOrNewAvatar(nodeUUID, mixerWeakPointer); if (matchingAvatar) { Avatar* avatar = static_cast(matchingAvatar.data()); QByteArray billboard = packet.mid(headerSize + NUM_BYTES_RFC4122_UUID); @@ -234,7 +244,9 @@ void AvatarManager::processKillAvatar(const QByteArray& datagram) { AvatarHash::iterator AvatarManager::erase(const AvatarHash::iterator& iterator) { if (iterator.key() != MY_AVATAR_KEY) { qDebug() << "Removing Avatar with UUID" << iterator.key() << "from AvatarManager hash."; - _avatarFades.push_back(iterator.value()); + if (reinterpret_cast(iterator.value().data())->isInitialized()) { + _avatarFades.push_back(iterator.value()); + } return AvatarHashMap::erase(iterator); } else { // never remove _myAvatar from the list diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 06494f309c..bd04dddb78 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -39,9 +39,11 @@ public slots: private: AvatarManager(const AvatarManager& other); + AvatarSharedPointer matchingOrNewAvatar(const QUuid& nodeUUID, const QWeakPointer& mixerWeakPointer); + void processAvatarDataPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer); - void processAvatarIdentityPacket(const QByteArray& packet); - void processAvatarBillboardPacket(const QByteArray& packet); + void processAvatarIdentityPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer); + void processAvatarBillboardPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer); void processKillAvatar(const QByteArray& datagram); void simulateAvatarFades(float deltaTime); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 38948071ff..9bd00a0019 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -64,13 +64,10 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, bool wantMenuItems, co _quatLibrary(), _vec3Library() { - QByteArray fileNameAscii = fileNameString.toLocal8Bit(); - const char* scriptMenuName = fileNameAscii.data(); - // some clients will use these menu features if (!fileNameString.isEmpty()) { _scriptMenuName = "Stop "; - _scriptMenuName.append(scriptMenuName); + _scriptMenuName.append(qPrintable(fileNameString)); _scriptMenuName.append(QString(" [%1]").arg(_scriptNumber)); } else { _scriptMenuName = "Stop Script "; @@ -79,6 +76,72 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, bool wantMenuItems, co _scriptNumber++; } +ScriptEngine::ScriptEngine(const QUrl& scriptURL, bool wantMenuItems, + AbstractControllerScriptingInterface* controllerScriptingInterface) : + _scriptContents(), + _isFinished(false), + _isRunning(false), + _isInitialized(false), + _engine(), + _isAvatar(false), + _avatarIdentityTimer(NULL), + _avatarBillboardTimer(NULL), + _timerFunctionMap(), + _isListeningToAudioStream(false), + _avatarSound(NULL), + _numAvatarSoundSentBytes(0), + _controllerScriptingInterface(controllerScriptingInterface), + _avatarData(NULL), + _wantMenuItems(wantMenuItems), + _scriptMenuName(), + _fileNameString(), + _quatLibrary(), + _vec3Library() +{ + QString scriptURLString = scriptURL.toString(); + _fileNameString = scriptURLString; + // some clients will use these menu features + if (!scriptURLString.isEmpty()) { + _scriptMenuName = "Stop "; + _scriptMenuName.append(qPrintable(scriptURLString)); + _scriptMenuName.append(QString(" [%1]").arg(_scriptNumber)); + } else { + _scriptMenuName = "Stop Script "; + _scriptMenuName.append(_scriptNumber); + } + _scriptNumber++; + + QUrl url(scriptURL); + + // if the scheme is empty, maybe they typed in a file, let's try + if (url.scheme().isEmpty()) { + url = QUrl::fromLocalFile(scriptURLString); + } + + // ok, let's see if it's valid... and if so, load it + if (url.isValid()) { + if (url.scheme() == "file") { + QString fileName = url.toLocalFile(); + QFile scriptFile(fileName); + if (scriptFile.open(QFile::ReadOnly | QFile::Text)) { + qDebug() << "Loading file:" << fileName; + QTextStream in(&scriptFile); + _scriptContents = in.readAll(); + } else { + qDebug() << "ERROR Loading file:" << fileName; + } + } else { + QNetworkAccessManager* networkManager = new QNetworkAccessManager(this); + QNetworkReply* reply = networkManager->get(QNetworkRequest(url)); + qDebug() << "Downloading included script at" << url; + QEventLoop loop; + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + _scriptContents = reply->readAll(); + } + } +} + void ScriptEngine::setIsAvatar(bool isAvatar) { _isAvatar = isAvatar; @@ -113,11 +176,12 @@ void ScriptEngine::cleanupMenuItems() { } } -bool ScriptEngine::setScriptContents(const QString& scriptContents) { +bool ScriptEngine::setScriptContents(const QString& scriptContents, const QString& fileNameString) { if (_isRunning) { return false; } _scriptContents = scriptContents; + _fileNameString = fileNameString; return true; } @@ -436,3 +500,55 @@ void ScriptEngine::stopTimer(QTimer *timer) { delete timer; } } + +QUrl ScriptEngine::resolveInclude(const QString& include) const { + // first lets check to see if it's already a full URL + QUrl url(include); + if (!url.scheme().isEmpty()) { + return url; + } + + // we apparently weren't a fully qualified url, so, let's assume we're relative + // to the original URL of our script + QUrl parentURL(_fileNameString); + + // if the parent URL's scheme is empty, then this is probably a local file... + if (parentURL.scheme().isEmpty()) { + parentURL = QUrl::fromLocalFile(_fileNameString); + } + + // at this point we should have a legitimate fully qualified URL for our parent + url = parentURL.resolved(url); + return url; +} + +void ScriptEngine::include(const QString& includeFile) { + QUrl url = resolveInclude(includeFile); + QString includeContents; + + if (url.scheme() == "file") { + QString fileName = url.toLocalFile(); + QFile scriptFile(fileName); + if (scriptFile.open(QFile::ReadOnly | QFile::Text)) { + qDebug() << "Loading file:" << fileName; + QTextStream in(&scriptFile); + includeContents = in.readAll(); + } else { + qDebug() << "ERROR Loading file:" << fileName; + } + } else { + QNetworkAccessManager* networkManager = new QNetworkAccessManager(this); + QNetworkReply* reply = networkManager->get(QNetworkRequest(url)); + qDebug() << "Downloading included script at" << includeFile; + QEventLoop loop; + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + includeContents = reply->readAll(); + } + + QScriptValue result = _engine.evaluate(includeContents); + if (_engine.hasUncaughtException()) { + int line = _engine.uncaughtExceptionLineNumber(); + qDebug() << "Uncaught exception at (" << includeFile << ") line" << line << ":" << result.toString(); + } +} diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 755418b0c1..4fc90d2959 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -33,8 +33,11 @@ const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0 / 60.0f) * 1000 * 10 class ScriptEngine : public QObject { Q_OBJECT public: - ScriptEngine(const QString& scriptContents = NO_SCRIPT, bool wantMenuItems = false, - const QString& scriptMenuName = QString(""), + ScriptEngine(const QUrl& scriptURL, bool wantMenuItems = false, + AbstractControllerScriptingInterface* controllerScriptingInterface = NULL); + + ScriptEngine(const QString& scriptContents = NO_SCRIPT, bool wantMenuItems = false, + const QString& fileNameString = QString(""), AbstractControllerScriptingInterface* controllerScriptingInterface = NULL); /// Access the VoxelsScriptingInterface in order to initialize it with a custom packet sender and jurisdiction listener @@ -44,7 +47,7 @@ public: static ParticlesScriptingInterface* getParticlesScriptingInterface() { return &_particlesScriptingInterface; } /// sets the script contents, will return false if failed, will fail if script is already running - bool setScriptContents(const QString& scriptContents); + bool setScriptContents(const QString& scriptContents, const QString& fileNameString = QString("")); const QString& getScriptMenuName() const { return _scriptMenuName; } void cleanupMenuItems(); @@ -68,6 +71,8 @@ public: void timerFired(); + bool hasScript() const { return !_scriptContents.isEmpty(); } + public slots: void stop(); @@ -75,6 +80,7 @@ public slots: QObject* setTimeout(const QScriptValue& function, int timeoutMS); void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast(timer)); } + void include(const QString& includeFile); signals: void update(float deltaTime); @@ -97,6 +103,7 @@ protected: int _numAvatarSoundSentBytes; private: + QUrl resolveInclude(const QString& include) const; void sendAvatarIdentityPacket(); void sendAvatarBillboardPacket(); diff --git a/libraries/shared/src/ThreadedAssignment.cpp b/libraries/shared/src/ThreadedAssignment.cpp index f4ea383399..642f471cc5 100644 --- a/libraries/shared/src/ThreadedAssignment.cpp +++ b/libraries/shared/src/ThreadedAssignment.cpp @@ -82,9 +82,9 @@ void ThreadedAssignment::sendStatsPacket() { void ThreadedAssignment::checkInWithDomainServerOrExit() { if (NodeList::getInstance()->getNumNoReplyDomainCheckIns() == MAX_SILENT_DOMAIN_SERVER_CHECK_INS) { - qDebug() << "NRDC:" << NodeList::getInstance()->getNumNoReplyDomainCheckIns(); setFinished(true); } else { + qDebug() << "Sending DS check in. There are" << NodeList::getInstance()->getNumNoReplyDomainCheckIns() << "unreplied."; NodeList::getInstance()->sendDomainServerCheckIn(); } }