diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index 009bd42e88..da1feda7a0 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -41,73 +42,65 @@ AssignmentClient::AssignmentClient(int &argc, char **argv) : setOrganizationDomain("highfidelity.io"); setApplicationName("assignment-client"); QSettings::setDefaultFormat(QSettings::IniFormat); - - QStringList argumentList = arguments(); - - // register meta type is required for queued invoke method on Assignment subclasses - + // set the logging target to the the CHILD_TARGET_NAME Logging::setTargetName(ASSIGNMENT_CLIENT_TARGET_NAME); - - const QString ASSIGNMENT_TYPE_OVVERIDE_OPTION = "-t"; - int argumentIndex = argumentList.indexOf(ASSIGNMENT_TYPE_OVVERIDE_OPTION); - + + const QVariantMap argumentVariantMap = HifiConfigVariantMap::mergeCLParametersWithJSONConfig(arguments()); + + const QString ASSIGNMENT_TYPE_OVERRIDE_OPTION = "t"; + const QString ASSIGNMENT_POOL_OPTION = "pool"; + const QString ASSIGNMENT_WALLET_DESTINATION_ID_OPTION = "wallet"; + const QString CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION = "a"; + Assignment::Type requestAssignmentType = Assignment::AllTypes; - - if (argumentIndex != -1) { - requestAssignmentType = (Assignment::Type) argumentList[argumentIndex + 1].toInt(); + + // check for an assignment type passed on the command line or in the config + if (argumentVariantMap.contains(ASSIGNMENT_TYPE_OVERRIDE_OPTION)) { + requestAssignmentType = (Assignment::Type) argumentVariantMap.value(ASSIGNMENT_TYPE_OVERRIDE_OPTION).toInt(); } - - const QString ASSIGNMENT_POOL_OPTION = "--pool"; - - argumentIndex = argumentList.indexOf(ASSIGNMENT_POOL_OPTION); - + QString assignmentPool; - - if (argumentIndex != -1) { - assignmentPool = argumentList[argumentIndex + 1]; + + // check for an assignment pool passed on the command line or in the config + if (argumentVariantMap.contains(ASSIGNMENT_POOL_OPTION)) { + assignmentPool = argumentVariantMap.value(ASSIGNMENT_POOL_OPTION).toString(); } - + // setup our _requestAssignment member variable from the passed arguments _requestAssignment = Assignment(Assignment::RequestCommand, requestAssignmentType, assignmentPool); - - // check if we were passed a wallet UUID on the command line + + // check for a wallet UUID on the command line or in the config // this would represent where the user running AC wants funds sent to - - const QString ASSIGNMENT_WALLET_DESTINATION_ID_OPTION = "--wallet"; - if ((argumentIndex = argumentList.indexOf(ASSIGNMENT_WALLET_DESTINATION_ID_OPTION)) != -1) { - QUuid walletUUID = QString(argumentList[argumentIndex + 1]); + if (argumentVariantMap.contains(ASSIGNMENT_WALLET_DESTINATION_ID_OPTION)) { + QUuid walletUUID = argumentVariantMap.value(ASSIGNMENT_WALLET_DESTINATION_ID_OPTION).toString(); qDebug() << "The destination wallet UUID for credits is" << uuidStringWithoutCurlyBraces(walletUUID); _requestAssignment.setWalletUUID(walletUUID); } - + // create a NodeList as an unassigned client NodeList* nodeList = NodeList::createInstance(NodeType::Unassigned); - + // check for an overriden assignment server hostname - const QString CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION = "-a"; - - argumentIndex = argumentList.indexOf(CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION); - - if (argumentIndex != -1) { - _assignmentServerHostname = argumentList[argumentIndex + 1]; - + if (argumentVariantMap.contains(CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION)) { + _assignmentServerHostname = argumentVariantMap.value(CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION).toString(); + // set the custom assignment socket on our NodeList HifiSockAddr customAssignmentSocket = HifiSockAddr(_assignmentServerHostname, DEFAULT_DOMAIN_SERVER_PORT); - + nodeList->setAssignmentServerSocket(customAssignmentSocket); } - + // call a timer function every ASSIGNMENT_REQUEST_INTERVAL_MSECS to ask for assignment, if required qDebug() << "Waiting for assignment -" << _requestAssignment; - + QTimer* timer = new QTimer(this); connect(timer, SIGNAL(timeout()), SLOT(sendAssignmentRequest())); timer->start(ASSIGNMENT_REQUEST_INTERVAL_MSECS); - + // connect our readPendingDatagrams method to the readyRead() signal of the socket connect(&nodeList->getNodeSocket(), &QUdpSocket::readyRead, this, &AssignmentClient::readPendingDatagrams); - + // connections to AccountManager for authentication connect(&AccountManager::getInstance(), &AccountManager::authRequired, this, &AssignmentClient::handleAuthenticationRequest); @@ -121,49 +114,49 @@ void AssignmentClient::sendAssignmentRequest() { void AssignmentClient::readPendingDatagrams() { NodeList* nodeList = NodeList::getInstance(); - + QByteArray receivedPacket; HifiSockAddr senderSockAddr; - + while (nodeList->getNodeSocket().hasPendingDatagrams()) { receivedPacket.resize(nodeList->getNodeSocket().pendingDatagramSize()); nodeList->getNodeSocket().readDatagram(receivedPacket.data(), receivedPacket.size(), senderSockAddr.getAddressPointer(), senderSockAddr.getPortPointer()); - + if (nodeList->packetVersionAndHashMatch(receivedPacket)) { if (packetTypeForPacket(receivedPacket) == PacketTypeCreateAssignment) { // construct the deployed assignment from the packet data _currentAssignment = SharedAssignmentPointer(AssignmentFactory::unpackAssignment(receivedPacket)); - + if (_currentAssignment) { qDebug() << "Received an assignment -" << *_currentAssignment; - + // switch our DomainHandler hostname and port to whoever sent us the assignment - + nodeList->getDomainHandler().setSockAddr(senderSockAddr, _assignmentServerHostname); nodeList->getDomainHandler().setAssignmentUUID(_currentAssignment->getUUID()); - + qDebug() << "Destination IP for assignment is" << nodeList->getDomainHandler().getIP().toString(); - + // start the deployed assignment AssignmentThread* workerThread = new AssignmentThread(_currentAssignment, this); - + connect(workerThread, &QThread::started, _currentAssignment.data(), &ThreadedAssignment::run); connect(_currentAssignment.data(), &ThreadedAssignment::finished, workerThread, &QThread::quit); connect(_currentAssignment.data(), &ThreadedAssignment::finished, this, &AssignmentClient::assignmentCompleted); connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater); - + _currentAssignment->moveToThread(workerThread); - + // move the NodeList to the thread used for the _current assignment nodeList->moveToThread(workerThread); - + // let the assignment handle the incoming datagrams for its duration disconnect(&nodeList->getNodeSocket(), 0, this, 0); connect(&nodeList->getNodeSocket(), &QUdpSocket::readyRead, _currentAssignment.data(), &ThreadedAssignment::readPendingDatagrams); - + // Starts an event loop, and emits workerThread->started() workerThread->start(); } else { @@ -180,15 +173,15 @@ void AssignmentClient::readPendingDatagrams() { void AssignmentClient::handleAuthenticationRequest() { const QString DATA_SERVER_USERNAME_ENV = "HIFI_AC_USERNAME"; const QString DATA_SERVER_PASSWORD_ENV = "HIFI_AC_PASSWORD"; - + // this node will be using an authentication server, let's make sure we have a username/password QProcessEnvironment sysEnvironment = QProcessEnvironment::systemEnvironment(); - + QString username = sysEnvironment.value(DATA_SERVER_USERNAME_ENV); QString password = sysEnvironment.value(DATA_SERVER_PASSWORD_ENV); - + AccountManager& accountManager = AccountManager::getInstance(); - + if (!username.isEmpty() && !password.isEmpty()) { // ask the account manager to log us in from the env variables accountManager.requestAccessToken(username, password); @@ -196,7 +189,7 @@ void AssignmentClient::handleAuthenticationRequest() { qDebug() << "Authentication was requested against" << qPrintable(accountManager.getAuthURL().toString()) << "but both or one of" << qPrintable(DATA_SERVER_USERNAME_ENV) << "/" << qPrintable(DATA_SERVER_PASSWORD_ENV) << "are not set. Unable to authenticate."; - + return; } } @@ -204,15 +197,15 @@ void AssignmentClient::handleAuthenticationRequest() { void AssignmentClient::assignmentCompleted() { // reset the logging target to the the CHILD_TARGET_NAME Logging::setTargetName(ASSIGNMENT_CLIENT_TARGET_NAME); - + qDebug("Assignment finished or never started - waiting for new assignment."); - + NodeList* nodeList = NodeList::getInstance(); // have us handle incoming NodeList datagrams again disconnect(&nodeList->getNodeSocket(), 0, _currentAssignment.data(), 0); connect(&nodeList->getNodeSocket(), &QUdpSocket::readyRead, this, &AssignmentClient::readPendingDatagrams); - + // clear our current assignment shared pointer now that we're done with it // if the assignment thread is still around it has its own shared pointer to the assignment _currentAssignment.clear(); diff --git a/assignment-client/src/models/ModelServer.cpp b/assignment-client/src/models/ModelServer.cpp index 07359f001a..ae6ffaf969 100644 --- a/assignment-client/src/models/ModelServer.cpp +++ b/assignment-client/src/models/ModelServer.cpp @@ -86,7 +86,7 @@ bool ModelServer::hasSpecialPacketToSend(const SharedNodePointer& node) { return shouldSendDeletedModels; } -int ModelServer::sendSpecialPacket(OctreeQueryNode* queryNode, const SharedNodePointer& node) { +int ModelServer::sendSpecialPacket(const SharedNodePointer& node, OctreeQueryNode* queryNode, int& packetsSent) { unsigned char outputBuffer[MAX_PACKET_SIZE]; size_t packetLength = 0; @@ -99,6 +99,7 @@ int ModelServer::sendSpecialPacket(OctreeQueryNode* queryNode, const SharedNodeP bool hasMoreToSend = true; // TODO: is it possible to send too many of these packets? what if you deleted 1,000,000 models? + packetsSent = 0; while (hasMoreToSend) { hasMoreToSend = tree->encodeModelsDeletedSince(queryNode->getSequenceNumber(), deletedModelsSentAt, outputBuffer, MAX_PACKET_SIZE, packetLength); @@ -107,6 +108,7 @@ int ModelServer::sendSpecialPacket(OctreeQueryNode* queryNode, const SharedNodeP NodeList::getInstance()->writeDatagram((char*) outputBuffer, packetLength, SharedNodePointer(node)); queryNode->packetSent(outputBuffer, packetLength); + packetsSent++; } nodeData->setLastDeletedModelsSentAt(deletePacketSentAt); diff --git a/assignment-client/src/models/ModelServer.h b/assignment-client/src/models/ModelServer.h index 7e7f239f2a..38acc7f1e1 100644 --- a/assignment-client/src/models/ModelServer.h +++ b/assignment-client/src/models/ModelServer.h @@ -37,7 +37,7 @@ public: // subclass may implement these method virtual void beforeRun(); virtual bool hasSpecialPacketToSend(const SharedNodePointer& node); - virtual int sendSpecialPacket(OctreeQueryNode* queryNode, const SharedNodePointer& node); + virtual int sendSpecialPacket(const SharedNodePointer& node, OctreeQueryNode* queryNode, int& packetsSent); virtual void modelCreated(const ModelItem& newModel, const SharedNodePointer& senderNode); diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index 30e3011ae8..cb149b1d96 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -85,7 +85,6 @@ bool OctreeSendThread::process() { if (nodeData && !nodeData->isShuttingDown()) { bool viewFrustumChanged = nodeData->updateCurrentViewFrustum(); packetDistributor(nodeData, viewFrustumChanged); - resendNackedPackets(nodeData); } } } @@ -281,26 +280,6 @@ int OctreeSendThread::handlePacketSend(OctreeQueryNode* nodeData, int& trueBytes return packetsSent; } -int OctreeSendThread::resendNackedPackets(OctreeQueryNode* nodeData) { - - const int MAX_PACKETS_RESEND = 10; - int packetsSent = 0; - - const QByteArray* packet; - while (nodeData->hasNextNackedPacket() && packetsSent < MAX_PACKETS_RESEND) { - packet = nodeData->getNextNackedPacket(); - if (packet) { - NodeList::getInstance()->writeDatagram(*packet, _node); - packetsSent++; - - _totalBytes += packet->size(); - _totalPackets++; - _totalWastedBytes += MAX_PACKET_SIZE - packet->size(); - } - } - return packetsSent; -} - /// Version of voxel distributor that sends the deepest LOD level at once int OctreeSendThread::packetDistributor(OctreeQueryNode* nodeData, bool viewFrustumChanged) { @@ -311,6 +290,10 @@ int OctreeSendThread::packetDistributor(OctreeQueryNode* nodeData, bool viewFrus return 0; } + // calculate max number of packets that can be sent during this interval + int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxOctreePacketsPerSecond() / INTERVALS_PER_SECOND)); + int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); + int truePacketsSent = 0; int trueBytesSent = 0; int packetsSentThisInterval = 0; @@ -408,9 +391,6 @@ int OctreeSendThread::packetDistributor(OctreeQueryNode* nodeData, bool viewFrus //quint64 startCompressTimeMsecs = OctreePacketData::getCompressContentTime() / 1000; //quint64 startCompressCalls = OctreePacketData::getCompressContentCalls(); - int clientMaxPacketsPerInterval = std::max(1,(nodeData->getMaxOctreePacketsPerSecond() / INTERVALS_PER_SECOND)); - int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); - int extraPackingAttempts = 0; bool completedScene = false; while (somethingToSend && packetsSentThisInterval < maxPacketsPerInterval && !nodeData->isShuttingDown()) { @@ -581,12 +561,26 @@ int OctreeSendThread::packetDistributor(OctreeQueryNode* nodeData, bool viewFrus // send the environment packet // TODO: should we turn this into a while loop to better handle sending multiple special packets if (_myServer->hasSpecialPacketToSend(_node) && !nodeData->isShuttingDown()) { - trueBytesSent += _myServer->sendSpecialPacket(nodeData, _node); + int specialPacketsSent; + trueBytesSent += _myServer->sendSpecialPacket(_node, nodeData, specialPacketsSent); nodeData->resetOctreePacket(); // because nodeData's _sequenceNumber has changed - truePacketsSent++; - packetsSentThisInterval++; + truePacketsSent += specialPacketsSent; + packetsSentThisInterval += specialPacketsSent; } + // Re-send packets that were nacked by the client + while (nodeData->hasNextNackedPacket() && packetsSentThisInterval < maxPacketsPerInterval) { + const QByteArray* packet = nodeData->getNextNackedPacket(); + if (packet) { + NodeList::getInstance()->writeDatagram(*packet, _node); + truePacketsSent++; + packetsSentThisInterval++; + + _totalBytes += packet->size(); + _totalPackets++; + _totalWastedBytes += MAX_PACKET_SIZE - packet->size(); + } + } quint64 end = usecTimestampNow(); int elapsedmsec = (end - start)/USECS_PER_MSEC; diff --git a/assignment-client/src/octree/OctreeSendThread.h b/assignment-client/src/octree/OctreeSendThread.h index e7599ebcd2..d8eed27802 100644 --- a/assignment-client/src/octree/OctreeSendThread.h +++ b/assignment-client/src/octree/OctreeSendThread.h @@ -55,9 +55,6 @@ private: int _nodeMissingCount; bool _isShuttingDown; - - int resendNackedPackets(OctreeQueryNode* nodeData); - }; #endif // hifi_OctreeSendThread_h diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index f0db93feb3..5595d139be 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -72,7 +72,7 @@ public: // subclass may implement these method virtual void beforeRun() { }; virtual bool hasSpecialPacketToSend(const SharedNodePointer& node) { return false; } - virtual int sendSpecialPacket(OctreeQueryNode* queryNode, const SharedNodePointer& node) { return 0; } + virtual int sendSpecialPacket(const SharedNodePointer& node, OctreeQueryNode* queryNode, int& packetsSent) { return 0; } static void attachQueryNodeToNode(Node* newNode); diff --git a/assignment-client/src/particles/ParticleServer.cpp b/assignment-client/src/particles/ParticleServer.cpp index e7a0f75dfd..674d22145f 100644 --- a/assignment-client/src/particles/ParticleServer.cpp +++ b/assignment-client/src/particles/ParticleServer.cpp @@ -86,7 +86,7 @@ bool ParticleServer::hasSpecialPacketToSend(const SharedNodePointer& node) { return shouldSendDeletedParticles; } -int ParticleServer::sendSpecialPacket(OctreeQueryNode* queryNode, const SharedNodePointer& node) { +int ParticleServer::sendSpecialPacket(const SharedNodePointer& node, OctreeQueryNode* queryNode, int& packetsSent) { unsigned char outputBuffer[MAX_PACKET_SIZE]; size_t packetLength = 0; @@ -99,6 +99,7 @@ int ParticleServer::sendSpecialPacket(OctreeQueryNode* queryNode, const SharedNo bool hasMoreToSend = true; // TODO: is it possible to send too many of these packets? what if you deleted 1,000,000 particles? + packetsSent = 0; while (hasMoreToSend) { hasMoreToSend = tree->encodeParticlesDeletedSince(queryNode->getSequenceNumber(), deletedParticlesSentAt, outputBuffer, MAX_PACKET_SIZE, packetLength); @@ -107,6 +108,7 @@ int ParticleServer::sendSpecialPacket(OctreeQueryNode* queryNode, const SharedNo NodeList::getInstance()->writeDatagram((char*) outputBuffer, packetLength, SharedNodePointer(node)); queryNode->packetSent(outputBuffer, packetLength); + packetsSent++; } nodeData->setLastDeletedParticlesSentAt(deletePacketSentAt); diff --git a/assignment-client/src/particles/ParticleServer.h b/assignment-client/src/particles/ParticleServer.h index 3066c5fa98..d444368a9d 100644 --- a/assignment-client/src/particles/ParticleServer.h +++ b/assignment-client/src/particles/ParticleServer.h @@ -37,7 +37,7 @@ public: // subclass may implement these method virtual void beforeRun(); virtual bool hasSpecialPacketToSend(const SharedNodePointer& node); - virtual int sendSpecialPacket(OctreeQueryNode* queryNode, const SharedNodePointer& node); + virtual int sendSpecialPacket(const SharedNodePointer& node, OctreeQueryNode* queryNode, int& packetsSent); virtual void particleCreated(const Particle& newParticle, const SharedNodePointer& senderNode); diff --git a/assignment-client/src/voxels/VoxelServer.cpp b/assignment-client/src/voxels/VoxelServer.cpp index 34b01f529a..b021ddd9f6 100644 --- a/assignment-client/src/voxels/VoxelServer.cpp +++ b/assignment-client/src/voxels/VoxelServer.cpp @@ -40,7 +40,7 @@ bool VoxelServer::hasSpecialPacketToSend(const SharedNodePointer& node) { return shouldSendEnvironments; } -int VoxelServer::sendSpecialPacket(OctreeQueryNode* queryNode, const SharedNodePointer& node) { +int VoxelServer::sendSpecialPacket(const SharedNodePointer& node, OctreeQueryNode* queryNode, int& packetsSent) { unsigned char* copyAt = _tempOutputBuffer; @@ -76,6 +76,7 @@ int VoxelServer::sendSpecialPacket(OctreeQueryNode* queryNode, const SharedNodeP NodeList::getInstance()->writeDatagram((char*) _tempOutputBuffer, envPacketLength, SharedNodePointer(node)); queryNode->packetSent(_tempOutputBuffer, envPacketLength); + packetsSent = 1; return envPacketLength; } diff --git a/assignment-client/src/voxels/VoxelServer.h b/assignment-client/src/voxels/VoxelServer.h index 4e04c48cfd..b13f83b65f 100644 --- a/assignment-client/src/voxels/VoxelServer.h +++ b/assignment-client/src/voxels/VoxelServer.h @@ -46,7 +46,7 @@ public: // subclass may implement these method virtual void beforeRun(); virtual bool hasSpecialPacketToSend(const SharedNodePointer& node); - virtual int sendSpecialPacket(OctreeQueryNode* queryNode, const SharedNodePointer& node); + virtual int sendSpecialPacket(const SharedNodePointer& node, OctreeQueryNode* queryNode, int& packetsSent); private: bool _sendEnvironments; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index b5c2d00d3e..f7185063a9 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -47,15 +47,15 @@ DomainServer::DomainServer(int argc, char* argv[]) : setOrganizationDomain("highfidelity.io"); setApplicationName("domain-server"); QSettings::setDefaultFormat(QSettings::IniFormat); - + _argumentVariantMap = HifiConfigVariantMap::mergeCLParametersWithJSONConfig(arguments()); - + _networkAccessManager = new QNetworkAccessManager(this); - + if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth() && optionallySetupAssignmentPayment()) { // we either read a certificate and private key or were not passed one // and completed login or did not need to - + qDebug() << "Setting up LimitedNodeList and assignments."; setupNodeListAndAssignments(); } @@ -65,58 +65,58 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { const QString X509_CERTIFICATE_OPTION = "cert"; const QString X509_PRIVATE_KEY_OPTION = "key"; const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE"; - + QString certPath = _argumentVariantMap.value(X509_CERTIFICATE_OPTION).toString(); QString keyPath = _argumentVariantMap.value(X509_PRIVATE_KEY_OPTION).toString(); - + if (!certPath.isEmpty() && !keyPath.isEmpty()) { // the user wants to use DTLS to encrypt communication with nodes // let's make sure we can load the key and certificate // _x509Credentials = new gnutls_certificate_credentials_t; // gnutls_certificate_allocate_credentials(_x509Credentials); - + QString keyPassphraseString = QProcessEnvironment::systemEnvironment().value(X509_KEY_PASSPHRASE_ENV); - + qDebug() << "Reading certificate file at" << certPath << "for DTLS."; qDebug() << "Reading key file at" << keyPath << "for DTLS."; - + // int gnutlsReturn = gnutls_certificate_set_x509_key_file2(*_x509Credentials, // certPath.toLocal8Bit().constData(), // keyPath.toLocal8Bit().constData(), // GNUTLS_X509_FMT_PEM, // keyPassphraseString.toLocal8Bit().constData(), // 0); -// +// // if (gnutlsReturn < 0) { // qDebug() << "Unable to load certificate or key file." << "Error" << gnutlsReturn << "- domain-server will now quit."; // QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); // return false; // } - + // qDebug() << "Successfully read certificate and private key."; - + // we need to also pass this certificate and private key to the HTTPS manager // this is used for Oauth callbacks when authorizing users against a data server - + QFile certFile(certPath); certFile.open(QIODevice::ReadOnly); - + QFile keyFile(keyPath); keyFile.open(QIODevice::ReadOnly); - + QSslCertificate sslCertificate(&certFile); QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8()); - + _httpsManager = new HTTPSManager(DOMAIN_SERVER_HTTPS_PORT, sslCertificate, privateKey, QString(), this, this); - + qDebug() << "TCP server listening for HTTPS connections on" << DOMAIN_SERVER_HTTPS_PORT; - + } else if (!certPath.isEmpty() || !keyPath.isEmpty()) { qDebug() << "Missing certificate or private key. domain-server will now quit."; QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); return false; } - + return true; } @@ -125,12 +125,12 @@ bool DomainServer::optionallySetupOAuth() { const QString OAUTH_CLIENT_ID_OPTION = "oauth-client-id"; const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET"; const QString REDIRECT_HOSTNAME_OPTION = "hostname"; - + _oauthProviderURL = QUrl(_argumentVariantMap.value(OAUTH_PROVIDER_URL_OPTION).toString()); _oauthClientID = _argumentVariantMap.value(OAUTH_CLIENT_ID_OPTION).toString(); _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); _hostname = _argumentVariantMap.value(REDIRECT_HOSTNAME_OPTION).toString(); - + if (!_oauthClientID.isEmpty()) { if (_oauthProviderURL.isEmpty() || _hostname.isEmpty() @@ -144,47 +144,47 @@ bool DomainServer::optionallySetupOAuth() { qDebug() << "OAuth Client ID is" << _oauthClientID; } } - + return true; } void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { - + const QString CUSTOM_PORT_OPTION = "port"; unsigned short domainServerPort = DEFAULT_DOMAIN_SERVER_PORT; - + if (_argumentVariantMap.contains(CUSTOM_PORT_OPTION)) { domainServerPort = (unsigned short) _argumentVariantMap.value(CUSTOM_PORT_OPTION).toUInt(); } - + unsigned short domainServerDTLSPort = 0; - + if (_isUsingDTLS) { domainServerDTLSPort = DEFAULT_DOMAIN_SERVER_DTLS_PORT; - + const QString CUSTOM_DTLS_PORT_OPTION = "dtls-port"; - + if (_argumentVariantMap.contains(CUSTOM_DTLS_PORT_OPTION)) { domainServerDTLSPort = (unsigned short) _argumentVariantMap.value(CUSTOM_DTLS_PORT_OPTION).toUInt(); } } - + QSet parsedTypes; parseAssignmentConfigs(parsedTypes); - + populateDefaultStaticAssignmentsExcludingTypes(parsedTypes); - + LimitedNodeList* nodeList = LimitedNodeList::createInstance(domainServerPort, domainServerDTLSPort); - + connect(nodeList, &LimitedNodeList::nodeAdded, this, &DomainServer::nodeAdded); connect(nodeList, &LimitedNodeList::nodeKilled, this, &DomainServer::nodeKilled); - + QTimer* silentNodeTimer = new QTimer(this); connect(silentNodeTimer, SIGNAL(timeout()), nodeList, SLOT(removeSilentNodes())); silentNodeTimer->start(NODE_SILENCE_THRESHOLD_MSECS); - + connect(&nodeList->getNodeSocket(), SIGNAL(readyRead()), SLOT(readAvailableDatagrams())); - + // add whatever static assignments that have been parsed to the queue addStaticAssignmentsToQueue(); } @@ -194,21 +194,22 @@ bool DomainServer::optionallySetupAssignmentPayment() { const QString PAY_FOR_ASSIGNMENTS_OPTION = "pay-for-assignments"; const QString HIFI_USERNAME_ENV_KEY = "DOMAIN_SERVER_USERNAME"; const QString HIFI_PASSWORD_ENV_KEY = "DOMAIN_SERVER_PASSWORD"; - - if (_argumentVariantMap.contains(PAY_FOR_ASSIGNMENTS_OPTION)) { + + if (_argumentVariantMap.contains(PAY_FOR_ASSIGNMENTS_OPTION) && + _argumentVariantMap.value(PAY_FOR_ASSIGNMENTS_OPTION).toBool()) { if (!_oauthProviderURL.isEmpty()) { - + AccountManager& accountManager = AccountManager::getInstance(); accountManager.setAuthURL(_oauthProviderURL); - + if (!accountManager.hasValidAccessToken()) { // we don't have a valid access token so we need to get one QString username = QProcessEnvironment::systemEnvironment().value(HIFI_USERNAME_ENV_KEY); QString password = QProcessEnvironment::systemEnvironment().value(HIFI_PASSWORD_ENV_KEY); - + if (!username.isEmpty() && !password.isEmpty()) { accountManager.requestAccessToken(username, password); - + // connect to loginFailed signal from AccountManager so we can quit if that is the case connect(&accountManager, &AccountManager::loginFailed, this, &DomainServer::loginFailed); } else { @@ -217,31 +218,31 @@ bool DomainServer::optionallySetupAssignmentPayment() { return false; } } - + qDebug() << "Assignments will be paid for via" << qPrintable(_oauthProviderURL.toString()); - + // assume that the fact we are authing against HF data server means we will pay for assignments // setup a timer to send transactions to pay assigned nodes every 30 seconds QTimer* creditSetupTimer = new QTimer(this); connect(creditSetupTimer, &QTimer::timeout, this, &DomainServer::setupPendingAssignmentCredits); - + const qint64 CREDIT_CHECK_INTERVAL_MSECS = 5 * 1000; creditSetupTimer->start(CREDIT_CHECK_INTERVAL_MSECS); - + QTimer* nodePaymentTimer = new QTimer(this); connect(nodePaymentTimer, &QTimer::timeout, this, &DomainServer::sendPendingTransactionsToServer); - + const qint64 TRANSACTION_SEND_INTERVAL_MSECS = 30 * 1000; nodePaymentTimer->start(TRANSACTION_SEND_INTERVAL_MSECS); - + } else { qDebug() << "Missing OAuth provider URL, but assigned node payment was enabled. domain-server will now quit."; QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); - + return false; } } - + return true; } @@ -254,35 +255,35 @@ void DomainServer::parseAssignmentConfigs(QSet& excludedTypes) // check for configs from the command line, these take precedence const QString ASSIGNMENT_CONFIG_REGEX_STRING = "config-([\\d]+)"; QRegExp assignmentConfigRegex(ASSIGNMENT_CONFIG_REGEX_STRING); - + // scan for assignment config keys QStringList variantMapKeys = _argumentVariantMap.keys(); int configIndex = variantMapKeys.indexOf(assignmentConfigRegex); - + while (configIndex != -1) { // figure out which assignment type this matches Assignment::Type assignmentType = (Assignment::Type) assignmentConfigRegex.cap(1).toInt(); - + if (assignmentType < Assignment::AllTypes && !excludedTypes.contains(assignmentType)) { QVariant mapValue = _argumentVariantMap[variantMapKeys[configIndex]]; QJsonArray assignmentArray; - + if (mapValue.type() == QVariant::String) { QJsonDocument deserializedDocument = QJsonDocument::fromJson(mapValue.toString().toUtf8()); assignmentArray = deserializedDocument.array(); } else { assignmentArray = mapValue.toJsonValue().toArray(); } - + if (assignmentType != Assignment::AgentType) { createStaticAssignmentsForType(assignmentType, assignmentArray); } else { createScriptedAssignmentsFromArray(assignmentArray); } - + excludedTypes.insert(assignmentType); } - + configIndex = variantMapKeys.indexOf(assignmentConfigRegex, configIndex + 1); } } @@ -293,34 +294,34 @@ void DomainServer::addStaticAssignmentToAssignmentHash(Assignment* newAssignment _allAssignments.insert(newAssignment->getUUID(), SharedAssignmentPointer(newAssignment)); } -void DomainServer::createScriptedAssignmentsFromArray(const QJsonArray &configArray) { +void DomainServer::createScriptedAssignmentsFromArray(const QJsonArray &configArray) { foreach(const QJsonValue& jsonValue, configArray) { if (jsonValue.isObject()) { QJsonObject jsonObject = jsonValue.toObject(); - + // make sure we were passed a URL, otherwise this is an invalid scripted assignment const QString ASSIGNMENT_URL_KEY = "url"; QString assignmentURL = jsonObject[ASSIGNMENT_URL_KEY].toString(); - + if (!assignmentURL.isEmpty()) { // check the json for a pool const QString ASSIGNMENT_POOL_KEY = "pool"; QString assignmentPool = jsonObject[ASSIGNMENT_POOL_KEY].toString(); - + // check for a number of instances, if not passed then default is 1 const QString ASSIGNMENT_INSTANCES_KEY = "instances"; int numInstances = jsonObject[ASSIGNMENT_INSTANCES_KEY].toInt(); numInstances = (numInstances == 0 ? 1 : numInstances); - + qDebug() << "Adding a static scripted assignment from" << assignmentURL; - + for (int i = 0; i < numInstances; i++) { // add a scripted assignment to the queue for this instance Assignment* scriptAssignment = new Assignment(Assignment::CreateCommand, Assignment::AgentType, assignmentPool); scriptAssignment->setPayload(assignmentURL.toUtf8()); - + // scripts passed on CL or via JSON are static - so they are added back to the queue if the node dies addStaticAssignmentToAssignmentHash(scriptAssignment); } @@ -332,38 +333,38 @@ void DomainServer::createScriptedAssignmentsFromArray(const QJsonArray &configAr void DomainServer::createStaticAssignmentsForType(Assignment::Type type, const QJsonArray& configArray) { // we have a string for config for this type qDebug() << "Parsing config for assignment type" << type; - + int configCounter = 0; - + foreach(const QJsonValue& jsonValue, configArray) { if (jsonValue.isObject()) { QJsonObject jsonObject = jsonValue.toObject(); - + // check the config string for a pool const QString ASSIGNMENT_POOL_KEY = "pool"; QString assignmentPool; - + QJsonValue poolValue = jsonObject[ASSIGNMENT_POOL_KEY]; if (!poolValue.isUndefined()) { assignmentPool = poolValue.toString(); - + jsonObject.remove(ASSIGNMENT_POOL_KEY); } - + ++configCounter; qDebug() << "Type" << type << "config" << configCounter << "=" << jsonObject; - + Assignment* configAssignment = new Assignment(Assignment::CreateCommand, type, assignmentPool); - + // setup the payload as a semi-colon separated list of key = value QStringList payloadStringList; foreach(const QString& payloadKey, jsonObject.keys()) { QString dashes = payloadKey.size() == 1 ? "-" : "--"; payloadStringList << QString("%1%2 %3").arg(dashes).arg(payloadKey).arg(jsonObject[payloadKey].toString()); } - + configAssignment->setPayload(payloadStringList.join(' ').toUtf8()); - + addStaticAssignmentToAssignmentHash(configAssignment); } } @@ -393,27 +394,27 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock NodeType_t nodeType; HifiSockAddr publicSockAddr, localSockAddr; - + int numPreInterestBytes = parseNodeDataFromByteArray(nodeType, publicSockAddr, localSockAddr, packet, senderSockAddr); - + QUuid packetUUID = uuidFromPacketHeader(packet); // check if this connect request matches an assignment in the queue bool isAssignment = _pendingAssignedNodes.contains(packetUUID); SharedAssignmentPointer matchingQueuedAssignment = SharedAssignmentPointer(); PendingAssignedNodeData* pendingAssigneeData = NULL; - + if (isAssignment) { pendingAssigneeData = _pendingAssignedNodes.value(packetUUID); - + if (pendingAssigneeData) { matchingQueuedAssignment = matchingQueuedAssignmentForCheckIn(pendingAssigneeData->getAssignmentUUID(), nodeType); - + if (matchingQueuedAssignment) { qDebug() << "Assignment deployed with" << uuidStringWithoutCurlyBraces(packetUUID) << "matches unfulfilled assignment" << uuidStringWithoutCurlyBraces(matchingQueuedAssignment->getUUID()); - + // remove this unique assignment deployment from the hash of pending assigned nodes // cleanup of the PendingAssignedNodeData happens below after the node has been added to the LimitedNodeList _pendingAssignedNodes.remove(packetUUID); @@ -424,9 +425,9 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock return; } } - + } - + if (!isAssignment && !_oauthProviderURL.isEmpty() && _argumentVariantMap.contains(ALLOWED_ROLES_CONFIG_KEY)) { // this is an Agent, and we require authentication so we can compare the user's roles to our list of allowed ones if (_sessionAuthenticationHash.contains(packetUUID)) { @@ -445,37 +446,37 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock QDataStream oauthRequestStream(&oauthRequestByteArray, QIODevice::Append); QUrl authorizationURL = packetUUID.isNull() ? oauthAuthorizationURL() : oauthAuthorizationURL(packetUUID); oauthRequestStream << authorizationURL; - + // send this oauth request datagram back to the client LimitedNodeList::getInstance()->writeUnverifiedDatagram(oauthRequestByteArray, senderSockAddr); - + return; } } - + if ((!isAssignment && !STATICALLY_ASSIGNED_NODES.contains(nodeType)) || (isAssignment && matchingQueuedAssignment)) { // this was either not a static assignment or it was and we had a matching one in the queue - + // create a new session UUID for this node QUuid nodeUUID = QUuid::createUuid(); - + SharedNodePointer newNode = LimitedNodeList::getInstance()->addOrUpdateNode(nodeUUID, nodeType, publicSockAddr, localSockAddr); // when the newNode is created the linked data is also created // if this was a static assignment set the UUID, set the sendingSockAddr DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); - + if (isAssignment) { nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setWalletUUID(pendingAssigneeData->getWalletUUID()); - + // now that we've pulled the wallet UUID and added the node to our list, delete the pending assignee data delete pendingAssigneeData; } - + nodeData->setSendingSockAddr(senderSockAddr); - + // reply back to the user with a PacketTypeDomainList sendDomainListToNode(newNode, senderSockAddr, nodeInterestListFromPacket(packet, numPreInterestBytes)); } @@ -492,26 +493,26 @@ QUrl DomainServer::oauthAuthorizationURL(const QUuid& stateUUID) { // for now these are all interface clients that have a GUI // so just send them back the full authorization URL QUrl authorizationURL = _oauthProviderURL; - + const QString OAUTH_AUTHORIZATION_PATH = "/oauth/authorize"; authorizationURL.setPath(OAUTH_AUTHORIZATION_PATH); - + QUrlQuery authorizationQuery; - + authorizationQuery.addQueryItem(OAUTH_CLIENT_ID_QUERY_KEY, _oauthClientID); - + const QString OAUTH_RESPONSE_TYPE_QUERY_KEY = "response_type"; const QString OAUTH_REPSONSE_TYPE_QUERY_VALUE = "code"; authorizationQuery.addQueryItem(OAUTH_RESPONSE_TYPE_QUERY_KEY, OAUTH_REPSONSE_TYPE_QUERY_VALUE); - + const QString OAUTH_STATE_QUERY_KEY = "state"; // create a new UUID that will be the state parameter for oauth authorization AND the new session UUID for that node authorizationQuery.addQueryItem(OAUTH_STATE_QUERY_KEY, uuidStringWithoutCurlyBraces(stateUUID)); - + authorizationQuery.addQueryItem(OAUTH_REDIRECT_URI_QUERY_KEY, oauthRedirectURL().toString()); - + authorizationURL.setQuery(authorizationQuery); - + return authorizationURL; } @@ -520,14 +521,14 @@ int DomainServer::parseNodeDataFromByteArray(NodeType_t& nodeType, HifiSockAddr& const HifiSockAddr& senderSockAddr) { QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); - + packetStream >> nodeType; packetStream >> publicSockAddr >> localSockAddr; - + if (publicSockAddr.getAddress().isNull()) { // this node wants to use us its STUN server // so set the node public address to whatever we perceive the public address to be - + // if the sender is on our box then leave its public address to 0 so that // other users attempt to reach it on the same address they have for the domain-server if (senderSockAddr.getAddress().isLoopback()) { @@ -536,95 +537,95 @@ int DomainServer::parseNodeDataFromByteArray(NodeType_t& nodeType, HifiSockAddr& publicSockAddr.setAddress(senderSockAddr.getAddress()); } } - + return packetStream.device()->pos(); } NodeSet DomainServer::nodeInterestListFromPacket(const QByteArray& packet, int numPreceedingBytes) { QDataStream packetStream(packet); packetStream.skipRawData(numPreceedingBytes); - + quint8 numInterestTypes = 0; packetStream >> numInterestTypes; - + quint8 nodeType; NodeSet nodeInterestSet; - + for (int i = 0; i < numInterestTypes; i++) { packetStream >> nodeType; nodeInterestSet.insert((NodeType_t) nodeType); } - + return nodeInterestSet; } void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr, const NodeSet& nodeInterestList) { - + QByteArray broadcastPacket = byteArrayWithPopulatedHeader(PacketTypeDomainList); - + // always send the node their own UUID back QDataStream broadcastDataStream(&broadcastPacket, QIODevice::Append); broadcastDataStream << node->getUUID(); - + int numBroadcastPacketLeadBytes = broadcastDataStream.device()->pos(); - + DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); - + LimitedNodeList* nodeList = LimitedNodeList::getInstance(); - + if (nodeInterestList.size() > 0) { - + // DTLSServerSession* dtlsSession = _isUsingDTLS ? _dtlsSessions[senderSockAddr] : NULL; int dataMTU = MAX_PACKET_SIZE; - + if (nodeData->isAuthenticated()) { // if this authenticated node has any interest types, send back those nodes as well foreach (const SharedNodePointer& otherNode, nodeList->getNodeHash()) { - + // reset our nodeByteArray and nodeDataStream QByteArray nodeByteArray; QDataStream nodeDataStream(&nodeByteArray, QIODevice::Append); - + if (otherNode->getUUID() != node->getUUID() && nodeInterestList.contains(otherNode->getType())) { - + // don't send avatar nodes to other avatars, that will come from avatar mixer nodeDataStream << *otherNode.data(); - + // pack the secret that these two nodes will use to communicate with each other QUuid secretUUID = nodeData->getSessionSecretHash().value(otherNode->getUUID()); if (secretUUID.isNull()) { // generate a new secret UUID these two nodes can use secretUUID = QUuid::createUuid(); - + // set that on the current Node's sessionSecretHash nodeData->getSessionSecretHash().insert(otherNode->getUUID(), secretUUID); - + // set it on the other Node's sessionSecretHash reinterpret_cast(otherNode->getLinkedData()) ->getSessionSecretHash().insert(node->getUUID(), secretUUID); - + } - + nodeDataStream << secretUUID; - + if (broadcastPacket.size() + nodeByteArray.size() > dataMTU) { // we need to break here and start a new packet // so send the current one - + nodeList->writeDatagram(broadcastPacket, node, senderSockAddr); - + // reset the broadcastPacket structure broadcastPacket.resize(numBroadcastPacketLeadBytes); broadcastDataStream.device()->seek(numBroadcastPacketLeadBytes); } - + // append the nodeByteArray to the current state of broadcastDataStream broadcastPacket.append(nodeByteArray); } } } - + // always write the last broadcastPacket nodeList->writeDatagram(broadcastPacket, node, senderSockAddr); } @@ -635,7 +636,7 @@ void DomainServer::readAvailableDatagrams() { HifiSockAddr senderSockAddr; QByteArray receivedPacket; - + static QByteArray assignmentPacket = byteArrayWithPopulatedHeader(PacketTypeCreateAssignment); static int numAssignmentPacketHeaderBytes = assignmentPacket.size(); @@ -648,44 +649,44 @@ void DomainServer::readAvailableDatagrams() { // construct the requested assignment from the packet data Assignment requestAssignment(receivedPacket); - + // Suppress these for Assignment::AgentType to once per 5 seconds static QElapsedTimer noisyMessageTimer; static bool wasNoisyTimerStarted = false; - + if (!wasNoisyTimerStarted) { noisyMessageTimer.start(); wasNoisyTimerStarted = true; } - + const quint64 NOISY_MESSAGE_INTERVAL_MSECS = 5 * 1000; - + if (requestAssignment.getType() != Assignment::AgentType || noisyMessageTimer.elapsed() > NOISY_MESSAGE_INTERVAL_MSECS) { qDebug() << "Received a request for assignment type" << requestAssignment.getType() << "from" << senderSockAddr; noisyMessageTimer.restart(); } - + SharedAssignmentPointer assignmentToDeploy = deployableAssignmentForRequest(requestAssignment); - + if (assignmentToDeploy) { qDebug() << "Deploying assignment -" << *assignmentToDeploy.data() << "- to" << senderSockAddr; - + // give this assignment out, either the type matches or the requestor said they will take any assignmentPacket.resize(numAssignmentPacketHeaderBytes); - + // setup a copy of this assignment that will have a unique UUID, for packaging purposes Assignment uniqueAssignment(*assignmentToDeploy.data()); uniqueAssignment.setUUID(QUuid::createUuid()); - + QDataStream assignmentStream(&assignmentPacket, QIODevice::Append); - + assignmentStream << uniqueAssignment; - + nodeList->getNodeSocket().writeDatagram(assignmentPacket, senderSockAddr.getAddress(), senderSockAddr.getPort()); - + // add the information for that deployed assignment to the hash of pending assigned nodes PendingAssignedNodeData* pendingNodeData = new PendingAssignedNodeData(assignmentToDeploy->getUUID(), requestAssignment.getWalletUUID()); @@ -705,7 +706,7 @@ void DomainServer::readAvailableDatagrams() { // we're using DTLS, so tell the sender to get back to us using DTLS static QByteArray dtlsRequiredPacket = byteArrayWithPopulatedHeader(PacketTypeDomainServerRequireDTLS); static int numBytesDTLSHeader = numBytesForPacketHeaderGivenPacketType(PacketTypeDomainServerRequireDTLS); - + if (dtlsRequiredPacket.size() == numBytesDTLSHeader) { // pack the port that we accept DTLS traffic on unsigned short dtlsPort = nodeList->getDTLSSocket().localPort(); @@ -721,12 +722,12 @@ void DomainServer::setupPendingAssignmentCredits() { // enumerate the NodeList to find the assigned nodes foreach (const SharedNodePointer& node, LimitedNodeList::getInstance()->getNodeHash()) { DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); - + if (!nodeData->getAssignmentUUID().isNull() && !nodeData->getWalletUUID().isNull()) { // check if we have a non-finalized transaction for this node to add this amount to TransactionHash::iterator i = _pendingAssignmentCredits.find(nodeData->getWalletUUID()); WalletTransaction* existingTransaction = NULL; - + while (i != _pendingAssignmentCredits.end() && i.key() == nodeData->getWalletUUID()) { if (!i.value()->isFinalized()) { existingTransaction = i.value(); @@ -735,16 +736,16 @@ void DomainServer::setupPendingAssignmentCredits() { ++i; } } - + qint64 elapsedMsecsSinceLastPayment = nodeData->getPaymentIntervalTimer().elapsed(); nodeData->getPaymentIntervalTimer().restart(); - + const float CREDITS_PER_HOUR = 0.10f; const float CREDITS_PER_MSEC = CREDITS_PER_HOUR / (60 * 60 * 1000); const int SATOSHIS_PER_MSEC = CREDITS_PER_MSEC * SATOSHIS_PER_CREDIT; - + float pendingCredits = elapsedMsecsSinceLastPayment * SATOSHIS_PER_MSEC; - + if (existingTransaction) { existingTransaction->incrementAmount(pendingCredits); } else { @@ -757,30 +758,30 @@ void DomainServer::setupPendingAssignmentCredits() { } void DomainServer::sendPendingTransactionsToServer() { - + AccountManager& accountManager = AccountManager::getInstance(); - + if (accountManager.hasValidAccessToken()) { - + // enumerate the pending transactions and send them to the server to complete payment TransactionHash::iterator i = _pendingAssignmentCredits.begin(); - + JSONCallbackParameters transactionCallbackParams; - + transactionCallbackParams.jsonCallbackReceiver = this; transactionCallbackParams.jsonCallbackMethod = "transactionJSONCallback"; - + while (i != _pendingAssignmentCredits.end()) { accountManager.authenticatedRequest("api/v1/transactions", QNetworkAccessManager::PostOperation, transactionCallbackParams, i.value()->postJson().toJson()); - + // set this transaction to finalized so we don't add additional credits to it i.value()->setIsFinalized(true); - + ++i; } } - + } void DomainServer::transactionJSONCallback(const QJsonObject& data) { @@ -789,18 +790,18 @@ void DomainServer::transactionJSONCallback(const QJsonObject& data) { // create a dummy wallet transaction to unpack the JSON to WalletTransaction dummyTransaction; dummyTransaction.loadFromJson(data); - + TransactionHash::iterator i = _pendingAssignmentCredits.find(dummyTransaction.getDestinationUUID()); - + while (i != _pendingAssignmentCredits.end() && i.key() == dummyTransaction.getDestinationUUID()) { if (i.value()->getUUID() == dummyTransaction.getUUID()) { // we have a match - we can remove this from the hash of pending credits // and delete it for clean up - + WalletTransaction* matchingTransaction = i.value(); _pendingAssignmentCredits.erase(i); delete matchingTransaction; - + break; } else { ++i; @@ -811,28 +812,28 @@ void DomainServer::transactionJSONCallback(const QJsonObject& data) { void DomainServer::processDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr) { LimitedNodeList* nodeList = LimitedNodeList::getInstance(); - + if (nodeList->packetVersionAndHashMatch(receivedPacket)) { PacketType requestType = packetTypeForPacket(receivedPacket); - + if (requestType == PacketTypeDomainConnectRequest) { handleConnectRequest(receivedPacket, senderSockAddr); } else if (requestType == PacketTypeDomainListRequest) { QUuid nodeUUID = uuidFromPacketHeader(receivedPacket); - + if (!nodeUUID.isNull() && nodeList->nodeWithUUID(nodeUUID)) { NodeType_t throwawayNodeType; HifiSockAddr nodePublicAddress, nodeLocalAddress; - + int numNodeInfoBytes = parseNodeDataFromByteArray(throwawayNodeType, nodePublicAddress, nodeLocalAddress, receivedPacket, senderSockAddr); - + SharedNodePointer checkInNode = nodeList->updateSocketsForNode(nodeUUID, nodePublicAddress, nodeLocalAddress); - + // update last receive to now quint64 timeNow = usecTimestampNow(); checkInNode->setLastHeardMicrostamp(timeNow); - + sendDomainListToNode(checkInNode, senderSockAddr, nodeInterestListFromPacket(receivedPacket, numNodeInfoBytes)); } } else if (requestType == PacketTypeNodeJsonStats) { @@ -871,36 +872,36 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) { // add the node UUID nodeJson[JSON_KEY_UUID] = uuidStringWithoutCurlyBraces(node->getUUID()); - + // add the node type nodeJson[JSON_KEY_TYPE] = nodeTypeName; // add the node socket information nodeJson[JSON_KEY_PUBLIC_SOCKET] = jsonForSocket(node->getPublicSocket()); nodeJson[JSON_KEY_LOCAL_SOCKET] = jsonForSocket(node->getLocalSocket()); - + // add the node uptime in our list nodeJson[JSON_KEY_WAKE_TIMESTAMP] = QString::number(node->getWakeTimestamp()); - + // if the node has pool information, add it DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); SharedAssignmentPointer matchingAssignment = _allAssignments.value(nodeData->getAssignmentUUID()); if (matchingAssignment) { nodeJson[JSON_KEY_POOL] = matchingAssignment->getPool(); - + if (!nodeData->getWalletUUID().isNull()) { TransactionHash::iterator i = _pendingAssignmentCredits.find(nodeData->getWalletUUID()); float pendingCreditAmount = 0; - + while (i != _pendingAssignmentCredits.end() && i.key() == nodeData->getWalletUUID()) { pendingCreditAmount += i.value()->getAmount() / SATOSHIS_PER_CREDIT; ++i; } - + nodeJson[JSON_KEY_PENDING_CREDITS] = pendingCreditAmount; } } - + return nodeJson; } @@ -916,157 +917,157 @@ QString pathForAssignmentScript(const QUuid& assignmentUUID) { bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url) { const QString JSON_MIME_TYPE = "application/json"; - + const QString URI_ASSIGNMENT = "/assignment"; const QString URI_NODES = "/nodes"; - + const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; - + if (connection->requestOperation() == QNetworkAccessManager::GetOperation) { if (url.path() == "/assignments.json") { // user is asking for json list of assignments - + // setup the JSON QJsonObject assignmentJSON; QJsonObject assignedNodesJSON; - + // enumerate the NodeList to find the assigned nodes foreach (const SharedNodePointer& node, LimitedNodeList::getInstance()->getNodeHash()) { DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); - + if (!nodeData->getAssignmentUUID().isNull()) { // add the node using the UUID as the key QString uuidString = uuidStringWithoutCurlyBraces(nodeData->getAssignmentUUID()); assignedNodesJSON[uuidString] = jsonObjectForNode(node); } } - + assignmentJSON["fulfilled"] = assignedNodesJSON; - + QJsonObject queuedAssignmentsJSON; - + // add the queued but unfilled assignments to the json foreach(const SharedAssignmentPointer& assignment, _unfulfilledAssignments) { QJsonObject queuedAssignmentJSON; - + QString uuidString = uuidStringWithoutCurlyBraces(assignment->getUUID()); queuedAssignmentJSON[JSON_KEY_TYPE] = QString(assignment->getTypeName()); - + // if the assignment has a pool, add it if (!assignment->getPool().isEmpty()) { queuedAssignmentJSON[JSON_KEY_POOL] = assignment->getPool(); } - + // add this queued assignment to the JSON queuedAssignmentsJSON[uuidString] = queuedAssignmentJSON; } - + assignmentJSON["queued"] = queuedAssignmentsJSON; - + // print out the created JSON QJsonDocument assignmentDocument(assignmentJSON); connection->respond(HTTPConnection::StatusCode200, assignmentDocument.toJson(), qPrintable(JSON_MIME_TYPE)); - + // we've processed this request return true; } else if (url.path() == "/transactions.json") { // enumerate our pending transactions and display them in an array QJsonObject rootObject; QJsonArray transactionArray; - + TransactionHash::iterator i = _pendingAssignmentCredits.begin(); while (i != _pendingAssignmentCredits.end()) { transactionArray.push_back(i.value()->toJson()); ++i; } - + rootObject["pending_transactions"] = transactionArray; - + // print out the created JSON QJsonDocument transactionsDocument(rootObject); connection->respond(HTTPConnection::StatusCode200, transactionsDocument.toJson(), qPrintable(JSON_MIME_TYPE)); - + return true; } else if (url.path() == QString("%1.json").arg(URI_NODES)) { // setup the JSON QJsonObject rootJSON; QJsonArray nodesJSONArray; - + // enumerate the NodeList to find the assigned nodes LimitedNodeList* nodeList = LimitedNodeList::getInstance(); - + foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { // add the node using the UUID as the key nodesJSONArray.append(jsonObjectForNode(node)); } - + rootJSON["nodes"] = nodesJSONArray; - + // print out the created JSON QJsonDocument nodesDocument(rootJSON); - + // send the response connection->respond(HTTPConnection::StatusCode200, nodesDocument.toJson(), qPrintable(JSON_MIME_TYPE)); - + return true; } else { // check if this is for json stats for a node const QString NODE_JSON_REGEX_STRING = QString("\\%1\\/(%2).json\\/?$").arg(URI_NODES).arg(UUID_REGEX_STRING); QRegExp nodeShowRegex(NODE_JSON_REGEX_STRING); - + if (nodeShowRegex.indexIn(url.path()) != -1) { QUuid matchingUUID = QUuid(nodeShowRegex.cap(1)); - + // see if we have a node that matches this ID SharedNodePointer matchingNode = LimitedNodeList::getInstance()->nodeWithUUID(matchingUUID); if (matchingNode) { // create a QJsonDocument with the stats QJsonObject QJsonObject statsObject = reinterpret_cast(matchingNode->getLinkedData())->getStatsJSONObject(); - + // add the node type to the JSON data for output purposes statsObject["node_type"] = NodeType::getNodeTypeName(matchingNode->getType()).toLower().replace(' ', '-'); - + QJsonDocument statsDocument(statsObject); - + // send the response connection->respond(HTTPConnection::StatusCode200, statsDocument.toJson(), qPrintable(JSON_MIME_TYPE)); - + // tell the caller we processed the request return true; } - + return false; } - + // check if this is a request for a scripted assignment (with a temp unique UUID) const QString ASSIGNMENT_REGEX_STRING = QString("\\%1\\/(%2)\\/?$").arg(URI_ASSIGNMENT).arg(UUID_REGEX_STRING); QRegExp assignmentRegex(ASSIGNMENT_REGEX_STRING); - + if (assignmentRegex.indexIn(url.path()) != -1) { QUuid matchingUUID = QUuid(assignmentRegex.cap(1)); - + SharedAssignmentPointer matchingAssignment = _allAssignments.value(matchingUUID); if (!matchingAssignment) { // check if we have a pending assignment that matches this temp UUID, and it is a scripted assignment PendingAssignedNodeData* pendingData = _pendingAssignedNodes.value(matchingUUID); if (pendingData) { matchingAssignment = _allAssignments.value(pendingData->getAssignmentUUID()); - + if (matchingAssignment && matchingAssignment->getType() == Assignment::AgentType) { // we have a matching assignment and it is for the right type, have the HTTP manager handle it // via correct URL for the script so the client can download - + QUrl scriptURL = url; scriptURL.setPath(URI_ASSIGNMENT + "/" + uuidStringWithoutCurlyBraces(pendingData->getAssignmentUUID())); - + // have the HTTPManager serve the appropriate script file return _httpManager.handleHTTPRequest(connection, scriptURL); } } } - + // request not handled return false; } @@ -1075,92 +1076,92 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url if (url.path() == URI_ASSIGNMENT) { // this is a script upload - ask the HTTPConnection to parse the form data QList formData = connection->parseFormData(); - + // check optional headers for # of instances and pool const QString ASSIGNMENT_INSTANCES_HEADER = "ASSIGNMENT-INSTANCES"; const QString ASSIGNMENT_POOL_HEADER = "ASSIGNMENT-POOL"; - + QByteArray assignmentInstancesValue = connection->requestHeaders().value(ASSIGNMENT_INSTANCES_HEADER.toLocal8Bit()); - + int numInstances = 1; - + if (!assignmentInstancesValue.isEmpty()) { // the user has requested a specific number of instances // so set that on the created assignment - + numInstances = assignmentInstancesValue.toInt(); } - + QString assignmentPool = emptyPool; QByteArray assignmentPoolValue = connection->requestHeaders().value(ASSIGNMENT_POOL_HEADER.toLocal8Bit()); - + if (!assignmentPoolValue.isEmpty()) { // specific pool requested, set that on the created assignment assignmentPool = QString(assignmentPoolValue); } - - + + for (int i = 0; i < numInstances; i++) { - + // create an assignment for this saved script Assignment* scriptAssignment = new Assignment(Assignment::CreateCommand, Assignment::AgentType, assignmentPool); - + QString newPath = pathForAssignmentScript(scriptAssignment->getUUID()); - + // create a file with the GUID of the assignment in the script host location QFile scriptFile(newPath); scriptFile.open(QIODevice::WriteOnly); scriptFile.write(formData[0].second); - + qDebug() << qPrintable(QString("Saved a script for assignment at %1%2") .arg(newPath).arg(assignmentPool == emptyPool ? "" : " - pool is " + assignmentPool)); - + // add the script assigment to the assignment queue SharedAssignmentPointer sharedScriptedAssignment(scriptAssignment); _unfulfilledAssignments.enqueue(sharedScriptedAssignment); _allAssignments.insert(sharedScriptedAssignment->getUUID(), sharedScriptedAssignment); } - + // respond with a 200 code for successful upload connection->respond(HTTPConnection::StatusCode200); - + return true; } } else if (connection->requestOperation() == QNetworkAccessManager::DeleteOperation) { const QString ALL_NODE_DELETE_REGEX_STRING = QString("\\%1\\/?$").arg(URI_NODES); const QString NODE_DELETE_REGEX_STRING = QString("\\%1\\/(%2)\\/$").arg(URI_NODES).arg(UUID_REGEX_STRING); - + QRegExp allNodesDeleteRegex(ALL_NODE_DELETE_REGEX_STRING); QRegExp nodeDeleteRegex(NODE_DELETE_REGEX_STRING); - + if (nodeDeleteRegex.indexIn(url.path()) != -1) { // this is a request to DELETE one node by UUID - + // pull the captured string, if it exists QUuid deleteUUID = QUuid(nodeDeleteRegex.cap(1)); - + SharedNodePointer nodeToKill = LimitedNodeList::getInstance()->nodeWithUUID(deleteUUID); - + if (nodeToKill) { // start with a 200 response connection->respond(HTTPConnection::StatusCode200); - + // we have a valid UUID and node - kill the node that has this assignment QMetaObject::invokeMethod(LimitedNodeList::getInstance(), "killNodeWithUUID", Q_ARG(const QUuid&, deleteUUID)); - + // successfully processed request return true; } - + return true; } else if (allNodesDeleteRegex.indexIn(url.path()) != -1) { qDebug() << "Received request to kill all nodes."; LimitedNodeList::getInstance()->eraseAllNodes(); - + return true; } } - + // didn't process the request, let the HTTPManager try and handle return false; } @@ -1168,44 +1169,44 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &url) { const QString URI_OAUTH = "/oauth"; if (url.path() == URI_OAUTH) { - + QUrlQuery codeURLQuery(url); - + const QString CODE_QUERY_KEY = "code"; QString authorizationCode = codeURLQuery.queryItemValue(CODE_QUERY_KEY); - + const QString STATE_QUERY_KEY = "state"; QUuid stateUUID = QUuid(codeURLQuery.queryItemValue(STATE_QUERY_KEY)); - - + + if (!authorizationCode.isEmpty() && !stateUUID.isNull()) { // fire off a request with this code and state to get an access token for the user - + const QString OAUTH_TOKEN_REQUEST_PATH = "/oauth/token"; QUrl tokenRequestUrl = _oauthProviderURL; tokenRequestUrl.setPath(OAUTH_TOKEN_REQUEST_PATH); - + const QString OAUTH_GRANT_TYPE_POST_STRING = "grant_type=authorization_code"; QString tokenPostBody = OAUTH_GRANT_TYPE_POST_STRING; tokenPostBody += QString("&code=%1&redirect_uri=%2&client_id=%3&client_secret=%4") .arg(authorizationCode, oauthRedirectURL().toString(), _oauthClientID, _oauthClientSecret); - + QNetworkRequest tokenRequest(tokenRequestUrl); tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - + QNetworkReply* tokenReply = _networkAccessManager->post(tokenRequest, tokenPostBody.toLocal8Bit()); - + qDebug() << "Requesting a token for user with session UUID" << uuidStringWithoutCurlyBraces(stateUUID); - + // insert this to our pending token replies so we can associate the returned access token with the right UUID _networkReplyUUIDMap.insert(tokenReply, stateUUID); - + connect(tokenReply, &QNetworkReply::finished, this, &DomainServer::handleTokenRequestFinished); } - + // respond with a 200 code indicating that login is complete connection->respond(HTTPConnection::StatusCode200); - + return true; } else { return false; @@ -1217,25 +1218,25 @@ const QString OAUTH_JSON_ACCESS_TOKEN_KEY = "access_token"; void DomainServer::handleTokenRequestFinished() { QNetworkReply* networkReply = reinterpret_cast(sender()); QUuid matchingSessionUUID = _networkReplyUUIDMap.take(networkReply); - + if (!matchingSessionUUID.isNull() && networkReply->error() == QNetworkReply::NoError) { // pull the access token from the returned JSON and store it with the matching session UUID QJsonDocument returnedJSON = QJsonDocument::fromJson(networkReply->readAll()); QString accessToken = returnedJSON.object()[OAUTH_JSON_ACCESS_TOKEN_KEY].toString(); - + qDebug() << "Received access token for user with UUID" << uuidStringWithoutCurlyBraces(matchingSessionUUID); - + // fire off a request to get this user's identity so we can see if we will let them in QUrl profileURL = _oauthProviderURL; profileURL.setPath("/api/v1/users/profile"); profileURL.setQuery(QString("%1=%2").arg(OAUTH_JSON_ACCESS_TOKEN_KEY, accessToken)); - + QNetworkReply* profileReply = _networkAccessManager->get(QNetworkRequest(profileURL)); - + qDebug() << "Requesting access token for user with session UUID" << uuidStringWithoutCurlyBraces(matchingSessionUUID); - + connect(profileReply, &QNetworkReply::finished, this, &DomainServer::handleProfileRequestFinished); - + _networkReplyUUIDMap.insert(profileReply, matchingSessionUUID); } } @@ -1243,18 +1244,18 @@ void DomainServer::handleTokenRequestFinished() { void DomainServer::handleProfileRequestFinished() { QNetworkReply* networkReply = reinterpret_cast(sender()); QUuid matchingSessionUUID = _networkReplyUUIDMap.take(networkReply); - + if (!matchingSessionUUID.isNull() && networkReply->error() == QNetworkReply::NoError) { QJsonDocument profileJSON = QJsonDocument::fromJson(networkReply->readAll()); - + if (profileJSON.object()["status"].toString() == "success") { // pull the user roles from the response QJsonArray userRolesArray = profileJSON.object()["data"].toObject()["user"].toObject()["roles"].toArray(); - + QJsonArray allowedRolesArray = _argumentVariantMap.value(ALLOWED_ROLES_CONFIG_KEY).toJsonValue().toArray(); - + bool shouldAllowUserToConnect = false; - + foreach(const QJsonValue& roleValue, userRolesArray) { if (allowedRolesArray.contains(roleValue)) { // the user has a role that lets them in @@ -1263,10 +1264,10 @@ void DomainServer::handleProfileRequestFinished() { break; } } - + qDebug() << "Confirmed authentication state for user" << uuidStringWithoutCurlyBraces(matchingSessionUUID) << "-" << shouldAllowUserToConnect; - + // insert this UUID and a flag that indicates if they are allowed to connect _sessionAuthenticationHash.insert(matchingSessionUUID, shouldAllowUserToConnect); } @@ -1276,15 +1277,15 @@ void DomainServer::handleProfileRequestFinished() { void DomainServer::refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& assignment) { QUuid oldUUID = assignment->getUUID(); assignment->resetUUID(); - + qDebug() << "Reset UUID for assignment -" << *assignment.data() << "- and added to queue. Old UUID was" << uuidStringWithoutCurlyBraces(oldUUID); - + if (assignment->getType() == Assignment::AgentType && assignment->getPayload().isEmpty()) { // if this was an Agent without a script URL, we need to rename the old file so it can be retrieved at the new UUID QFile::rename(pathForAssignmentScript(oldUUID), pathForAssignmentScript(assignment->getUUID())); } - + // add the static assignment back under the right UUID, and to the queue _allAssignments.insert(assignment->getUUID(), assignment); _unfulfilledAssignments.enqueue(assignment); @@ -1296,19 +1297,19 @@ void DomainServer::nodeAdded(SharedNodePointer node) { } void DomainServer::nodeKilled(SharedNodePointer node) { - + DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); - + if (nodeData) { // if this node's UUID matches a static assignment we need to throw it back in the assignment queue if (!nodeData->getAssignmentUUID().isNull()) { SharedAssignmentPointer matchedAssignment = _allAssignments.take(nodeData->getAssignmentUUID()); - + if (matchedAssignment && matchedAssignment->isStatic()) { refreshStaticAssignmentAndAddToQueue(matchedAssignment); } } - + // cleanup the connection secrets that we set up for this node (on the other nodes) foreach (const QUuid& otherNodeSessionUUID, nodeData->getSessionSecretHash().keys()) { SharedNodePointer otherNode = LimitedNodeList::getInstance()->nodeWithUUID(otherNodeSessionUUID); @@ -1321,19 +1322,19 @@ void DomainServer::nodeKilled(SharedNodePointer node) { SharedAssignmentPointer DomainServer::matchingQueuedAssignmentForCheckIn(const QUuid& assignmentUUID, NodeType_t nodeType) { QQueue::iterator i = _unfulfilledAssignments.begin(); - + while (i != _unfulfilledAssignments.end()) { if (i->data()->getType() == Assignment::typeForNodeType(nodeType) && i->data()->getUUID() == assignmentUUID) { // we have an unfulfilled assignment to return - + // return the matching assignment return _unfulfilledAssignments.takeAt(i - _unfulfilledAssignments.begin()); } else { ++i; } } - + return SharedAssignmentPointer(); } @@ -1348,17 +1349,17 @@ SharedAssignmentPointer DomainServer::deployableAssignmentForRequest(const Assig bool assignmentTypesMatch = assignment->getType() == requestAssignment.getType(); bool nietherHasPool = assignment->getPool().isEmpty() && requestAssignment.getPool().isEmpty(); bool assignmentPoolsMatch = assignment->getPool() == requestAssignment.getPool(); - + if ((requestIsAllTypes || assignmentTypesMatch) && (nietherHasPool || assignmentPoolsMatch)) { - + // remove the assignment from the queue SharedAssignmentPointer deployableAssignment = _unfulfilledAssignments.takeAt(sharedAssignment - _unfulfilledAssignments.begin()); - + // until we get a connection for this assignment // put assignment back in queue but stick it at the back so the others have a chance to go out _unfulfilledAssignments.enqueue(deployableAssignment); - + // stop looping, we've handed out an assignment return deployableAssignment; } else { @@ -1366,7 +1367,7 @@ SharedAssignmentPointer DomainServer::deployableAssignmentForRequest(const Assig ++sharedAssignment; } } - + return SharedAssignmentPointer(); } @@ -1375,7 +1376,7 @@ void DomainServer::removeMatchingAssignmentFromQueue(const SharedAssignmentPoint while (potentialMatchingAssignment != _unfulfilledAssignments.end()) { if (potentialMatchingAssignment->data()->getUUID() == removableAssignment->getUUID()) { _unfulfilledAssignments.erase(potentialMatchingAssignment); - + // we matched and removed an assignment, bail out break; } else { @@ -1393,19 +1394,19 @@ void DomainServer::addStaticAssignmentsToQueue() { while (staticAssignment != staticHashCopy.end()) { // add any of the un-matched static assignments to the queue bool foundMatchingAssignment = false; - + // enumerate the nodes and check if there is one with an attached assignment with matching UUID foreach (const SharedNodePointer& node, LimitedNodeList::getInstance()->getNodeHash()) { if (node->getUUID() == staticAssignment->data()->getUUID()) { foundMatchingAssignment = true; } } - + if (!foundMatchingAssignment) { // this assignment has not been fulfilled - reset the UUID and add it to the assignment queue refreshStaticAssignmentAndAddToQueue(*staticAssignment); } - + ++staticAssignment; } } diff --git a/examples/editModels.js b/examples/editModels.js index 93a34b9a3a..53633d3c0c 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -691,6 +691,10 @@ function rayPlaneIntersection(pickRay, point, normal) { } function mousePressEvent(event) { + if (altIsPressed) { + return; + } + mouseLastPosition = { x: event.x, y: event.y }; modelSelected = false; var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); @@ -790,6 +794,10 @@ var oldModifier = 0; var modifier = 0; var wasShifted = false; function mouseMoveEvent(event) { + if (altIsPressed) { + return; + } + var pickRay = Camera.computePickRay(event.x, event.y); if (!modelSelected) { @@ -894,6 +902,10 @@ function mouseMoveEvent(event) { } function mouseReleaseEvent(event) { + if (altIsPressed) { + return; + } + modelSelected = false; glowedModelID.id = -1; @@ -962,4 +974,16 @@ Menu.menuItemEvent.connect(function(menuItem){ } }); +// handling of inspect.js concurrence +altIsPressed = false; +Controller.keyPressEvent.connect(function(event) { + if (event.text == "ALT") { + altIsPressed = true; + } +}); +Controller.keyReleaseEvent.connect(function(event) { + if (event.text == "ALT") { + altIsPressed = false; + } +}); diff --git a/examples/growTrees.js b/examples/growTrees.js new file mode 100644 index 0000000000..142c72300a --- /dev/null +++ b/examples/growTrees.js @@ -0,0 +1,832 @@ +// +// growPlants.js +// examples +// +// Created by Benjamin Arnold on May 29, 2014 +// Copyright 2014 High Fidelity, Inc. +// +// This sample script allows the user to grow different types of plants on the voxels +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var zFightingSizeAdjust = 0.002; // used to adjust preview voxels to prevent z fighting +var previewLineWidth = 2.0; + +var voxelSize = 1; +var windowDimensions = Controller.getViewportDimensions(); +var toolIconUrl = "http://highfidelity-public.s3-us-west-1.amazonaws.com/images/tools/"; + +var MAX_VOXEL_SCALE_POWER = 5; +var MIN_VOXEL_SCALE_POWER = -8; +var MAX_VOXEL_SCALE = Math.pow(2.0, MAX_VOXEL_SCALE_POWER); +var MIN_VOXEL_SCALE = Math.pow(2.0, MIN_VOXEL_SCALE_POWER); + + +var linePreviewTop = Overlays.addOverlay("line3d", { + position: { x: 0, y: 0, z: 0}, + end: { x: 0, y: 0, z: 0}, + color: { red: 0, green: 255, blue: 0}, + alpha: 1, + visible: false, + lineWidth: previewLineWidth + }); + +var linePreviewBottom = Overlays.addOverlay("line3d", { + position: { x: 0, y: 0, z: 0}, + end: { x: 0, y: 0, z: 0}, + color: { red: 0, green: 255, blue: 0}, + alpha: 1, + visible: false, + lineWidth: previewLineWidth + }); + +var linePreviewLeft = Overlays.addOverlay("line3d", { + position: { x: 0, y: 0, z: 0}, + end: { x: 0, y: 0, z: 0}, + color: { red: 0, green: 255, blue: 0}, + alpha: 1, + visible: false, + lineWidth: previewLineWidth + }); + +var linePreviewRight = Overlays.addOverlay("line3d", { + position: { x: 0, y: 0, z: 0}, + end: { x: 0, y: 0, z: 0}, + color: { red: 0, green: 255, blue: 0}, + alpha: 1, + visible: false, + lineWidth: previewLineWidth + }); + + +var UIColor = { red: 0, green: 160, blue: 0}; +var activeUIColor = { red: 0, green: 255, blue: 0}; + +var toolHeight = 50; +var toolWidth = 50; + +var editToolsOn = true; + +var voxelToolSelected = false; + +var scaleSelectorWidth = 144; +var scaleSelectorHeight = 37; + +var scaleSelectorX = windowDimensions.x / 5.0; +var scaleSelectorY = windowDimensions.y - scaleSelectorHeight; + +var voxelTool = Overlays.addOverlay("image", { + x: scaleSelectorX + scaleSelectorWidth + 1, y: windowDimensions.y - toolHeight, width: toolWidth, height: toolHeight, + subImage: { x: 0, y: toolHeight, width: toolWidth, height: toolHeight }, + imageURL: toolIconUrl + "voxel-tool.svg", + visible: editToolsOn, + color: UIColor, + alpha: 0.9 + }); + +var copyScale = true; +function ScaleSelector() { + this.x = scaleSelectorX; + this.y = scaleSelectorY; + this.width = scaleSelectorWidth; + this.height = scaleSelectorHeight; + + this.displayPower = false; + this.scale = 1.0; + this.power = 0; + + this.FIRST_PART = this.width * 40.0 / 100.0; + this.SECOND_PART = this.width * 37.0 / 100.0; + + this.buttonsOverlay = Overlays.addOverlay("image", { + x: this.x, y: this.y, + width: this.width, height: this.height, + //subImage: { x: 0, y: toolHeight, width: toolWidth, height: toolHeight }, + imageURL: toolIconUrl + "voxel-size-selector.svg", + alpha: 0.9, + visible: editToolsOn, + color: activeUIColor + }); + this.textOverlay = Overlays.addOverlay("text", { + x: this.x + this.FIRST_PART, y: this.y, + width: this.SECOND_PART, height: this.height, + topMargin: 13, + text: this.scale.toString(), + alpha: 0.0, + visible: editToolsOn, + color: activeUIColor + }); + this.powerOverlay = Overlays.addOverlay("text", { + x: this.x + this.FIRST_PART, y: this.y, + width: this.SECOND_PART, height: this.height, + leftMargin: 28, + text: this.power.toString(), + alpha: 0.0, + visible: false, + color: activeUIColor + }); + this.setScale = function(scale) { + if (scale > MAX_VOXEL_SCALE) { + scale = MAX_VOXEL_SCALE; + } + if (scale < MIN_VOXEL_SCALE) { + scale = MIN_VOXEL_SCALE; + } + + this.scale = scale; + this.power = Math.floor(Math.log(scale) / Math.log(2)); + this.update(); + } + + this.show = function(doShow) { + Overlays.editOverlay(this.buttonsOverlay, {visible: doShow}); + Overlays.editOverlay(this.textOverlay, {visible: doShow}); + Overlays.editOverlay(this.powerOverlay, {visible: doShow && this.displayPower}); + } + + this.move = function() { + this.x = swatchesX + swatchesWidth; + this.y = swatchesY; + + Overlays.editOverlay(this.buttonsOverlay, { + x: this.x, y: this.y, + }); + Overlays.editOverlay(this.textOverlay, { + x: this.x + this.FIRST_PART, y: this.y, + }); + Overlays.editOverlay(this.powerOverlay, { + x: this.x + this.FIRST_PART, y: this.y, + }); + } + + + this.switchDisplay = function() { + this.displayPower = !this.displayPower; + + if (this.displayPower) { + Overlays.editOverlay(this.textOverlay, { + leftMargin: 18, + text: "2" + }); + Overlays.editOverlay(this.powerOverlay, { + text: this.power.toString(), + visible: editToolsOn + }); + } else { + Overlays.editOverlay(this.textOverlay, { + leftMargin: 13, + text: this.scale.toString() + }); + Overlays.editOverlay(this.powerOverlay, { + visible: false + }); + } + } + + this.update = function() { + if (this.displayPower) { + Overlays.editOverlay(this.powerOverlay, {text: this.power.toString()}); + } else { + Overlays.editOverlay(this.textOverlay, {text: this.scale.toString()}); + } + } + + this.incrementScale = function() { + copyScale = false; + if (this.power < MAX_VOXEL_SCALE_POWER) { + ++this.power; + this.scale *= 2.0; + this.update(); + } + } + + this.decrementScale = function() { + copyScale = false; + if (MIN_VOXEL_SCALE_POWER < this.power) { + --this.power; + this.scale /= 2.0; + this.update(); + } + } + + this.clicked = function(x, y) { + if (this.x < x && x < this.x + this.width && + this.y < y && y < this.y + this.height) { + + if (x < this.x + this.FIRST_PART) { + this.decrementScale(); + } else if (x < this.x + this.FIRST_PART + this.SECOND_PART) { + this.switchDisplay(); + } else { + this.incrementScale(); + } + return true; + } + return false; + } + + this.cleanup = function() { + Overlays.deleteOverlay(this.buttonsOverlay); + Overlays.deleteOverlay(this.textOverlay); + Overlays.deleteOverlay(this.powerOverlay); + } + +} +var scaleSelector = new ScaleSelector(); + + +function calculateVoxelFromIntersection(intersection, operation) { + + var resultVoxel; + + var x; + var y; + var z; + + // if our "target voxel size" is larger than the voxel we intersected with, then we need to find the closest + // ancestor voxel of our target size that contains our intersected voxel. + if (voxelSize > intersection.voxel.s) { + x = Math.floor(intersection.voxel.x / voxelSize) * voxelSize; + y = Math.floor(intersection.voxel.y / voxelSize) * voxelSize; + z = Math.floor(intersection.voxel.z / voxelSize) * voxelSize; + } else { + // otherwise, calculate the enclosed voxel of size voxelSize that the intersection point falls inside of. + // if you have a voxelSize that's smaller than the voxel you're intersecting, this calculation will result + // in the subvoxel that the intersection point falls in, if the target voxelSize matches the intersecting + // voxel this still works and results in returning the intersecting voxel which is what we want + var adjustToCenter = Vec3.multiply(Voxels.getFaceVector(intersection.face), (voxelSize * -0.5)); + + var centerOfIntersectingVoxel = Vec3.sum(intersection.intersection, adjustToCenter); + x = Math.floor(centerOfIntersectingVoxel.x / voxelSize) * voxelSize; + y = Math.floor(centerOfIntersectingVoxel.y / voxelSize) * voxelSize; + z = Math.floor(centerOfIntersectingVoxel.z / voxelSize) * voxelSize; + } + resultVoxel = { x: x, y: y, z: z, s: voxelSize }; + var highlightAt = { x: x, y: y, z: z, s: voxelSize }; + + + + // we only do the "add to the face we're pointing at" adjustment, if the operation is an add + // operation, and the target voxel size is equal to or smaller than the intersecting voxel. + var wantAddAdjust = (operation == "add" && (voxelSize <= intersection.voxel.s)); + + // now we also want to calculate the "edge square" for the face for this voxel + if (intersection.face == "MIN_X_FACE") { + + highlightAt.x = x - zFightingSizeAdjust; + if (wantAddAdjust) { + resultVoxel.x -= voxelSize; + } + + resultVoxel.bottomLeft = {x: highlightAt.x, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.bottomRight = {x: highlightAt.x, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z + voxelSize - zFightingSizeAdjust }; + resultVoxel.topLeft = {x: highlightAt.x, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.topRight = {x: highlightAt.x, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z + voxelSize - zFightingSizeAdjust }; + + } else if (intersection.face == "MAX_X_FACE") { + + highlightAt.x = x + voxelSize + zFightingSizeAdjust; + if (wantAddAdjust) { + resultVoxel.x += resultVoxel.s; + } + + resultVoxel.bottomRight = {x: highlightAt.x, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.bottomLeft = {x: highlightAt.x, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z + voxelSize - zFightingSizeAdjust }; + resultVoxel.topRight = {x: highlightAt.x, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.topLeft = {x: highlightAt.x, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z + voxelSize - zFightingSizeAdjust }; + + } else if (intersection.face == "MIN_Y_FACE") { + + highlightAt.y = y - zFightingSizeAdjust; + if (wantAddAdjust) { + resultVoxel.y -= voxelSize; + } + + resultVoxel.topRight = {x: highlightAt.x + zFightingSizeAdjust , y: highlightAt.y, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.topLeft = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.bottomRight = {x: highlightAt.x + zFightingSizeAdjust , y: highlightAt.y, z: highlightAt.z + voxelSize - zFightingSizeAdjust }; + resultVoxel.bottomLeft = {x: highlightAt.x + voxelSize - zFightingSizeAdjust , y: highlightAt.y, z: highlightAt.z + voxelSize - zFightingSizeAdjust }; + + } else if (intersection.face == "MAX_Y_FACE") { + + highlightAt.y = y + voxelSize + zFightingSizeAdjust; + if (wantAddAdjust) { + resultVoxel.y += voxelSize; + } + + resultVoxel.bottomRight = {x: highlightAt.x + zFightingSizeAdjust, y: highlightAt.y, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.bottomLeft = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y, z: highlightAt.z + zFightingSizeAdjust}; + resultVoxel.topRight = {x: highlightAt.x + zFightingSizeAdjust, y: highlightAt.y, z: highlightAt.z + voxelSize - zFightingSizeAdjust}; + resultVoxel.topLeft = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y, z: highlightAt.z + voxelSize - zFightingSizeAdjust}; + + } else if (intersection.face == "MIN_Z_FACE") { + + highlightAt.z = z - zFightingSizeAdjust; + if (wantAddAdjust) { + resultVoxel.z -= voxelSize; + } + + resultVoxel.bottomRight = {x: highlightAt.x + zFightingSizeAdjust, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z }; + resultVoxel.bottomLeft = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z}; + resultVoxel.topRight = {x: highlightAt.x + zFightingSizeAdjust, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z }; + resultVoxel.topLeft = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z}; + + } else if (intersection.face == "MAX_Z_FACE") { + + highlightAt.z = z + voxelSize + zFightingSizeAdjust; + if (wantAddAdjust) { + resultVoxel.z += voxelSize; + } + + resultVoxel.bottomLeft = {x: highlightAt.x + zFightingSizeAdjust, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z }; + resultVoxel.bottomRight = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z}; + resultVoxel.topLeft = {x: highlightAt.x + zFightingSizeAdjust, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z }; + resultVoxel.topRight = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z}; + + } + return resultVoxel; +} + +var trackLastMouseX = 0; +var trackLastMouseY = 0; + +function showPreviewLines() { + + var pickRay = Camera.computePickRay(trackLastMouseX, trackLastMouseY); + + var intersection = Voxels.findRayIntersection(pickRay); + + if (intersection.intersects) { + var resultVoxel = calculateVoxelFromIntersection(intersection, ""); + Overlays.editOverlay(linePreviewTop, { position: resultVoxel.topLeft, end: resultVoxel.topRight, visible: true }); + Overlays.editOverlay(linePreviewBottom, { position: resultVoxel.bottomLeft, end: resultVoxel.bottomRight, visible: true }); + Overlays.editOverlay(linePreviewLeft, { position: resultVoxel.topLeft, end: resultVoxel.bottomLeft, visible: true }); + Overlays.editOverlay(linePreviewRight, { position: resultVoxel.topRight, end: resultVoxel.bottomRight, visible: true }); + } else { + Overlays.editOverlay(linePreviewTop, { visible: false }); + Overlays.editOverlay(linePreviewBottom, { visible: false }); + Overlays.editOverlay(linePreviewLeft, { visible: false }); + Overlays.editOverlay(linePreviewRight, { visible: false }); + } +} + +function mouseMoveEvent(event) { + trackLastMouseX = event.x; + trackLastMouseY = event.y; + if (!voxelToolSelected) { + return; + } + showPreviewLines(); +} + + +// Array of possible trees, right now there is only one +var treeTypes = []; + +treeTypes.push({ + name: "Tall Green", + // Voxel Colors + wood: { r: 133, g: 81, b: 53 }, + leaves: { r: 22, g: 83, b: 31 }, + + // How tall the tree is + height: { min: 20, max: 60 }, + middleHeight: 0.3, + + // Chance of making a branch + branchChance: { min: 0.01, max: 0.1 }, + branchLength: { min: 30, max: 60 }, + branchThickness: { min: 2, max: 7}, + + // The width of the core, affects width and shape + coreWidth: { min: 1, max: 4 }, + + //TODO: Make this quadratic splines instead of linear + bottomThickness: { min: 2, max: 8 }, + middleThickness: { min: 1, max: 4 }, + topThickness: { min: 3, max: 6 }, + + //Modifies leaves at top + leafCapSizeOffset: 0 +}); + +// Applies noise to color +var colorNoiseRange = 0.2; + + +// Useful constants +var LEFT = 0; +var BACK = 1; +var RIGHT = 2; +var FRONT = 3; +var UP = 4; + +// Interpolates between min and max of treevar based on b +function interpolate(treeVar, b) { + return (treeVar.min + (treeVar.max - treeVar.min) * b); +} + +function makeBranch(x, y, z, step, length, dir, thickness, wood, leaves) { + var moveDir; + + var currentThickness; + //thickness attenuates to thickness - 3 + var finalThickness = thickness - 3; + if (finalThickness < 1) { + finalThickness = 1; + } + //Iterative branch generation + while (true) { + + //If we are at the end, place a ball of leaves + if (step == 0) { + makeSphere(x, y, z, 2 + finalThickness, leaves); + return; + } + //thickness attenuation + currentThickness = Math.round((finalThickness + (thickness - finalThickness) * (step/length))) - 1; + + + // If the branch is thick, grow a vertical slice + if (currentThickness > 0) { + for (var i = -currentThickness; i <= currentThickness; i++) { + var len = currentThickness - Math.abs(i); + switch (dir) { + case 0: //left + case 2: //right + growInDirection(x, y + i * voxelSize, z, len, len, BACK, wood, false, true); + growInDirection(x, y + i * voxelSize, z, len, len, FRONT, wood, false, false) + break; + case 1: //back + case 3: //front + growInDirection(x, y + i * voxelSize, z, len, len, LEFT, wood, false, true); + growInDirection(x, y + i * voxelSize, z, len, len, RIGHT, wood, false, false) + break; + } + } + } else { + //Otherwise place a single voxel + var colorNoise = (colorNoiseRange * Math.random() - colorNoiseRange * 0.5) + 1.0; + Voxels.setVoxel(x, y, z, voxelSize, wood.r * colorNoise, wood.g * colorNoise, wood.b * colorNoise); + } + + // determines random change in direction for branch + var r = Math.floor(Math.random() * 9); + + + if (r >= 6){ + moveDir = dir; //in same direction + } else if (r >= 4) { + moveDir = UP; //up + } + else if (dir == LEFT){ + if (r >= 2){ + moveDir = FRONT; + } + else{ + moveDir = BACK; + } + } + else if (dir == BACK){ + if (r >= 2){ + moveDir = LEFT; + } + else{ + moveDir = RIGHT; + } + } + else if (dir == RIGHT){ + if (r >= 2){ + moveDir = BACK; + } + else{ + moveDir = FRONT; + } + } + else if (dir == FRONT){ + if (r >= 2){ + moveDir = RIGHT; + } + else{ + moveDir = LEFT; + } + } + + //Move the branch by moveDir + switch (moveDir) { + case 0: //left + x = x - voxelSize; + break; + case 1: //back + z = z - voxelSize; + break; + case 2: //right + x = x + voxelSize; + break; + case 3: //front + z = z + voxelSize; + break; + case 4: //up + y = y + voxelSize; + break; + } + + step--; + } +} + +// Places a sphere of voxels +function makeSphere(x, y, z, radius, color) { + if (radius <= 0) { + return; + } + var width = radius * 2 + 1; + var distance; + + for (var i = -radius; i <= radius; i++){ + for (var j = -radius; j <= radius; j++){ + for (var k = -radius; k <= radius; k++){ + distance = Math.sqrt(i * i + j * j + k * k); + if (distance <= radius){ + var colorNoise = (colorNoiseRange * Math.random() - colorNoiseRange * 0.5) + 1.0; + Voxels.setVoxel(x + i * voxelSize, y + j * voxelSize, z + k * voxelSize, voxelSize, color.r * colorNoise, color.g * colorNoise, color.b * colorNoise); + } + } + } + } +} + +function growInDirection(x, y, z, step, length, dir, color, isSideBranching, addVoxel) { + + + if (addVoxel == true) { + var colorNoise = (colorNoiseRange * Math.random() - colorNoiseRange * 0.5) + 1.0; + Voxels.setVoxel(x, y, z, voxelSize, color.r * colorNoise, color.g * colorNoise, color.b * colorNoise); + } + + // If this is a main vein, it will branch outward perpendicular to its motion + if (isSideBranching == true){ + var step2; + if (step >= length - 1){ + step2 = length; + } + else{ + step2 = step + 1; + } + growInDirection(x, y, z, step, length, BACK, color, false, false); + growInDirection(x, y, z, step, length, FRONT, color, false, false); + } + + if (step < 1) return; + + // Recursively move in the direction + if (dir == LEFT) { //left + growInDirection(x - voxelSize, y, z, step - 1, length, dir, color, isSideBranching, true); + } + else if (dir == BACK) { //back + growInDirection(x, y, z - voxelSize, step - 1, length, dir, color, isSideBranching, true); + } + else if (dir == RIGHT) { //right + growInDirection(x + voxelSize, y, z, step - 1, length, dir, color, isSideBranching, true); + } + else if (dir == FRONT) {//front + growInDirection(x, y, z + voxelSize, step - 1, length, dir, color, isSideBranching, true); + } + +} + +// Grows the thickness of the tree +function growHorizontalSlice(x, y, z, thickness, color, side) { + // The side variable determines which directions we should grow in + // it is an optimization that prevents us from visiting voxels multiple + // times for trees with a coreWidth > 1 + + // side: + // 8 == all directions + // 0 1 2 + // 3 -1 4 + // 5 6 7 + + Voxels.setVoxel(x, y, z, voxelSize, color.r, color.g, color.b); + + //We are done if there is no thickness + if (thickness == 0) { + return; + } + + + switch (side) { + case 0: + growInDirection(x, y, z, thickness, thickness, LEFT, color, true, false); + break; + case 1: + growInDirection(x, y, z, thickness, thickness, BACK, color, false, false); + break; + case 2: + growInDirection(x, y, z, thickness, thickness, RIGHT, color, true, false); + break; + case 3: + growInDirection(x, y, z, thickness, thickness, LEFT, color, false, false); + break; + case 4: + growInDirection(x, y, z, thickness, thickness, BACK, color, false, false); + break; + case 5: + growInDirection(x, y, z, thickness, thickness, RIGHT, color, true, false); + break; + case 6: + growInDirection(x, y, z, thickness, thickness, FRONT, color, false, false); + break; + case 7: + growInDirection(x, y, z, thickness, thickness, RIGHT, color, true, false); + break; + case 8: + if (thickness > 1){ + growInDirection(x, y, z, thickness, thickness, LEFT, color, true, false); + growInDirection(x, y, z, thickness, thickness, RIGHT, color, true, false) + } else if (thickness == 1){ + Voxels.setVoxel(x - voxelSize, y, z, voxelSize, color.r, color.g, color.b); + Voxels.setVoxel(x + voxelSize, y, z, voxelSize, color.r, color.g, color.b); + Voxels.setVoxel(x, y, z - voxelSize, voxelSize, color.r, color.g, color.b); + Voxels.setVoxel(x, y, z + voxelSize, voxelSize, color.r, color.g, color.b); + } + break; + } +} + +function computeSide(x, z, coreWidth) { + // side: + // 8 == all directions + // 0 1 2 + // 3 -1 4 + // 5 6 7 + + // if the core is only a single block, we can grow out in all directions + if (coreWidth == 1){ + return 8; + } + + // Back face + if (z == 0) { + if (x == 0) { + return 0; + } else if (x == coreWidth - 1) { + return 2; + } else { + return 1; + } + } + + // Front face + if (z == (coreWidth - 1)) { + if (x == 0) { + return 5; + } else if (x == (coreWidth - 1)) { + return 7; + } else { + return 6; + } + } + + // Left face + if (x == 0) { + return 3; + } + + // Right face + if (x == (coreWidth - 1)) { + return 4; + } + + //Interior + return -1; +} + + +function growTree(x, y, z, tree) { + + // The size of the tree, from 0-1 + var treeSize = Math.random(); + + // Get tree properties by interpolating with the treeSize + var height = interpolate(tree.height, treeSize); + var baseHeight = Math.ceil(tree.middleHeight * height); + var bottomThickness = interpolate(tree.bottomThickness, treeSize); + var middleThickness = interpolate(tree.middleThickness, treeSize); + var topThickness = interpolate(tree.topThickness, treeSize); + var coreWidth = Math.ceil(interpolate(tree.coreWidth, treeSize)); + + var thickness; + var side; + + //Loop upwards through each slice of the tree + for (var i = 0; i < height; i++){ + + //Branch properties are based on current height as well as the overall tree size + var branchChance = interpolate(tree.branchChance, i / height); + var branchLength = Math.ceil(interpolate(tree.branchLength, (i / height) * treeSize)); + var branchThickness = Math.round(interpolate(tree.branchThickness, (i / height) * treeSize)); + + // Get the "thickness" of the tree by doing linear interpolation between the middle thickness + // and the top and bottom thickness. + if (i <= baseHeight && baseHeight != 0){ + thickness = (i / (baseHeight) * (middleThickness - bottomThickness) + bottomThickness); + } else { + var denom = ((height - baseHeight)) * (topThickness - middleThickness) + middleThickness; + if (denom != 0) { + thickness = (i - baseHeight) / denom; + } else { + thickness = 0; + } + } + // The core of the tree is a vertical rectangular prism through the middle of the tree + + //Loop through the "core", which helps shape the trunk + var startX = x - Math.floor(coreWidth / 2) * voxelSize; + var startZ = z - Math.floor(coreWidth / 2) * voxelSize; + for (var j = 0; j < coreWidth; j++) { + for (var k = 0; k < coreWidth; k++) { + //determine which side of the tree we are on + side = computeSide(j, k, coreWidth); + //grow a horizontal slice of the tree + growHorizontalSlice(startX + j * voxelSize, y + i * voxelSize, startZ + k * voxelSize, Math.floor(thickness), tree.wood, side); + + // Branches + if (side != -1) { + var r = Math.random(); + if (r <= branchChance){ + var dir = Math.floor((Math.random() * 4)); + makeBranch(startX + j * voxelSize, y + i * voxelSize, startZ + k * voxelSize, branchLength, branchLength, dir, branchThickness, tree.wood, tree.leaves); + + } + } + } + } + } + + makeSphere(x, y + height * voxelSize, z, topThickness + coreWidth + tree.leafCapSizeOffset, tree.leaves); +} + +function mousePressEvent(event) { + var mouseX = event.x; + var mouseY = event.y; + + var clickedOnSomething = false; + // Check if we clicked an overlay + var clickedOverlay = Overlays.getOverlayAtPoint({x: mouseX, y: mouseY}); + + if (clickedOverlay == voxelTool) { + voxelToolSelected = !voxelToolSelected; + + if (voxelToolSelected == true) { + Overlays.editOverlay(voxelTool, { + color: activeUIColor + }); + } else { + Overlays.editOverlay(voxelTool, { + color: UIColor + }); + } + + clickedOnSomething = true; + } else if (scaleSelector.clicked(event.x, event.y)) { + clickedOnSomething = true; + voxelSize = scaleSelector.scale; + } + + // Return if we clicked on the UI or the voxel tool is disabled + if (clickedOnSomething || !voxelToolSelected) { + return; + } + + // Compute the picking ray for the click + var pickRay = Camera.computePickRay(event.x, event.y); + var intersection = Voxels.findRayIntersection(pickRay); + var resultVoxel = calculateVoxelFromIntersection(intersection, "add"); + + // Currently not in use, could randomly select a tree + var treeIndex = Math.floor(Math.random() * treeTypes.length); + + // Grow the first tree type + growTree(resultVoxel.x, resultVoxel.y, resultVoxel.z, treeTypes[0]); + +} + + +function scriptEnding() { + Overlays.deleteOverlay(linePreviewTop); + Overlays.deleteOverlay(linePreviewBottom); + Overlays.deleteOverlay(linePreviewLeft); + Overlays.deleteOverlay(linePreviewRight); + scaleSelector.cleanup(); + Overlays.deleteOverlay(voxelTool); +} + +Controller.mousePressEvent.connect(mousePressEvent); +Controller.mouseMoveEvent.connect(mouseMoveEvent); + +Script.scriptEnding.connect(scriptEnding); + +Voxels.setPacketsPerSecond(10000); diff --git a/examples/hydraMove.js b/examples/hydraMove.js index f211a450a3..675a885b6d 100644 --- a/examples/hydraMove.js +++ b/examples/hydraMove.js @@ -19,8 +19,8 @@ var damping = 0.9; var position = { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }; var joysticksCaptured = false; -var THRUST_CONTROLLER = 1; -var VIEW_CONTROLLER = 0; +var THRUST_CONTROLLER = 0; +var VIEW_CONTROLLER = 1; var INITIAL_THRUST_MULTPLIER = 1.0; var THRUST_INCREASE_RATE = 1.05; var MAX_THRUST_MULTIPLIER = 75.0; diff --git a/examples/inspect.js b/examples/inspect.js index 28db1e7735..af63957ec3 100644 --- a/examples/inspect.js +++ b/examples/inspect.js @@ -177,30 +177,45 @@ function keyReleaseEvent(event) { function mousePressEvent(event) { if (alt && !isActive) { - isActive = true; mouseLastX = event.x; mouseLastY = event.y; // Compute trajectories related values var pickRay = Camera.computePickRay(mouseLastX, mouseLastY); - var intersection = Voxels.findRayIntersection(pickRay); + var voxelIntersection = Voxels.findRayIntersection(pickRay); + var modelIntersection = Models.findRayIntersection(pickRay); position = Camera.getPosition(); - avatarTarget = MyAvatar.getTargetAvatarPosition(); - voxelTarget = intersection.intersection; - if (Vec3.length(Vec3.subtract(avatarTarget, position)) < Vec3.length(Vec3.subtract(voxelTarget, position))) { - if (avatarTarget.x != 0 || avatarTarget.y != 0 || avatarTarget.z != 0) { - center = avatarTarget; - } else { - center = voxelTarget; - } - } else { - if (voxelTarget.x != 0 || voxelTarget.y != 0 || voxelTarget.z != 0) { - center = voxelTarget; - } else { - center = avatarTarget; - } + var avatarTarget = MyAvatar.getTargetAvatarPosition(); + var voxelTarget = voxelIntersection.intersection; + + + var distance = -1; + var string; + + if (modelIntersection.intersects && modelIntersection.accurate) { + distance = modelIntersection.distance; + center = modelIntersection.modelProperties.position; + string = "Inspecting model"; + } + + if ((distance == -1 || Vec3.length(Vec3.subtract(avatarTarget, position)) < distance) && + (avatarTarget.x != 0 || avatarTarget.y != 0 || avatarTarget.z != 0)) { + distance = Vec3.length(Vec3.subtract(avatarTarget, position)); + center = avatarTarget; + string = "Inspecting avatar"; + } + + if ((distance == -1 || Vec3.length(Vec3.subtract(voxelTarget, position)) < distance) && + (voxelTarget.x != 0 || voxelTarget.y != 0 || voxelTarget.z != 0)) { + distance = Vec3.length(Vec3.subtract(voxelTarget, position)); + center = voxelTarget; + string = "Inspecting voxel"; + } + + if (distance == -1) { + return; } vector = Vec3.subtract(position, center); @@ -209,6 +224,8 @@ function mousePressEvent(event) { altitude = Math.asin(vector.y / Vec3.length(vector)); Camera.keepLookingAt(center); + print(string); + isActive = true; } } diff --git a/interface/resources/images/sixense-reticle.png b/interface/resources/images/sixense-reticle.png new file mode 100644 index 0000000000..b168b25106 Binary files /dev/null and b/interface/resources/images/sixense-reticle.png differ diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3cfec3190e..6b44503af4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -150,6 +150,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _mouseX(0), _mouseY(0), _lastMouseMove(usecTimestampNow()), + _lastMouseMoveType(QEvent::MouseMove), _mouseHidden(false), _seenMouseMove(false), _touchAvgX(0.0f), @@ -158,7 +159,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _mousePressed(false), _audio(STARTUP_JITTER_SAMPLES), _enableProcessVoxelsThread(true), - _voxelProcessor(), + _octreeProcessor(), _voxelHideShowThread(&_voxels), _packetsPerSecond(0), _bytesPerSecond(0), @@ -418,7 +419,7 @@ Application::~Application() { _audio.thread()->quit(); _audio.thread()->wait(); - _voxelProcessor.terminate(); + _octreeProcessor.terminate(); _voxelHideShowThread.terminate(); _voxelEditSender.terminate(); _particleEditSender.terminate(); @@ -517,7 +518,7 @@ void Application::initializeGL() { qDebug( "init() complete."); // create thread for parsing of voxel data independent of the main network and rendering threads - _voxelProcessor.initialize(_enableProcessVoxelsThread); + _octreeProcessor.initialize(_enableProcessVoxelsThread); _voxelEditSender.initialize(_enableProcessVoxelsThread); _voxelHideShowThread.initialize(_enableProcessVoxelsThread); _particleEditSender.initialize(_enableProcessVoxelsThread); @@ -1089,6 +1090,16 @@ void Application::focusOutEvent(QFocusEvent* event) { } void Application::mouseMoveEvent(QMouseEvent* event) { + + bool showMouse = true; + // If this mouse move event is emitted by a controller, dont show the mouse cursor + if (event->type() == CONTROLLER_MOVE_EVENT) { + showMouse = false; + } + + // Used by application overlay to determine how to draw cursor(s) + _lastMouseMoveType = event->type(); + _controllerScriptingInterface.emitMouseMoveEvent(event); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it @@ -1096,10 +1107,9 @@ void Application::mouseMoveEvent(QMouseEvent* event) { return; } - _lastMouseMove = usecTimestampNow(); - if (_mouseHidden && !OculusManager::isConnected()) { + if (_mouseHidden && showMouse && !OculusManager::isConnected()) { getGLWidget()->setCursor(Qt::ArrowCursor); _mouseHidden = false; _seenMouseMove = true; @@ -1369,6 +1379,9 @@ void Application::setEnable3DTVMode(bool enable3DTVMode) { resizeGL(_glWidget->width(),_glWidget->height()); } +void Application::setEnableVRMode(bool enableVRMode) { + resizeGL(_glWidget->width(), _glWidget->height()); +} void Application::setRenderVoxels(bool voxelRender) { _voxelEditSender.setShouldSend(voxelRender); @@ -1884,7 +1897,7 @@ void Application::updateThreads(float deltaTime) { // parse voxel packets if (!_enableProcessVoxelsThread) { - _voxelProcessor.threadRoutine(); + _octreeProcessor.threadRoutine(); _voxelHideShowThread.threadRoutine(); _voxelEditSender.threadRoutine(); _particleEditSender.threadRoutine(); @@ -2095,7 +2108,7 @@ void Application::updateMyAvatar(float deltaTime) { } } - // sent a nack packet containing missing sequence numbers of received packets + // sent nack packets containing missing sequence numbers of received packets from nodes { quint64 now = usecTimestampNow(); quint64 sinceLastNack = now - _lastNackTime; @@ -2125,6 +2138,12 @@ void Application::sendNack() { || node->getType() == NodeType::ModelServer) ) { + // if there are octree packets from this node that are waiting to be processed, + // don't send a NACK since the missing packets may be among those waiting packets. + if (_octreeProcessor.hasPacketsToProcessFrom(node)) { + continue; + } + QUuid nodeUUID = node->getUUID(); _octreeSceneStatsLock.lockForRead(); @@ -3242,6 +3261,11 @@ void Application::nodeAdded(SharedNodePointer node) { } void Application::nodeKilled(SharedNodePointer node) { + + // this is here because connecting NodeList::nodeKilled to OctreePacketProcessor::nodeKilled doesn't work: + // OctreePacketProcessor::nodeKilled is not called when NodeList::nodeKilled is emitted for some reason. + _octreeProcessor.nodeKilled(node); + if (node->getType() == NodeType::VoxelServer) { QUuid nodeUUID = node->getUUID(); // see if this is the first we've heard of this node... @@ -3487,8 +3511,10 @@ void Application::saveScripts() { } ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScriptFromEditor) { - if(loadScriptFromEditor && _scriptEnginesHash.contains(scriptName) && !_scriptEnginesHash[scriptName]->isFinished()){ - return _scriptEnginesHash[scriptName]; + QUrl scriptUrl(scriptName); + const QString& scriptURLString = scriptUrl.toString(); + if(loadScriptFromEditor && _scriptEnginesHash.contains(scriptURLString) && !_scriptEnginesHash[scriptURLString]->isFinished()){ + return _scriptEnginesHash[scriptURLString]; } ScriptEngine* scriptEngine; @@ -3496,9 +3522,8 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript scriptEngine = new ScriptEngine(NO_SCRIPT, "", &_controllerScriptingInterface); } else { // start the script on a new thread... - QUrl scriptUrl(scriptName); scriptEngine = new ScriptEngine(scriptUrl, &_controllerScriptingInterface); - _scriptEnginesHash.insert(scriptName, scriptEngine); + _scriptEnginesHash.insert(scriptURLString, scriptEngine); if (!scriptEngine->hasScript()) { qDebug() << "Application::loadScript(), script failed to load..."; diff --git a/interface/src/Application.h b/interface/src/Application.h index 170be43493..31e1f2291b 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -88,7 +88,7 @@ #include "voxels/VoxelFade.h" #include "voxels/VoxelHideShowThread.h" #include "voxels/VoxelImporter.h" -#include "voxels/VoxelPacketProcessor.h" +#include "voxels/OctreePacketProcessor.h" #include "voxels/VoxelSystem.h" @@ -129,7 +129,7 @@ static const float MIRROR_FIELD_OF_VIEW = 30.0f; class Application : public QApplication { Q_OBJECT - friend class VoxelPacketProcessor; + friend class OctreePacketProcessor; friend class VoxelEditPacketSender; friend class DatagramProcessor; @@ -192,7 +192,7 @@ public: ViewFrustum* getShadowViewFrustum() { return &_shadowViewFrustum; } VoxelSystem* getVoxels() { return &_voxels; } VoxelTree* getVoxelTree() { return _voxels.getTree(); } - const VoxelPacketProcessor& getVoxelPacketProcessor() const { return _voxelProcessor; } + const OctreePacketProcessor& getOctreePacketProcessor() const { return _octreeProcessor; } ParticleTreeRenderer* getParticles() { return &_particles; } MetavoxelSystem* getMetavoxels() { return &_metavoxels; } ModelTreeRenderer* getModels() { return &_models; } @@ -206,6 +206,7 @@ public: const glm::vec3& getMouseRayDirection() const { return _mouseRayDirection; } int getMouseX() const { return _mouseX; } int getMouseY() const { return _mouseY; } + unsigned int getLastMouseMoveType() const { return _lastMouseMoveType; } Faceplus* getFaceplus() { return &_faceplus; } Faceshift* getFaceshift() { return &_faceshift; } Visage* getVisage() { return &_visage; } @@ -345,6 +346,7 @@ private slots: void setFullscreen(bool fullscreen); void setEnable3DTVMode(bool enable3DTVMode); + void setEnableVRMode(bool enableVRMode); void cameraMenuChanged(); glm::vec2 getScaledScreenPoint(glm::vec2 projectedPoint); @@ -505,6 +507,7 @@ private: int _mouseDragStartedX; int _mouseDragStartedY; quint64 _lastMouseMove; + unsigned int _lastMouseMoveType; bool _mouseHidden; bool _seenMouseMove; @@ -521,6 +524,7 @@ private: QSet _keysPressed; + GeometryCache _geometryCache; AnimationCache _animationCache; TextureCache _textureCache; @@ -533,7 +537,7 @@ private: Audio _audio; bool _enableProcessVoxelsThread; - VoxelPacketProcessor _voxelProcessor; + OctreePacketProcessor _octreeProcessor; VoxelHideShowThread _voxelHideShowThread; VoxelEditPacketSender _voxelEditSender; ParticleEditPacketSender _particleEditSender; diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index ebc228a13d..271bcd5279 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -306,7 +306,7 @@ void linearResampling(int16_t* sourceSamples, int16_t* destinationSamples, } else { // this is a 48 to 24 resampling but both source and destination are two channels // squish two samples into one in each channel - for (int i = 0; i < numSourceSamples; i += 4) { + for (unsigned int i = 0; i < numSourceSamples; i += 4) { destinationSamples[i / 2] = (sourceSamples[i] / 2) + (sourceSamples[i + 2] / 2); destinationSamples[(i / 2) + 1] = (sourceSamples[i + 1] / 2) + (sourceSamples[i + 3] / 2); } @@ -1419,7 +1419,7 @@ bool Audio::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) // proportional to the accelerator ratio. #ifdef Q_OS_WIN -const float Audio::CALLBACK_ACCELERATOR_RATIO = 0.4f; +const float Audio::CALLBACK_ACCELERATOR_RATIO = 0.1f; #endif #ifdef Q_OS_MAC diff --git a/interface/src/DatagramProcessor.cpp b/interface/src/DatagramProcessor.cpp index 56078c1a8d..cdd7e7ef0f 100644 --- a/interface/src/DatagramProcessor.cpp +++ b/interface/src/DatagramProcessor.cpp @@ -71,7 +71,7 @@ void DatagramProcessor::processDatagrams() { case PacketTypeOctreeStats: case PacketTypeEnvironmentData: { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), - "Application::networkReceive()... _voxelProcessor.queueReceivedPacket()"); + "Application::networkReceive()... _octreeProcessor.queueReceivedPacket()"); bool wantExtraDebugging = application->getLogger()->extraDebugging(); if (wantExtraDebugging && packetTypeForPacket(incomingPacket) == PacketTypeVoxelData) { @@ -92,7 +92,7 @@ void DatagramProcessor::processDatagrams() { if (matchedNode) { // add this packet to our list of voxel packets and process them on the voxel processing - application->_voxelProcessor.queueReceivedPacket(matchedNode, incomingPacket); + application->_octreeProcessor.queueReceivedPacket(matchedNode, incomingPacket); } break; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index f791d20588..0dccf6256b 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -45,6 +45,7 @@ #include "ui/ModelsBrowser.h" #include "ui/LoginDialog.h" #include "ui/NodeBounds.h" +#include "devices/OculusManager.h" Menu* Menu::_instance = NULL; @@ -83,6 +84,7 @@ Menu::Menu() : _audioJitterBufferSamples(0), _bandwidthDialog(NULL), _fieldOfView(DEFAULT_FIELD_OF_VIEW_DEGREES), + _realWorldFieldOfView(DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), _faceshiftEyeDeflection(DEFAULT_FACESHIFT_EYE_DEFLECTION), _frustumDrawMode(FRUSTUM_DRAW_MODE_ALL), _viewFrustumOffset(DEFAULT_FRUSTUM_OFFSET), @@ -91,6 +93,9 @@ Menu::Menu() : _lodToolsDialog(NULL), _maxVoxels(DEFAULT_MAX_VOXELS_PER_SYSTEM), _voxelSizeScale(DEFAULT_OCTREE_SIZE_SCALE), + _oculusUIAngularSize(DEFAULT_OCULUS_UI_ANGULAR_SIZE), + _sixenseReticleMoveSpeed(DEFAULT_SIXENSE_RETICLE_MOVE_SPEED), + _invertSixenseButtons(DEFAULT_INVERT_SIXENSE_MOUSE_BUTTONS), _automaticAvatarLOD(true), _avatarLODDecreaseFPS(DEFAULT_ADJUST_AVATAR_LOD_DOWN_FPS), _avatarLODIncreaseFPS(ADJUST_LOD_UP_FPS), @@ -103,6 +108,7 @@ Menu::Menu() : _fastFPSAverage(ONE_SECOND_OF_FRAMES), _loginAction(NULL), _preferencesDialog(NULL), + _loginDialog(NULL), _snapshotsLocation() { Application *appInstance = Application::getInstance(); @@ -126,7 +132,7 @@ Menu::Menu() : toggleLoginMenuItem(); // connect to the appropriate slots of the AccountManager so that we can change the Login/Logout menu item - connect(&accountManager, &AccountManager::accessTokenChanged, this, &Menu::toggleLoginMenuItem); + connect(&accountManager, &AccountManager::profileChanged, this, &Menu::toggleLoginMenuItem); connect(&accountManager, &AccountManager::logoutComplete, this, &Menu::toggleLoginMenuItem); addDisabledActionAndSeparator(fileMenu, "Scripts"); @@ -164,6 +170,8 @@ Menu::Menu() : Qt::Key_At, this, SLOT(goTo())); + connect(&LocationManager::getInstance(), &LocationManager::multipleDestinationsFound, + this, &Menu::multipleDestinationsDecision); addDisabledActionAndSeparator(fileMenu, "Upload Avatar Model"); addActionToQMenuAndActionHash(fileMenu, MenuOption::UploadHead, 0, Application::getInstance(), SLOT(uploadHead())); @@ -253,6 +261,11 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::FullscreenMirror, Qt::Key_H, false, appInstance, SLOT(cameraMenuChanged())); + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::EnableVRMode, 0, + false, + appInstance, + SLOT(setEnableVRMode(bool))); + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Enable3DTVMode, 0, false, appInstance, @@ -320,7 +333,7 @@ Menu::Menu() : shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, "None", 0, true)); shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, MenuOption::SimpleShadows, 0, false)); shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, MenuOption::CascadedShadows, 0, false)); - + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Metavoxels, 0, true); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::BuckyBalls, 0, false); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Particles, 0, true); @@ -379,6 +392,9 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(oculusOptionsMenu, MenuOption::AllowOculusCameraModeChange, 0, false); addCheckableActionToQMenuAndActionHash(oculusOptionsMenu, MenuOption::DisplayOculusOverlays, 0, true); + QMenu* sixenseOptionsMenu = developerMenu->addMenu("Sixense Options"); + addCheckableActionToQMenuAndActionHash(sixenseOptionsMenu, MenuOption::SixenseMouseInput, 0, true); + QMenu* handOptionsMenu = developerMenu->addMenu("Hand Options"); addCheckableActionToQMenuAndActionHash(handOptionsMenu, @@ -397,7 +413,7 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::DisableNackPackets, 0, false); addDisabledActionAndSeparator(developerMenu, "Testing"); - + QMenu* timingMenu = developerMenu->addMenu("Timing and Statistics Tools"); QMenu* perfTimerMenu = timingMenu->addMenu("Performance Timer"); addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::DisplayTimingDetails, 0, true); @@ -452,7 +468,7 @@ Menu::Menu() : false, appInstance->getAudio(), SLOT(toggleToneInjection())); - addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioScope, + addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioScope, Qt::CTRL | Qt::Key_P, false, appInstance->getAudio(), SLOT(toggleScope())); @@ -913,9 +929,11 @@ void sendFakeEnterEvent() { const float DIALOG_RATIO_OF_WINDOW = 0.30f; void Menu::loginForCurrentDomain() { - LoginDialog* loginDialog = new LoginDialog(Application::getInstance()->getWindow()); - loginDialog->show(); - loginDialog->resizeAndPosition(false); + if (!_loginDialog) { + _loginDialog = new LoginDialog(Application::getInstance()->getWindow()); + _loginDialog->show(); + _loginDialog->resizeAndPosition(false); + } } void Menu::editPreferences() { @@ -1060,9 +1078,7 @@ bool Menu::goToURL(QString location) { } void Menu::goToUser(const QString& user) { - LocationManager* manager = &LocationManager::getInstance(); - manager->goTo(user); - connect(manager, &LocationManager::multipleDestinationsFound, this, &Menu::multipleDestinationsDecision); + LocationManager::getInstance().goTo(user); } /// Open a url, shortcutting any "hifi" scheme URLs to the local application. @@ -1084,13 +1100,12 @@ void Menu::multipleDestinationsDecision(const QJsonObject& userData, const QJson int userResponse = msgBox.exec(); if (userResponse == QMessageBox::Ok) { - Application::getInstance()->getAvatar()->goToLocationFromResponse(userData); + Application::getInstance()->getAvatar()->goToLocationFromAddress(userData["address"].toObject()); } else if (userResponse == QMessageBox::Open) { - Application::getInstance()->getAvatar()->goToLocationFromResponse(userData); + Application::getInstance()->getAvatar()->goToLocationFromAddress(placeData["address"].toObject()); } LocationManager* manager = reinterpret_cast(sender()); - disconnect(manager, &LocationManager::multipleDestinationsFound, this, &Menu::multipleDestinationsDecision); } void Menu::muteEnvironment() { @@ -1763,4 +1778,3 @@ QString Menu::getSnapshotsLocation() const { } return _snapshotsLocation; } - diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 69015a938b..4d2174a448 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -27,6 +27,7 @@ #include "ui/PreferencesDialog.h" #include "ui/ChatWindow.h" #include "ui/JSConsole.h" +#include "ui/LoginDialog.h" #include "ui/ScriptEditorWindow.h" const float ADJUST_LOD_DOWN_FPS = 40.0; @@ -89,6 +90,12 @@ public: void setFieldOfView(float fieldOfView) { _fieldOfView = fieldOfView; } float getRealWorldFieldOfView() const { return _realWorldFieldOfView; } void setRealWorldFieldOfView(float realWorldFieldOfView) { _realWorldFieldOfView = realWorldFieldOfView; } + float getOculusUIAngularSize() const { return _oculusUIAngularSize; } + void setOculusUIAngularSize(float oculusUIAngularSize) { _oculusUIAngularSize = oculusUIAngularSize; } + float getSixenseReticleMoveSpeed() const { return _sixenseReticleMoveSpeed; } + void setSixenseReticleMoveSpeed(float sixenseReticleMoveSpeed) { _sixenseReticleMoveSpeed = sixenseReticleMoveSpeed; } + bool getInvertSixenseButtons() const { return _invertSixenseButtons; } + void setInvertSixenseButtons(bool invertSixenseButtons) { _invertSixenseButtons = invertSixenseButtons; } float getFaceshiftEyeDeflection() const { return _faceshiftEyeDeflection; } void setFaceshiftEyeDeflection(float faceshiftEyeDeflection) { _faceshiftEyeDeflection = faceshiftEyeDeflection; } @@ -254,6 +261,9 @@ private: LodToolsDialog* _lodToolsDialog; int _maxVoxels; float _voxelSizeScale; + float _oculusUIAngularSize; + float _sixenseReticleMoveSpeed; + bool _invertSixenseButtons; bool _automaticAvatarLOD; float _avatarLODDecreaseFPS; float _avatarLODIncreaseFPS; @@ -270,6 +280,7 @@ private: QPointer _preferencesDialog; QPointer _attachmentsDialog; QPointer _animationsDialog; + QPointer _loginDialog; QAction* _chatAction; QString _snapshotsLocation; }; @@ -334,6 +345,7 @@ namespace MenuOption { const QString EchoLocalAudio = "Echo Local Audio"; const QString EchoServerAudio = "Echo Server Audio"; const QString Enable3DTVMode = "Enable 3DTV Mode"; + const QString EnableVRMode = "Enable VR Mode"; const QString ExpandMiscAvatarTiming = "Expand Misc MyAvatar Timing"; const QString ExpandAvatarUpdateTiming = "Expand MyAvatar update Timing"; const QString ExpandAvatarSimulateTiming = "Expand MyAvatar simulate Timing"; @@ -397,6 +409,7 @@ namespace MenuOption { const QString SettingsExport = "Export Settings"; const QString SettingsImport = "Import Settings"; const QString SimpleShadows = "Simple"; + const QString SixenseMouseInput = "Enable Sixense Mouse Input"; const QString ShowBordersVoxelNodes = "Show Voxel Nodes"; const QString ShowBordersModelNodes = "Show Model Nodes"; const QString ShowBordersParticleNodes = "Show Particle Nodes"; diff --git a/interface/src/Util.cpp b/interface/src/Util.cpp index 07ca65b286..5ce1435bd6 100644 --- a/interface/src/Util.cpp +++ b/interface/src/Util.cpp @@ -21,6 +21,8 @@ #include +#include + #include "InterfaceConfig.h" #include "ui/TextRenderer.h" #include "VoxelConstants.h" @@ -409,8 +411,44 @@ void runTimingTests() { float NSEC_TO_USEC = 1.0f / 1000.0f; elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; - qDebug("QElapsedTimer::nsecElapsed() usecs: %f", elapsedUsecs / (float) numTests); + qDebug("QElapsedTimer::nsecElapsed() usecs: %f", elapsedUsecs); + // Test sleep functions for accuracy + startTime.start(); + QThread::msleep(1); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("QThread::msleep(1) ms: %f", elapsedUsecs / 1000.0f); + + startTime.start(); + QThread::sleep(1); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("QThread::sleep(1) ms: %f", elapsedUsecs / 1000.0f); + + startTime.start(); + usleep(1); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("usleep(1) ms: %f", elapsedUsecs / 1000.0f); + + startTime.start(); + usleep(10); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("usleep(10) ms: %f", elapsedUsecs / 1000.0f); + + startTime.start(); + usleep(100); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("usleep(100) ms: %f", elapsedUsecs / 1000.0f); + + startTime.start(); + usleep(1000); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("usleep(1000) ms: %f", elapsedUsecs / 1000.0f); + + startTime.start(); + usleep(15000); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("usleep(15000) ms: %f", elapsedUsecs / 1000.0f); + // Random number generation startTime.start(); for (int i = 0; i < numTests; i++) { diff --git a/interface/src/XmppClient.cpp b/interface/src/XmppClient.cpp index 666906681c..ef9db55620 100644 --- a/interface/src/XmppClient.cpp +++ b/interface/src/XmppClient.cpp @@ -23,7 +23,7 @@ XmppClient::XmppClient() : _xmppMUCManager() { AccountManager& accountManager = AccountManager::getInstance(); - connect(&accountManager, SIGNAL(accessTokenChanged()), this, SLOT(connectToServer())); + connect(&accountManager, SIGNAL(profileChanged()), this, SLOT(connectToServer())); connect(&accountManager, SIGNAL(logoutComplete()), this, SLOT(disconnectFromServer())); } diff --git a/interface/src/avatar/FaceModel.cpp b/interface/src/avatar/FaceModel.cpp index 601dad5563..c3f31ff6e7 100644 --- a/interface/src/avatar/FaceModel.cpp +++ b/interface/src/avatar/FaceModel.cpp @@ -30,7 +30,7 @@ void FaceModel::simulate(float deltaTime, bool fullUpdate) { } setTranslation(neckPosition); glm::quat neckParentRotation; - if (!owningAvatar->getSkeletonModel().getNeckParentRotation(neckParentRotation)) { + if (!owningAvatar->getSkeletonModel().getNeckParentRotationFromDefaultOrientation(neckParentRotation)) { neckParentRotation = owningAvatar->getOrientation(); } setRotation(neckParentRotation); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index a14152aa04..54ed641d72 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1626,44 +1626,46 @@ void MyAvatar::resetSize() { } void MyAvatar::goToLocationFromResponse(const QJsonObject& jsonObject) { - if (jsonObject["status"].toString() == "success") { - - // send a node kill request, indicating to other clients that they should play the "disappeared" effect - sendKillAvatar(); - QJsonObject locationObject = jsonObject["data"].toObject()["address"].toObject(); - QString positionString = locationObject["position"].toString(); - QString orientationString = locationObject["orientation"].toString(); - QString domainHostnameString = locationObject["domain"].toString(); - - qDebug() << "Changing domain to" << domainHostnameString << - ", position to" << positionString << - ", and orientation to" << orientationString; - - QStringList coordinateItems = positionString.split(','); - QStringList orientationItems = orientationString.split(','); - - NodeList::getInstance()->getDomainHandler().setHostname(domainHostnameString); - - // orient the user to face the target - glm::quat newOrientation = glm::quat(glm::radians(glm::vec3(orientationItems[0].toFloat(), - orientationItems[1].toFloat(), - orientationItems[2].toFloat()))) - * glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); - setOrientation(newOrientation); - - // move the user a couple units away - const float DISTANCE_TO_USER = 2.0f; - glm::vec3 newPosition = glm::vec3(coordinateItems[0].toFloat(), coordinateItems[1].toFloat(), - coordinateItems[2].toFloat()) - newOrientation * IDENTITY_FRONT * DISTANCE_TO_USER; - setPosition(newPosition); - emit transformChanged(); + goToLocationFromAddress(locationObject); } else { QMessageBox::warning(Application::getInstance()->getWindow(), "", "That user or location could not be found."); } } +void MyAvatar::goToLocationFromAddress(const QJsonObject& locationObject) { + // send a node kill request, indicating to other clients that they should play the "disappeared" effect + sendKillAvatar(); + + QString positionString = locationObject["position"].toString(); + QString orientationString = locationObject["orientation"].toString(); + QString domainHostnameString = locationObject["domain"].toString(); + + qDebug() << "Changing domain to" << domainHostnameString << + ", position to" << positionString << + ", and orientation to" << orientationString; + + QStringList coordinateItems = positionString.split(','); + QStringList orientationItems = orientationString.split(','); + + NodeList::getInstance()->getDomainHandler().setHostname(domainHostnameString); + + // orient the user to face the target + glm::quat newOrientation = glm::quat(glm::radians(glm::vec3(orientationItems[0].toFloat(), + orientationItems[1].toFloat(), + orientationItems[2].toFloat()))) + * glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); + setOrientation(newOrientation); + + // move the user a couple units away + const float DISTANCE_TO_USER = 2.0f; + glm::vec3 newPosition = glm::vec3(coordinateItems[0].toFloat(), coordinateItems[1].toFloat(), + coordinateItems[2].toFloat()) - newOrientation * IDENTITY_FRONT * DISTANCE_TO_USER; + setPosition(newPosition); + emit transformChanged(); +} + void MyAvatar::updateMotionBehaviorsFromMenu() { Menu* menu = Menu::getInstance(); if (menu->isOptionChecked(MenuOption::ObeyEnvironmentalGravity)) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index d99102c356..2fbc488feb 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -129,6 +129,7 @@ public slots: void resetSize(); void goToLocationFromResponse(const QJsonObject& jsonObject); + void goToLocationFromAddress(const QJsonObject& jsonObject); // Set/Get update the thrust that will move the avatar around void addThrust(glm::vec3 newThrust) { _thrust += newThrust; }; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 1b1b2e032b..576545f115 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -434,7 +434,7 @@ bool SkeletonModel::getNeckPosition(glm::vec3& neckPosition) const { return isActive() && getJointPositionInWorldFrame(_geometry->getFBXGeometry().neckJointIndex, neckPosition); } -bool SkeletonModel::getNeckParentRotation(glm::quat& neckParentRotation) const { +bool SkeletonModel::getNeckParentRotationFromDefaultOrientation(glm::quat& neckParentRotation) const { if (!isActive()) { return false; } @@ -442,7 +442,13 @@ bool SkeletonModel::getNeckParentRotation(glm::quat& neckParentRotation) const { if (geometry.neckJointIndex == -1) { return false; } - return getJointRotationInWorldFrame(geometry.joints.at(geometry.neckJointIndex).parentIndex, neckParentRotation); + int parentIndex = geometry.joints.at(geometry.neckJointIndex).parentIndex; + glm::quat worldFrameRotation; + if (getJointRotationInWorldFrame(parentIndex, worldFrameRotation)) { + neckParentRotation = worldFrameRotation * _jointStates[parentIndex].getFBXJoint().inverseDefaultRotation; + return true; + } + return false; } bool SkeletonModel::getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const { diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 086973807d..3b8e67df47 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -86,9 +86,9 @@ public: /// \return whether or not the neck was found bool getNeckPosition(glm::vec3& neckPosition) const; - /// Returns the rotation of the neck joint's parent. + /// Returns the rotation of the neck joint's parent from default orientation /// \return whether or not the neck was found - bool getNeckParentRotation(glm::quat& neckRotation) const; + bool getNeckParentRotationFromDefaultOrientation(glm::quat& neckParentRotation) const; /// Retrieve the positions of up to two eye meshes. /// \return whether or not both eye meshes were found diff --git a/interface/src/devices/OculusManager.cpp b/interface/src/devices/OculusManager.cpp index fe8cc37168..b2ee4e8c18 100644 --- a/interface/src/devices/OculusManager.cpp +++ b/interface/src/devices/OculusManager.cpp @@ -72,6 +72,14 @@ void OculusManager::connect() { #endif } +bool OculusManager::isConnected() { +#ifdef HAVE_LIBOVR + return _isConnected && Menu::getInstance()->isOptionChecked(MenuOption::EnableVRMode); +#else + return false; +#endif +} + void OculusManager::configureCamera(Camera& camera, int screenWidth, int screenHeight) { #ifdef HAVE_LIBOVR _stereoConfig.SetFullViewport(Viewport(0, 0, screenWidth, screenHeight)); diff --git a/interface/src/devices/OculusManager.h b/interface/src/devices/OculusManager.h index 6376df05ca..21b9d67f4d 100644 --- a/interface/src/devices/OculusManager.h +++ b/interface/src/devices/OculusManager.h @@ -20,6 +20,8 @@ #include "renderer/ProgramObject.h" +const float DEFAULT_OCULUS_UI_ANGULAR_SIZE = 72.0f; + class Camera; /// Handles interaction with the Oculus Rift. @@ -27,7 +29,7 @@ class OculusManager { public: static void connect(); - static bool isConnected() { return _isConnected; } + static bool isConnected(); static void configureCamera(Camera& camera, int screenWidth, int screenHeight); diff --git a/interface/src/devices/SixenseManager.cpp b/interface/src/devices/SixenseManager.cpp index 8cd3cc059e..2c62a58800 100644 --- a/interface/src/devices/SixenseManager.cpp +++ b/interface/src/devices/SixenseManager.cpp @@ -39,6 +39,14 @@ SixenseManager::SixenseManager() { sixenseInit(); #endif + _triggerPressed[0] = false; + _bumperPressed[0] = false; + _oldX[0] = -1; + _oldY[0] = -1; + _triggerPressed[1] = false; + _bumperPressed[1] = false; + _oldX[1] = -1; + _oldY[1] = -1; } SixenseManager::~SixenseManager() { @@ -107,6 +115,12 @@ void SixenseManager::update(float deltaTime) { palm->setTrigger(data->trigger); palm->setJoystick(data->joystick_x, data->joystick_y); + + // Emulate the mouse so we can use scripts + if (Menu::getInstance()->isOptionChecked(MenuOption::SixenseMouseInput)) { + emulateMouse(palm, numActiveControllers - 1); + } + // NOTE: Sixense API returns pos data in millimeters but we IMMEDIATELY convert to meters. glm::vec3 position(data->pos[0], data->pos[1], data->pos[2]); position *= METERS_PER_MILLIMETER; @@ -171,6 +185,17 @@ void SixenseManager::update(float deltaTime) { #endif // HAVE_SIXENSE } +//Constants for getCursorPixelRangeMultiplier() +const float MIN_PIXEL_RANGE_MULT = 0.4f; +const float MAX_PIXEL_RANGE_MULT = 2.0f; +const float RANGE_MULT = (MAX_PIXEL_RANGE_MULT - MIN_PIXEL_RANGE_MULT) * 0.01; + +//Returns a multiplier to be applied to the cursor range for the controllers +float SixenseManager::getCursorPixelRangeMult() const { + //scales (0,100) to (MINIMUM_PIXEL_RANGE_MULT, MAXIMUM_PIXEL_RANGE_MULT) + return Menu::getInstance()->getSixenseReticleMoveSpeed() * RANGE_MULT + MIN_PIXEL_RANGE_MULT; +} + #ifdef HAVE_SIXENSE // the calibration sequence is: @@ -313,5 +338,109 @@ void SixenseManager::updateCalibration(const sixenseControllerData* controllers) } } } + +//Injecting mouse movements and clicks +void SixenseManager::emulateMouse(PalmData* palm, int index) { + MyAvatar* avatar = Application::getInstance()->getAvatar(); + QGLWidget* widget = Application::getInstance()->getGLWidget(); + QPoint pos; + // Get directon relative to avatar orientation + glm::vec3 direction = glm::inverse(avatar->getOrientation()) * palm->getFingerDirection(); + + Qt::MouseButton bumperButton; + Qt::MouseButton triggerButton; + + if (Menu::getInstance()->getInvertSixenseButtons()) { + bumperButton = Qt::LeftButton; + triggerButton = Qt::RightButton; + } else { + bumperButton = Qt::RightButton; + triggerButton = Qt::LeftButton; + } + + // Get the angles, scaled between (-0.5,0.5) + float xAngle = (atan2(direction.z, direction.x) + M_PI_2); + float yAngle = 0.5f - ((atan2(direction.z, direction.y) + M_PI_2)); + + // Get the pixel range over which the xAngle and yAngle are scaled + float cursorRange = widget->width() * getCursorPixelRangeMult(); + + pos.setX(widget->width() / 2.0f + cursorRange * xAngle); + pos.setY(widget->height() / 2.0f + cursorRange * yAngle); + + //If we are off screen then we should stop processing, and if a trigger or bumper is pressed, + //we should unpress them. + if (pos.x() < 0 || pos.x() > widget->width() || pos.y() < 0 || pos.y() > widget->height()) { + if (_bumperPressed[index]) { + QMouseEvent mouseEvent(QEvent::MouseButtonRelease, pos, bumperButton, bumperButton, 0); + + Application::getInstance()->mouseReleaseEvent(&mouseEvent); + + _bumperPressed[index] = false; + } + if (_triggerPressed[index]) { + QMouseEvent mouseEvent(QEvent::MouseButtonRelease, pos, triggerButton, triggerButton, 0); + + Application::getInstance()->mouseReleaseEvent(&mouseEvent); + + _triggerPressed[index] = false; + } + return; + } + + //If position has changed, emit a mouse move to the application + if (pos.x() != _oldX[index] || pos.y() != _oldY[index]) { + QMouseEvent mouseEvent(static_cast(CONTROLLER_MOVE_EVENT), pos, Qt::NoButton, Qt::NoButton, 0); + + //Only send the mouse event if the opposite left button isnt held down. + //This is specifically for edit voxels + if (triggerButton == Qt::LeftButton) { + if (!_triggerPressed[(int)(!index)]) { + Application::getInstance()->mouseMoveEvent(&mouseEvent); + } + } else { + if (!_bumperPressed[(int)(!index)]) { + Application::getInstance()->mouseMoveEvent(&mouseEvent); + } + } + } + _oldX[index] = pos.x(); + _oldY[index] = pos.y(); + + //Check for bumper press ( Right Click ) + if (palm->getControllerButtons() & BUTTON_FWD) { + if (!_bumperPressed[index]) { + _bumperPressed[index] = true; + + QMouseEvent mouseEvent(QEvent::MouseButtonPress, pos, bumperButton, bumperButton, 0); + + Application::getInstance()->mousePressEvent(&mouseEvent); + } + } else if (_bumperPressed[index]) { + QMouseEvent mouseEvent(QEvent::MouseButtonRelease, pos, bumperButton, bumperButton, 0); + + Application::getInstance()->mouseReleaseEvent(&mouseEvent); + + _bumperPressed[index] = false; + } + + //Check for trigger press ( Left Click ) + if (palm->getTrigger() == 1.0f) { + if (!_triggerPressed[index]) { + _triggerPressed[index] = true; + + QMouseEvent mouseEvent(QEvent::MouseButtonPress, pos, triggerButton, triggerButton, 0); + + Application::getInstance()->mousePressEvent(&mouseEvent); + } + } else if (_triggerPressed[index]) { + QMouseEvent mouseEvent(QEvent::MouseButtonRelease, pos, triggerButton, triggerButton, 0); + + Application::getInstance()->mouseReleaseEvent(&mouseEvent); + + _triggerPressed[index] = false; + } +} + #endif // HAVE_SIXENSE diff --git a/interface/src/devices/SixenseManager.h b/interface/src/devices/SixenseManager.h index a98d4c0e4e..8803c2c006 100644 --- a/interface/src/devices/SixenseManager.h +++ b/interface/src/devices/SixenseManager.h @@ -27,6 +27,12 @@ const unsigned int BUTTON_3 = 1U << 3; const unsigned int BUTTON_4 = 1U << 4; const unsigned int BUTTON_FWD = 1U << 7; +// Event type that represents moving the controller +const unsigned int CONTROLLER_MOVE_EVENT = 1500U; + +const float DEFAULT_SIXENSE_RETICLE_MOVE_SPEED = 37.5f; +const bool DEFAULT_INVERT_SIXENSE_MOUSE_BUTTONS = false; + /// Handles interaction with the Sixense SDK (e.g., Razer Hydra). class SixenseManager : public QObject { Q_OBJECT @@ -36,6 +42,7 @@ public: ~SixenseManager(); void update(float deltaTime); + float getCursorPixelRangeMult() const; public slots: @@ -44,6 +51,7 @@ public slots: private: #ifdef HAVE_SIXENSE void updateCalibration(const sixenseControllerData* controllers); + void emulateMouse(PalmData* palm, int index); int _calibrationState; @@ -65,6 +73,12 @@ private: #endif quint64 _lastMovement; glm::vec3 _amountMoved; + + // for mouse emulation with the two controllers + bool _triggerPressed[2]; + bool _bumperPressed[2]; + int _oldX[2]; + int _oldY[2]; }; #endif // hifi_SixenseManager_h diff --git a/interface/src/location/LocationManager.cpp b/interface/src/location/LocationManager.cpp index f80c331df4..1d783cc8e7 100644 --- a/interface/src/location/LocationManager.cpp +++ b/interface/src/location/LocationManager.cpp @@ -16,10 +16,11 @@ const QString GET_USER_ADDRESS = "/api/v1/users/%1/address"; const QString GET_PLACE_ADDRESS = "/api/v1/places/%1/address"; +const QString GET_ADDRESSES = "/api/v1/addresses/%1"; const QString POST_PLACE_CREATE = "/api/v1/places/"; -LocationManager::LocationManager() : _userData(), _placeData() { +LocationManager::LocationManager() { }; @@ -74,59 +75,38 @@ void LocationManager::goTo(QString destination) { // go to coordinate destination or to Username if (!goToDestination(destination)) { - // reset data on local variables - _userData = QJsonObject(); - _placeData = QJsonObject(); - destination = QString(QUrl::toPercentEncoding(destination)); + JSONCallbackParameters callbackParams; callbackParams.jsonCallbackReceiver = this; - callbackParams.jsonCallbackMethod = "goToUserFromResponse"; - AccountManager::getInstance().authenticatedRequest(GET_USER_ADDRESS.arg(destination), - QNetworkAccessManager::GetOperation, - callbackParams); - - callbackParams.jsonCallbackMethod = "goToLocationFromResponse"; - AccountManager::getInstance().authenticatedRequest(GET_PLACE_ADDRESS.arg(destination), + callbackParams.jsonCallbackMethod = "goToAddressFromResponse"; + AccountManager::getInstance().authenticatedRequest(GET_ADDRESSES.arg(destination), QNetworkAccessManager::GetOperation, callbackParams); } } -void LocationManager::goToUserFromResponse(const QJsonObject& jsonObject) { - _userData = jsonObject; - checkForMultipleDestinations(); -} +void LocationManager::goToAddressFromResponse(const QJsonObject& responseData) { + QJsonValue status = responseData["status"]; + qDebug() << responseData; + if (!status.isUndefined() && status.toString() == "success") { + const QJsonObject& data = responseData["data"].toObject(); + const QJsonValue& userObject = data["user"]; + const QJsonValue& placeObject = data["place"]; -void LocationManager::goToLocationFromResponse(const QJsonObject& jsonObject) { - _placeData = jsonObject; - checkForMultipleDestinations(); -} - -void LocationManager::checkForMultipleDestinations() { - if (!_userData.isEmpty() && !_placeData.isEmpty()) { - if (_userData.contains("status") && _userData["status"].toString() == "success" && - _placeData.contains("status") && _placeData["status"].toString() == "success") { - emit multipleDestinationsFound(_userData, _placeData); - return; + if (!placeObject.isUndefined() && !userObject.isUndefined()) { + emit multipleDestinationsFound(userObject.toObject(), placeObject.toObject()); + } else if (placeObject.isUndefined()) { + Application::getInstance()->getAvatar()->goToLocationFromAddress(userObject.toObject()["address"].toObject()); + } else { + Application::getInstance()->getAvatar()->goToLocationFromAddress(placeObject.toObject()["address"].toObject()); } - - if (_userData.contains("status") && _userData["status"].toString() == "success") { - Application::getInstance()->getAvatar()->goToLocationFromResponse(_userData); - return; - } - - if (_placeData.contains("status") && _placeData["status"].toString() == "success") { - Application::getInstance()->getAvatar()->goToLocationFromResponse(_placeData); - return; - } - + } else { QMessageBox::warning(Application::getInstance()->getWindow(), "", "That user or location could not be found."); } } void LocationManager::goToUser(QString userName) { - JSONCallbackParameters callbackParams; callbackParams.jsonCallbackReceiver = Application::getInstance()->getAvatar(); callbackParams.jsonCallbackMethod = "goToLocationFromResponse"; diff --git a/interface/src/location/LocationManager.h b/interface/src/location/LocationManager.h index ac66b3d08b..b781f3f54e 100644 --- a/interface/src/location/LocationManager.h +++ b/interface/src/location/LocationManager.h @@ -39,11 +39,7 @@ public: bool goToDestination(QString destination); private: - QJsonObject _userData; - QJsonObject _placeData; - void replaceLastOccurrence(const QChar search, const QChar replace, QString& string); - void checkForMultipleDestinations(); signals: void creationCompleted(LocationManager::NamedLocationCreateResponse response); @@ -52,8 +48,7 @@ signals: private slots: void namedLocationDataReceived(const QJsonObject& data); void errorDataReceived(QNetworkReply::NetworkError error, const QString& message); - void goToLocationFromResponse(const QJsonObject& jsonObject); - void goToUserFromResponse(const QJsonObject& jsonObject); + void goToAddressFromResponse(const QJsonObject& jsonObject); }; diff --git a/interface/src/models/ModelTreeRenderer.cpp b/interface/src/models/ModelTreeRenderer.cpp index 68bebd7b99..b20bdce8f5 100644 --- a/interface/src/models/ModelTreeRenderer.cpp +++ b/interface/src/models/ModelTreeRenderer.cpp @@ -57,21 +57,29 @@ void ModelTreeRenderer::render(RenderMode renderMode) { const FBXGeometry* ModelTreeRenderer::getGeometryForModel(const ModelItem& modelItem) { const FBXGeometry* result = NULL; + Model* model = getModel(modelItem); if (model) { result = &model->getGeometry()->getFBXGeometry(); - } return result; } Model* ModelTreeRenderer::getModel(const ModelItem& modelItem) { Model* model = NULL; - + if (modelItem.isKnownID()) { if (_knownModelsItemModels.find(modelItem.getID()) != _knownModelsItemModels.end()) { model = _knownModelsItemModels[modelItem.getID()]; } else { + + // Make sure we only create new models on the thread that owns the ModelTreeRenderer + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "getModel", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(Model*, model), Q_ARG(const ModelItem&, modelItem)); + return model; + } + model = new Model(); model->init(); model->setURL(QUrl(modelItem.getModelURL())); @@ -81,6 +89,13 @@ Model* ModelTreeRenderer::getModel(const ModelItem& modelItem) { if (_unknownModelsItemModels.find(modelItem.getCreatorTokenID()) != _unknownModelsItemModels.end()) { model = _unknownModelsItemModels[modelItem.getCreatorTokenID()]; } else { + // Make sure we only create new models on the thread that owns the ModelTreeRenderer + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "getModel", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(Model*, model), Q_ARG(const ModelItem&, modelItem)); + return model; + } + model = new Model(); model->init(); model->setURL(QUrl(modelItem.getModelURL())); @@ -187,92 +202,122 @@ void ModelTreeRenderer::renderElement(OctreeElement* element, RenderArgs* args) if (drawAsModel) { glPushMatrix(); + { const float alpha = 1.0f; Model* model = getModel(modelItem); - - model->setScaleToFit(true, radius * 2.0f); - model->setSnapModelToCenter(true); - - // set the rotation - glm::quat rotation = modelItem.getModelRotation(); - model->setRotation(rotation); - - // set the position - model->setTranslation(position); - // handle animations.. - if (modelItem.hasAnimation()) { - if (!modelItem.jointsMapped()) { - QStringList modelJointNames = model->getJointNames(); - modelItem.mapJoints(modelJointNames); + if (model) { + model->setScaleToFit(true, radius * 2.0f); + model->setSnapModelToCenter(true); + + // set the rotation + glm::quat rotation = modelItem.getModelRotation(); + model->setRotation(rotation); + + // set the position + model->setTranslation(position); + + // handle animations.. + if (modelItem.hasAnimation()) { + if (!modelItem.jointsMapped()) { + QStringList modelJointNames = model->getJointNames(); + modelItem.mapJoints(modelJointNames); + } + + QVector frameData = modelItem.getAnimationFrame(); + for (int i = 0; i < frameData.size(); i++) { + model->setJointState(i, true, frameData[i]); + } + } + + // make sure to simulate so everything gets set up correctly for rendering + model->simulate(0.0f); + + // TODO: should we allow modelItems to have alpha on their models? + Model::RenderMode modelRenderMode = args->_renderMode == OctreeRenderer::SHADOW_RENDER_MODE + ? Model::SHADOW_RENDER_MODE : Model::DEFAULT_RENDER_MODE; + + if (modelItem.getGlowLevel() > 0.0f) { + Glower glower(modelItem.getGlowLevel()); + + if (model->isActive()) { + model->render(alpha, modelRenderMode); + } else { + // if we couldn't get a model, then just draw a sphere + glColor3ub(modelItem.getColor()[RED_INDEX],modelItem.getColor()[GREEN_INDEX],modelItem.getColor()[BLUE_INDEX]); + glPushMatrix(); + glTranslatef(position.x, position.y, position.z); + glutSolidSphere(radius, 15, 15); + glPopMatrix(); + } + } else { + if (model->isActive()) { + model->render(alpha, modelRenderMode); + } else { + // if we couldn't get a model, then just draw a sphere + glColor3ub(modelItem.getColor()[RED_INDEX],modelItem.getColor()[GREEN_INDEX],modelItem.getColor()[BLUE_INDEX]); + glPushMatrix(); + glTranslatef(position.x, position.y, position.z); + glutSolidSphere(radius, 15, 15); + glPopMatrix(); + } } - QVector frameData = modelItem.getAnimationFrame(); - for (int i = 0; i < frameData.size(); i++) { - model->setJointState(i, true, frameData[i]); - } - } - - // make sure to simulate so everything gets set up correctly for rendering - model->simulate(0.0f); + if (!isShadowMode && displayModelBounds) { - // TODO: should we allow modelItems to have alpha on their models? - Model::RenderMode modelRenderMode = args->_renderMode == OctreeRenderer::SHADOW_RENDER_MODE - ? Model::SHADOW_RENDER_MODE : Model::DEFAULT_RENDER_MODE; - - if (modelItem.getGlowLevel() > 0.0f) { - Glower glower(modelItem.getGlowLevel()); - model->render(alpha, modelRenderMode); + glm::vec3 unRotatedMinimum = model->getUnscaledMeshExtents().minimum; + glm::vec3 unRotatedMaximum = model->getUnscaledMeshExtents().maximum; + glm::vec3 unRotatedExtents = unRotatedMaximum - unRotatedMinimum; + + float width = unRotatedExtents.x; + float height = unRotatedExtents.y; + float depth = unRotatedExtents.z; + + Extents rotatedExtents = model->getUnscaledMeshExtents(); + calculateRotatedExtents(rotatedExtents, rotation); + + glm::vec3 rotatedSize = rotatedExtents.maximum - rotatedExtents.minimum; + + const glm::vec3& modelScale = model->getScale(); + + glPushMatrix(); + glTranslatef(position.x, position.y, position.z); + + // draw the orignal bounding cube + glColor4f(1.0f, 1.0f, 0.0f, 1.0f); + glutWireCube(size); + + // draw the rotated bounding cube + glColor4f(0.0f, 0.0f, 1.0f, 1.0f); + glPushMatrix(); + glScalef(rotatedSize.x * modelScale.x, rotatedSize.y * modelScale.y, rotatedSize.z * modelScale.z); + glutWireCube(1.0); + glPopMatrix(); + + // draw the model relative bounding box + glm::vec3 axis = glm::axis(rotation); + glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); + glScalef(width * modelScale.x, height * modelScale.y, depth * modelScale.z); + glColor3f(0.0f, 1.0f, 0.0f); + glutWireCube(1.0); + + glPopMatrix(); + + } } else { - model->render(alpha, modelRenderMode); - } - - if (!isShadowMode && displayModelBounds) { - - glm::vec3 unRotatedMinimum = model->getUnscaledMeshExtents().minimum; - glm::vec3 unRotatedMaximum = model->getUnscaledMeshExtents().maximum; - glm::vec3 unRotatedExtents = unRotatedMaximum - unRotatedMinimum; - - float width = unRotatedExtents.x; - float height = unRotatedExtents.y; - float depth = unRotatedExtents.z; - - Extents rotatedExtents = model->getUnscaledMeshExtents(); - calculateRotatedExtents(rotatedExtents, rotation); - - glm::vec3 rotatedSize = rotatedExtents.maximum - rotatedExtents.minimum; - - const glm::vec3& modelScale = model->getScale(); - + // if we couldn't get a model, then just draw a sphere + glColor3ub(modelItem.getColor()[RED_INDEX],modelItem.getColor()[GREEN_INDEX],modelItem.getColor()[BLUE_INDEX]); glPushMatrix(); glTranslatef(position.x, position.y, position.z); - - // draw the orignal bounding cube - glColor4f(1.0f, 1.0f, 0.0f, 1.0f); - glutWireCube(size); - - // draw the rotated bounding cube - glColor4f(0.0f, 0.0f, 1.0f, 1.0f); - glPushMatrix(); - glScalef(rotatedSize.x * modelScale.x, rotatedSize.y * modelScale.y, rotatedSize.z * modelScale.z); - glutWireCube(1.0); - glPopMatrix(); - - // draw the model relative bounding box - glm::vec3 axis = glm::axis(rotation); - glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); - glScalef(width * modelScale.x, height * modelScale.y, depth * modelScale.z); - glColor3f(0.0f, 1.0f, 0.0f); - glutWireCube(1.0); - + glutSolidSphere(radius, 15, 15); glPopMatrix(); - } - + } glPopMatrix(); } else { - glColor3ub(modelItem.getColor()[RED_INDEX],modelItem.getColor()[GREEN_INDEX],modelItem.getColor()[BLUE_INDEX]); + //glColor3ub(modelItem.getColor()[RED_INDEX],modelItem.getColor()[GREEN_INDEX],modelItem.getColor()[BLUE_INDEX]); + glColor3f(1.0f, 0.0f, 0.0f); glPushMatrix(); glTranslatef(position.x, position.y, position.z); glutSolidSphere(radius, 15, 15); diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index f5de7459a8..471ff20d66 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -16,14 +16,25 @@ #include "Application.h" #include "ApplicationOverlay.h" +#include "devices/OculusManager.h" #include "ui/Stats.h" +// Fast helper functions +inline float max(float a, float b) { + return (a > b) ? a : b; +} + +inline float min(float a, float b) { + return (a < b) ? a : b; +} + ApplicationOverlay::ApplicationOverlay() : _framebufferObject(NULL), _oculusAngle(65.0f * RADIANS_PER_DEGREE), _distance(0.5f), - _uiType(HEMISPHERE) { + _textureFov(DEFAULT_OCULUS_UI_ANGULAR_SIZE * RADIANS_PER_DEGREE), + _crosshairTexture(0) { } @@ -35,23 +46,19 @@ ApplicationOverlay::~ApplicationOverlay() { const float WHITE_TEXT[] = { 0.93f, 0.93f, 0.93f }; +const float RETICLE_COLOR[] = { 0.0f, 198.0f / 255.0f, 244.0f / 255.0f }; + // Renders the overlays either to a texture or to the screen void ApplicationOverlay::renderOverlay(bool renderToTexture) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "ApplicationOverlay::displayOverlay()"); + _textureFov = Menu::getInstance()->getOculusUIAngularSize() * RADIANS_PER_DEGREE; + Application* application = Application::getInstance(); Overlays& overlays = application->getOverlays(); QGLWidget* glWidget = application->getGLWidget(); MyAvatar* myAvatar = application->getAvatar(); - Audio* audio = application->getAudio(); - const VoxelPacketProcessor& voxelPacketProcessor = application->getVoxelPacketProcessor(); - BandwidthMeter* bandwidthMeter = application->getBandwidthMeter(); - NodeBounds& nodeBoundsDisplay = application->getNodeBoundsDisplay(); - - int mouseX = application->getMouseX(); - int mouseY = application->getMouseY(); - bool renderPointer = renderToTexture; if (renderToTexture) { getFramebufferObject()->bind(); @@ -60,7 +67,7 @@ void ApplicationOverlay::renderOverlay(bool renderToTexture) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - // Render 2D overlay: I/O level bar graphs and text + // Render 2D overlay glMatrixMode(GL_PROJECTION); glPushMatrix(); @@ -69,6 +76,382 @@ void ApplicationOverlay::renderOverlay(bool renderToTexture) { glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING); + renderAudioMeter(); + + if (Menu::getInstance()->isOptionChecked(MenuOption::HeadMouse)) { + myAvatar->renderHeadMouse(glWidget->width(), glWidget->height()); + } + + renderStatsAndLogs(); + + // give external parties a change to hook in + emit application->renderingOverlay(); + + overlays.render2D(); + + renderPointers(); + + glPopMatrix(); + + glMatrixMode(GL_MODELVIEW); + glEnable(GL_DEPTH_TEST); + glEnable(GL_LIGHTING); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); + + if (renderToTexture) { + getFramebufferObject()->release(); + } +} + +// Draws the FBO texture for the screen +void ApplicationOverlay::displayOverlayTexture(Camera& whichCamera) { + + Application* application = Application::getInstance(); + QGLWidget* glWidget = application->getGLWidget(); + + glEnable(GL_TEXTURE_2D); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, getFramebufferObject()->texture()); + + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + + glLoadIdentity(); + gluOrtho2D(0, glWidget->width(), glWidget->height(), 0); + glDisable(GL_DEPTH_TEST); + glDisable(GL_LIGHTING); + + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2i(0, glWidget->height()); + glTexCoord2f(1, 0); glVertex2i(glWidget->width(), glWidget->height()); + glTexCoord2f(1, 1); glVertex2i(glWidget->width(), 0); + glTexCoord2f(0, 1); glVertex2i(0, 0); + glEnd(); + + glPopMatrix(); + glDisable(GL_TEXTURE_2D); +} + +void ApplicationOverlay::computeOculusPickRay(float x, float y, glm::vec3& direction) const { + glm::quat rot = Application::getInstance()->getAvatar()->getOrientation(); + + //invert y direction + y = 1.0 - y; + + //Get position on hemisphere UI + x = sin((x - 0.5f) * _textureFov); + y = sin((y - 0.5f) * _textureFov); + + float dist = sqrt(x * x + y * y); + float z = -sqrt(1.0f - dist * dist); + + //Rotate the UI pick ray by the avatar orientation + direction = glm::normalize(rot * glm::vec3(x, y, z)); +} + +// Draws the FBO texture for Oculus rift. TODO: Draw a curved texture instead of plane. +void ApplicationOverlay::displayOverlayTextureOculus(Camera& whichCamera) { + + Application* application = Application::getInstance(); + + QGLWidget* glWidget = application->getGLWidget(); + MyAvatar* myAvatar = application->getAvatar(); + const glm::vec3& viewMatrixTranslation = application->getViewMatrixTranslation(); + + + glActiveTexture(GL_TEXTURE0); + + glEnable(GL_BLEND); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); + glBindTexture(GL_TEXTURE_2D, getFramebufferObject()->texture()); + glEnable(GL_DEPTH_TEST); + glDisable(GL_LIGHTING); + glEnable(GL_TEXTURE_2D); + + glMatrixMode(GL_MODELVIEW); + + glPushMatrix(); + glLoadIdentity(); + // Transform to world space + glm::quat rotation = whichCamera.getRotation(); + glm::vec3 axis2 = glm::axis(rotation); + glRotatef(-glm::degrees(glm::angle(rotation)), axis2.x, axis2.y, axis2.z); + glTranslatef(viewMatrixTranslation.x, viewMatrixTranslation.y, viewMatrixTranslation.z); + + // Translate to the front of the camera + glm::vec3 pos = whichCamera.getPosition(); + glm::quat rot = myAvatar->getOrientation(); + glm::vec3 axis = glm::axis(rot); + + glTranslatef(pos.x, pos.y, pos.z); + glRotatef(glm::degrees(glm::angle(rot)), axis.x, axis.y, axis.z); + + glColor3f(1.0f, 1.0f, 1.0f); + + glDepthMask(GL_TRUE); + + glEnable(GL_ALPHA_TEST); + glAlphaFunc(GL_GREATER, 0.01f); + + //Draw the magnifiers + for (int i = 0; i < _numMagnifiers; i++) { + renderMagnifier(_mouseX[i], _mouseY[i]); + } + + glDepthMask(GL_FALSE); + glDisable(GL_ALPHA_TEST); + + renderTexturedHemisphere(); + + renderControllerPointersOculus(); + + glPopMatrix(); + + glDepthMask(GL_TRUE); + glBindTexture(GL_TEXTURE_2D, 0); + glDisable(GL_TEXTURE_2D); + + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); + glEnable(GL_LIGHTING); + +} + +//Renders optional pointers +void ApplicationOverlay::renderPointers() { + Application* application = Application::getInstance(); + // Render a crosshair over the mouse when in Oculus + _numMagnifiers = 0; + int mouseX = application->getMouseX(); + int mouseY = application->getMouseY(); + + //lazily load crosshair texture + if (_crosshairTexture == 0) { + _crosshairTexture = Application::getInstance()->getGLWidget()->bindTexture(QImage(Application::resourcesPath() + "images/sixense-reticle.png")); + } + glEnable(GL_TEXTURE_2D); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, _crosshairTexture); + + + if (OculusManager::isConnected() && application->getLastMouseMoveType() == QEvent::MouseMove) { + //If we are in oculus, render reticle later + _numMagnifiers = 1; + _mouseX[0] = application->getMouseX(); + _mouseY[0] = application->getMouseY(); + } else if (application->getLastMouseMoveType() == CONTROLLER_MOVE_EVENT && Menu::getInstance()->isOptionChecked(MenuOption::SixenseMouseInput)) { + //only render controller pointer if we aren't already rendering a mouse pointer + renderControllerPointers(); + } + glBindTexture(GL_TEXTURE_2D, 0); + glDisable(GL_TEXTURE_2D); + +} + +void ApplicationOverlay::renderControllerPointers() { + Application* application = Application::getInstance(); + QGLWidget* glWidget = application->getGLWidget(); + MyAvatar* myAvatar = application->getAvatar(); + + const HandData* handData = Application::getInstance()->getAvatar()->getHandData(); + + for (unsigned int palmIndex = 2; palmIndex < 4; palmIndex++) { + const PalmData* palmData = NULL; + + if (palmIndex >= handData->getPalms().size()) { + return; + } + + if (handData->getPalms()[palmIndex].isActive()) { + palmData = &handData->getPalms()[palmIndex]; + } else { + continue; + } + + // Get directon relative to avatar orientation + glm::vec3 direction = glm::inverse(myAvatar->getOrientation()) * palmData->getFingerDirection(); + + // Get the angles, scaled between (-0.5,0.5) + float xAngle = (atan2(direction.z, direction.x) + M_PI_2) ; + float yAngle = 0.5f - ((atan2(direction.z, direction.y) + M_PI_2)); + + // Get the pixel range over which the xAngle and yAngle are scaled + float cursorRange = glWidget->width() * application->getSixenseManager()->getCursorPixelRangeMult(); + + int mouseX = glWidget->width() / 2.0f + cursorRange * xAngle; + int mouseY = glWidget->height() / 2.0f + cursorRange * yAngle; + + //If the cursor is out of the screen then don't render it + if (mouseX < 0 || mouseX >= glWidget->width() || mouseY < 0 || mouseY >= glWidget->height()) { + continue; + } + + float pointerWidth = 40; + float pointerHeight = 40; + + //if we have the oculus, we should make the cursor smaller since it will be + //magnified + if (OculusManager::isConnected()) { + + _mouseX[_numMagnifiers] = mouseX; + _mouseY[_numMagnifiers] = mouseY; + _numMagnifiers++; + //If oculus is enabled, we draw the crosshairs later + continue; + } + + mouseX -= pointerWidth / 2.0f; + mouseY += pointerHeight / 2.0f; + + glBegin(GL_QUADS); + + glColor3f(RETICLE_COLOR[0], RETICLE_COLOR[1], RETICLE_COLOR[2]); + + glTexCoord2d(0.0f, 0.0f); glVertex2i(mouseX, mouseY); + glTexCoord2d(1.0f, 0.0f); glVertex2i(mouseX + pointerWidth, mouseY); + glTexCoord2d(1.0f, 1.0f); glVertex2i(mouseX + pointerWidth, mouseY - pointerHeight); + glTexCoord2d(0.0f, 1.0f); glVertex2i(mouseX, mouseY - pointerHeight); + + glEnd(); + } +} + +void ApplicationOverlay::renderControllerPointersOculus() { + Application* application = Application::getInstance(); + QGLWidget* glWidget = application->getGLWidget(); + + const int widgetWidth = glWidget->width(); + const int widgetHeight = glWidget->height(); + + const float reticleSize = 50.0f; + + glBindTexture(GL_TEXTURE_2D, _crosshairTexture); + glDisable(GL_DEPTH_TEST); + + for (int i = 0; i < _numMagnifiers; i++) { + + float mouseX = (float)_mouseX[i]; + float mouseY = (float)_mouseY[i]; + mouseX -= reticleSize / 2; + mouseY += reticleSize / 2; + + //Get new UV coordinates from our magnification window + float newULeft = mouseX / widgetWidth; + float newURight = (mouseX + reticleSize) / widgetWidth; + float newVBottom = 1.0 - mouseY / widgetHeight; + float newVTop = 1.0 - (mouseY - reticleSize) / widgetHeight; + + // Project our position onto the hemisphere using the UV coordinates + float lX = sin((newULeft - 0.5f) * _textureFov); + float rX = sin((newURight - 0.5f) * _textureFov); + float bY = sin((newVBottom - 0.5f) * _textureFov); + float tY = sin((newVTop - 0.5f) * _textureFov); + + float dist; + //Bottom Left + dist = sqrt(lX * lX + bY * bY); + float blZ = sqrt(1.0f - dist * dist); + //Top Left + dist = sqrt(lX * lX + tY * tY); + float tlZ = sqrt(1.0f - dist * dist); + //Bottom Right + dist = sqrt(rX * rX + bY * bY); + float brZ = sqrt(1.0f - dist * dist); + //Top Right + dist = sqrt(rX * rX + tY * tY); + float trZ = sqrt(1.0f - dist * dist); + + glBegin(GL_QUADS); + + glColor3f(RETICLE_COLOR[0], RETICLE_COLOR[1], RETICLE_COLOR[2]); + + glTexCoord2f(0.0f, 0.0f); glVertex3f(lX, tY, -tlZ); + glTexCoord2f(1.0f, 0.0f); glVertex3f(rX, tY, -trZ); + glTexCoord2f(1.0f, 1.0f); glVertex3f(rX, bY, -brZ); + glTexCoord2f(0.0f, 1.0f); glVertex3f(lX, bY, -blZ); + + glEnd(); + + } + glEnable(GL_DEPTH_TEST); +} + +//Renders a small magnification of the currently bound texture at the coordinates +void ApplicationOverlay::renderMagnifier(int mouseX, int mouseY) +{ + Application* application = Application::getInstance(); + QGLWidget* glWidget = application->getGLWidget(); + + + const int widgetWidth = glWidget->width(); + const int widgetHeight = glWidget->height(); + const float magnification = 4.0f; + + float magnifyWidth = 80.0f; + float magnifyHeight = 60.0f; + + mouseX -= magnifyWidth / 2; + mouseY -= magnifyHeight / 2; + + float newWidth = magnifyWidth * magnification; + float newHeight = magnifyHeight * magnification; + + // Magnification Texture Coordinates + float magnifyULeft = mouseX / (float)widgetWidth; + float magnifyURight = (mouseX + magnifyWidth) / (float)widgetWidth; + float magnifyVBottom = 1.0f - mouseY / (float)widgetHeight; + float magnifyVTop = 1.0f - (mouseY + magnifyHeight) / (float)widgetHeight; + + // Coordinates of magnification overlay + float newMouseX = (mouseX + magnifyWidth / 2) - newWidth / 2.0f; + float newMouseY = (mouseY + magnifyHeight / 2) + newHeight / 2.0f; + + // Get position on hemisphere using angle + + //Get new UV coordinates from our magnification window + float newULeft = newMouseX / widgetWidth; + float newURight = (newMouseX + newWidth) / widgetWidth; + float newVBottom = 1.0 - newMouseY / widgetHeight; + float newVTop = 1.0 - (newMouseY - newHeight) / widgetHeight; + + // Project our position onto the hemisphere using the UV coordinates + float lX = sin((newULeft - 0.5f) * _textureFov); + float rX = sin((newURight - 0.5f) * _textureFov); + float bY = sin((newVBottom - 0.5f) * _textureFov); + float tY = sin((newVTop - 0.5f) * _textureFov); + + float dist; + //Bottom Left + dist = sqrt(lX * lX + bY * bY); + float blZ = sqrt(1.0f - dist * dist); + //Top Left + dist = sqrt(lX * lX + tY * tY); + float tlZ = sqrt(1.0f - dist * dist); + //Bottom Right + dist = sqrt(rX * rX + bY * bY); + float brZ = sqrt(1.0f - dist * dist); + //Top Right + dist = sqrt(rX * rX + tY * tY); + float trZ = sqrt(1.0f - dist * dist); + + glBegin(GL_QUADS); + + glTexCoord2f(magnifyULeft, magnifyVBottom); glVertex3f(lX, tY, -tlZ); + glTexCoord2f(magnifyURight, magnifyVBottom); glVertex3f(rX, tY, -trZ); + glTexCoord2f(magnifyURight, magnifyVTop); glVertex3f(rX, bY, -brZ); + glTexCoord2f(magnifyULeft, magnifyVTop); glVertex3f(lX, bY, -blZ); + + glEnd(); + +} + +void ApplicationOverlay::renderAudioMeter() { + + Application* application = Application::getInstance(); + + QGLWidget* glWidget = application->getGLWidget(); + Audio* audio = application->getAudio(); + // Display a single screen-size quad to create an alpha blended 'collision' flash if (audio->getCollisionFlashesScreen()) { float collisionSoundMagnitude = audio->getCollisionSoundMagnitude(); @@ -187,11 +570,16 @@ void ApplicationOverlay::renderOverlay(bool renderToTexture) { glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET + audioLevel, audioMeterY + AUDIO_METER_HEIGHT - AUDIO_METER_INSET); glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET, audioMeterY + AUDIO_METER_HEIGHT - AUDIO_METER_INSET); glEnd(); +} +void ApplicationOverlay::renderStatsAndLogs() { - if (Menu::getInstance()->isOptionChecked(MenuOption::HeadMouse)) { - myAvatar->renderHeadMouse(glWidget->width(), glWidget->height()); - } + Application* application = Application::getInstance(); + + QGLWidget* glWidget = application->getGLWidget(); + const OctreePacketProcessor& octreePacketProcessor = application->getOctreePacketProcessor(); + BandwidthMeter* bandwidthMeter = application->getBandwidthMeter(); + NodeBounds& nodeBoundsDisplay = application->getNodeBoundsDisplay(); // Display stats and log text onscreen glLineWidth(1.0f); @@ -200,7 +588,7 @@ void ApplicationOverlay::renderOverlay(bool renderToTexture) { if (Menu::getInstance()->isOptionChecked(MenuOption::Stats)) { // let's set horizontal offset to give stats some margin to mirror int horizontalOffset = MIRROR_VIEW_WIDTH + MIRROR_VIEW_LEFT_PADDING * 2; - int voxelPacketsToProcess = voxelPacketProcessor.packetsToProcessCount(); + int voxelPacketsToProcess = octreePacketProcessor.packetsToProcessCount(); // Onscreen text about position, servers, etc Stats::getInstance()->display(WHITE_TEXT, horizontalOffset, application->getFps(), application->getPacketsPerSecond(), application->getBytesPerSecond(), voxelPacketsToProcess); // Bandwidth meter @@ -222,346 +610,22 @@ void ApplicationOverlay::renderOverlay(bool renderToTexture) { drawText(glWidget->width() - 100, glWidget->height() - timerBottom, 0.30f, 0.0f, 0, frameTimer, WHITE_TEXT); } nodeBoundsDisplay.drawOverlay(); - - // give external parties a change to hook in - emit application->renderingOverlay(); - - overlays.render2D(); - - // Render a crosshair over the pointer when in Oculus - if (renderPointer) { - const float pointerWidth = 10; - const float pointerHeight = 10; - const float crossPad = 4; - - mouseX -= pointerWidth / 2.0f; - mouseY += pointerHeight / 2.0f; - - glBegin(GL_QUADS); - - glColor3f(1, 0, 0); - - //Horizontal crosshair - glVertex2i(mouseX, mouseY - crossPad); - glVertex2i(mouseX + pointerWidth, mouseY - crossPad); - glVertex2i(mouseX + pointerWidth, mouseY - pointerHeight + crossPad); - glVertex2i(mouseX, mouseY - pointerHeight + crossPad); - - //Vertical crosshair - glVertex2i(mouseX + crossPad, mouseY); - glVertex2i(mouseX + pointerWidth - crossPad, mouseY); - glVertex2i(mouseX + pointerWidth - crossPad, mouseY - pointerHeight); - glVertex2i(mouseX + crossPad, mouseY - pointerHeight); - - glEnd(); - } - - glPopMatrix(); - - glMatrixMode(GL_MODELVIEW); - glEnable(GL_DEPTH_TEST); - glEnable(GL_LIGHTING); - glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); - - if (renderToTexture) { - getFramebufferObject()->release(); - } -} - -// Draws the FBO texture for the screen -void ApplicationOverlay::displayOverlayTexture(Camera& whichCamera) { - - Application* application = Application::getInstance(); - QGLWidget* glWidget = application->getGLWidget(); - - glEnable(GL_TEXTURE_2D); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, getFramebufferObject()->texture()); - - glMatrixMode(GL_PROJECTION); - glPushMatrix(); - - glLoadIdentity(); - gluOrtho2D(0, glWidget->width(), glWidget->height(), 0); - glDisable(GL_DEPTH_TEST); - glDisable(GL_LIGHTING); - - glBegin(GL_QUADS); - glTexCoord2f(0, 0); glVertex2i(0, glWidget->height()); - glTexCoord2f(1, 0); glVertex2i(glWidget->width(), glWidget->height()); - glTexCoord2f(1, 1); glVertex2i(glWidget->width(), 0); - glTexCoord2f(0, 1); glVertex2i(0, 0); - glEnd(); - - glPopMatrix(); - glDisable(GL_TEXTURE_2D); -} - -const float textureFov = PI / 2.5f; - -void ApplicationOverlay::computeOculusPickRay(float x, float y, glm::vec3& direction) const { - glm::quat rot = Application::getInstance()->getAvatar()->getOrientation(); - - //invert y direction - y = 1.0 - y; - - //Get position on hemisphere UI - x = sin((x - 0.5f) * textureFov); - y = sin((y - 0.5f) * textureFov); - - float dist = sqrt(x * x + y * y); - float z = -sqrt(1.0f - dist * dist); - - //Rotate the UI pick ray by the avatar orientation - direction = glm::normalize(rot * glm::vec3(x, y, z)); -} - -// Fast helper functions -inline float max(float a, float b) { - return (a > b) ? a : b; -} - -inline float min(float a, float b) { - return (a < b) ? a : b; -} - -// Draws the FBO texture for Oculus rift. TODO: Draw a curved texture instead of plane. -void ApplicationOverlay::displayOverlayTextureOculus(Camera& whichCamera) { - - Application* application = Application::getInstance(); - - QGLWidget* glWidget = application->getGLWidget(); - MyAvatar* myAvatar = application->getAvatar(); - const glm::vec3& viewMatrixTranslation = application->getViewMatrixTranslation(); - - int mouseX = application->getMouseX(); - int mouseY = application->getMouseY(); - const int widgetWidth = glWidget->width(); - const int widgetHeight = glWidget->height(); - float magnifyWidth = 80.0f; - float magnifyHeight = 60.0f; - const float magnification = 4.0f; - - // Get vertical FoV of the displayed overlay texture - const float halfVerticalAngle = _oculusAngle / 2.0f; - const float overlayAspectRatio = glWidget->width() / (float)glWidget->height(); - const float halfOverlayHeight = _distance * tan(halfVerticalAngle); - const float overlayHeight = halfOverlayHeight * 2.0f; - - // The more vertices, the better the curve - const int numHorizontalVertices = 20; - const int numVerticalVertices = 20; - // U texture coordinate width at each quad - const float quadTexWidth = 1.0f / (numHorizontalVertices - 1); - const float quadTexHeight = 1.0f / (numVerticalVertices - 1); - - // Get horizontal angle and angle increment from vertical angle and aspect ratio - const float horizontalAngle = halfVerticalAngle * 2.0f * overlayAspectRatio; - const float angleIncrement = horizontalAngle / (numHorizontalVertices - 1); - const float halfHorizontalAngle = horizontalAngle / 2; - - const float verticalAngleIncrement = _oculusAngle / (numVerticalVertices - 1); - - glActiveTexture(GL_TEXTURE0); - - glEnable(GL_BLEND); - glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); - glBindTexture(GL_TEXTURE_2D, getFramebufferObject()->texture()); - glEnable(GL_DEPTH_TEST); - glDisable(GL_LIGHTING); - glEnable(GL_TEXTURE_2D); - - glMatrixMode(GL_MODELVIEW); - - glPushMatrix(); - glLoadIdentity(); - // Transform to world space - glm::quat rotation = whichCamera.getRotation(); - glm::vec3 axis2 = glm::axis(rotation); - glRotatef(-glm::degrees(glm::angle(rotation)), axis2.x, axis2.y, axis2.z); - glTranslatef(viewMatrixTranslation.x, viewMatrixTranslation.y, viewMatrixTranslation.z); - - // Translate to the front of the camera - glm::vec3 pos = whichCamera.getPosition(); - glm::quat rot = myAvatar->getOrientation(); - glm::vec3 axis = glm::axis(rot); - - glTranslatef(pos.x, pos.y, pos.z); - glRotatef(glm::degrees(glm::angle(rot)), axis.x, axis.y, axis.z); - - glColor3f(1.0f, 1.0f, 1.0f); - - glDepthMask(GL_TRUE); - - glEnable(GL_ALPHA_TEST); - glAlphaFunc(GL_GREATER, 0.01f); - - //Draw the magnifying glass - - mouseX -= magnifyWidth / 2; - mouseY -= magnifyHeight / 2; - - //clamp the magnification - if (mouseX < 0) { - magnifyWidth += mouseX; - mouseX = 0; - } else if (mouseX + magnifyWidth > widgetWidth) { - magnifyWidth = widgetWidth - mouseX; - } - if (mouseY < 0) { - magnifyHeight += mouseY; - mouseY = 0; - } else if (mouseY + magnifyHeight > widgetHeight) { - magnifyHeight = widgetHeight - mouseY; - } - - const float halfMagnifyHeight = magnifyHeight / 2.0f; - - float newWidth = magnifyWidth * magnification; - float newHeight = magnifyHeight * magnification; - - // Magnification Texture Coordinates - float magnifyULeft = mouseX / (float)widgetWidth; - float magnifyURight = (mouseX + magnifyWidth) / (float)widgetWidth; - float magnifyVBottom = 1.0f - mouseY / (float)widgetHeight; - float magnifyVTop = 1.0f - (mouseY + magnifyHeight) / (float)widgetHeight; - - // Coordinates of magnification overlay - float newMouseX = (mouseX + magnifyWidth / 2) - newWidth / 2.0f; - float newMouseY = (mouseY + magnifyHeight / 2) + newHeight / 2.0f; - - // Get angle on the UI - float leftAngle = (newMouseX / (float)widgetWidth) * horizontalAngle - halfHorizontalAngle; - float rightAngle = ((newMouseX + newWidth) / (float)widgetWidth) * horizontalAngle - halfHorizontalAngle; - - float bottomAngle = (newMouseY / (float)widgetHeight) * _oculusAngle - halfVerticalAngle; - float topAngle = ((newMouseY - newHeight) / (float)widgetHeight) * _oculusAngle - halfVerticalAngle; - - float leftX, rightX, leftZ, rightZ, topZ, bottomZ; - - // Get position on hemisphere using angle - if (_uiType == HEMISPHERE) { - - //Get new UV coordinates from our magnification window - float newULeft = newMouseX / widgetWidth; - float newURight = (newMouseX + newWidth) / widgetWidth; - float newVBottom = 1.0 - newMouseY / widgetHeight; - float newVTop = 1.0 - (newMouseY - newHeight) / widgetHeight; - - // Project our position onto the hemisphere using the UV coordinates - float lX = sin((newULeft - 0.5f) * textureFov); - float rX = sin((newURight - 0.5f) * textureFov); - float bY = sin((newVBottom - 0.5f) * textureFov); - float tY = sin((newVTop - 0.5f) * textureFov); - - float dist; - //Bottom Left - dist = sqrt(lX * lX + bY * bY); - float blZ = sqrt(1.0f - dist * dist); - //Top Left - dist = sqrt(lX * lX + tY * tY); - float tlZ = sqrt(1.0f - dist * dist); - //Bottom Right - dist = sqrt(rX * rX + bY * bY); - float brZ = sqrt(1.0f - dist * dist); - //Top Right - dist = sqrt(rX * rX + tY * tY); - float trZ = sqrt(1.0f - dist * dist); - - glBegin(GL_QUADS); - - glTexCoord2f(magnifyULeft, magnifyVBottom); glVertex3f(lX, tY, -tlZ); - glTexCoord2f(magnifyURight, magnifyVBottom); glVertex3f(rX, tY, -trZ); - glTexCoord2f(magnifyURight, magnifyVTop); glVertex3f(rX, bY, -brZ); - glTexCoord2f(magnifyULeft, magnifyVTop); glVertex3f(lX, bY, -blZ); - - glEnd(); - - } else { - leftX = sin(leftAngle) * _distance; - rightX = sin(rightAngle) * _distance; - leftZ = -cos(leftAngle) * _distance; - rightZ = -cos(rightAngle) * _distance; - if (_uiType == CURVED_SEMICIRCLE) { - topZ = -cos(topAngle * overlayAspectRatio) * _distance; - bottomZ = -cos(bottomAngle * overlayAspectRatio) * _distance; - } else { - // Dont want to use topZ or bottomZ for SEMICIRCLE - topZ = -99999; - bottomZ = -99999; - } - - float bottomY = (1.0 - newMouseY / (float)widgetHeight) * halfOverlayHeight * 2.0f - halfOverlayHeight; - float topY = bottomY + (newHeight / widgetHeight) * halfOverlayHeight * 2; - - //TODO: Remove immediate mode in favor of VBO - glBegin(GL_QUADS); - - glTexCoord2f(magnifyULeft, magnifyVBottom); glVertex3f(leftX, topY, max(topZ, leftZ)); - glTexCoord2f(magnifyURight, magnifyVBottom); glVertex3f(rightX, topY, max(topZ, rightZ)); - glTexCoord2f(magnifyURight, magnifyVTop); glVertex3f(rightX, bottomY, max(bottomZ, rightZ)); - glTexCoord2f(magnifyULeft, magnifyVTop); glVertex3f(leftX, bottomY, max(bottomZ, leftZ)); - - glEnd(); - } - glDepthMask(GL_FALSE); - glDisable(GL_ALPHA_TEST); - - //TODO: Remove immediate mode in favor of VBO - if (_uiType == HEMISPHERE) { - renderTexturedHemisphere(); - } else{ - glBegin(GL_QUADS); - // Place the vertices in a semicircle curve around the camera - for (int i = 0; i < numHorizontalVertices - 1; i++) { - for (int j = 0; j < numVerticalVertices - 1; j++) { - - // Calculate the X and Z coordinates from the angles and radius from camera - leftX = sin(angleIncrement * i - halfHorizontalAngle) * _distance; - rightX = sin(angleIncrement * (i + 1) - halfHorizontalAngle) * _distance; - leftZ = -cos(angleIncrement * i - halfHorizontalAngle) * _distance; - rightZ = -cos(angleIncrement * (i + 1) - halfHorizontalAngle) * _distance; - if (_uiType == 2) { - topZ = -cos((verticalAngleIncrement * (j + 1) - halfVerticalAngle) * overlayAspectRatio) * _distance; - bottomZ = -cos((verticalAngleIncrement * j - halfVerticalAngle) * overlayAspectRatio) * _distance; - } else { - topZ = -99999; - bottomZ = -99999; - } - - glTexCoord2f(quadTexWidth * i, (j + 1) * quadTexHeight); - glVertex3f(leftX, (j + 1) * quadTexHeight * overlayHeight - halfOverlayHeight, max(topZ, leftZ)); - glTexCoord2f(quadTexWidth * (i + 1), (j + 1) * quadTexHeight); - glVertex3f(rightX, (j + 1) * quadTexHeight * overlayHeight - halfOverlayHeight, max(topZ, rightZ)); - glTexCoord2f(quadTexWidth * (i + 1), j * quadTexHeight); - glVertex3f(rightX, j * quadTexHeight * overlayHeight - halfOverlayHeight, max(bottomZ, rightZ)); - glTexCoord2f(quadTexWidth * i, j * quadTexHeight); - glVertex3f(leftX, j * quadTexHeight * overlayHeight - halfOverlayHeight, max(bottomZ, leftZ)); - } - } - - glEnd(); - } - - glPopMatrix(); - - glDepthMask(GL_TRUE); - glBindTexture(GL_TEXTURE_2D, 0); - glDisable(GL_TEXTURE_2D); - - glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); - glEnable(GL_LIGHTING); - } +//Renders a hemisphere with texture coordinates. void ApplicationOverlay::renderTexturedHemisphere() { const int slices = 80; const int stacks = 80; + //UV mapping source: http://www.mvps.org/directx/articles/spheremap.htm static VerticesIndices vbo(0, 0); int vertices = slices * (stacks - 1) + 1; int indices = slices * 2 * 3 * (stacks - 2) + slices * 3; - if (vbo.first == 0) { + + static float oldTextureFOV = _textureFov; + //We only generate the VBO when the _textureFov changes + if (vbo.first == 0 || oldTextureFOV != _textureFov) { + oldTextureFOV = _textureFov; TextureVertex* vertexData = new TextureVertex[vertices]; TextureVertex* vertex = vertexData; for (int i = 0; i < stacks - 1; i++) { @@ -574,8 +638,8 @@ void ApplicationOverlay::renderTexturedHemisphere() { vertex->position.x = sinf(theta) * radius; vertex->position.y = cosf(theta) * radius; vertex->position.z = z; - vertex->uv.x = asin(vertex->position.x) / (textureFov) + 0.5f; - vertex->uv.y = asin(vertex->position.y) / (textureFov) + 0.5f; + vertex->uv.x = asin(vertex->position.x) / (_textureFov) + 0.5f; + vertex->uv.y = asin(vertex->position.y) / (_textureFov) + 0.5f; vertex++; } } @@ -586,7 +650,9 @@ void ApplicationOverlay::renderTexturedHemisphere() { vertex->uv.y = 0.5f; vertex++; - glGenBuffers(1, &vbo.first); + if (vbo.first == 0){ + glGenBuffers(1, &vbo.first); + } glBindBuffer(GL_ARRAY_BUFFER, vbo.first); const int BYTES_PER_VERTEX = sizeof(TextureVertex); glBufferData(GL_ARRAY_BUFFER, vertices * BYTES_PER_VERTEX, vertexData, GL_STATIC_DRAW); diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index 1bf0e18816..53a0125dae 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -19,8 +19,6 @@ class QOpenGLFramebufferObject; class ApplicationOverlay { public: - enum UIType { HEMISPHERE, SEMICIRCLE, CURVED_SEMICIRCLE }; - ApplicationOverlay(); ~ApplicationOverlay(); @@ -31,12 +29,7 @@ public: // Getters QOpenGLFramebufferObject* getFramebufferObject(); - float getOculusAngle() const { return _oculusAngle; } - - // Setters - void setOculusAngle(float oculusAngle) { _oculusAngle = oculusAngle; } - void setUIType(UIType uiType) { _uiType = uiType; } - + private: // Interleaved vertex data struct TextureVertex { @@ -46,13 +39,24 @@ private: typedef QPair VerticesIndices; + void renderPointers(); + void renderControllerPointers(); + void renderControllerPointersOculus(); + void renderMagnifier(int mouseX, int mouseY); + void renderAudioMeter(); + void renderStatsAndLogs(); void renderTexturedHemisphere(); QOpenGLFramebufferObject* _framebufferObject; float _trailingAudioLoudness; float _oculusAngle; float _distance; - UIType _uiType; + float _textureFov; + int _mouseX[2]; + int _mouseY[2]; + int _numMagnifiers; + + GLuint _crosshairTexture; }; #endif // hifi_ApplicationOverlay_h \ No newline at end of file diff --git a/interface/src/ui/OAuthWebViewHandler.cpp b/interface/src/ui/OAuthWebViewHandler.cpp index 5415d5d9c3..5b4431bd0f 100644 --- a/interface/src/ui/OAuthWebViewHandler.cpp +++ b/interface/src/ui/OAuthWebViewHandler.cpp @@ -103,8 +103,9 @@ void OAuthWebViewHandler::displayWebviewForAuthorizationURL(const QUrl& authoriz codedAuthorizationURL.setQuery(authQuery); } + connect(_activeWebView.data(), &QWebView::urlChanged, this, &OAuthWebViewHandler::handleURLChanged); + _activeWebView->load(codedAuthorizationURL); - _activeWebView->show(); connect(_activeWebView->page()->networkAccessManager(), &QNetworkAccessManager::sslErrors, this, &OAuthWebViewHandler::handleSSLErrors); @@ -137,3 +138,17 @@ void OAuthWebViewHandler::handleLoadFinished(bool success) { void OAuthWebViewHandler::handleWebViewDestroyed(QObject* destroyedObject) { _webViewRedisplayTimer.restart(); } + +void OAuthWebViewHandler::handleURLChanged(const QUrl& url) { + // check if this is the authorization screen - if it is then we need to show the OAuthWebViewHandler + const QString ACCESS_TOKEN_URL_REGEX_STRING = "redirect_uri=[\\w:\\/\\.]+&access_token="; + QRegExp accessTokenRegex(ACCESS_TOKEN_URL_REGEX_STRING); + + if (accessTokenRegex.indexIn(url.toString()) != -1) { + _activeWebView->show(); + } else if (url.toString() == DEFAULT_NODE_AUTH_URL.toString() + "/login") { + // this is a login request - we're going to close the webview and signal the AccountManager that we need a login + _activeWebView->close(); + AccountManager::getInstance().checkAndSignalForAccessToken(); + } +} diff --git a/interface/src/ui/OAuthWebViewHandler.h b/interface/src/ui/OAuthWebViewHandler.h index 3a30705f88..8f0c01c90d 100644 --- a/interface/src/ui/OAuthWebViewHandler.h +++ b/interface/src/ui/OAuthWebViewHandler.h @@ -32,6 +32,7 @@ private slots: void handleSSLErrors(QNetworkReply* networkReply, const QList& errorList); void handleLoadFinished(bool success); void handleWebViewDestroyed(QObject* destroyedObject); + void handleURLChanged(const QUrl& url); private: QPointer _activeWebView; QElapsedTimer _webViewRedisplayTimer; diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index d5c6079d4b..5e6c6984eb 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -137,6 +137,13 @@ void PreferencesDialog::loadPreferences() { ui.maxVoxelsSpin->setValue(menuInstance->getMaxVoxels()); ui.maxVoxelsPPSSpin->setValue(menuInstance->getMaxVoxelPacketsPerSecond()); + + ui.oculusUIAngularSizeSpin->setValue(menuInstance->getOculusUIAngularSize()); + + ui.sixenseReticleMoveSpeedSpin->setValue(menuInstance->getSixenseReticleMoveSpeed()); + + ui.invertSixenseButtonsCheckBox->setChecked(menuInstance->getInvertSixenseButtons()); + } void PreferencesDialog::savePreferences() { @@ -189,6 +196,12 @@ void PreferencesDialog::savePreferences() { (float)ui.faceshiftEyeDeflectionSider->maximum()); Menu::getInstance()->setMaxVoxelPacketsPerSecond(ui.maxVoxelsPPSSpin->value()); + Menu::getInstance()->setOculusUIAngularSize(ui.oculusUIAngularSizeSpin->value()); + + Menu::getInstance()->setSixenseReticleMoveSpeed(ui.sixenseReticleMoveSpeedSpin->value()); + + Menu::getInstance()->setInvertSixenseButtons(ui.invertSixenseButtonsCheckBox->isChecked()); + Menu::getInstance()->setAudioJitterBufferSamples(ui.audioJitterSpin->value()); Application::getInstance()->getAudio()->setJitterBufferSamples(ui.audioJitterSpin->value()); diff --git a/interface/src/voxels/VoxelPacketProcessor.cpp b/interface/src/voxels/OctreePacketProcessor.cpp similarity index 92% rename from interface/src/voxels/VoxelPacketProcessor.cpp rename to interface/src/voxels/OctreePacketProcessor.cpp index 095c378c04..66190a5689 100644 --- a/interface/src/voxels/VoxelPacketProcessor.cpp +++ b/interface/src/voxels/OctreePacketProcessor.cpp @@ -1,5 +1,5 @@ // -// VoxelPacketProcessor.cpp +// OctreePacketProcessor.cpp // interface/src/voxels // // Created by Brad Hefta-Gaub on 8/12/13. @@ -13,18 +13,18 @@ #include "Application.h" #include "Menu.h" -#include "VoxelPacketProcessor.h" +#include "OctreePacketProcessor.h" -void VoxelPacketProcessor::processPacket(const SharedNodePointer& sendingNode, const QByteArray& packet) { +void OctreePacketProcessor::processPacket(const SharedNodePointer& sendingNode, const QByteArray& packet) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), - "VoxelPacketProcessor::processPacket()"); + "OctreePacketProcessor::processPacket()"); QByteArray mutablePacket = packet; const int WAY_BEHIND = 300; if (packetsToProcessCount() > WAY_BEHIND && Application::getInstance()->getLogger()->extraDebugging()) { - qDebug("VoxelPacketProcessor::processPacket() packets to process=%d", packetsToProcessCount()); + qDebug("OctreePacketProcessor::processPacket() packets to process=%d", packetsToProcessCount()); } ssize_t messageLength = mutablePacket.size(); diff --git a/interface/src/voxels/VoxelPacketProcessor.h b/interface/src/voxels/OctreePacketProcessor.h similarity index 76% rename from interface/src/voxels/VoxelPacketProcessor.h rename to interface/src/voxels/OctreePacketProcessor.h index 36456c5cc2..bdf25806f3 100644 --- a/interface/src/voxels/VoxelPacketProcessor.h +++ b/interface/src/voxels/OctreePacketProcessor.h @@ -1,5 +1,5 @@ // -// VoxelPacketProcessor.h +// OctreePacketProcessor.h // interface/src/voxels // // Created by Brad Hefta-Gaub on 8/12/13. @@ -9,16 +9,16 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_VoxelPacketProcessor_h -#define hifi_VoxelPacketProcessor_h +#ifndef hifi_OctreePacketProcessor_h +#define hifi_OctreePacketProcessor_h #include /// Handles processing of incoming voxel packets for the interface application. As with other ReceivedPacketProcessor classes /// the user is responsible for reading inbound packets and adding them to the processing queue by calling queueReceivedPacket() -class VoxelPacketProcessor : public ReceivedPacketProcessor { +class OctreePacketProcessor : public ReceivedPacketProcessor { Q_OBJECT protected: virtual void processPacket(const SharedNodePointer& sendingNode, const QByteArray& packet); }; -#endif // hifi_VoxelPacketProcessor_h +#endif // hifi_OctreePacketProcessor_h diff --git a/interface/ui/preferencesDialog.ui b/interface/ui/preferencesDialog.ui index a1c2073ab6..f00d7c4788 100644 --- a/interface/ui/preferencesDialog.ui +++ b/interface/ui/preferencesDialog.ui @@ -154,9 +154,9 @@ color: #0e7077 0 - -204 - 494 - 1091 + -1002 + 477 + 1386 @@ -1605,6 +1605,331 @@ padding: 10px;margin-top:10px + + + + + 0 + 0 + + + + + 0 + 40 + + + + + Arial + 20 + 50 + false + + + + color: #0e7077 + + + Oculus Rift + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + 0 + + + 10 + + + 0 + + + 10 + + + + + + Arial + + + + color: rgb(51, 51, 51) + + + User Interface Angular Size + + + 15 + + + maxVoxelsSpin + + + + + + + + Arial + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 125 + 36 + + + + + Arial + + + + 30 + + + 160 + + + 1 + + + 72 + + + + + + + + + + 0 + 0 + + + + + 0 + 40 + + + + + Arial + 20 + 50 + false + + + + color: #0e7077 + + + Sixense Controllers + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + 0 + + + 10 + + + 0 + + + 10 + + + + + + Arial + + + + color: rgb(51, 51, 51) + + + Invert Mouse Buttons + + + 15 + + + maxVoxelsSpin + + + + + + + + Arial + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 32 + 0 + + + + + 0 + 0 + + + + + + + + 32 + 32 + + + + + + + + + + 0 + + + 10 + + + 0 + + + 10 + + + + + + Arial + + + + color: rgb(51, 51, 51) + + + Reticle Movement Speed + + + 15 + + + maxVoxelsSpin + + + + + + + + Arial + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 125 + 36 + + + + + Arial + + + + 100 + + + 1 + + + 50 + + + + + diff --git a/libraries/metavoxels/src/Bitstream.cpp b/libraries/metavoxels/src/Bitstream.cpp index d9242ad9b7..d7e15a9dd6 100644 --- a/libraries/metavoxels/src/Bitstream.cpp +++ b/libraries/metavoxels/src/Bitstream.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -30,6 +31,7 @@ REGISTER_SIMPLE_TYPE_STREAMER(uint) REGISTER_SIMPLE_TYPE_STREAMER(float) REGISTER_SIMPLE_TYPE_STREAMER(QByteArray) REGISTER_SIMPLE_TYPE_STREAMER(QColor) +REGISTER_SIMPLE_TYPE_STREAMER(QScriptValue) REGISTER_SIMPLE_TYPE_STREAMER(QString) REGISTER_SIMPLE_TYPE_STREAMER(QUrl) REGISTER_SIMPLE_TYPE_STREAMER(QVariantList) @@ -285,6 +287,12 @@ void Bitstream::writeDelta(const QVariant& value, const QVariant& reference) { streamer->writeRawDelta(*this, value, reference); } +void Bitstream::writeRawDelta(const QVariant& value, const QVariant& reference) { + const TypeStreamer* streamer = getTypeStreamers().value(value.userType()); + _typeStreamerStreamer << streamer; + streamer->writeRawDelta(*this, value, reference); +} + void Bitstream::readRawDelta(QVariant& value, const QVariant& reference) { TypeReader typeReader; _typeStreamerStreamer >> typeReader; @@ -309,6 +317,272 @@ void Bitstream::readRawDelta(QObject*& value, const QObject* reference) { value = objectReader.readDelta(*this, reference); } +void Bitstream::writeRawDelta(const QScriptValue& value, const QScriptValue& reference) { + if (reference.isUndefined() || reference.isNull()) { + *this << value; + + } else if (reference.isBool()) { + if (value.isBool()) { + *this << false; + *this << value.toBool(); + + } else { + *this << true; + *this << value; + } + } else if (reference.isNumber()) { + if (value.isNumber()) { + *this << false; + *this << value.toNumber(); + + } else { + *this << true; + *this << value; + } + } else if (reference.isString()) { + if (value.isString()) { + *this << false; + *this << value.toString(); + + } else { + *this << true; + *this << value; + } + } else if (reference.isVariant()) { + if (value.isVariant()) { + *this << false; + writeRawDelta(value.toVariant(), reference.toVariant()); + + } else { + *this << true; + *this << value; + } + } else if (reference.isQObject()) { + if (value.isQObject()) { + *this << false; + writeRawDelta(value.toQObject(), reference.toQObject()); + + } else { + *this << true; + *this << value; + } + } else if (reference.isQMetaObject()) { + if (value.isQMetaObject()) { + *this << false; + *this << value.toQMetaObject(); + + } else { + *this << true; + *this << value; + } + } else if (reference.isDate()) { + if (value.isDate()) { + *this << false; + *this << value.toDateTime(); + + } else { + *this << true; + *this << value; + } + } else if (reference.isRegExp()) { + if (value.isRegExp()) { + *this << false; + *this << value.toRegExp(); + + } else { + *this << true; + *this << value; + } + } else if (reference.isArray()) { + if (value.isArray()) { + *this << false; + int length = value.property(ScriptCache::getInstance()->getLengthString()).toInt32(); + *this << length; + int referenceLength = reference.property(ScriptCache::getInstance()->getLengthString()).toInt32(); + for (int i = 0; i < length; i++) { + if (i < referenceLength) { + writeDelta(value.property(i), reference.property(i)); + } else { + *this << value.property(i); + } + } + } else { + *this << true; + *this << value; + } + } else if (reference.isObject()) { + if (value.isObject() && !(value.isArray() || value.isRegExp() || value.isDate() || + value.isQMetaObject() || value.isQObject() || value.isVariant())) { + *this << false; + for (QScriptValueIterator it(value); it.hasNext(); ) { + it.next(); + QScriptValue referenceValue = reference.property(it.scriptName()); + if (it.value() != referenceValue) { + *this << it.scriptName(); + writeRawDelta(it.value(), referenceValue); + } + } + for (QScriptValueIterator it(reference); it.hasNext(); ) { + it.next(); + if (!value.property(it.scriptName()).isValid()) { + *this << it.scriptName(); + writeRawDelta(QScriptValue(), it.value()); + } + } + *this << QScriptString(); + + } else { + *this << true; + *this << value; + } + } else { + *this << value; + } +} + +void Bitstream::readRawDelta(QScriptValue& value, const QScriptValue& reference) { + if (reference.isUndefined() || reference.isNull()) { + *this >> value; + + } else if (reference.isBool()) { + bool typeChanged; + *this >> typeChanged; + if (typeChanged) { + *this >> value; + + } else { + bool boolValue; + *this >> boolValue; + value = QScriptValue(boolValue); + } + } else if (reference.isNumber()) { + bool typeChanged; + *this >> typeChanged; + if (typeChanged) { + *this >> value; + + } else { + qsreal numberValue; + *this >> numberValue; + value = QScriptValue(numberValue); + } + } else if (reference.isString()) { + bool typeChanged; + *this >> typeChanged; + if (typeChanged) { + *this >> value; + + } else { + QString stringValue; + *this >> stringValue; + value = QScriptValue(stringValue); + } + } else if (reference.isVariant()) { + bool typeChanged; + *this >> typeChanged; + if (typeChanged) { + *this >> value; + + } else { + QVariant variant; + readRawDelta(variant, reference.toVariant()); + value = ScriptCache::getInstance()->getEngine()->newVariant(variant); + } + } else if (reference.isQObject()) { + bool typeChanged; + *this >> typeChanged; + if (typeChanged) { + *this >> value; + + } else { + QObject* object; + readRawDelta(object, reference.toQObject()); + value = ScriptCache::getInstance()->getEngine()->newQObject(object, QScriptEngine::ScriptOwnership); + } + } else if (reference.isQMetaObject()) { + bool typeChanged; + *this >> typeChanged; + if (typeChanged) { + *this >> value; + + } else { + const QMetaObject* metaObject; + *this >> metaObject; + value = ScriptCache::getInstance()->getEngine()->newQMetaObject(metaObject); + } + } else if (reference.isDate()) { + bool typeChanged; + *this >> typeChanged; + if (typeChanged) { + *this >> value; + + } else { + QDateTime dateTime; + *this >> dateTime; + value = ScriptCache::getInstance()->getEngine()->newDate(dateTime); + } + } else if (reference.isRegExp()) { + bool typeChanged; + *this >> typeChanged; + if (typeChanged) { + *this >> value; + + } else { + QRegExp regExp; + *this >> regExp; + value = ScriptCache::getInstance()->getEngine()->newRegExp(regExp); + } + } else if (reference.isArray()) { + bool typeChanged; + *this >> typeChanged; + if (typeChanged) { + *this >> value; + + } else { + int length; + *this >> length; + value = ScriptCache::getInstance()->getEngine()->newArray(length); + int referenceLength = reference.property(ScriptCache::getInstance()->getLengthString()).toInt32(); + for (int i = 0; i < length; i++) { + QScriptValue element; + if (i < referenceLength) { + readDelta(element, reference.property(i)); + } else { + *this >> element; + } + value.setProperty(i, element); + } + } + } else if (reference.isObject()) { + bool typeChanged; + *this >> typeChanged; + if (typeChanged) { + *this >> value; + + } else { + // start by shallow-copying the reference + value = ScriptCache::getInstance()->getEngine()->newObject(); + for (QScriptValueIterator it(reference); it.hasNext(); ) { + it.next(); + value.setProperty(it.scriptName(), it.value()); + } + // then apply the requested changes + forever { + QScriptString name; + *this >> name; + if (!name.isValid()) { + break; + } + QScriptValue scriptValue; + readRawDelta(scriptValue, reference.property(name)); + value.setProperty(name, scriptValue); + } + } + } else { + *this >> value; + } +} + Bitstream& Bitstream::operator<<(bool value) { if (value) { _byte |= (1 << _position); @@ -350,6 +624,14 @@ Bitstream& Bitstream::operator>>(uint& value) { return *this; } +Bitstream& Bitstream::operator<<(qint64 value) { + return write(&value, 64); +} + +Bitstream& Bitstream::operator>>(qint64& value) { + return read(&value, 64); +} + Bitstream& Bitstream::operator<<(float value) { return write(&value, 32); } @@ -358,6 +640,14 @@ Bitstream& Bitstream::operator>>(float& value) { return read(&value, 32); } +Bitstream& Bitstream::operator<<(double value) { + return write(&value, 64); +} + +Bitstream& Bitstream::operator>>(double& value) { + return read(&value, 64); +} + Bitstream& Bitstream::operator<<(const glm::vec3& value) { return *this << value.x << value.y << value.z; } @@ -420,6 +710,40 @@ Bitstream& Bitstream::operator>>(QUrl& url) { return *this; } +Bitstream& Bitstream::operator<<(const QDateTime& dateTime) { + return *this << dateTime.toMSecsSinceEpoch(); +} + +Bitstream& Bitstream::operator>>(QDateTime& dateTime) { + qint64 msecsSinceEpoch; + *this >> msecsSinceEpoch; + dateTime = QDateTime::fromMSecsSinceEpoch(msecsSinceEpoch); + return *this; +} + +Bitstream& Bitstream::operator<<(const QRegExp& regExp) { + *this << regExp.pattern(); + Qt::CaseSensitivity caseSensitivity = regExp.caseSensitivity(); + write(&caseSensitivity, 1); + QRegExp::PatternSyntax syntax = regExp.patternSyntax(); + write(&syntax, 3); + return *this << regExp.isMinimal(); +} + +Bitstream& Bitstream::operator>>(QRegExp& regExp) { + QString pattern; + *this >> pattern; + Qt::CaseSensitivity caseSensitivity = (Qt::CaseSensitivity)0; + read(&caseSensitivity, 1); + QRegExp::PatternSyntax syntax = (QRegExp::PatternSyntax)0; + read(&syntax, 3); + regExp = QRegExp(pattern, caseSensitivity, syntax); + bool minimal; + *this >> minimal; + regExp.setMinimal(minimal); + return *this; +} + Bitstream& Bitstream::operator<<(const QVariant& value) { if (!value.isValid()) { _typeStreamerStreamer << NULL; @@ -543,6 +867,185 @@ Bitstream& Bitstream::operator>>(QScriptString& string) { return *this; } +enum ScriptValueType { + INVALID_SCRIPT_VALUE, + UNDEFINED_SCRIPT_VALUE, + NULL_SCRIPT_VALUE, + BOOL_SCRIPT_VALUE, + NUMBER_SCRIPT_VALUE, + STRING_SCRIPT_VALUE, + VARIANT_SCRIPT_VALUE, + QOBJECT_SCRIPT_VALUE, + QMETAOBJECT_SCRIPT_VALUE, + DATE_SCRIPT_VALUE, + REGEXP_SCRIPT_VALUE, + ARRAY_SCRIPT_VALUE, + OBJECT_SCRIPT_VALUE +}; + +const int SCRIPT_VALUE_BITS = 4; + +static void writeScriptValueType(Bitstream& out, ScriptValueType type) { + out.write(&type, SCRIPT_VALUE_BITS); +} + +static ScriptValueType readScriptValueType(Bitstream& in) { + ScriptValueType type = (ScriptValueType)0; + in.read(&type, SCRIPT_VALUE_BITS); + return type; +} + +Bitstream& Bitstream::operator<<(const QScriptValue& value) { + if (value.isUndefined()) { + writeScriptValueType(*this, UNDEFINED_SCRIPT_VALUE); + + } else if (value.isNull()) { + writeScriptValueType(*this, NULL_SCRIPT_VALUE); + + } else if (value.isBool()) { + writeScriptValueType(*this, BOOL_SCRIPT_VALUE); + *this << value.toBool(); + + } else if (value.isNumber()) { + writeScriptValueType(*this, NUMBER_SCRIPT_VALUE); + *this << value.toNumber(); + + } else if (value.isString()) { + writeScriptValueType(*this, STRING_SCRIPT_VALUE); + *this << value.toString(); + + } else if (value.isVariant()) { + writeScriptValueType(*this, VARIANT_SCRIPT_VALUE); + *this << value.toVariant(); + + } else if (value.isQObject()) { + writeScriptValueType(*this, QOBJECT_SCRIPT_VALUE); + *this << value.toQObject(); + + } else if (value.isQMetaObject()) { + writeScriptValueType(*this, QMETAOBJECT_SCRIPT_VALUE); + *this << value.toQMetaObject(); + + } else if (value.isDate()) { + writeScriptValueType(*this, DATE_SCRIPT_VALUE); + *this << value.toDateTime(); + + } else if (value.isRegExp()) { + writeScriptValueType(*this, REGEXP_SCRIPT_VALUE); + *this << value.toRegExp(); + + } else if (value.isArray()) { + writeScriptValueType(*this, ARRAY_SCRIPT_VALUE); + int length = value.property(ScriptCache::getInstance()->getLengthString()).toInt32(); + *this << length; + for (int i = 0; i < length; i++) { + *this << value.property(i); + } + } else if (value.isObject()) { + writeScriptValueType(*this, OBJECT_SCRIPT_VALUE); + for (QScriptValueIterator it(value); it.hasNext(); ) { + it.next(); + *this << it.scriptName(); + *this << it.value(); + } + *this << QScriptString(); + + } else { + writeScriptValueType(*this, INVALID_SCRIPT_VALUE); + } + return *this; +} + +Bitstream& Bitstream::operator>>(QScriptValue& value) { + switch (readScriptValueType(*this)) { + case UNDEFINED_SCRIPT_VALUE: + value = QScriptValue(QScriptValue::UndefinedValue); + break; + + case NULL_SCRIPT_VALUE: + value = QScriptValue(QScriptValue::NullValue); + break; + + case BOOL_SCRIPT_VALUE: { + bool boolValue; + *this >> boolValue; + value = QScriptValue(boolValue); + break; + } + case NUMBER_SCRIPT_VALUE: { + qsreal numberValue; + *this >> numberValue; + value = QScriptValue(numberValue); + break; + } + case STRING_SCRIPT_VALUE: { + QString stringValue; + *this >> stringValue; + value = QScriptValue(stringValue); + break; + } + case VARIANT_SCRIPT_VALUE: { + QVariant variantValue; + *this >> variantValue; + value = ScriptCache::getInstance()->getEngine()->newVariant(variantValue); + break; + } + case QOBJECT_SCRIPT_VALUE: { + QObject* object; + *this >> object; + ScriptCache::getInstance()->getEngine()->newQObject(object, QScriptEngine::ScriptOwnership); + break; + } + case QMETAOBJECT_SCRIPT_VALUE: { + const QMetaObject* metaObject; + *this >> metaObject; + ScriptCache::getInstance()->getEngine()->newQMetaObject(metaObject); + break; + } + case DATE_SCRIPT_VALUE: { + QDateTime dateTime; + *this >> dateTime; + value = ScriptCache::getInstance()->getEngine()->newDate(dateTime); + break; + } + case REGEXP_SCRIPT_VALUE: { + QRegExp regExp; + *this >> regExp; + value = ScriptCache::getInstance()->getEngine()->newRegExp(regExp); + break; + } + case ARRAY_SCRIPT_VALUE: { + int length; + *this >> length; + value = ScriptCache::getInstance()->getEngine()->newArray(length); + for (int i = 0; i < length; i++) { + QScriptValue element; + *this >> element; + value.setProperty(i, element); + } + break; + } + case OBJECT_SCRIPT_VALUE: { + value = ScriptCache::getInstance()->getEngine()->newObject(); + forever { + QScriptString name; + *this >> name; + if (!name.isValid()) { + break; + } + QScriptValue scriptValue; + *this >> scriptValue; + value.setProperty(name, scriptValue); + } + break; + } + default: + value = QScriptValue(); + break; + } + return *this; +} + Bitstream& Bitstream::operator<<(const SharedObjectPointer& object) { _sharedObjectStreamer << object; return *this; @@ -561,7 +1064,7 @@ Bitstream& Bitstream::operator<(const QMetaObject* metaObject) { if (_metadataType == NO_METADATA) { return *this; } - const QVector& propertyWriters = getPropertyWriters().value(metaObject); + const PropertyWriterVector& propertyWriters = getPropertyWriters().value(metaObject); *this << propertyWriters.size(); QCryptographicHash hash(QCryptographicHash::Md5); foreach (const PropertyWriter& propertyWriter, propertyWriters) { @@ -600,7 +1103,7 @@ Bitstream& Bitstream::operator>(ObjectReader& objectReader) { } int storedPropertyCount; *this >> storedPropertyCount; - QVector properties(storedPropertyCount); + PropertyReaderVector properties(storedPropertyCount); for (int i = 0; i < storedPropertyCount; i++) { TypeReader typeReader; *this >> typeReader; @@ -619,7 +1122,7 @@ Bitstream& Bitstream::operator>(ObjectReader& objectReader) { QCryptographicHash hash(QCryptographicHash::Md5); bool matches = true; if (metaObject) { - const QVector& propertyWriters = getPropertyWriters().value(metaObject); + const PropertyWriterVector& propertyWriters = getPropertyWriters().value(metaObject); if (propertyWriters.size() == properties.size()) { for (int i = 0; i < propertyWriters.size(); i++) { const PropertyWriter& propertyWriter = propertyWriters.at(i); @@ -899,14 +1402,17 @@ Bitstream& Bitstream::operator>(AttributePointer& attribute) { return *this; } +const QString INVALID_STRING("%INVALID%"); + Bitstream& Bitstream::operator<(const QScriptString& string) { - return *this << string.toString(); + return *this << (string.isValid() ? string.toString() : INVALID_STRING); } Bitstream& Bitstream::operator>(QScriptString& string) { QString rawString; *this >> rawString; - string = ScriptCache::getInstance()->getEngine()->toStringHandle(rawString); + string = (rawString == INVALID_STRING) ? QScriptString() : + ScriptCache::getInstance()->getEngine()->toStringHandle(rawString); return *this; } @@ -990,17 +1496,17 @@ QHash& Bitstream::getTypeStreamers() { return typeStreamers; } -const QHash, const TypeStreamer*>& Bitstream::getEnumStreamers() { - static QHash, const TypeStreamer*> enumStreamers = createEnumStreamers(); +const QHash& Bitstream::getEnumStreamers() { + static QHash enumStreamers = createEnumStreamers(); return enumStreamers; } -QHash, const TypeStreamer*> Bitstream::createEnumStreamers() { - QHash, const TypeStreamer*> enumStreamers; +QHash Bitstream::createEnumStreamers() { + QHash enumStreamers; foreach (const QMetaObject* metaObject, getMetaObjects()) { for (int i = 0; i < metaObject->enumeratorCount(); i++) { QMetaEnum metaEnum = metaObject->enumerator(i); - const TypeStreamer*& streamer = enumStreamers[QPair(metaEnum.scope(), metaEnum.name())]; + const TypeStreamer*& streamer = enumStreamers[ScopeNamePair(metaEnum.scope(), metaEnum.name())]; if (!streamer) { streamer = new EnumTypeStreamer(metaEnum); } @@ -1022,15 +1528,15 @@ QHash Bitstream::createEnumStreamersByName() { return enumStreamersByName; } -const QHash >& Bitstream::getPropertyReaders() { - static QHash > propertyReaders = createPropertyReaders(); +const QHash& Bitstream::getPropertyReaders() { + static QHash propertyReaders = createPropertyReaders(); return propertyReaders; } -QHash > Bitstream::createPropertyReaders() { - QHash > propertyReaders; +QHash Bitstream::createPropertyReaders() { + QHash propertyReaders; foreach (const QMetaObject* metaObject, getMetaObjects()) { - QVector& readers = propertyReaders[metaObject]; + PropertyReaderVector& readers = propertyReaders[metaObject]; for (int i = 0; i < metaObject->propertyCount(); i++) { QMetaProperty property = metaObject->property(i); if (!property.isStored()) { @@ -1039,7 +1545,7 @@ QHash > Bitstream::createPropertyRea const TypeStreamer* streamer; if (property.isEnumType()) { QMetaEnum metaEnum = property.enumerator(); - streamer = getEnumStreamers().value(QPair( + streamer = getEnumStreamers().value(ScopeNamePair( QByteArray::fromRawData(metaEnum.scope(), strlen(metaEnum.scope())), QByteArray::fromRawData(metaEnum.name(), strlen(metaEnum.name())))); } else { @@ -1053,15 +1559,15 @@ QHash > Bitstream::createPropertyRea return propertyReaders; } -const QHash >& Bitstream::getPropertyWriters() { - static QHash > propertyWriters = createPropertyWriters(); +const QHash& Bitstream::getPropertyWriters() { + static QHash propertyWriters = createPropertyWriters(); return propertyWriters; } -QHash > Bitstream::createPropertyWriters() { - QHash > propertyWriters; +QHash Bitstream::createPropertyWriters() { + QHash propertyWriters; foreach (const QMetaObject* metaObject, getMetaObjects()) { - QVector& writers = propertyWriters[metaObject]; + PropertyWriterVector& writers = propertyWriters[metaObject]; for (int i = 0; i < metaObject->propertyCount(); i++) { QMetaProperty property = metaObject->property(i); if (!property.isStored()) { @@ -1070,7 +1576,7 @@ QHash > Bitstream::createPropertyWri const TypeStreamer* streamer; if (property.isEnumType()) { QMetaEnum metaEnum = property.enumerator(); - streamer = getEnumStreamers().value(QPair( + streamer = getEnumStreamers().value(ScopeNamePair( QByteArray::fromRawData(metaEnum.scope(), strlen(metaEnum.scope())), QByteArray::fromRawData(metaEnum.name(), strlen(metaEnum.name())))); } else { @@ -1324,7 +1830,7 @@ void FieldReader::readDelta(Bitstream& in, const TypeStreamer* streamer, QVarian } ObjectReader::ObjectReader(const QByteArray& className, const QMetaObject* metaObject, - const QVector& properties) : + const PropertyReaderVector& properties) : _className(className), _metaObject(metaObject), _properties(properties) { diff --git a/libraries/metavoxels/src/Bitstream.h b/libraries/metavoxels/src/Bitstream.h index 80adfc4e8b..d05f6574c0 100644 --- a/libraries/metavoxels/src/Bitstream.h +++ b/libraries/metavoxels/src/Bitstream.h @@ -29,6 +29,7 @@ class QByteArray; class QColor; class QDataStream; +class QScriptValue; class QUrl; class Attribute; @@ -44,6 +45,10 @@ class TypeStreamer; typedef SharedObjectPointerTemplate AttributePointer; +typedef QPair ScopeNamePair; +typedef QVector PropertyReaderVector; +typedef QVector PropertyWriterVector; + /// Streams integer identifiers that conform to the following pattern: each ID encountered in the stream is either one that /// has been sent (received) before, or is one more than the highest previously encountered ID (starting at zero). This allows /// us to use the minimum number of bits to encode the IDs. @@ -287,11 +292,15 @@ public: template void writeDelta(const T& value, const T& reference); template void readDelta(T& value, const T& reference); + void writeRawDelta(const QVariant& value, const QVariant& reference); void readRawDelta(QVariant& value, const QVariant& reference); void writeRawDelta(const QObject* value, const QObject* reference); void readRawDelta(QObject*& value, const QObject* reference); + void writeRawDelta(const QScriptValue& value, const QScriptValue& reference); + void readRawDelta(QScriptValue& value, const QScriptValue& reference); + template void writeRawDelta(const T& value, const T& reference); template void readRawDelta(T& value, const T& reference); @@ -316,9 +325,15 @@ public: Bitstream& operator<<(uint value); Bitstream& operator>>(uint& value); + Bitstream& operator<<(qint64 value); + Bitstream& operator>>(qint64& value); + Bitstream& operator<<(float value); Bitstream& operator>>(float& value); + Bitstream& operator<<(double value); + Bitstream& operator>>(double& value); + Bitstream& operator<<(const glm::vec3& value); Bitstream& operator>>(glm::vec3& value); @@ -337,6 +352,12 @@ public: Bitstream& operator<<(const QUrl& url); Bitstream& operator>>(QUrl& url); + Bitstream& operator<<(const QDateTime& dateTime); + Bitstream& operator>>(QDateTime& dateTime); + + Bitstream& operator<<(const QRegExp& regExp); + Bitstream& operator>>(QRegExp& regExp); + Bitstream& operator<<(const QVariant& value); Bitstream& operator>>(QVariant& value); @@ -372,6 +393,9 @@ public: Bitstream& operator<<(const QScriptString& string); Bitstream& operator>>(QScriptString& string); + Bitstream& operator<<(const QScriptValue& value); + Bitstream& operator>>(QScriptValue& value); + Bitstream& operator<<(const SharedObjectPointer& object); Bitstream& operator>>(SharedObjectPointer& object); @@ -422,14 +446,18 @@ private: static QHash& getMetaObjects(); static QMultiHash& getMetaObjectSubClasses(); static QHash& getTypeStreamers(); - static const QHash, const TypeStreamer*>& getEnumStreamers(); - static QHash, const TypeStreamer*> createEnumStreamers(); + + static const QHash& getEnumStreamers(); + static QHash createEnumStreamers(); + static const QHash& getEnumStreamersByName(); static QHash createEnumStreamersByName(); - static const QHash >& getPropertyReaders(); - static QHash > createPropertyReaders(); - static const QHash >& getPropertyWriters(); - static QHash > createPropertyWriters(); + + static const QHash& getPropertyReaders(); + static QHash createPropertyReaders(); + + static const QHash& getPropertyWriters(); + static QHash createPropertyWriters(); }; template inline void Bitstream::writeDelta(const T& value, const T& reference) { diff --git a/libraries/metavoxels/src/ScriptCache.cpp b/libraries/metavoxels/src/ScriptCache.cpp index dd090613b7..9648a047cb 100644 --- a/libraries/metavoxels/src/ScriptCache.cpp +++ b/libraries/metavoxels/src/ScriptCache.cpp @@ -13,11 +13,95 @@ #include #include +#include #include #include "AttributeRegistry.h" #include "ScriptCache.h" +static int scriptValueMetaTypeId = qRegisterMetaType(); +static bool scriptValueComparators = QMetaType::registerComparators(); + +bool operator==(const QScriptValue& first, const QScriptValue& second) { + if (first.isUndefined()) { + return second.isUndefined(); + + } else if (first.isNull()) { + return second.isNull(); + + } else if (first.isBool()) { + return second.isBool() && first.toBool() == second.toBool(); + + } else if (first.isNumber()) { + return second.isNumber() && first.toNumber() == second.toNumber(); + + } else if (first.isString()) { + return second.isString() && first.toString() == second.toString(); + + } else if (first.isVariant()) { + return second.isVariant() && first.toVariant() == second.toVariant(); + + } else if (first.isQObject()) { + return second.isQObject() && first.toQObject() == second.toQObject(); + + } else if (first.isQMetaObject()) { + return second.isQMetaObject() && first.toQMetaObject() == second.toQMetaObject(); + + } else if (first.isDate()) { + return second.isDate() && first.toDateTime() == second.toDateTime(); + + } else if (first.isRegExp()) { + return second.isRegExp() && first.toRegExp() == second.toRegExp(); + + } else if (first.isArray()) { + if (!second.isArray()) { + return false; + } + int length = first.property(ScriptCache::getInstance()->getLengthString()).toInt32(); + if (second.property(ScriptCache::getInstance()->getLengthString()).toInt32() != length) { + return false; + } + for (int i = 0; i < length; i++) { + if (first.property(i) != second.property(i)) { + return false; + } + } + return true; + + } else if (first.isObject()) { + if (!second.isObject()) { + return false; + } + int propertyCount = 0; + for (QScriptValueIterator it(first); it.hasNext(); ) { + it.next(); + if (second.property(it.scriptName()) != it.value()) { + return false; + } + propertyCount++; + } + // make sure the second has exactly as many properties as the first + for (QScriptValueIterator it(second); it.hasNext(); ) { + it.next(); + if (--propertyCount < 0) { + return false; + } + } + return true; + + } else { + return !second.isValid(); + } +} + +bool operator!=(const QScriptValue& first, const QScriptValue& second) { + return !(first == second); +} + +bool operator<(const QScriptValue& first, const QScriptValue& second) { + return first.lessThan(second); +} + ScriptCache* ScriptCache::getInstance() { static ScriptCache cache; return &cache; diff --git a/libraries/metavoxels/src/ScriptCache.h b/libraries/metavoxels/src/ScriptCache.h index f393d0e0a8..5d29157b3d 100644 --- a/libraries/metavoxels/src/ScriptCache.h +++ b/libraries/metavoxels/src/ScriptCache.h @@ -65,6 +65,12 @@ private: QScriptString _generatorString; }; +Q_DECLARE_METATYPE(QScriptValue) + +bool operator==(const QScriptValue& first, const QScriptValue& second); +bool operator!=(const QScriptValue& first, const QScriptValue& second); +bool operator<(const QScriptValue& first, const QScriptValue& second); + /// A program loaded from the network. class NetworkProgram : public Resource { Q_OBJECT diff --git a/libraries/models/src/ModelTreeElement.cpp b/libraries/models/src/ModelTreeElement.cpp index b0c7e125b4..75b9670d0f 100644 --- a/libraries/models/src/ModelTreeElement.cpp +++ b/libraries/models/src/ModelTreeElement.cpp @@ -186,6 +186,11 @@ bool ModelTreeElement::findDetailedRayIntersection(const glm::vec3& origin, cons if (fbxGeometry && fbxGeometry->meshExtents.isValid()) { Extents extents = fbxGeometry->meshExtents; + // NOTE: If the model has a bad mesh, then extents will be 0,0,0 & 0,0,0 + if (extents.minimum == extents.maximum && extents.minimum == glm::vec3(0,0,0)) { + extents.maximum = glm::vec3(1.0f,1.0f,1.0f); // in this case we will simulate the unit cube + } + // NOTE: these extents are model space, so we need to scale and center them accordingly // size is our "target size in world space" // we need to set our model scale so that the extents of the mesh, fit in a cube that size... diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index b4aedbcb7c..918261a953 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -55,13 +55,13 @@ AccountManager::AccountManager() : { qRegisterMetaType("OAuthAccessToken"); qRegisterMetaTypeStreamOperators("OAuthAccessToken"); - + qRegisterMetaType("DataServerAccountInfo"); qRegisterMetaTypeStreamOperators("DataServerAccountInfo"); - + qRegisterMetaType("QNetworkAccessManager::Operation"); qRegisterMetaType("JSONCallbackParameters"); - + connect(&_accountInfo, &DataServerAccountInfo::balanceChanged, this, &AccountManager::accountInfoBalanceChanged); } @@ -70,18 +70,18 @@ const QString DOUBLE_SLASH_SUBSTITUTE = "slashslash"; void AccountManager::logout() { // a logout means we want to delete the DataServerAccountInfo we currently have for this URL, in-memory and in file _accountInfo = DataServerAccountInfo(); - + emit balanceChanged(0); connect(&_accountInfo, &DataServerAccountInfo::balanceChanged, this, &AccountManager::accountInfoBalanceChanged); - + QSettings settings; settings.beginGroup(ACCOUNTS_GROUP); - + QString keyURLString(_authURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE)); settings.remove(keyURLString); - + qDebug() << "Removed account info for" << _authURL << "from in-memory accounts and .ini file"; - + emit logoutComplete(); // the username has changed to blank emit usernameChanged(QString()); @@ -93,7 +93,7 @@ void AccountManager::updateBalance() { JSONCallbackParameters callbackParameters; callbackParameters.jsonCallbackReceiver = &_accountInfo; callbackParameters.jsonCallbackMethod = "setBalanceFromJSON"; - + authenticatedRequest("/api/v1/wallets/mine", QNetworkAccessManager::GetOperation, callbackParameters); } } @@ -105,28 +105,33 @@ void AccountManager::accountInfoBalanceChanged(qint64 newBalance) { void AccountManager::setAuthURL(const QUrl& authURL) { if (_authURL != authURL) { _authURL = authURL; - + qDebug() << "URL for node authentication has been changed to" << qPrintable(_authURL.toString()); qDebug() << "Re-setting authentication flow."; - + // check if there are existing access tokens to load from settings QSettings settings; settings.beginGroup(ACCOUNTS_GROUP); - + foreach(const QString& key, settings.allKeys()) { // take a key copy to perform the double slash replacement QString keyCopy(key); QUrl keyURL(keyCopy.replace("slashslash", "//")); - + if (keyURL == _authURL) { // pull out the stored access token and store it in memory _accountInfo = settings.value(key).value(); qDebug() << "Found a data-server access token for" << qPrintable(keyURL.toString()); - - emit accessTokenChanged(); + + // profile info isn't guaranteed to be saved too + if (_accountInfo.hasProfile()) { + emit profileChanged(); + } else { + requestProfile(); + } } } - + // tell listeners that the auth endpoint has changed emit authEndpointChanged(); } @@ -147,36 +152,36 @@ void AccountManager::authenticatedRequest(const QString& path, QNetworkAccessMan void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager::Operation operation, const JSONCallbackParameters& callbackParams, const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart) { - + if (!_networkAccessManager) { _networkAccessManager = new QNetworkAccessManager(this); } - + if (hasValidAccessToken()) { QNetworkRequest authenticatedRequest; - + QUrl requestURL = _authURL; - + if (path.startsWith("/")) { requestURL.setPath(path); } else { requestURL.setPath("/" + path); } - + requestURL.setQuery("access_token=" + _accountInfo.getAccessToken().token); - + authenticatedRequest.setUrl(requestURL); - + if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qDebug() << "Making an authenticated request to" << qPrintable(requestURL.toString()); - + if (!dataByteArray.isEmpty()) { qDebug() << "The POST/PUT body -" << QString(dataByteArray); } } - + QNetworkReply* networkReply = NULL; - + switch (operation) { case QNetworkAccessManager::GetOperation: networkReply = _networkAccessManager->get(authenticatedRequest); @@ -198,24 +203,24 @@ void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager:: networkReply = _networkAccessManager->put(authenticatedRequest, dataByteArray); } } - + break; default: // other methods not yet handled break; } - + if (networkReply) { if (!callbackParams.isEmpty()) { // if we have information for a callback, insert the callbackParams into our local map _pendingCallbackMap.insert(networkReply, callbackParams); - + if (callbackParams.updateReciever && !callbackParams.updateSlot.isEmpty()) { callbackParams.updateReciever->connect(networkReply, SIGNAL(uploadProgress(qint64, qint64)), callbackParams.updateSlot.toStdString().c_str()); } } - + // if we ended up firing of a request, hook up to it now connect(networkReply, SIGNAL(finished()), SLOT(processReply())); } @@ -224,7 +229,7 @@ void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager:: void AccountManager::processReply() { QNetworkReply* requestReply = reinterpret_cast(sender()); - + if (requestReply->error() == QNetworkReply::NoError) { passSuccessToCallback(requestReply); } else { @@ -235,17 +240,17 @@ void AccountManager::processReply() { void AccountManager::passSuccessToCallback(QNetworkReply* requestReply) { QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); - + JSONCallbackParameters callbackParams = _pendingCallbackMap.value(requestReply); - + if (callbackParams.jsonCallbackReceiver) { // invoke the right method on the callback receiver QMetaObject::invokeMethod(callbackParams.jsonCallbackReceiver, qPrintable(callbackParams.jsonCallbackMethod), Q_ARG(const QJsonObject&, jsonResponse.object())); - + // remove the related reply-callback group from the map _pendingCallbackMap.remove(requestReply); - + } else { if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qDebug() << "Received JSON response from data-server that has no matching callback."; @@ -256,13 +261,13 @@ void AccountManager::passSuccessToCallback(QNetworkReply* requestReply) { void AccountManager::passErrorToCallback(QNetworkReply* requestReply) { JSONCallbackParameters callbackParams = _pendingCallbackMap.value(requestReply); - + if (callbackParams.errorCallbackReceiver) { // invoke the right method on the callback receiver QMetaObject::invokeMethod(callbackParams.errorCallbackReceiver, qPrintable(callbackParams.errorCallbackMethod), Q_ARG(QNetworkReply::NetworkError, requestReply->error()), Q_ARG(const QString&, requestReply->errorString())); - + // remove the related reply-callback group from the map _pendingCallbackMap.remove(requestReply); } else { @@ -274,12 +279,12 @@ void AccountManager::passErrorToCallback(QNetworkReply* requestReply) { } bool AccountManager::hasValidAccessToken() { - + if (_accountInfo.getAccessToken().token.isEmpty() || _accountInfo.getAccessToken().isExpired()) { if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qDebug() << "An access token is required for requests to" << qPrintable(_authURL.toString()); } - + return false; } else { return true; @@ -288,12 +293,12 @@ bool AccountManager::hasValidAccessToken() { bool AccountManager::checkAndSignalForAccessToken() { bool hasToken = hasValidAccessToken(); - + if (!hasToken) { // emit a signal so somebody can call back to us and request an access token given a username and password emit authRequired(); } - + return hasToken; } @@ -304,36 +309,36 @@ void AccountManager::requestAccessToken(const QString& login, const QString& pas } QNetworkRequest request; - + QUrl grantURL = _authURL; grantURL.setPath("/oauth/token"); - + const QString ACCOUNT_MANAGER_REQUESTED_SCOPE = "owner"; - + QByteArray postData; postData.append("grant_type=password&"); postData.append("username=" + login + "&"); postData.append("password=" + QUrl::toPercentEncoding(password) + "&"); postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); - + request.setUrl(grantURL); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - + QNetworkReply* requestReply = _networkAccessManager->post(request, postData); - connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestFinished); - connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); + connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestAccessTokenFinished); + connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); } -void AccountManager::requestFinished() { +void AccountManager::requestAccessTokenFinished() { QNetworkReply* requestReply = reinterpret_cast(sender()); - + QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); const QJsonObject& rootObject = jsonResponse.object(); - + if (!rootObject.contains("error")) { // construct an OAuthAccessToken from the json object - + if (!rootObject.contains("access_token") || !rootObject.contains("expires_in") || !rootObject.contains("token_type")) { // TODO: error handling - malformed token response @@ -342,23 +347,21 @@ void AccountManager::requestFinished() { // clear the path from the response URL so we have the right root URL for this access token QUrl rootURL = requestReply->url(); rootURL.setPath(""); - + qDebug() << "Storing an account with access-token for" << qPrintable(rootURL.toString()); - - _accountInfo = DataServerAccountInfo(rootObject); - + + _accountInfo = DataServerAccountInfo(); + _accountInfo.setAccessTokenFromJSON(rootObject); + emit loginComplete(rootURL); - // the username has changed to whatever came back - emit usernameChanged(_accountInfo.getUsername()); - - // we have found or requested an access token - emit accessTokenChanged(); - + // store this access token into the local settings QSettings localSettings; localSettings.beginGroup(ACCOUNTS_GROUP); localSettings.setValue(rootURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE), QVariant::fromValue(_accountInfo)); + + requestProfile(); } } else { // TODO: error handling @@ -367,7 +370,53 @@ void AccountManager::requestFinished() { } } -void AccountManager::requestError(QNetworkReply::NetworkError error) { +void AccountManager::requestAccessTokenError(QNetworkReply::NetworkError error) { // TODO: error handling qDebug() << "AccountManager requestError - " << error; } + +void AccountManager::requestProfile() { + if (!_networkAccessManager) { + _networkAccessManager = new QNetworkAccessManager(this); + } + + QUrl profileURL = _authURL; + profileURL.setPath("/api/v1/users/profile"); + profileURL.setQuery("access_token=" + _accountInfo.getAccessToken().token); + + QNetworkReply* profileReply = _networkAccessManager->get(QNetworkRequest(profileURL)); + connect(profileReply, &QNetworkReply::finished, this, &AccountManager::requestProfileFinished); + connect(profileReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestProfileError(QNetworkReply::NetworkError))); +} + +void AccountManager::requestProfileFinished() { + QNetworkReply* profileReply = reinterpret_cast(sender()); + + QJsonDocument jsonResponse = QJsonDocument::fromJson(profileReply->readAll()); + const QJsonObject& rootObject = jsonResponse.object(); + + if (rootObject.contains("status") && rootObject["status"].toString() == "success") { + _accountInfo.setProfileInfoFromJSON(rootObject); + + emit profileChanged(); + + // the username has changed to whatever came back + emit usernameChanged(_accountInfo.getUsername()); + + // store the whole profile into the local settings + QUrl rootURL = profileReply->url(); + rootURL.setPath(""); + QSettings localSettings; + localSettings.beginGroup(ACCOUNTS_GROUP); + localSettings.setValue(rootURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE), + QVariant::fromValue(_accountInfo)); + } else { + // TODO: error handling + qDebug() << "Error in response for profile"; + } +} + +void AccountManager::requestProfileError(QNetworkReply::NetworkError error) { + // TODO: error handling + qDebug() << "AccountManager requestProfileError - " << error; +} diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 628b084ea8..c18836ca54 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -23,9 +23,9 @@ class JSONCallbackParameters { public: JSONCallbackParameters(); - + bool isEmpty() const { return !jsonCallbackReceiver && !errorCallbackReceiver; } - + QObject* jsonCallbackReceiver; QString jsonCallbackMethod; QObject* errorCallbackReceiver; @@ -38,30 +38,33 @@ class AccountManager : public QObject { Q_OBJECT public: static AccountManager& getInstance(); - + void authenticatedRequest(const QString& path, QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation, const JSONCallbackParameters& callbackParams = JSONCallbackParameters(), const QByteArray& dataByteArray = QByteArray(), QHttpMultiPart* dataMultiPart = NULL); - + const QUrl& getAuthURL() const { return _authURL; } void setAuthURL(const QUrl& authURL); bool hasAuthEndpoint() { return !_authURL.isEmpty(); } - + bool isLoggedIn() { return !_authURL.isEmpty() && hasValidAccessToken(); } bool hasValidAccessToken(); Q_INVOKABLE bool checkAndSignalForAccessToken(); - + void requestAccessToken(const QString& login, const QString& password); - + void requestProfile(); + const DataServerAccountInfo& getAccountInfo() const { return _accountInfo; } - + void destroy() { delete _networkAccessManager; } - + public slots: - void requestFinished(); - void requestError(QNetworkReply::NetworkError error); + void requestAccessTokenFinished(); + void requestProfileFinished(); + void requestAccessTokenError(QNetworkReply::NetworkError error); + void requestProfileError(QNetworkReply::NetworkError error); void logout(); void updateBalance(); void accountInfoBalanceChanged(qint64 newBalance); @@ -69,7 +72,7 @@ signals: void authRequired(); void authEndpointChanged(); void usernameChanged(const QString& username); - void accessTokenChanged(); + void profileChanged(); void loginComplete(const QUrl& authURL); void loginFailed(); void logoutComplete(); @@ -80,19 +83,19 @@ private: AccountManager(); AccountManager(AccountManager const& other); // not implemented void operator=(AccountManager const& other); // not implemented - + void passSuccessToCallback(QNetworkReply* reply); void passErrorToCallback(QNetworkReply* reply); - + Q_INVOKABLE void invokedRequest(const QString& path, QNetworkAccessManager::Operation operation, const JSONCallbackParameters& callbackParams, const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart); - + QUrl _authURL; QNetworkAccessManager* _networkAccessManager; QMap _pendingCallbackMap; - + DataServerAccountInfo _accountInfo; }; diff --git a/libraries/networking/src/DataServerAccountInfo.cpp b/libraries/networking/src/DataServerAccountInfo.cpp index 809f083e35..c60a17e0d8 100644 --- a/libraries/networking/src/DataServerAccountInfo.cpp +++ b/libraries/networking/src/DataServerAccountInfo.cpp @@ -21,20 +21,7 @@ DataServerAccountInfo::DataServerAccountInfo() : _balance(0), _hasBalance(false) { - -} -DataServerAccountInfo::DataServerAccountInfo(const QJsonObject& jsonObject) : - _accessToken(jsonObject), - _username(), - _xmppPassword(), - _balance(0), - _hasBalance(false) -{ - QJsonObject userJSONObject = jsonObject["user"].toObject(); - setUsername(userJSONObject["username"].toString()); - setXMPPPassword(userJSONObject["xmpp_password"].toString()); - setDiscourseApiKey(userJSONObject["discourse_api_key"].toString()); } DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherInfo) { @@ -54,7 +41,7 @@ DataServerAccountInfo& DataServerAccountInfo::operator=(const DataServerAccountI void DataServerAccountInfo::swap(DataServerAccountInfo& otherInfo) { using std::swap; - + swap(_accessToken, otherInfo._accessToken); swap(_username, otherInfo._username); swap(_xmppPassword, otherInfo._xmppPassword); @@ -63,10 +50,14 @@ void DataServerAccountInfo::swap(DataServerAccountInfo& otherInfo) { swap(_hasBalance, otherInfo._hasBalance); } +void DataServerAccountInfo::setAccessTokenFromJSON(const QJsonObject& jsonObject) { + _accessToken = OAuthAccessToken(jsonObject); +} + void DataServerAccountInfo::setUsername(const QString& username) { if (_username != username) { _username = username; - + qDebug() << "Username changed to" << username; } } @@ -87,7 +78,7 @@ void DataServerAccountInfo::setBalance(qint64 balance) { if (!_hasBalance || _balance != balance) { _balance = balance; _hasBalance = true; - + emit balanceChanged(_balance); } } @@ -99,6 +90,17 @@ void DataServerAccountInfo::setBalanceFromJSON(const QJsonObject& jsonObject) { } } +bool DataServerAccountInfo::hasProfile() const { + return _username.length() > 0; +} + +void DataServerAccountInfo::setProfileInfoFromJSON(const QJsonObject& jsonObject) { + QJsonObject user = jsonObject["data"].toObject()["user"].toObject(); + setUsername(user["username"].toString()); + setXMPPPassword(user["xmpp_password"].toString()); + setDiscourseApiKey(user["discourse_api_key"].toString()); +} + QDataStream& operator<<(QDataStream &out, const DataServerAccountInfo& info) { out << info._accessToken << info._username << info._xmppPassword << info._discourseApiKey; return out; diff --git a/libraries/networking/src/DataServerAccountInfo.h b/libraries/networking/src/DataServerAccountInfo.h index e0209326f9..5800942376 100644 --- a/libraries/networking/src/DataServerAccountInfo.h +++ b/libraries/networking/src/DataServerAccountInfo.h @@ -22,12 +22,12 @@ class DataServerAccountInfo : public QObject { Q_OBJECT public: DataServerAccountInfo(); - DataServerAccountInfo(const QJsonObject& jsonObject); DataServerAccountInfo(const DataServerAccountInfo& otherInfo); DataServerAccountInfo& operator=(const DataServerAccountInfo& otherInfo); - + const OAuthAccessToken& getAccessToken() const { return _accessToken; } - + void setAccessTokenFromJSON(const QJsonObject& jsonObject); + const QString& getUsername() const { return _username; } void setUsername(const QString& username); @@ -36,20 +36,24 @@ public: const QString& getDiscourseApiKey() const { return _discourseApiKey; } void setDiscourseApiKey(const QString& discourseApiKey); - + qint64 getBalance() const { return _balance; } void setBalance(qint64 balance); bool hasBalance() const { return _hasBalance; } void setHasBalance(bool hasBalance) { _hasBalance = hasBalance; } Q_INVOKABLE void setBalanceFromJSON(const QJsonObject& jsonObject); + bool hasProfile() const; + + void setProfileInfoFromJSON(const QJsonObject& jsonObject); + friend QDataStream& operator<<(QDataStream &out, const DataServerAccountInfo& info); friend QDataStream& operator>>(QDataStream &in, DataServerAccountInfo& info); signals: qint64 balanceChanged(qint64 newBalance); private: void swap(DataServerAccountInfo& otherInfo); - + OAuthAccessToken _accessToken; QString _username; QString _xmppPassword; diff --git a/libraries/networking/src/NetworkPacket.cpp b/libraries/networking/src/NetworkPacket.cpp index bf18aa9b37..a8110847e1 100644 --- a/libraries/networking/src/NetworkPacket.cpp +++ b/libraries/networking/src/NetworkPacket.cpp @@ -17,9 +17,9 @@ #include "NetworkPacket.h" -void NetworkPacket::copyContents(const SharedNodePointer& destinationNode, const QByteArray& packet) { +void NetworkPacket::copyContents(const SharedNodePointer& node, const QByteArray& packet) { if (packet.size() && packet.size() <= MAX_PACKET_SIZE) { - _destinationNode = destinationNode; + _node = node; _byteArray = packet; } else { qDebug(">>> NetworkPacket::copyContents() unexpected length = %d", packet.size()); @@ -27,28 +27,28 @@ void NetworkPacket::copyContents(const SharedNodePointer& destinationNode, const } NetworkPacket::NetworkPacket(const NetworkPacket& packet) { - copyContents(packet.getDestinationNode(), packet.getByteArray()); + copyContents(packet.getNode(), packet.getByteArray()); } -NetworkPacket::NetworkPacket(const SharedNodePointer& destinationNode, const QByteArray& packet) { - copyContents(destinationNode, packet); +NetworkPacket::NetworkPacket(const SharedNodePointer& node, const QByteArray& packet) { + copyContents(node, packet); }; // copy assignment NetworkPacket& NetworkPacket::operator=(NetworkPacket const& other) { - copyContents(other.getDestinationNode(), other.getByteArray()); + copyContents(other.getNode(), other.getByteArray()); return *this; } #ifdef HAS_MOVE_SEMANTICS // move, same as copy, but other packet won't be used further NetworkPacket::NetworkPacket(NetworkPacket && packet) { - copyContents(packet.getDestinationNode(), packet.getByteArray()); + copyContents(packet.getNode(), packet.getByteArray()); } // move assignment NetworkPacket& NetworkPacket::operator=(NetworkPacket&& other) { - copyContents(other.getDestinationNode(), other.getByteArray()); + copyContents(other.getNode(), other.getByteArray()); return *this; } #endif diff --git a/libraries/networking/src/NetworkPacket.h b/libraries/networking/src/NetworkPacket.h index 94ddf8d56e..0be35f9fff 100644 --- a/libraries/networking/src/NetworkPacket.h +++ b/libraries/networking/src/NetworkPacket.h @@ -26,6 +26,7 @@ /// Storage of not-yet processed inbound, or not yet sent outbound generic UDP network packet class NetworkPacket { public: + NetworkPacket() { } NetworkPacket(const NetworkPacket& packet); // copy constructor NetworkPacket& operator= (const NetworkPacket& other); // copy assignment @@ -34,15 +35,15 @@ public: NetworkPacket& operator= (NetworkPacket&& other); // move assignment #endif - NetworkPacket(const SharedNodePointer& destinationNode, const QByteArray& byteArray); + NetworkPacket(const SharedNodePointer& node, const QByteArray& byteArray); - const SharedNodePointer& getDestinationNode() const { return _destinationNode; } + const SharedNodePointer& getNode() const { return _node; } const QByteArray& getByteArray() const { return _byteArray; } private: - void copyContents(const SharedNodePointer& destinationNode, const QByteArray& byteArray); + void copyContents(const SharedNodePointer& node, const QByteArray& byteArray); - SharedNodePointer _destinationNode; + SharedNodePointer _node; QByteArray _byteArray; }; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 9a298ce26c..b872ec12cf 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -281,7 +281,7 @@ void NodeList::processSTUNResponse(const QByteArray& packet) { int byteIndex = attributeStartIndex + NUM_BYTES_STUN_ATTR_TYPE_AND_LENGTH + NUM_BYTES_FAMILY_ALIGN; uint8_t addressFamily = 0; - memcpy(&addressFamily, packet.data(), sizeof(addressFamily)); + memcpy(&addressFamily, packet.data() + byteIndex, sizeof(addressFamily)); byteIndex += sizeof(addressFamily); diff --git a/libraries/networking/src/PacketSender.cpp b/libraries/networking/src/PacketSender.cpp index 5f7502a738..3edfc47c04 100644 --- a/libraries/networking/src/PacketSender.cpp +++ b/libraries/networking/src/PacketSender.cpp @@ -271,7 +271,7 @@ bool PacketSender::nonThreadedProcess() { unlock(); // send the packet through the NodeList... - NodeList::getInstance()->writeDatagram(temporary.getByteArray(), temporary.getDestinationNode()); + NodeList::getInstance()->writeDatagram(temporary.getByteArray(), temporary.getNode()); packetsSentThisCall++; _packetsOverCheckInterval++; _totalPacketsSent++; diff --git a/libraries/networking/src/ReceivedPacketProcessor.cpp b/libraries/networking/src/ReceivedPacketProcessor.cpp index d54e165285..3ef518bbc2 100644 --- a/libraries/networking/src/ReceivedPacketProcessor.cpp +++ b/libraries/networking/src/ReceivedPacketProcessor.cpp @@ -17,13 +17,14 @@ void ReceivedPacketProcessor::terminating() { _hasPackets.wakeAll(); } -void ReceivedPacketProcessor::queueReceivedPacket(const SharedNodePointer& destinationNode, const QByteArray& packet) { +void ReceivedPacketProcessor::queueReceivedPacket(const SharedNodePointer& sendingNode, const QByteArray& packet) { // Make sure our Node and NodeList knows we've heard from this node. - destinationNode->setLastHeardMicrostamp(usecTimestampNow()); + sendingNode->setLastHeardMicrostamp(usecTimestampNow()); - NetworkPacket networkPacket(destinationNode, packet); + NetworkPacket networkPacket(sendingNode, packet); lock(); _packets.push_back(networkPacket); + _nodePacketCounts[sendingNode->getUUID()]++; unlock(); // Make sure to wake our actual processing thread because we now have packets for it to process. @@ -42,8 +43,15 @@ bool ReceivedPacketProcessor::process() { NetworkPacket& packet = _packets.front(); // get the oldest packet NetworkPacket temporary = packet; // make a copy of the packet in case the vector is resized on us _packets.erase(_packets.begin()); // remove the oldest packet + _nodePacketCounts[temporary.getNode()->getUUID()]--; unlock(); // let others add to the packets - processPacket(temporary.getDestinationNode(), temporary.getByteArray()); // process our temporary copy + processPacket(temporary.getNode(), temporary.getByteArray()); // process our temporary copy } return isStillRunning(); // keep running till they terminate us } + +void ReceivedPacketProcessor::nodeKilled(SharedNodePointer node) { + lock(); + _nodePacketCounts.remove(node->getUUID()); + unlock(); +} diff --git a/libraries/networking/src/ReceivedPacketProcessor.h b/libraries/networking/src/ReceivedPacketProcessor.h index f8306b4896..94ad2d9c41 100644 --- a/libraries/networking/src/ReceivedPacketProcessor.h +++ b/libraries/networking/src/ReceivedPacketProcessor.h @@ -28,20 +28,26 @@ public: /// \param packetData pointer to received data /// \param ssize_t packetLength size of received data /// \thread network receive thread - void queueReceivedPacket(const SharedNodePointer& destinationNode, const QByteArray& packet); + void queueReceivedPacket(const SharedNodePointer& sendingNode, const QByteArray& packet); /// Are there received packets waiting to be processed bool hasPacketsToProcess() const { return _packets.size() > 0; } + /// Are there received packets waiting to be processed from a certain node + bool hasPacketsToProcessFrom(const SharedNodePointer& sendingNode) const { + return _nodePacketCounts[sendingNode->getUUID()] > 0; + } + /// How many received packets waiting are to be processed int packetsToProcessCount() const { return _packets.size(); } +public slots: + void nodeKilled(SharedNodePointer node); + protected: /// Callback for processing of recieved packets. Implement this to process the incoming packets. - /// \param sockaddr& senderAddress the address of the sender - /// \param packetData pointer to received data - /// \param ssize_t packetLength size of received data - /// \thread "this" individual processing thread + /// \param SharedNodePointer& sendingNode the node that sent this packet + /// \param QByteArray& the packet to be processed virtual void processPacket(const SharedNodePointer& sendingNode, const QByteArray& packet) = 0; /// Implements generic processing behavior for this thread. @@ -51,7 +57,9 @@ protected: private: - std::vector _packets; + QVector _packets; + QHash _nodePacketCounts; + QWaitCondition _hasPackets; QMutex _waitingOnPacketsMutex; }; diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index e8ab59ce2d..02cce80104 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -19,71 +19,70 @@ #include "HifiConfigVariantMap.h" QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringList& argumentList) { - + QVariantMap mergedMap; - + // Add anything in the CL parameter list to the variant map. // Take anything with a dash in it as a key, and the values after it as the value. - + const QString DASHED_KEY_REGEX_STRING = "(^-{1,2})([\\w-]+)"; QRegExp dashedKeyRegex(DASHED_KEY_REGEX_STRING); - + int keyIndex = argumentList.indexOf(dashedKeyRegex); int nextKeyIndex = 0; - + // check if there is a config file to read where we can pull config info not passed on command line const QString CONFIG_FILE_OPTION = "--config"; - + while (keyIndex != -1) { if (argumentList[keyIndex] != CONFIG_FILE_OPTION) { // we have a key - look forward to see how many values associate to it QString key = dashedKeyRegex.cap(2); - + nextKeyIndex = argumentList.indexOf(dashedKeyRegex, keyIndex + 1); - + if (nextKeyIndex == keyIndex + 1 || keyIndex == argumentList.size() - 1) { - // there's no value associated with this option, it's a boolean - // so add it to the variant map with NULL as value - mergedMap.insertMulti(key, QVariant()); + // this option is simply a switch, so add it to the map with a value of `true` + mergedMap.insertMulti(key, QVariant(true)); } else { - int maxIndex = (nextKeyIndex == -1) ? argumentList.size() - 1: nextKeyIndex; - + int maxIndex = (nextKeyIndex == -1) ? argumentList.size() : nextKeyIndex; + // there's at least one value associated with the option // pull the first value to start QString value = argumentList[keyIndex + 1]; - + // for any extra values, append them, with a space, to the value string - for (int i = keyIndex + 2; i <= maxIndex; i++) { + for (int i = keyIndex + 2; i < maxIndex; i++) { value += " " + argumentList[i]; } - + // add the finalized value to the merged map mergedMap.insert(key, value); } - + keyIndex = nextKeyIndex; } else { keyIndex = argumentList.indexOf(dashedKeyRegex, keyIndex + 1); } } - + int configIndex = argumentList.indexOf(CONFIG_FILE_OPTION); - + if (configIndex != -1) { // we have a config file - try and read it QString configFilePath = argumentList[configIndex + 1]; QFile configFile(configFilePath); - + if (configFile.exists()) { qDebug() << "Reading JSON config file at" << configFilePath; configFile.open(QIODevice::ReadOnly); - + QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll()); QJsonObject rootObject = configDocument.object(); - + // enumerate the keys of the configDocument object foreach(const QString& key, rootObject.keys()) { - + if (!mergedMap.contains(key)) { // no match in existing list, add it mergedMap.insert(key, QVariant(rootObject[key])); @@ -93,6 +92,6 @@ QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringL qDebug() << "Could not find JSON config file at" << configFilePath; } } - + return mergedMap; } diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index f29e8e3345..e4d2e1c835 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include "OctalCode.h" #include "SharedUtil.h" @@ -415,13 +416,17 @@ void printVoxelCode(unsigned char* voxelCode) { #ifdef _WIN32 void usleep(int waitTime) { - __int64 time1 = 0, time2 = 0, sysFreq = 0; - - QueryPerformanceCounter((LARGE_INTEGER *)&time1); - QueryPerformanceFrequency((LARGE_INTEGER *)&sysFreq); - do { - QueryPerformanceCounter((LARGE_INTEGER *)&time2); - } while( (time2 - time1) < waitTime); + const quint64 BUSY_LOOP_USECS = 2000; + quint64 compTime = waitTime + usecTimestampNow(); + quint64 compTimeSleep = compTime - BUSY_LOOP_USECS; + while (true) { + if (usecTimestampNow() < compTimeSleep) { + QThread::msleep(1); + } + if (usecTimestampNow() >= compTime) { + break; + } + } } #endif diff --git a/tests/metavoxels/src/MetavoxelTests.cpp b/tests/metavoxels/src/MetavoxelTests.cpp index 81f1840342..6ec2331b14 100644 --- a/tests/metavoxels/src/MetavoxelTests.cpp +++ b/tests/metavoxels/src/MetavoxelTests.cpp @@ -11,6 +11,8 @@ #include +#include + #include #include @@ -41,6 +43,8 @@ static int streamedBytesReceived = 0; static int sharedObjectsCreated = 0; static int sharedObjectsDestroyed = 0; static int objectMutationsPerformed = 0; +static int scriptObjectsCreated = 0; +static int scriptMutationsPerformed = 0; static QByteArray createRandomBytes(int minimumSize, int maximumSize) { QByteArray bytes(randIntInRange(minimumSize, maximumSize), 0); @@ -79,6 +83,48 @@ static TestSharedObjectA::TestFlags getRandomTestFlags() { return flags; } +static QScriptValue createRandomScriptValue(bool complex = false) { + scriptObjectsCreated++; + switch (randIntInRange(0, complex ? 5 : 3)) { + case 0: + return QScriptValue(QScriptValue::NullValue); + + case 1: + return QScriptValue(randomBoolean()); + + case 2: + return QScriptValue(randFloat()); + + case 3: + return QScriptValue(QString(createRandomBytes())); + + case 4: { + int length = randIntInRange(2, 6); + QScriptValue value = ScriptCache::getInstance()->getEngine()->newArray(length); + for (int i = 0; i < length; i++) { + value.setProperty(i, createRandomScriptValue()); + } + return value; + } + default: { + QScriptValue value = ScriptCache::getInstance()->getEngine()->newObject(); + if (randomBoolean()) { + value.setProperty("foo", createRandomScriptValue()); + } + if (randomBoolean()) { + value.setProperty("bar", createRandomScriptValue()); + } + if (randomBoolean()) { + value.setProperty("baz", createRandomScriptValue()); + } + if (randomBoolean()) { + value.setProperty("bong", createRandomScriptValue()); + } + return value; + } + } +} + static TestMessageC createRandomMessageC() { TestMessageC message; message.foo = randomBoolean(); @@ -86,6 +132,7 @@ static TestMessageC createRandomMessageC() { message.baz = randFloat(); message.bong.foo = createRandomBytes(); message.bong.baz = getRandomTestEnum(); + message.bizzle = createRandomScriptValue(true); return message; } @@ -201,6 +248,7 @@ bool MetavoxelTests::run() { datagramsReceived << "with" << bytesReceived << "bytes"; qDebug() << "Created" << sharedObjectsCreated << "shared objects, destroyed" << sharedObjectsDestroyed; qDebug() << "Performed" << objectMutationsPerformed << "object mutations"; + qDebug() << "Created" << scriptObjectsCreated << "script objects, mutated" << scriptMutationsPerformed; qDebug(); qDebug() << "Running serialization tests..."; @@ -284,7 +332,7 @@ static QVariant createRandomMessage() { } static SharedObjectPointer mutate(const SharedObjectPointer& state) { - switch(randIntInRange(0, 3)) { + switch (randIntInRange(0, 4)) { case 0: { SharedObjectPointer newState = state->clone(true); static_cast(newState.data())->setFoo(randFloat()); @@ -303,6 +351,38 @@ static SharedObjectPointer mutate(const SharedObjectPointer& state) { objectMutationsPerformed++; return newState; } + case 3: { + SharedObjectPointer newState = state->clone(true); + QScriptValue oldValue = static_cast(newState.data())->getBizzle(); + QScriptValue newValue = ScriptCache::getInstance()->getEngine()->newObject(); + for (QScriptValueIterator it(oldValue); it.hasNext(); ) { + it.next(); + newValue.setProperty(it.scriptName(), it.value()); + } + switch (randIntInRange(0, 2)) { + case 0: { + QScriptValue oldArray = oldValue.property("foo"); + int oldLength = oldArray.property(ScriptCache::getInstance()->getLengthString()).toInt32(); + QScriptValue newArray = ScriptCache::getInstance()->getEngine()->newArray(oldLength); + for (int i = 0; i < oldLength; i++) { + newArray.setProperty(i, oldArray.property(i)); + } + newArray.setProperty(randIntInRange(0, oldLength - 1), createRandomScriptValue(true)); + break; + } + case 1: + newValue.setProperty("bar", QScriptValue(randFloat())); + break; + + default: + newValue.setProperty("baz", createRandomScriptValue(true)); + break; + } + static_cast(newState.data())->setBizzle(newValue); + scriptMutationsPerformed++; + objectMutationsPerformed++; + return newState; + } default: return state; } @@ -503,7 +583,10 @@ TestSharedObjectA::TestSharedObjectA(float foo, TestEnum baz, TestFlags bong) : _foo(foo), _baz(baz), _bong(bong) { - sharedObjectsCreated++; + sharedObjectsCreated++; + + _bizzle = ScriptCache::getInstance()->getEngine()->newObject(); + _bizzle.setProperty("foo", ScriptCache::getInstance()->getEngine()->newArray(4)); } TestSharedObjectA::~TestSharedObjectA() { diff --git a/tests/metavoxels/src/MetavoxelTests.h b/tests/metavoxels/src/MetavoxelTests.h index 345ea624df..ac9eda2659 100644 --- a/tests/metavoxels/src/MetavoxelTests.h +++ b/tests/metavoxels/src/MetavoxelTests.h @@ -16,6 +16,7 @@ #include #include +#include class SequencedTestMessage; @@ -96,7 +97,8 @@ class TestSharedObjectA : public SharedObject { Q_PROPERTY(float foo READ getFoo WRITE setFoo NOTIFY fooChanged) Q_PROPERTY(TestEnum baz READ getBaz WRITE setBaz) Q_PROPERTY(TestFlags bong READ getBong WRITE setBong) - + Q_PROPERTY(QScriptValue bizzle READ getBizzle WRITE setBizzle) + public: enum TestEnum { FIRST_TEST_ENUM, SECOND_TEST_ENUM, THIRD_TEST_ENUM }; @@ -116,6 +118,9 @@ public: void setBong(TestFlags bong) { _bong = bong; } TestFlags getBong() const { return _bong; } + void setBizzle(const QScriptValue& bizzle) { _bizzle = bizzle; } + const QScriptValue& getBizzle() const { return _bizzle; } + signals: void fooChanged(float foo); @@ -125,6 +130,7 @@ private: float _foo; TestEnum _baz; TestFlags _bong; + QScriptValue _bizzle; }; DECLARE_ENUM_METATYPE(TestSharedObjectA, TestEnum) @@ -204,6 +210,7 @@ class TestMessageC : STREAM public TestMessageA { public: STREAM TestMessageB bong; + STREAM QScriptValue bizzle; }; DECLARE_STREAMABLE_METATYPE(TestMessageC)