diff --git a/CMakeLists.txt b/CMakeLists.txt index fe7aad8939..b7c0a24863 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,9 @@ if (POLICY CMP0042) cmake_policy(SET CMP0042 OLD) endif () +set_property(GLOBAL PROPERTY USE_FOLDERS ON) +set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "CMakeTargets") + project(hifi) add_definitions(-DGLM_FORCE_RADIANS) @@ -174,9 +177,13 @@ endif () # add subdirectories for all targets if (NOT ANDROID) add_subdirectory(assignment-client) + set_target_properties(assignment-client PROPERTIES FOLDER "Apps") add_subdirectory(domain-server) + set_target_properties(domain-server PROPERTIES FOLDER "Apps") add_subdirectory(ice-server) + set_target_properties(ice-server PROPERTIES FOLDER "Apps") add_subdirectory(interface) + set_target_properties(interface PROPERTIES FOLDER "Apps") add_subdirectory(tests) add_subdirectory(tools) endif () diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 9616c8cb21..176fd51eea 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -20,9 +20,9 @@ #include #include #include +#include #include "AvatarMixerClientData.h" - #include "AvatarMixer.h" const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer"; @@ -119,12 +119,25 @@ void AvatarMixer::broadcastAvatarData() { auto nodeList = DependencyManager::get(); - AvatarMixerClientData* nodeData = NULL; - AvatarMixerClientData* otherNodeData = NULL; - - nodeList->eachNode([&](const SharedNodePointer& node) { - if (node->getLinkedData() && node->getType() == NodeType::Agent && node->getActiveSocket() - && (nodeData = reinterpret_cast(node->getLinkedData()))->getMutex().tryLock()) { + nodeList->eachMatchingNode( + [&](const SharedNodePointer& node)->bool { + if (!node->getLinkedData()) { + return false; + } + if (node->getType() != NodeType::Agent) { + return false; + } + if (!node->getActiveSocket()) { + return false; + } + return true; + }, + [&](const SharedNodePointer& node) { + AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); + MutexTryLocker lock(nodeData->getMutex()); + if (!lock.isLocked()) { + return; + } ++_sumListeners; // reset packet pointers for this node @@ -132,83 +145,97 @@ void AvatarMixer::broadcastAvatarData() { AvatarData& avatar = nodeData->getAvatar(); glm::vec3 myPosition = avatar.getPosition(); + // TODO use this along with the distance in the calculation of whether to send an update + // about a given otherNode to this node + // FIXME does this mean we should sort the othernodes by distance before iterating + // over them? + float outputBandwidth = node->getOutboundBandwidth(); // this is an AGENT we have received head data from // send back a packet with other active node data to this node - nodeList->eachNode([&](const SharedNodePointer& otherNode) { - if (otherNode->getLinkedData() && otherNode->getUUID() != node->getUUID() - && (otherNodeData = reinterpret_cast(otherNode->getLinkedData()))->getMutex().tryLock()) { - - AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); + nodeList->eachMatchingNode( + [&](const SharedNodePointer& otherNode)->bool { + if (!otherNode->getLinkedData()) { + return false; + } + if (otherNode->getUUID() == node->getUUID()) { + return false; + } + + // Check throttling value + if (!(_performanceThrottlingRatio == 0 || randFloat() < (1.0f - _performanceThrottlingRatio))) { + return false; + } + return true; + }, + [&](const SharedNodePointer& otherNode) { + AvatarMixerClientData* otherNodeData = otherNodeData = reinterpret_cast(otherNode->getLinkedData()); + MutexTryLocker lock(otherNodeData->getMutex()); + if (!lock.isLocked()) { + return; + } AvatarData& otherAvatar = otherNodeData->getAvatar(); - glm::vec3 otherPosition = otherAvatar.getPosition(); - - float distanceToAvatar = glm::length(myPosition - otherPosition); + // Decide whether to send this avatar's data based on it's distance from us // The full rate distance is the distance at which EVERY update will be sent for this avatar // at a distance of twice the full rate distance, there will be a 50% chance of sending this avatar's update const float FULL_RATE_DISTANCE = 2.0f; - - // Decide whether to send this avatar's data based on it's distance from us - if ((_performanceThrottlingRatio == 0 || randFloat() < (1.0f - _performanceThrottlingRatio)) - && (distanceToAvatar == 0.0f || randFloat() < FULL_RATE_DISTANCE / distanceToAvatar)) { - QByteArray avatarByteArray; - avatarByteArray.append(otherNode->getUUID().toRfc4122()); - avatarByteArray.append(otherAvatar.toByteArray()); - - if (avatarByteArray.size() + mixedAvatarByteArray.size() > MAX_PACKET_SIZE) { - nodeList->writeDatagram(mixedAvatarByteArray, node); - - // reset the packet - mixedAvatarByteArray.resize(numPacketHeaderBytes); - } - - // 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 = !nodeData->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; - } + glm::vec3 otherPosition = otherAvatar.getPosition(); + float distanceToAvatar = glm::length(myPosition - otherPosition); + + if (!(distanceToAvatar == 0.0f || randFloat() < FULL_RATE_DISTANCE / distanceToAvatar)) { + return; + } + + QByteArray avatarByteArray; + avatarByteArray.append(otherNode->getUUID().toRfc4122()); + avatarByteArray.append(otherAvatar.toByteArray()); + + if (avatarByteArray.size() + mixedAvatarByteArray.size() > MAX_PACKET_SIZE) { + nodeList->writeDatagram(mixedAvatarByteArray, node); + + // reset the packet + mixedAvatarByteArray.resize(numPacketHeaderBytes); + } + + // 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 = !nodeData->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; } - - otherNodeData->getMutex().unlock(); - } }); - nodeList->writeDatagram(mixedAvatarByteArray, node); - - nodeData->getMutex().unlock(); - } }); _lastFrameTimestamp = QDateTime::currentMSecsSinceEpoch(); diff --git a/cmake/macros/LinkHifiLibraries.cmake b/cmake/macros/LinkHifiLibraries.cmake index ed68103c0c..ef7467e170 100644 --- a/cmake/macros/LinkHifiLibraries.cmake +++ b/cmake/macros/LinkHifiLibraries.cmake @@ -16,6 +16,7 @@ macro(LINK_HIFI_LIBRARIES) foreach(HIFI_LIBRARY ${LIBRARIES_TO_LINK}) if (NOT TARGET ${HIFI_LIBRARY}) add_subdirectory("${RELATIVE_LIBRARY_DIR_PATH}/${HIFI_LIBRARY}" "${RELATIVE_LIBRARY_DIR_PATH}/${HIFI_LIBRARY}") + set_target_properties(${HIFI_LIBRARY} PROPERTIES FOLDER "Libraries") endif () include_directories("${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src") diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 2e3505dd4d..57e133c599 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -612,13 +612,15 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock QByteArray usernameSignature; packetStream >> nodeInterestList >> username >> usernameSignature; - - if (!isAssignment && !shouldAllowConnectionFromNode(username, usernameSignature, senderSockAddr)) { + + QString reason; + if (!isAssignment && !shouldAllowConnectionFromNode(username, usernameSignature, senderSockAddr, reason)) { // this is an agent and we've decided we won't let them connect - send them a packet to deny connection - QByteArray usernameRequestByteArray = byteArrayWithPopulatedHeader(PacketTypeDomainConnectionDenied); - - // send this oauth request datagram back to the client - DependencyManager::get()->writeUnverifiedDatagram(usernameRequestByteArray, senderSockAddr); + QByteArray connectionDeniedByteArray = byteArrayWithPopulatedHeader(PacketTypeDomainConnectionDenied); + QDataStream out(&connectionDeniedByteArray, QIODevice::WriteOnly | QIODevice::Append); + out << reason; + // tell client it has been refused. + DependencyManager::get()->writeUnverifiedDatagram(connectionDeniedByteArray, senderSockAddr); return; } @@ -680,9 +682,63 @@ unsigned int DomainServer::countConnectedUsers() { } +bool DomainServer::verifyUsersKey (const QString& username, + const QByteArray& usernameSignature, + QString& reasonReturn) { + // it's possible this user can be allowed to connect, but we need to check their username signature + + QByteArray publicKeyArray = _userPublicKeys.value(username); + if (!publicKeyArray.isEmpty()) { + // if we do have a public key for the user, check for a signature match + + const unsigned char* publicKeyData = reinterpret_cast(publicKeyArray.constData()); + + // first load up the public key into an RSA struct + RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, publicKeyArray.size()); + + if (rsaPublicKey) { + QByteArray decryptedArray(RSA_size(rsaPublicKey), 0); + int decryptResult = + RSA_public_decrypt(usernameSignature.size(), + reinterpret_cast(usernameSignature.constData()), + reinterpret_cast(decryptedArray.data()), + rsaPublicKey, RSA_PKCS1_PADDING); + + if (decryptResult != -1) { + if (username.toLower() == decryptedArray) { + qDebug() << "Username signature matches for" << username << "- allowing connection."; + + // free up the public key before we return + RSA_free(rsaPublicKey); + + return true; + } else { + qDebug() << "Username signature did not match for" << username << "- denying connection."; + reasonReturn = "Username signature did not match."; + } + } else { + qDebug() << "Couldn't decrypt user signature for" << username << "- denying connection."; + reasonReturn = "Couldn't decrypt user signature."; + } + + // free up the public key, we don't need it anymore + RSA_free(rsaPublicKey); + } else { + // we can't let this user in since we couldn't convert their public key to an RSA key we could use + qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection."; + reasonReturn = "Couldn't convert data to RSA key."; + } + } + + requestUserPublicKey(username); // no joy. maybe next time? + return false; +} + + bool DomainServer::shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature, - const HifiSockAddr& senderSockAddr) { + const HifiSockAddr& senderSockAddr, + QString& reasonReturn) { const QVariant* allowedUsersVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ALLOWED_USERS_SETTINGS_KEYPATH); @@ -693,72 +749,46 @@ bool DomainServer::shouldAllowConnectionFromNode(const QString& username, || senderSockAddr.getAddress() == QHostAddress::LocalHost) { return true; } - - const QVariant* maximumUserCapacityVariant = valueForKeyPath(_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY); - unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0; - if (maximumUserCapacity > 0) { - unsigned int connectedUsers = countConnectedUsers(); - if (connectedUsers >= maximumUserCapacity) { - // too many users, deny the new connection. - qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, denying new connection."; - return false; - } - qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, perhaps allowing new connection."; - } if (allowedUsers.count() > 0) { if (allowedUsers.contains(username, Qt::CaseInsensitive)) { - // it's possible this user can be allowed to connect, but we need to check their username signature - - QByteArray publicKeyArray = _userPublicKeys.value(username); - if (!publicKeyArray.isEmpty()) { - // if we do have a public key for the user, check for a signature match - - const unsigned char* publicKeyData = reinterpret_cast(publicKeyArray.constData()); - - // first load up the public key into an RSA struct - RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, publicKeyArray.size()); - - if (rsaPublicKey) { - QByteArray decryptedArray(RSA_size(rsaPublicKey), 0); - int decryptResult = RSA_public_decrypt(usernameSignature.size(), - reinterpret_cast(usernameSignature.constData()), - reinterpret_cast(decryptedArray.data()), - rsaPublicKey, RSA_PKCS1_PADDING); - - if (decryptResult != -1) { - if (username.toLower() == decryptedArray) { - qDebug() << "Username signature matches for" << username << "- allowing connection."; - - // free up the public key before we return - RSA_free(rsaPublicKey); - - return true; - } else { - qDebug() << "Username signature did not match for" << username << "- denying connection."; - } - } else { - qDebug() << "Couldn't decrypt user signature for" << username << "- denying connection."; - } - - // free up the public key, we don't need it anymore - RSA_free(rsaPublicKey); - } else { - // we can't let this user in since we couldn't convert their public key to an RSA key we could use - qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection."; - } + if (verifyUsersKey(username, usernameSignature, reasonReturn)) { + return true; } - - requestUserPublicKey(username); } else { qDebug() << "Connect request denied for user" << username << "not in allowed users list."; + reasonReturn = "User not on whitelist."; } + return false; } else { - // since we have no allowed user list, let them all in + // we have no allowed user list. + + // if this user is in the editors list, exempt them from the max-capacity check + const QVariant* allowedEditorsVariant = + valueForKeyPath(_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH); + QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList(); + if (allowedEditors.contains(username)) { + if (verifyUsersKey(username, usernameSignature, reasonReturn)) { + return true; + } + } + + // if we haven't reached max-capacity, let them in. + const QVariant* maximumUserCapacityVariant = valueForKeyPath(_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY); + unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0; + if (maximumUserCapacity > 0) { + unsigned int connectedUsers = countConnectedUsers(); + if (connectedUsers >= maximumUserCapacity) { + // too many users, deny the new connection. + qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, denying new connection."; + reasonReturn = "Too many connected users."; + return false; + } + qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, perhaps allowing new connection."; + } + return true; } - - return false; } void DomainServer::preloadAllowedUserPublicKeys() { diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 7052cd65a8..79303ef098 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -84,8 +84,9 @@ private: void handleConnectRequest(const QByteArray& packet, const HifiSockAddr& senderSockAddr); unsigned int countConnectedUsers(); + bool verifyUsersKey (const QString& username, const QByteArray& usernameSignature, QString& reasonReturn); bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature, - const HifiSockAddr& senderSockAddr); + const HifiSockAddr& senderSockAddr, QString& reasonReturn); void preloadAllowedUserPublicKeys(); void requestUserPublicKey(const QString& username); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 55d453fde8..cc84951666 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -869,8 +869,10 @@ void Application::controlledBroadcastToNodes(const QByteArray& packet, const Nod } } -void Application::importSVOFromURL(QUrl url) { +bool Application::importSVOFromURL(const QString& urlString) { + QUrl url(urlString); emit svoImportRequested(url.url()); + return true; // assume it's accepted } bool Application::event(QEvent* event) { @@ -883,12 +885,9 @@ bool Application::event(QEvent* event) { QUrl url = fileEvent->url(); if (!url.isEmpty()) { - if (url.scheme() == HIFI_URL_SCHEME) { - DependencyManager::get()->handleLookupString(fileEvent->url().toString()); - } else if (url.path().toLower().endsWith(SVO_EXTENSION)) { - emit svoImportRequested(url.url()); - } else if (url.path().toLower().endsWith(JS_EXTENSION)) { - askToLoadScript(url.toString()); + QString urlString = url.toString(); + if (canAcceptURL(urlString)) { + return acceptURL(urlString); } } return false; @@ -1449,40 +1448,15 @@ void Application::wheelEvent(QWheelEvent* event) { } void Application::dropEvent(QDropEvent *event) { - QString snapshotPath; const QMimeData *mimeData = event->mimeData(); bool atLeastOneFileAccepted = false; foreach (QUrl url, mimeData->urls()) { - auto lower = url.path().toLower(); - if (lower.endsWith(SNAPSHOT_EXTENSION)) { - snapshotPath = url.toLocalFile(); - - - SnapshotMetaData* snapshotData = Snapshot::parseSnapshotData(snapshotPath); - if (snapshotData) { - if (!snapshotData->getDomain().isEmpty()) { - DependencyManager::get()->getDomainHandler().setHostnameAndPort(snapshotData->getDomain()); - } - - _myAvatar->setPosition(snapshotData->getLocation()); - _myAvatar->setOrientation(snapshotData->getOrientation()); + QString urlString = url.toString(); + if (canAcceptURL(urlString)) { + if (acceptURL(urlString)) { atLeastOneFileAccepted = true; - break; // don't process further files - } else { - QMessageBox msgBox; - msgBox.setText("No location details were found in the file " - + snapshotPath + ", try dragging in an authentic Hifi snapshot."); - - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.exec(); + break; } - } else if (lower.endsWith(SVO_EXTENSION)) { - emit svoImportRequested(url.url()); - event->acceptProposedAction(); - atLeastOneFileAccepted = true; - } else if (lower.endsWith(JS_EXTENSION)) { - askToLoadScript(url.url()); - atLeastOneFileAccepted = true; } } @@ -1491,6 +1465,29 @@ void Application::dropEvent(QDropEvent *event) { } } +bool Application::acceptSnapshot(const QString& urlString) { + QUrl url(urlString); + QString snapshotPath = url.toLocalFile(); + + SnapshotMetaData* snapshotData = Snapshot::parseSnapshotData(snapshotPath); + if (snapshotData) { + if (!snapshotData->getDomain().isEmpty()) { + DependencyManager::get()->getDomainHandler().setHostnameAndPort(snapshotData->getDomain()); + } + + _myAvatar->setPosition(snapshotData->getLocation()); + _myAvatar->setOrientation(snapshotData->getOrientation()); + } else { + QMessageBox msgBox; + msgBox.setText("No location details were found in the file " + + snapshotPath + ", try dragging in an authentic Hifi snapshot."); + + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.exec(); + } + return true; +} + void Application::sendPingPackets() { QByteArray pingPacket = DependencyManager::get()->constructPingPacket(); controlledBroadcastToNodes(pingPacket, NodeSet() @@ -3594,7 +3591,109 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri workerThread->start(); } -void Application::askToLoadScript(const QString& scriptFilenameOrURL) { +void Application::initializeAcceptedFiles() { + if (_acceptedExtensions.size() == 0) { + _acceptedExtensions[SNAPSHOT_EXTENSION] = &Application::acceptSnapshot; + _acceptedExtensions[SVO_EXTENSION] = &Application::importSVOFromURL; + _acceptedExtensions[JS_EXTENSION] = &Application::askToLoadScript; + _acceptedExtensions[FST_EXTENSION] = &Application::askToSetAvatarUrl; + } +} + +bool Application::canAcceptURL(const QString& urlString) { + initializeAcceptedFiles(); + + QUrl url(urlString); + if (urlString.startsWith(HIFI_URL_SCHEME)) { + return true; + } + QHashIterator i(_acceptedExtensions); + QString lowerPath = url.path().toLower(); + while (i.hasNext()) { + i.next(); + if (lowerPath.endsWith(i.key())) { + return true; + } + } + return false; +} + +bool Application::acceptURL(const QString& urlString) { + initializeAcceptedFiles(); + + if (urlString.startsWith(HIFI_URL_SCHEME)) { + // this is a hifi URL - have the AddressManager handle it + QMetaObject::invokeMethod(DependencyManager::get().data(), "handleLookupString", + Qt::AutoConnection, Q_ARG(const QString&, urlString)); + return true; + } else { + QUrl url(urlString); + QHashIterator i(_acceptedExtensions); + QString lowerPath = url.path().toLower(); + while (i.hasNext()) { + i.next(); + if (lowerPath.endsWith(i.key())) { + AcceptURLMethod method = i.value(); + (this->*method)(urlString); + return true; + } + } + } + return false; +} + + +bool Application::askToSetAvatarUrl(const QString& url) { + QUrl realUrl(url); + if (realUrl.isLocalFile()) { + QString message = "You can not use local files for avatar components."; + + QMessageBox msgBox; + msgBox.setText(message); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setIcon(QMessageBox::Warning); + msgBox.exec(); + return false; + } + + QString message = "Would you like to use this model for part of avatar:\n" + url; + QMessageBox msgBox; + + msgBox.setIcon(QMessageBox::Question); + msgBox.setWindowTitle("Set Avatar"); + msgBox.setText(message); + + QPushButton* headButton = msgBox.addButton(tr("Head"), QMessageBox::ActionRole); + QPushButton* bodyButton = msgBox.addButton(tr("Body"), QMessageBox::ActionRole); + QPushButton* bodyAndHeadButton = msgBox.addButton(tr("Body + Head"), QMessageBox::ActionRole); + msgBox.addButton(QMessageBox::Cancel); + + msgBox.exec(); + + if (msgBox.clickedButton() == headButton) { + qDebug() << "Chose to use for head: " << url; + _myAvatar->setFaceModelURL(url); + UserActivityLogger::getInstance().changedModel("head", url); + _myAvatar->sendIdentityPacket(); + } else if (msgBox.clickedButton() == bodyButton) { + qDebug() << "Chose to use for body: " << url; + _myAvatar->setSkeletonModelURL(url); + UserActivityLogger::getInstance().changedModel("skeleton", url); + _myAvatar->sendIdentityPacket(); + } else if (msgBox.clickedButton() == bodyAndHeadButton) { + qDebug() << "Chose to use for body + head: " << url; + _myAvatar->setFaceModelURL(QString()); + _myAvatar->setSkeletonModelURL(url); + UserActivityLogger::getInstance().changedModel("skeleton", url); + _myAvatar->sendIdentityPacket(); + } else { + qDebug() << "Declined to use the avatar: " << url; + } + return true; +} + + +bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { QMessageBox::StandardButton reply; QString message = "Would you like to run this script:\n" + scriptFilenameOrURL; reply = QMessageBox::question(getWindow(), "Run Script", message, QMessageBox::Yes|QMessageBox::No); @@ -3605,6 +3704,7 @@ void Application::askToLoadScript(const QString& scriptFilenameOrURL) { } else { qDebug() << "Declined to run the script: " << scriptFilenameOrURL; } + return true; } ScriptEngine* Application::loadScript(const QString& scriptFilename, bool isUserLoaded, diff --git a/interface/src/Application.h b/interface/src/Application.h index 49fdde5fb2..4038b21739 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -96,6 +96,7 @@ static const float NODE_KILLED_BLUE = 0.0f; static const QString SNAPSHOT_EXTENSION = ".jpg"; static const QString SVO_EXTENSION = ".svo"; static const QString JS_EXTENSION = ".js"; +static const QString FST_EXTENSION = ".fst"; static const float BILLBOARD_FIELD_OF_VIEW = 30.0f; // degrees static const float BILLBOARD_DISTANCE = 5.56f; // meters @@ -127,6 +128,8 @@ class Application; #endif #define qApp (static_cast(QCoreApplication::instance())) +typedef bool (Application::* AcceptURLMethod)(const QString &); + class Application : public QApplication, public AbstractViewStateInterface, AbstractScriptingServicesInterface { Q_OBJECT @@ -221,7 +224,7 @@ public: float getFieldOfView() { return _fieldOfView.get(); } void setFieldOfView(float fov) { _fieldOfView.set(fov); } - void importSVOFromURL(QUrl url); + bool importSVOFromURL(const QString& urlString); NodeToOctreeSceneStats* getOcteeSceneStats() { return &_octreeServerSceneStats; } void lockOctreeSceneStats() { _octreeSceneStatsLock.lockForRead(); } @@ -306,6 +309,10 @@ public: QString getScriptsLocation(); void setScriptsLocation(const QString& scriptsLocation); + + void initializeAcceptedFiles(); + bool canAcceptURL(const QString& url); + bool acceptURL(const QString& url); signals: @@ -341,7 +348,9 @@ public slots: void loadDialog(); void loadScriptURLDialog(); void toggleLogDialog(); - void askToLoadScript(const QString& scriptFilenameOrURL); + bool acceptSnapshot(const QString& urlString); + bool askToSetAvatarUrl(const QString& url); + bool askToLoadScript(const QString& scriptFilenameOrURL); ScriptEngine* loadScript(const QString& scriptFilename = QString(), bool isUserLoaded = true, bool loadScriptFromEditor = false, bool activateMainWindow = false); void scriptFinished(const QString& scriptName); @@ -595,6 +604,8 @@ private: QWidget* _fullscreenMenuWidget = new QWidget(); int _menuBarHeight; + + QHash _acceptedExtensions; }; #endif // hifi_Application_h diff --git a/interface/src/DatagramProcessor.cpp b/interface/src/DatagramProcessor.cpp index 1e63ce6655..475ce406bb 100644 --- a/interface/src/DatagramProcessor.cpp +++ b/interface/src/DatagramProcessor.cpp @@ -117,9 +117,15 @@ void DatagramProcessor::processDatagrams() { break; } case PacketTypeDomainConnectionDenied: { + int headerSize = numBytesForPacketHeaderGivenPacketType(PacketTypeDomainConnectionDenied); + QDataStream packetStream(QByteArray(incomingPacket.constData() + headerSize, + incomingPacket.size() - headerSize)); + QString reason; + packetStream >> reason; + // output to the log so the user knows they got a denied connection request // and check and signal for an access token so that we can make sure they are logged in - qDebug() << "The domain-server denied a connection request."; + qDebug() << "The domain-server denied a connection request: " << reason; qDebug() << "You may need to re-log to generate a keypair so you can provide a username signature."; AccountManager::getInstance().checkAndSignalForAccessToken(); break; diff --git a/interface/src/GLCanvas.cpp b/interface/src/GLCanvas.cpp index 7c01f1ad58..97709d2d53 100644 --- a/interface/src/GLCanvas.cpp +++ b/interface/src/GLCanvas.cpp @@ -174,8 +174,8 @@ void GLCanvas::wheelEvent(QWheelEvent* event) { void GLCanvas::dragEnterEvent(QDragEnterEvent* event) { const QMimeData* mimeData = event->mimeData(); foreach (QUrl url, mimeData->urls()) { - auto lower = url.path().toLower(); - if (lower.endsWith(SNAPSHOT_EXTENSION) || lower.endsWith(SVO_EXTENSION) || lower.endsWith(JS_EXTENSION)) { + auto urlString = url.toString(); + if (Application::getInstance()->canAcceptURL(urlString)) { event->acceptProposedAction(); break; } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index bdb0877cda..ab4989a651 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1028,7 +1028,28 @@ void MyAvatar::renderBody(ViewFrustum* renderFrustum, RenderMode renderMode, boo if (!(_skeletonModel.isRenderable() && getHead()->getFaceModel().isRenderable())) { return; // wait until both models are loaded } - + + Camera *camera = Application::getInstance()->getCamera(); + const glm::vec3 cameraPos = camera->getPosition(); + + // Set near clip distance according to skeleton model dimensions if first person and there is no separate head model. + if (shouldRenderHead(cameraPos, renderMode) || !getHead()->getFaceModel().getURL().isEmpty()) { + renderFrustum->setNearClip(DEFAULT_NEAR_CLIP); + } else { + float clipDistance = _skeletonModel.getHeadClipDistance(); + if (OculusManager::isConnected()) { + // If avatar is horizontally in front of camera, increase clip distance by the amount it is in front. + glm::vec3 cameraToAvatar = _position - cameraPos; + cameraToAvatar.y = 0.0f; + glm::vec3 cameraLookAt = camera->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f); + float headOffset = glm::dot(cameraLookAt, cameraToAvatar); + if (headOffset > 0) { + clipDistance += headOffset; + } + } + renderFrustum->setNearClip(clipDistance); + } + // Render the body's voxels and head Model::RenderMode modelRenderMode = (renderMode == SHADOW_RENDER_MODE) ? Model::SHADOW_RENDER_MODE : Model::DEFAULT_RENDER_MODE; @@ -1040,8 +1061,6 @@ void MyAvatar::renderBody(ViewFrustum* renderFrustum, RenderMode renderMode, boo } // Render head so long as the camera isn't inside it - const Camera *camera = Application::getInstance()->getCamera(); - const glm::vec3 cameraPos = camera->getPosition(); if (shouldRenderHead(cameraPos, renderMode)) { getHead()->render(1.0f, renderFrustum, modelRenderMode, postLighting); } @@ -1357,6 +1376,9 @@ void MyAvatar::updatePositionWithPhysics(float deltaTime) { // rotate back into world-frame _velocity = rotation * newLocalVelocity; + + _velocity += _thrust * deltaTime; + _thrust = glm::vec3(0.0f); } void MyAvatar::updateCollisionSound(const glm::vec3 &penetration, float deltaTime, float frequency) { diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 4fdebd5f6f..3a61f69dee 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -37,7 +37,8 @@ SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent) : _defaultEyeModelPosition(glm::vec3(0.0f, 0.0f, 0.0f)), _standingFoot(NO_FOOT), _standingOffset(0.0f), - _clampedFootPosition(0.0f) + _clampedFootPosition(0.0f), + _headClipDistance(DEFAULT_NEAR_CLIP) { } @@ -78,6 +79,10 @@ void SkeletonModel::setJointStates(QVector states) { buildShapes(); } + Extents meshExtents = getMeshExtents(); + _headClipDistance = -(meshExtents.minimum.z / _scale.z - _defaultEyeModelPosition.z); + _headClipDistance = std::max(_headClipDistance, DEFAULT_NEAR_CLIP); + emit skeletonLoaded(); } diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 5427fcaf25..298d74fb7a 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -110,6 +110,8 @@ public: bool hasSkeleton(); + float getHeadClipDistance() const { return _headClipDistance; } + signals: void skeletonLoaded(); @@ -160,6 +162,8 @@ private: int _standingFoot; glm::vec3 _standingOffset; glm::vec3 _clampedFootPosition; + + float _headClipDistance; // Near clip distance to use if no separate head model }; #endif // hifi_SkeletonModel_h diff --git a/interface/src/ui/DataWebPage.cpp b/interface/src/ui/DataWebPage.cpp index c24e34fb64..ab27509d28 100644 --- a/interface/src/ui/DataWebPage.cpp +++ b/interface/src/ui/DataWebPage.cpp @@ -32,22 +32,13 @@ void DataWebPage::javaScriptConsoleMessage(const QString& message, int lineNumbe } bool DataWebPage::acceptNavigationRequest(QWebFrame* frame, const QNetworkRequest& request, QWebPage::NavigationType type) { - - if (!request.url().toString().startsWith(HIFI_URL_SCHEME)) { - if (request.url().path().toLower().endsWith(SVO_EXTENSION)) { - Application::getInstance()->importSVOFromURL(request.url()); - return false; - } else if (request.url().path().toLower().endsWith(JS_EXTENSION)) { - Application::getInstance()->askToLoadScript(request.url().toString()); - return false; + QString urlString = request.url().toString(); + if (Application::getInstance()->canAcceptURL(urlString)) { + if (Application::getInstance()->acceptURL(urlString)) { + return false; // we handled it, so QWebPage doesn't need to handle it } - return true; - } else { - // this is a hifi URL - have the AddressManager handle it - QMetaObject::invokeMethod(DependencyManager::get().data(), "handleLookupString", - Qt::AutoConnection, Q_ARG(const QString&, request.url().toString())); - return false; } + return true; } QString DataWebPage::userAgentForUrl(const QUrl& url) const { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index a5e75f760b..b005f67f5e 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -410,13 +410,30 @@ const Model* EntityTreeRenderer::getModelForEntityItem(const EntityItem* entityI if (entityItem->getType() == EntityTypes::Model) { const RenderableModelEntityItem* constModelEntityItem = dynamic_cast(entityItem); RenderableModelEntityItem* modelEntityItem = const_cast(constModelEntityItem); - assert(modelEntityItem); // we need this!!! - result = modelEntityItem->getModel(this); } return result; } +const FBXGeometry* EntityTreeRenderer::getCollisionGeometryForEntity(const EntityItem* entityItem) { + const FBXGeometry* result = NULL; + + if (entityItem->getType() == EntityTypes::Model) { + const RenderableModelEntityItem* constModelEntityItem = dynamic_cast(entityItem); + if (constModelEntityItem->hasCollisionModel()) { + RenderableModelEntityItem* modelEntityItem = const_cast(constModelEntityItem); + Model* model = modelEntityItem->getModel(this); + if (model) { + const QSharedPointer collisionNetworkGeometry = model->getCollisionGeometry(); + if (!collisionNetworkGeometry.isNull()) { + result = &collisionNetworkGeometry->getFBXGeometry(); + } + } + } + } + return result; +} + void EntityTreeRenderer::renderElementProxy(EntityTreeElement* entityTreeElement) { glm::vec3 elementCenter = entityTreeElement->getAACube().calcCenter(); float elementSize = entityTreeElement->getScale(); @@ -591,7 +608,7 @@ void EntityTreeRenderer::processEraseMessage(const QByteArray& dataByteArray, co static_cast(_tree)->processEraseMessage(dataByteArray, sourceNode); } -Model* EntityTreeRenderer::allocateModel(const QString& url) { +Model* EntityTreeRenderer::allocateModel(const QString& url, const QString& collisionUrl) { Model* model = NULL; // Make sure we only create and delete models on the thread that owns the EntityTreeRenderer if (QThread::currentThread() != thread()) { @@ -604,10 +621,11 @@ Model* EntityTreeRenderer::allocateModel(const QString& url) { model = new Model(); model->init(); model->setURL(QUrl(url)); + model->setCollisionModelURL(QUrl(collisionUrl)); return model; } -Model* EntityTreeRenderer::updateModel(Model* original, const QString& newUrl) { +Model* EntityTreeRenderer::updateModel(Model* original, const QString& newUrl, const QString& collisionUrl) { Model* model = NULL; // The caller shouldn't call us if the URL doesn't need to change. But if they @@ -636,6 +654,7 @@ Model* EntityTreeRenderer::updateModel(Model* original, const QString& newUrl) { model = new Model(); model->init(); model->setURL(QUrl(newUrl)); + model->setCollisionModelURL(QUrl(collisionUrl)); return model; } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 0da85f360b..c4aa5a42d0 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -59,15 +59,16 @@ public: virtual const FBXGeometry* getGeometryForEntity(const EntityItem* entityItem); virtual const Model* getModelForEntityItem(const EntityItem* entityItem); + virtual const FBXGeometry* getCollisionGeometryForEntity(const EntityItem* entityItem); /// clears the tree virtual void clear(); /// if a renderable entity item needs a model, we will allocate it for them - Q_INVOKABLE Model* allocateModel(const QString& url); + Q_INVOKABLE Model* allocateModel(const QString& url, const QString& collisionUrl); /// if a renderable entity item needs to update the URL of a model, we will handle that for the entity - Q_INVOKABLE Model* updateModel(Model* original, const QString& newUrl); + Q_INVOKABLE Model* updateModel(Model* original, const QString& newUrl, const QString& collisionUrl); /// if a renderable entity item is done with a model, it should return it to us void releaseModel(Model* model); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 210fa70b65..d4eefc0986 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -223,10 +223,10 @@ Model* RenderableModelEntityItem::getModel(EntityTreeRenderer* renderer) { // if we have a previously allocated model, but it's URL doesn't match // then we need to let our renderer update our model for us. if (_model && QUrl(getModelURL()) != _model->getURL()) { - result = _model = _myRenderer->updateModel(_model, getModelURL()); + result = _model = _myRenderer->updateModel(_model, getModelURL(), getCollisionModelURL()); _needsInitialSimulation = true; } else if (!_model) { // if we don't yet have a model, then we want our renderer to allocate one - result = _model = _myRenderer->allocateModel(getModelURL()); + result = _model = _myRenderer->allocateModel(getModelURL(), getCollisionModelURL()); _needsInitialSimulation = true; } else { // we already have the model we want... result = _model; @@ -267,36 +267,32 @@ bool RenderableModelEntityItem::findDetailedRayIntersection(const glm::vec3& ori } bool RenderableModelEntityItem::isReadyToComputeShape() { - if (_collisionModelURL == "") { + + if (!_model) { + return false; // hmm... + } + + if (_model->getCollisionURL().isEmpty()) { // no model url, so we're ready to compute a shape. return true; } - if (! _collisionNetworkGeometry.isNull() && _collisionNetworkGeometry->isLoadedWithTextures()) { - // we have a _collisionModelURL AND a _collisionNetworkGeometry AND it's fully loaded. + const QSharedPointer collisionNetworkGeometry = _model->getCollisionGeometry(); + if (! collisionNetworkGeometry.isNull() && collisionNetworkGeometry->isLoadedWithTextures()) { + // we have a _collisionModelURL AND a collisionNetworkGeometry AND it's fully loaded. return true; } - if (_collisionNetworkGeometry.isNull()) { - // we have a _collisionModelURL but we don't yet have a _collisionNetworkGeometry. - _collisionNetworkGeometry = - DependencyManager::get()->getGeometry(_collisionModelURL, QUrl(), false, false); - - if (! _collisionNetworkGeometry.isNull() && _collisionNetworkGeometry->isLoadedWithTextures()) { - // shortcut in case it's already loaded. - return true; - } - } - // the model is still being downloaded. return false; } void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { - if (_collisionModelURL == "") { + if (_model->getCollisionURL().isEmpty()) { info.setParams(getShapeType(), 0.5f * getDimensions()); } else { - const FBXGeometry& fbxGeometry = _collisionNetworkGeometry->getFBXGeometry(); + const QSharedPointer collisionNetworkGeometry = _model->getCollisionGeometry(); + const FBXGeometry& fbxGeometry = collisionNetworkGeometry->getFBXGeometry(); _points.clear(); foreach (const FBXMesh& mesh, fbxGeometry.meshes) { @@ -310,9 +306,9 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { ShapeType RenderableModelEntityItem::getShapeType() const { // XXX make hull an option in edit.js ? - if (_collisionModelURL != "") { - return SHAPE_TYPE_CONVEX_HULL; - } else { + if (!_model || _model->getCollisionURL().isEmpty()) { return _shapeType; + } else { + return SHAPE_TYPE_CONVEX_HULL; } } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 6d50a52a95..f02dd537fb 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -30,8 +30,7 @@ public: _needsInitialSimulation(true), _needsModelReload(true), _myRenderer(NULL), - _originalTexturesRead(false), - _collisionNetworkGeometry(QSharedPointer()) { } + _originalTexturesRead(false) { } virtual ~RenderableModelEntityItem(); @@ -67,8 +66,6 @@ private: QString _currentTextures; QStringList _originalTextures; bool _originalTexturesRead; - - QSharedPointer _collisionNetworkGeometry; QVector _points; }; diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index e1521ebd50..8536e74e9a 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -31,6 +31,7 @@ class EntityItemFBXService { public: virtual const FBXGeometry* getGeometryForEntity(const EntityItem* entityItem) = 0; virtual const Model* getModelForEntityItem(const EntityItem* entityItem) = 0; + virtual const FBXGeometry* getCollisionGeometryForEntity(const EntityItem* entityItem) = 0; }; diff --git a/libraries/networking/src/BandwidthRecorder.h b/libraries/networking/src/BandwidthRecorder.h index c22665d2cc..f87d9d4d06 100644 --- a/libraries/networking/src/BandwidthRecorder.h +++ b/libraries/networking/src/BandwidthRecorder.h @@ -17,7 +17,6 @@ #include #include #include "DependencyManager.h" -#include "Node.h" #include "SimpleMovingAverage.h" diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 520dc650ed..98e1ed0572 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -263,8 +263,10 @@ qint64 LimitedNodeList::writeDatagram(const QByteArray& datagram, } emit dataSent(destinationNode->getType(), datagram.size()); - - return writeDatagram(datagram, *destinationSockAddr, destinationNode->getConnectionSecret()); + auto bytesWritten = writeDatagram(datagram, *destinationSockAddr, destinationNode->getConnectionSecret()); + // Keep track of per-destination-node bandwidth + destinationNode->recordBytesSent(bytesWritten); + return bytesWritten; } // didn't have a destinationNode to send to, return 0 diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index a071eced31..532e8ffcf4 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -151,7 +151,18 @@ public: functor(it->second); } } - + + template + void eachMatchingNode(PredLambda predicate, NodeLambda functor) { + QReadLocker readLock(&_nodeMutex); + + for (NodeHash::const_iterator it = _nodeHash.cbegin(); it != _nodeHash.cend(); ++it) { + if (predicate(it->second)) { + functor(it->second); + } + } + } + template void eachNodeBreakable(BreakableNodeLambda functor) { QReadLocker readLock(&_nodeMutex); diff --git a/libraries/networking/src/NetworkPeer.cpp b/libraries/networking/src/NetworkPeer.cpp index eaaf57471c..c6026b3a23 100644 --- a/libraries/networking/src/NetworkPeer.cpp +++ b/libraries/networking/src/NetworkPeer.cpp @@ -15,6 +15,7 @@ #include #include "NetworkPeer.h" +#include "BandwidthRecorder.h" NetworkPeer::NetworkPeer() : _uuid(), @@ -96,4 +97,37 @@ QDebug operator<<(QDebug debug, const NetworkPeer &peer) { << "- public:" << peer.getPublicSocket() << "- local:" << peer.getLocalSocket(); return debug; -} \ No newline at end of file +} + + +// FIXME this is a temporary implementation to determine if this is the right approach. +// If so, migrate the BandwidthRecorder into the NetworkPeer class +using BandwidthRecorderPtr = QSharedPointer; +static QHash PEER_BANDWIDTH; + +BandwidthRecorder& getBandwidthRecorder(const QUuid & uuid) { + if (!PEER_BANDWIDTH.count(uuid)) { + PEER_BANDWIDTH.insert(uuid, BandwidthRecorderPtr(new BandwidthRecorder())); + } + return *PEER_BANDWIDTH[uuid].data(); +} + +void NetworkPeer::recordBytesSent(int count) { + auto& bw = getBandwidthRecorder(_uuid); + bw.updateOutboundData(0, count); +} + +void NetworkPeer::recordBytesReceived(int count) { + auto& bw = getBandwidthRecorder(_uuid); + bw.updateInboundData(0, count); +} + +float NetworkPeer::getOutboundBandwidth() { + auto& bw = getBandwidthRecorder(_uuid); + return bw.getAverageOutputKilobitsPerSecond(0); +} + +float NetworkPeer::getInboundBandwidth() { + auto& bw = getBandwidthRecorder(_uuid); + return bw.getAverageInputKilobitsPerSecond(0); +} diff --git a/libraries/networking/src/NetworkPeer.h b/libraries/networking/src/NetworkPeer.h index bb92c54eb8..5bf798d2c5 100644 --- a/libraries/networking/src/NetworkPeer.h +++ b/libraries/networking/src/NetworkPeer.h @@ -54,6 +54,12 @@ public: int getConnectionAttempts() const { return _connectionAttempts; } void incrementConnectionAttempts() { ++_connectionAttempts; } void resetConnectionAttemps() { _connectionAttempts = 0; } + + void recordBytesSent(int count); + void recordBytesReceived(int count); + + float getOutboundBandwidth(); + float getInboundBandwidth(); friend QDataStream& operator<<(QDataStream& out, const NetworkPeer& peer); friend QDataStream& operator>>(QDataStream& in, NetworkPeer& peer); diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 8ae85ba67a..90224caa0c 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -298,6 +298,7 @@ void PhysicsEngine::stepSimulation() { _clock.reset(); float timeStep = btMin(dt, MAX_TIMESTEP); + _avatarData->lockForRead(); if (_avatarData->isPhysicsEnabled()) { // update character controller glm::quat rotation = _avatarData->getOrientation(); @@ -307,6 +308,7 @@ void PhysicsEngine::stepSimulation() { btVector3 walkVelocity = glmToBullet(_avatarData->getVelocity()); _characterController->setVelocityForTimeInterval(walkVelocity, timeStep); } + _avatarData->unlock(); // This is step (2). int numSubsteps = _dynamicsWorld->stepSimulation(timeStep, MAX_NUM_SUBSTEPS, PHYSICS_ENGINE_FIXED_SUBSTEP); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index f95c87e99f..890ea87d25 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -106,11 +106,11 @@ public: /// \param delayLoad if true, don't load the model immediately; wait until actually requested Q_INVOKABLE void setURL(const QUrl& url, const QUrl& fallback = QUrl(), bool retainCurrent = false, bool delayLoad = false); + const QUrl& getURL() const { return _url; } // Set the model to use for collisions Q_INVOKABLE void setCollisionModelURL(const QUrl& url, const QUrl& fallback = QUrl(), bool delayLoad = false); - - const QUrl& getURL() const { return _url; } + const QUrl& getCollisionURL() const { return _collisionUrl; } /// Sets the distance parameter used for LOD computations. void setLODDistance(float distance) { _lodDistance = distance; } @@ -132,6 +132,9 @@ public: /// Returns a reference to the shared geometry. const QSharedPointer& getGeometry() const { return _geometry; } + + /// Returns a reference to the shared collision geometry. + const QSharedPointer getCollisionGeometry() {return _collisionGeometry; } /// Returns the number of joint states in the model. int getJointStateCount() const { return _jointStates.size(); } diff --git a/libraries/shared/src/TryLocker.h b/libraries/shared/src/TryLocker.h new file mode 100644 index 0000000000..a5c8077484 --- /dev/null +++ b/libraries/shared/src/TryLocker.h @@ -0,0 +1,28 @@ +// +// TryLocker.h +// libraries/shared/src +// +// Created by Brad Davis on 2015/03/16. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_TryLocker_h +#define hifi_TryLocker_h + +#include + +class MutexTryLocker { + QMutex& _mutex; + bool _locked{ false }; +public: + MutexTryLocker(QMutex &m) : _mutex(m), _locked(m.tryLock()) {} + ~MutexTryLocker() { if (_locked) _mutex.unlock(); } + bool isLocked() { + return _locked; + } +}; + +#endif // hifi_TryLocker_h diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 862792d8ac..b6b57ca530 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,5 +4,6 @@ list(REMOVE_ITEM TEST_SUBDIRS "CMakeFiles") foreach(DIR ${TEST_SUBDIRS}) if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${DIR}") add_subdirectory(${DIR}) + set_target_properties("${DIR}-tests" PROPERTIES FOLDER "Tests") endif() endforeach() \ No newline at end of file diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 08fe8fd7f3..5c7c306a62 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1,8 +1,12 @@ # add the tool directories add_subdirectory(mtc) +set_target_properties(mtc PROPERTIES FOLDER "Tools") add_subdirectory(scribe) +set_target_properties(scribe PROPERTIES FOLDER "Tools") + find_package(VHACD) if(VHACD_FOUND) add_subdirectory(vhacd) +set_target_properties(vhacd PROPERTIES FOLDER "Tools") endif()