diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp
index b3a92f2f54..009bd42e88 100644
--- a/assignment-client/src/AssignmentClient.cpp
+++ b/assignment-client/src/AssignmentClient.cpp
@@ -67,9 +67,20 @@ AssignmentClient::AssignmentClient(int &argc, char **argv) :
if (argumentIndex != -1) {
assignmentPool = argumentList[argumentIndex + 1];
}
+
// 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
+ // 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]);
+ 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);
diff --git a/domain-server/resources/web/index.shtml b/domain-server/resources/web/index.shtml
index afd0af1679..b6ba8f67db 100644
--- a/domain-server/resources/web/index.shtml
+++ b/domain-server/resources/web/index.shtml
@@ -13,6 +13,7 @@
Public |
Local |
Uptime (s) |
+ Pending Credits |
Kill? |
diff --git a/domain-server/resources/web/js/tables.js b/domain-server/resources/web/js/tables.js
index a4884486c3..b564d9392f 100644
--- a/domain-server/resources/web/js/tables.js
+++ b/domain-server/resources/web/js/tables.js
@@ -42,6 +42,8 @@ $(document).ready(function(){
var uptimeSeconds = (Date.now() - data.wake_timestamp) / 1000;
nodesTableBody += "" + uptimeSeconds.toLocaleString() + " | ";
+ nodesTableBody += "" + (typeof data.pending_credits == 'number' ? data.pending_credits.toLocaleString() : 'N/A') + " | ";
+
nodesTableBody += " | ";
nodesTableBody += "";
});
diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp
index f6af63fd7a..9ad36e6956 100644
--- a/domain-server/src/DomainServer.cpp
+++ b/domain-server/src/DomainServer.cpp
@@ -35,6 +35,7 @@ DomainServer::DomainServer(int argc, char* argv[]) :
_httpsManager(NULL),
_allAssignments(),
_unfulfilledAssignments(),
+ _pendingAssignedNodes(),
_isUsingDTLS(false),
_oauthProviderURL(),
_oauthClientID(),
@@ -49,13 +50,14 @@ DomainServer::DomainServer(int argc, char* argv[]) :
_argumentVariantMap = HifiConfigVariantMap::mergeCLParametersWithJSONConfig(arguments());
- if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) {
- // we either read a certificate and private key or were not passed one, good to load assignments
- // and set up the node list
+ _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();
-
- _networkAccessManager = new QNetworkAccessManager(this);
}
}
@@ -129,7 +131,7 @@ bool DomainServer::optionallySetupOAuth() {
_oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV);
_hostname = _argumentVariantMap.value(REDIRECT_HOSTNAME_OPTION).toString();
- if (!_oauthProviderURL.isEmpty() || !_hostname.isEmpty() || !_oauthClientID.isEmpty()) {
+ if (!_oauthClientID.isEmpty()) {
if (_oauthProviderURL.isEmpty()
|| _hostname.isEmpty()
|| _oauthClientID.isEmpty()
@@ -187,6 +189,65 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) {
addStaticAssignmentsToQueue();
}
+bool DomainServer::optionallySetupAssignmentPayment() {
+ // check if we have a username and password set via env
+ 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 (!_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 {
+ qDebug() << "Missing access-token or username and password combination. domain-server will now quit.";
+ QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection);
+ return false;
+ }
+ }
+
+ // 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;
+}
+
+void DomainServer::loginFailed() {
+ qDebug() << "Login to data server has failed. domain-server will now quit";
+ QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection);
+}
+
void DomainServer::parseAssignmentConfigs(QSet& excludedTypes) {
// check for configs from the command line, these take precedence
const QString ASSIGNMENT_CONFIG_REGEX_STRING = "config-([\\d]+)";
@@ -339,10 +400,23 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock
QUuid packetUUID = uuidFromPacketHeader(packet);
// check if this connect request matches an assignment in the queue
- bool isFulfilledOrUnfulfilledAssignment = _allAssignments.contains(packetUUID);
+ bool isAssignment = _pendingAssignedNodes.contains(packetUUID);
SharedAssignmentPointer matchingQueuedAssignment = SharedAssignmentPointer();
- if (isFulfilledOrUnfulfilledAssignment) {
- matchingQueuedAssignment = matchingQueuedAssignmentForCheckIn(packetUUID, nodeType);
+ PendingAssignedNodeData* pendingAssigneeData = NULL;
+
+ if (isAssignment) {
+ pendingAssigneeData = _pendingAssignedNodes.take(packetUUID);
+
+ if (pendingAssigneeData) {
+ matchingQueuedAssignment = matchingQueuedAssignmentForCheckIn(pendingAssigneeData->getAssignmentUUID(), nodeType);
+
+ if (matchingQueuedAssignment) {
+ qDebug() << "Assignment deployed with" << uuidStringWithoutCurlyBraces(packetUUID)
+ << "matches unfulfilled assignment"
+ << uuidStringWithoutCurlyBraces(matchingQueuedAssignment->getUUID());
+ }
+ }
+
}
if (!matchingQueuedAssignment && !_oauthProviderURL.isEmpty() && _argumentVariantMap.contains(ALLOWED_ROLES_CONFIG_KEY)) {
@@ -371,8 +445,8 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock
}
}
- if ((!isFulfilledOrUnfulfilledAssignment && !STATICALLY_ASSIGNED_NODES.contains(nodeType))
- || (isFulfilledOrUnfulfilledAssignment && matchingQueuedAssignment)) {
+ 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
@@ -384,8 +458,12 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock
// if this was a static assignment set the UUID, set the sendingSockAddr
DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData());
- if (isFulfilledOrUnfulfilledAssignment) {
- nodeData->setAssignmentUUID(packetUUID);
+ 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);
@@ -564,14 +642,21 @@ void DomainServer::readAvailableDatagrams() {
Assignment requestAssignment(receivedPacket);
// Suppress these for Assignment::AgentType to once per 5 seconds
- static quint64 lastNoisyMessage = usecTimestampNow();
- quint64 timeNow = usecTimestampNow();
- const quint64 NOISY_TIME_ELAPSED = 5 * USECS_PER_SECOND;
- bool noisyMessage = false;
- if (requestAssignment.getType() != Assignment::AgentType || (timeNow - lastNoisyMessage) > NOISY_TIME_ELAPSED) {
+ 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;
- noisyMessage = true;
+ << "from" << senderSockAddr;
+ noisyMessageTimer.restart();
}
SharedAssignmentPointer assignmentToDeploy = deployableAssignmentForRequest(requestAssignment);
@@ -582,23 +667,29 @@ void DomainServer::readAvailableDatagrams() {
// 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 << *assignmentToDeploy.data();
+ 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());
+ _pendingAssignedNodes.insert(uniqueAssignment.getUUID(), pendingNodeData);
} else {
- if (requestAssignment.getType() != Assignment::AgentType || (timeNow - lastNoisyMessage) > NOISY_TIME_ELAPSED) {
+ if (requestAssignment.getType() != Assignment::AgentType
+ || noisyMessageTimer.elapsed() > NOISY_MESSAGE_INTERVAL_MSECS) {
qDebug() << "Unable to fulfill assignment request of type" << requestAssignment.getType()
- << "from" << senderSockAddr;
- noisyMessage = true;
+ << "from" << senderSockAddr;
+ noisyMessageTimer.restart();
}
}
-
- if (noisyMessage) {
- lastNoisyMessage = timeNow;
- }
} else if (!_isUsingDTLS) {
// not using DTLS, process datagram normally
processDatagram(receivedPacket, senderSockAddr);
@@ -618,6 +709,97 @@ void DomainServer::readAvailableDatagrams() {
}
}
+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();
+ break;
+ } else {
+ ++i;
+ }
+ }
+
+ qint64 elapsedMsecsSinceLastPayment = nodeData->getPaymentIntervalTimer().elapsed();
+ nodeData->getPaymentIntervalTimer().restart();
+
+ const float CREDITS_PER_HOUR = 3;
+ const float CREDITS_PER_MSEC = CREDITS_PER_HOUR / (60 * 60 * 1000);
+
+ float pendingCredits = elapsedMsecsSinceLastPayment * CREDITS_PER_MSEC;
+
+ if (existingTransaction) {
+ existingTransaction->incrementAmount(pendingCredits);
+ } else {
+ // create a fresh transaction to pay this node, there is no transaction to append to
+ WalletTransaction* freshTransaction = new WalletTransaction(nodeData->getWalletUUID(), pendingCredits);
+ _pendingAssignmentCredits.insert(nodeData->getWalletUUID(), freshTransaction);
+ }
+ }
+ }
+}
+
+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) {
+ // check if this was successful - if so we can remove it from our list of pending
+ if (data.value("status").toString() == "success") {
+ // 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;
+ }
+ }
+ }
+}
+
void DomainServer::processDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr) {
LimitedNodeList* nodeList = LimitedNodeList::getInstance();
@@ -667,6 +849,7 @@ const char JSON_KEY_TYPE[] = "type";
const char JSON_KEY_PUBLIC_SOCKET[] = "public";
const char JSON_KEY_LOCAL_SOCKET[] = "local";
const char JSON_KEY_POOL[] = "pool";
+const char JSON_KEY_PENDING_CREDITS[] = "pending_credits";
const char JSON_KEY_WAKE_TIMESTAMP[] = "wake_timestamp";
QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) {
@@ -695,6 +878,18 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) {
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());
+ double pendingCreditAmount = 0;
+
+ while (i != _pendingAssignmentCredits.end() && i.key() == nodeData->getWalletUUID()) {
+ pendingCreditAmount += i.value()->getAmount();
+ ++i;
+ }
+
+ nodeJson[JSON_KEY_PENDING_CREDITS] = pendingCreditAmount;
+ }
}
return nodeJson;
@@ -764,6 +959,24 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
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
@@ -1062,11 +1275,15 @@ void DomainServer::nodeKilled(SharedNodePointer node) {
}
}
-SharedAssignmentPointer DomainServer::matchingQueuedAssignmentForCheckIn(const QUuid& checkInUUID, NodeType_t nodeType) {
+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() == checkInUUID) {
+ 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;
diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h
index 71809d9e16..b038850b3d 100644
--- a/domain-server/src/DomainServer.h
+++ b/domain-server/src/DomainServer.h
@@ -24,7 +24,12 @@
#include
#include
+#include "WalletTransaction.h"
+
+#include "PendingAssignedNodeData.h"
+
typedef QSharedPointer SharedAssignmentPointer;
+typedef QMultiHash TransactionHash;
class DomainServer : public QCoreApplication, public HTTPSRequestHandler {
Q_OBJECT
@@ -42,13 +47,18 @@ public slots:
/// Called by NodeList to inform us a node has been killed
void nodeKilled(SharedNodePointer node);
-private slots:
+ void transactionJSONCallback(const QJsonObject& data);
+private slots:
+ void loginFailed();
void readAvailableDatagrams();
+ void setupPendingAssignmentCredits();
+ void sendPendingTransactionsToServer();
private:
void setupNodeListAndAssignments(const QUuid& sessionUUID = QUuid::createUuid());
bool optionallySetupOAuth();
bool optionallyReadX509KeyAndCertificate();
+ bool optionallySetupAssignmentPayment();
void processDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr);
@@ -85,6 +95,8 @@ private:
QHash _allAssignments;
QQueue _unfulfilledAssignments;
+ QHash _pendingAssignedNodes;
+ TransactionHash _pendingAssignmentCredits;
QVariantMap _argumentVariantMap;
diff --git a/domain-server/src/DomainServerNodeData.cpp b/domain-server/src/DomainServerNodeData.cpp
index a43e17ae60..68903cc106 100644
--- a/domain-server/src/DomainServerNodeData.cpp
+++ b/domain-server/src/DomainServerNodeData.cpp
@@ -20,11 +20,13 @@
DomainServerNodeData::DomainServerNodeData() :
_sessionSecretHash(),
_assignmentUUID(),
+ _walletUUID(),
+ _paymentIntervalTimer(),
_statsJSONObject(),
_sendingSockAddr(),
_isAuthenticated(true)
{
-
+ _paymentIntervalTimer.start();
}
void DomainServerNodeData::parseJSONStatsPacket(const QByteArray& statsPacket) {
diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h
index 011bee57c1..a7d7233874 100644
--- a/domain-server/src/DomainServerNodeData.h
+++ b/domain-server/src/DomainServerNodeData.h
@@ -12,6 +12,8 @@
#ifndef hifi_DomainServerNodeData_h
#define hifi_DomainServerNodeData_h
+
+#include
#include
#include
@@ -30,6 +32,11 @@ public:
void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; }
const QUuid& getAssignmentUUID() const { return _assignmentUUID; }
+ void setWalletUUID(const QUuid& walletUUID) { _walletUUID = walletUUID; }
+ const QUuid& getWalletUUID() const { return _walletUUID; }
+
+ QElapsedTimer& getPaymentIntervalTimer() { return _paymentIntervalTimer; }
+
void setSendingSockAddr(const HifiSockAddr& sendingSockAddr) { _sendingSockAddr = sendingSockAddr; }
const HifiSockAddr& getSendingSockAddr() { return _sendingSockAddr; }
@@ -42,6 +49,8 @@ private:
QHash _sessionSecretHash;
QUuid _assignmentUUID;
+ QUuid _walletUUID;
+ QElapsedTimer _paymentIntervalTimer;
QJsonObject _statsJSONObject;
HifiSockAddr _sendingSockAddr;
bool _isAuthenticated;
diff --git a/domain-server/src/PendingAssignedNodeData.cpp b/domain-server/src/PendingAssignedNodeData.cpp
new file mode 100644
index 0000000000..21b3aa4ca4
--- /dev/null
+++ b/domain-server/src/PendingAssignedNodeData.cpp
@@ -0,0 +1,19 @@
+//
+// PendingAssignedNodeData.cpp
+// domain-server/src
+//
+// Created by Stephen Birarda on 2014-05-20.
+// Copyright 2014 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#include "PendingAssignedNodeData.h"
+
+PendingAssignedNodeData::PendingAssignedNodeData(const QUuid& assignmentUUID, const QUuid& walletUUID) :
+ _assignmentUUID(assignmentUUID),
+ _walletUUID(walletUUID)
+{
+
+}
\ No newline at end of file
diff --git a/domain-server/src/PendingAssignedNodeData.h b/domain-server/src/PendingAssignedNodeData.h
new file mode 100644
index 0000000000..93d99a6f8f
--- /dev/null
+++ b/domain-server/src/PendingAssignedNodeData.h
@@ -0,0 +1,33 @@
+//
+// PendingAssignedNodeData.h
+// domain-server/src
+//
+// Created by Stephen Birarda on 2014-05-20.
+// Copyright 2014 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#ifndef hifi_PendingAssignedNodeData_h
+#define hifi_PendingAssignedNodeData_h
+
+#include
+#include
+
+class PendingAssignedNodeData : public QObject {
+ Q_OBJECT
+public:
+ PendingAssignedNodeData(const QUuid& assignmentUUID, const QUuid& walletUUID);
+
+ void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; }
+ const QUuid& getAssignmentUUID() const { return _assignmentUUID; }
+
+ void setWalletUUID(const QUuid& walletUUID) { _walletUUID = walletUUID; }
+ const QUuid& getWalletUUID() const { return _walletUUID; }
+private:
+ QUuid _assignmentUUID;
+ QUuid _walletUUID;
+};
+
+#endif // hifi_PendingAssignedNodeData_h
\ No newline at end of file
diff --git a/domain-server/src/WalletTransaction.cpp b/domain-server/src/WalletTransaction.cpp
new file mode 100644
index 0000000000..6ff57f063c
--- /dev/null
+++ b/domain-server/src/WalletTransaction.cpp
@@ -0,0 +1,67 @@
+//
+// WalletTransaction.cpp
+// domain-server/src
+//
+// Created by Stephen Birarda on 2014-05-20.
+// Copyright 2014 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#include
+
+#include
+
+#include "WalletTransaction.h"
+
+WalletTransaction::WalletTransaction() :
+ _uuid(),
+ _destinationUUID(),
+ _amount(),
+ _isFinalized(false)
+{
+
+}
+
+WalletTransaction::WalletTransaction(const QUuid& destinationUUID, double amount) :
+ _uuid(QUuid::createUuid()),
+ _destinationUUID(destinationUUID),
+ _amount(amount),
+ _isFinalized(false)
+{
+
+}
+
+const QString TRANSACTION_ID_KEY = "id";
+const QString TRANSACTION_DESTINATION_WALLET_ID_KEY = "destination_wallet_id";
+const QString TRANSACTION_AMOUNT_KEY = "amount";
+
+const QString ROOT_OBJECT_TRANSACTION_KEY = "transaction";
+
+QJsonDocument WalletTransaction::postJson() {
+ QJsonObject rootObject;
+
+ rootObject.insert(ROOT_OBJECT_TRANSACTION_KEY, toJson());
+
+ return QJsonDocument(rootObject);
+}
+
+QJsonObject WalletTransaction::toJson() {
+ QJsonObject transactionObject;
+
+ transactionObject.insert(TRANSACTION_ID_KEY, uuidStringWithoutCurlyBraces(_uuid));
+ transactionObject.insert(TRANSACTION_DESTINATION_WALLET_ID_KEY, uuidStringWithoutCurlyBraces(_destinationUUID));
+ transactionObject.insert(TRANSACTION_AMOUNT_KEY, _amount);
+
+ return transactionObject;
+}
+
+void WalletTransaction::loadFromJson(const QJsonObject& jsonObject) {
+ // pull the destination wallet and ID of the transaction to match it
+ QJsonObject transactionObject = jsonObject.value("data").toObject().value(ROOT_OBJECT_TRANSACTION_KEY).toObject();
+
+ _uuid = QUuid(transactionObject.value(TRANSACTION_ID_KEY).toString());
+ _destinationUUID = QUuid(transactionObject.value(TRANSACTION_DESTINATION_WALLET_ID_KEY).toString());
+ _amount = transactionObject.value(TRANSACTION_AMOUNT_KEY).toDouble();
+}
\ No newline at end of file
diff --git a/domain-server/src/WalletTransaction.h b/domain-server/src/WalletTransaction.h
new file mode 100644
index 0000000000..8f36d10302
--- /dev/null
+++ b/domain-server/src/WalletTransaction.h
@@ -0,0 +1,46 @@
+//
+// WalletTransaction.h
+// domain-server/src
+//
+// Created by Stephen Birarda on 2014-05-20.
+// Copyright 2014 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#ifndef hifi_WalletTransaction_h
+#define hifi_WalletTransaction_h
+
+#include
+#include
+#include
+
+class WalletTransaction : public QObject {
+public:
+ WalletTransaction();
+ WalletTransaction(const QUuid& destinationUUID, double amount);
+
+ const QUuid& getUUID() const { return _uuid; }
+
+ void setDestinationUUID(const QUuid& destinationUUID) { _destinationUUID = destinationUUID; }
+ const QUuid& getDestinationUUID() const { return _destinationUUID; }
+
+ double getAmount() const { return _amount; }
+ void setAmount(double amount) { _amount = amount; }
+ void incrementAmount(double increment) { _amount += increment; }
+
+ bool isFinalized() const { return _isFinalized; }
+ void setIsFinalized(bool isFinalized) { _isFinalized = isFinalized; }
+
+ QJsonDocument postJson();
+ QJsonObject toJson();
+ void loadFromJson(const QJsonObject& jsonObject);
+private:
+ QUuid _uuid;
+ QUuid _destinationUUID;
+ double _amount;
+ bool _isFinalized;
+};
+
+#endif // hifi_WalletTransaction_h
\ No newline at end of file
diff --git a/examples/editVoxels.js b/examples/editVoxels.js
index b9f5d925d9..9040306bf6 100644
--- a/examples/editVoxels.js
+++ b/examples/editVoxels.js
@@ -630,7 +630,7 @@ var trackAsDelete = false;
var trackAsRecolor = false;
var trackAsEyedropper = false;
-var voxelToolSelected = true;
+var voxelToolSelected = false;
var recolorToolSelected = false;
var eyedropperToolSelected = false;
var pasteMode = false;
@@ -848,7 +848,7 @@ function showPreviewLines() {
}
function showPreviewGuides() {
- if (editToolsOn && !isImporting) {
+ if (editToolsOn && !isImporting && (voxelToolSelected || recolorToolSelected || eyedropperToolSelected)) {
if (previewAsVoxel) {
showPreviewVoxel();
@@ -964,7 +964,7 @@ function mousePressEvent(event) {
if (clickedOverlay == voxelTool) {
modeSwitchSound.play(0);
- voxelToolSelected = true;
+ voxelToolSelected = !voxelToolSelected;
recolorToolSelected = false;
eyedropperToolSelected = false;
moveTools();
@@ -972,7 +972,7 @@ function mousePressEvent(event) {
} else if (clickedOverlay == recolorTool) {
modeSwitchSound.play(1);
voxelToolSelected = false;
- recolorToolSelected = true;
+ recolorToolSelected = !recolorToolSelected;
eyedropperToolSelected = false;
moveTools();
clickedOnSomething = true;
@@ -980,7 +980,7 @@ function mousePressEvent(event) {
modeSwitchSound.play(2);
voxelToolSelected = false;
recolorToolSelected = false;
- eyedropperToolSelected = true;
+ eyedropperToolSelected = !eyedropperToolSelected;
moveTools();
clickedOnSomething = true;
} else if (scaleSelector.clicked(event.x, event.y)) {
@@ -1000,7 +1000,7 @@ function mousePressEvent(event) {
}
}
}
- if (clickedOnSomething || isImporting) {
+ if (clickedOnSomething || isImporting || (!voxelToolSelected && !recolorToolSelected && !eyedropperToolSelected)) {
return; // no further processing
}
@@ -1344,7 +1344,7 @@ function moveTools() {
recolorToolOffset = 2;
} else if (eyedropperToolSelected) {
eyedropperToolOffset = 2;
- } else {
+ } else if (voxelToolSelected) {
if (pasteMode) {
voxelToolColor = pasteModeColor;
}
diff --git a/examples/sit.js b/examples/sit.js
new file mode 100644
index 0000000000..1df877dba6
--- /dev/null
+++ b/examples/sit.js
@@ -0,0 +1,153 @@
+//
+// sit.js
+// examples
+//
+// Created by Mika Impola on February 8, 2014
+// Copyright 2014 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+
+var buttonImageUrl = "https://worklist-prod.s3.amazonaws.com/attachment/0aca88e1-9bd8-5c1d.svg";
+
+var windowDimensions = Controller.getViewportDimensions();
+
+var buttonWidth = 37;
+var buttonHeight = 46;
+var buttonPadding = 10;
+
+var buttonPositionX = windowDimensions.x - buttonPadding - buttonWidth;
+var buttonPositionY = (windowDimensions.y - buttonHeight) / 2 ;
+
+var sitDownButton = Overlays.addOverlay("image", {
+ x: buttonPositionX, y: buttonPositionY, width: buttonWidth, height: buttonHeight,
+ subImage: { x: 0, y: buttonHeight, width: buttonWidth, height: buttonHeight},
+ imageURL: buttonImageUrl,
+ visible: true,
+ alpha: 1.0
+ });
+var standUpButton = Overlays.addOverlay("image", {
+ x: buttonPositionX, y: buttonPositionY, width: buttonWidth, height: buttonHeight,
+ subImage: { x: buttonWidth, y: buttonHeight, width: buttonWidth, height: buttonHeight},
+ imageURL: buttonImageUrl,
+ visible: false,
+ alpha: 1.0
+ });
+
+var passedTime = 0.0;
+var startPosition = null;
+var animationLenght = 2.0;
+
+// This is the pose we would like to end up
+var pose = [
+ {joint:"RightUpLeg", rotation: {x:100.0, y:15.0, z:0.0}},
+ {joint:"RightLeg", rotation: {x:-130.0, y:15.0, z:0.0}},
+ {joint:"RightFoot", rotation: {x:30, y:15.0, z:0.0}},
+ {joint:"LeftUpLeg", rotation: {x:100.0, y:-15.0, z:0.0}},
+ {joint:"LeftLeg", rotation: {x:-130.0, y:-15.0, z:0.0}},
+ {joint:"LeftFoot", rotation: {x:30, y:15.0, z:0.0}},
+
+ {joint:"Spine2", rotation: {x:20, y:0.0, z:0.0}},
+
+ {joint:"RightShoulder", rotation: {x:0.0, y:40.0, z:0.0}},
+ {joint:"LeftShoulder", rotation: {x:0.0, y:-40.0, z:0.0}}
+
+];
+
+var startPoseAndTransition = [];
+
+function storeStartPoseAndTransition() {
+ for (var i = 0; i < pose.length; i++){
+ var startRotation = Quat.safeEulerAngles(MyAvatar.getJointRotation(pose[i].joint));
+ var transitionVector = Vec3.subtract( pose[i].rotation, startRotation );
+ startPoseAndTransition.push({joint: pose[i].joint, start: startRotation, transition: transitionVector});
+ }
+}
+
+function updateJoints(factor){
+ for (var i = 0; i < startPoseAndTransition.length; i++){
+ var scaledTransition = Vec3.multiply(startPoseAndTransition[i].transition, factor);
+ var rotation = Vec3.sum(startPoseAndTransition[i].start, scaledTransition);
+ MyAvatar.setJointData(startPoseAndTransition[i].joint, Quat.fromVec3Degrees( rotation ));
+ }
+}
+
+var sittingDownAnimation = function(deltaTime) {
+
+ passedTime += deltaTime;
+ var factor = passedTime/animationLenght;
+
+ if ( passedTime <= animationLenght ) {
+ updateJoints(factor);
+
+ var pos = { x: startPosition.x - 0.3 * factor, y: startPosition.y - 0.5 * factor, z: startPosition.z};
+ MyAvatar.position = pos;
+ }
+}
+
+var standingUpAnimation = function(deltaTime){
+
+ passedTime += deltaTime;
+ var factor = 1 - passedTime/animationLenght;
+
+ if ( passedTime <= animationLenght ) {
+
+ updateJoints(factor);
+
+ var pos = { x: startPosition.x + 0.3 * (passedTime/animationLenght), y: startPosition.y + 0.5 * (passedTime/animationLenght), z: startPosition.z};
+ MyAvatar.position = pos;
+ }
+}
+
+Controller.mousePressEvent.connect(function(event){
+
+ var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y});
+
+ if (clickedOverlay == sitDownButton) {
+ passedTime = 0.0;
+ startPosition = MyAvatar.position;
+ storeStartPoseAndTransition();
+ try{
+ Script.update.disconnect(standingUpAnimation);
+ } catch(e){
+ // no need to handle. if it wasn't connected no harm done
+ }
+ Script.update.connect(sittingDownAnimation);
+ Overlays.editOverlay(sitDownButton, { visible: false });
+ Overlays.editOverlay(standUpButton, { visible: true });
+ } else if (clickedOverlay == standUpButton) {
+ passedTime = 0.0;
+ startPosition = MyAvatar.position;
+ try{
+ Script.update.disconnect(sittingDownAnimation);
+ } catch (e){}
+ Script.update.connect(standingUpAnimation);
+ Overlays.editOverlay(standUpButton, { visible: false });
+ Overlays.editOverlay(sitDownButton, { visible: true });
+ }
+})
+
+function update(deltaTime){
+ var newWindowDimensions = Controller.getViewportDimensions();
+ if( newWindowDimensions.x != windowDimensions.x || newWindowDimensions.y != windowDimensions.y ){
+ windowDimensions = newWindowDimensions;
+ var newX = windowDimensions.x - buttonPadding - buttonWidth;
+ var newY = (windowDimensions.y - buttonHeight) / 2 ;
+ Overlays.editOverlay( standUpButton, {x: newX, y: newY} );
+ Overlays.editOverlay( sitDownButton, {x: newX, y: newY} );
+ }
+}
+
+Script.update.connect(update);
+
+Script.scriptEnding.connect(function() {
+
+ for (var i = 0; i < pose.length; i++){
+ MyAvatar.clearJointData(pose[i][0]);
+ }
+
+ Overlays.deleteOverlay(sitDownButton);
+ Overlays.deleteOverlay(standUpButton);
+});
diff --git a/interface/resources/shaders/model.frag b/interface/resources/shaders/model.frag
index 3964bd5b97..a9d93f2f6a 100644
--- a/interface/resources/shaders/model.frag
+++ b/interface/resources/shaders/model.frag
@@ -20,11 +20,14 @@ varying vec4 normal;
void main(void) {
// compute the base color based on OpenGL lighting model
vec4 normalizedNormal = normalize(normal);
+ float diffuse = dot(normalizedNormal, gl_LightSource[0].position);
+ float facingLight = step(0.0, diffuse);
vec4 base = gl_Color * (gl_FrontLightModelProduct.sceneColor + gl_FrontLightProduct[0].ambient +
- gl_FrontLightProduct[0].diffuse * max(0.0, dot(normalizedNormal, gl_LightSource[0].position)));
+ gl_FrontLightProduct[0].diffuse * (diffuse * facingLight));
// compute the specular component (sans exponent)
- float specular = max(0.0, dot(gl_LightSource[0].position, normalizedNormal));
+ float specular = facingLight * max(0.0, dot(normalize(gl_LightSource[0].position + vec4(0.0, 0.0, 1.0, 0.0)),
+ normalizedNormal));
// modulate texture by base color and add specular contribution
gl_FragColor = base * texture2D(diffuseMap, gl_TexCoord[0].st) +
diff --git a/interface/resources/shaders/model_normal_map.frag b/interface/resources/shaders/model_normal_map.frag
index a4f7a887c5..392be1f1cf 100644
--- a/interface/resources/shaders/model_normal_map.frag
+++ b/interface/resources/shaders/model_normal_map.frag
@@ -32,12 +32,15 @@ void main(void) {
// compute the base color based on OpenGL lighting model
vec4 viewNormal = vec4(normalizedTangent * localNormal.x +
normalizedBitangent * localNormal.y + normalizedNormal * localNormal.z, 0.0);
+ float diffuse = dot(viewNormal, gl_LightSource[0].position);
+ float facingLight = step(0.0, diffuse);
vec4 base = gl_Color * (gl_FrontLightModelProduct.sceneColor + gl_FrontLightProduct[0].ambient +
- gl_FrontLightProduct[0].diffuse * max(0.0, dot(viewNormal, gl_LightSource[0].position)));
+ gl_FrontLightProduct[0].diffuse * (diffuse * facingLight));
// compute the specular component (sans exponent)
- float specular = max(0.0, dot(gl_LightSource[0].position, viewNormal));
-
+ float specular = facingLight * max(0.0, dot(normalize(gl_LightSource[0].position + vec4(0.0, 0.0, 1.0, 0.0)),
+ viewNormal));
+
// modulate texture by base color and add specular contribution
gl_FragColor = base * texture2D(diffuseMap, gl_TexCoord[0].st) +
vec4(pow(specular, gl_FrontMaterial.shininess) * gl_FrontLightProduct[0].specular.rgb, 0.0);
diff --git a/interface/resources/shaders/model_normal_specular_map.frag b/interface/resources/shaders/model_normal_specular_map.frag
index f5b9d2b06b..dbbb343c62 100644
--- a/interface/resources/shaders/model_normal_specular_map.frag
+++ b/interface/resources/shaders/model_normal_specular_map.frag
@@ -35,12 +35,15 @@ void main(void) {
// compute the base color based on OpenGL lighting model
vec4 viewNormal = vec4(normalizedTangent * localNormal.x +
normalizedBitangent * localNormal.y + normalizedNormal * localNormal.z, 0.0);
+ float diffuse = dot(viewNormal, gl_LightSource[0].position);
+ float facingLight = step(0.0, diffuse);
vec4 base = gl_Color * (gl_FrontLightModelProduct.sceneColor + gl_FrontLightProduct[0].ambient +
- gl_FrontLightProduct[0].diffuse * max(0.0, dot(viewNormal, gl_LightSource[0].position)));
+ gl_FrontLightProduct[0].diffuse * (diffuse * facingLight));
// compute the specular component (sans exponent)
- float specular = max(0.0, dot(gl_LightSource[0].position, viewNormal));
-
+ float specular = facingLight * max(0.0, dot(normalize(gl_LightSource[0].position + vec4(0.0, 0.0, 1.0, 0.0)),
+ viewNormal));
+
// modulate texture by base color and add specular contribution
gl_FragColor = base * texture2D(diffuseMap, gl_TexCoord[0].st) + vec4(pow(specular, gl_FrontMaterial.shininess) *
gl_FrontLightProduct[0].specular.rgb * texture2D(specularMap, gl_TexCoord[0].st).rgb, 0.0);
diff --git a/interface/resources/shaders/model_specular_map.frag b/interface/resources/shaders/model_specular_map.frag
index 4e2f3d0c98..b955b5cfa6 100644
--- a/interface/resources/shaders/model_specular_map.frag
+++ b/interface/resources/shaders/model_specular_map.frag
@@ -23,12 +23,15 @@ varying vec4 normal;
void main(void) {
// compute the base color based on OpenGL lighting model
vec4 normalizedNormal = normalize(normal);
+ float diffuse = dot(normalizedNormal, gl_LightSource[0].position);
+ float facingLight = step(0.0, diffuse);
vec4 base = gl_Color * (gl_FrontLightModelProduct.sceneColor + gl_FrontLightProduct[0].ambient +
- gl_FrontLightProduct[0].diffuse * max(0.0, dot(normalizedNormal, gl_LightSource[0].position)));
+ gl_FrontLightProduct[0].diffuse * (diffuse * facingLight));
// compute the specular component (sans exponent)
- float specular = max(0.0, dot(gl_LightSource[0].position, normalizedNormal));
-
+ float specular = facingLight * max(0.0, dot(normalize(gl_LightSource[0].position + vec4(0.0, 0.0, 1.0, 0.0)),
+ normalizedNormal));
+
// modulate texture by base color and add specular contribution
gl_FragColor = base * texture2D(diffuseMap, gl_TexCoord[0].st) + vec4(pow(specular, gl_FrontMaterial.shininess) *
gl_FrontLightProduct[0].specular.rgb * texture2D(specularMap, gl_TexCoord[0].st).rgb, 0.0);
diff --git a/interface/src/AbstractLoggerInterface.h b/interface/src/AbstractLoggerInterface.h
index f6cf136a71..fe45346e4c 100644
--- a/interface/src/AbstractLoggerInterface.h
+++ b/interface/src/AbstractLoggerInterface.h
@@ -20,12 +20,12 @@ class AbstractLoggerInterface : public QObject {
Q_OBJECT
public:
- AbstractLoggerInterface(QObject* parent = NULL) : QObject(parent) {};
- inline bool extraDebugging() { return _extraDebugging; };
- inline void setExtraDebugging(bool debugging) { _extraDebugging = debugging; };
+ AbstractLoggerInterface(QObject* parent = NULL) : QObject(parent) {}
+ inline bool extraDebugging() { return _extraDebugging; }
+ inline void setExtraDebugging(bool debugging) { _extraDebugging = debugging; }
virtual void addMessage(QString) = 0;
- virtual QStringList getLogData() = 0;
+ virtual QString getLogData() = 0;
virtual void locateLog() = 0;
signals:
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 53be1c0faf..2b552de0ee 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -173,7 +173,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
_previousScriptLocation(),
_runningScriptsWidget(new RunningScriptsWidget(_window)),
_runningScriptsWidgetWasVisible(false)
-{
+{
// read the ApplicationInfo.ini file for Name/Version/Domain information
QSettings applicationInfo(Application::resourcesPath() + "info/ApplicationInfo.ini", QSettings::IniFormat);
@@ -235,10 +235,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
connect(&nodeList->getDomainHandler(), SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&)));
connect(&nodeList->getDomainHandler(), SIGNAL(connectedToDomain(const QString&)), SLOT(connectedToDomain(const QString&)));
-
+
// update our location every 5 seconds in the data-server, assuming that we are authenticated with one
const float DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5.0f * 1000.0f;
-
+
QTimer* locationUpdateTimer = new QTimer(this);
connect(locationUpdateTimer, &QTimer::timeout, this, &Application::updateLocationInServer);
locationUpdateTimer->start(DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS);
@@ -334,7 +334,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
// when -url in command line, teleport to location
urlGoTo(argc, constArgv);
-
+
// For now we're going to set the PPS for outbound packets to be super high, this is
// probably not the right long term solution. But for now, we're going to do this to
// allow you to move a particle around in your hand
@@ -361,23 +361,23 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
// clear the scripts, and set out script to our default scripts
clearScriptsBeforeRunning();
loadScript("http://public.highfidelity.io/scripts/defaultScripts.js");
-
+
QMutexLocker locker(&_settingsMutex);
_settings->setValue("firstRun",QVariant(false));
} else {
// do this as late as possible so that all required subsystems are inialized
loadScripts();
-
+
QMutexLocker locker(&_settingsMutex);
_previousScriptLocation = _settings->value("LastScriptLocation", QVariant("")).toString();
}
-
+
connect(_window, &MainWindow::windowGeometryChanged,
_runningScriptsWidget, &RunningScriptsWidget::setBoundary);
-
- //When -url in command line, teleport to location
- urlGoTo(argc, constArgv);
-
+
+ //When -url in command line, teleport to location
+ urlGoTo(argc, constArgv);
+
// call the OAuthWebviewHandler static getter so that its instance lives in our thread
OAuthWebViewHandler::getInstance();
// make sure the High Fidelity root CA is in our list of trusted certs
@@ -390,11 +390,11 @@ Application::~Application() {
// make sure we don't call the idle timer any more
delete idleTimer;
-
+
_sharedVoxelSystem.changeTree(new VoxelTree);
-
+
saveSettings();
-
+
delete _voxelImporter;
// let the avatar mixer know we're out
@@ -433,7 +433,7 @@ Application::~Application() {
void Application::saveSettings() {
Menu::getInstance()->saveSettings();
_rearMirrorTools->saveSettings(_settings);
-
+
if (_voxelImporter) {
_voxelImporter->saveSettings(_settings);
}
@@ -515,7 +515,7 @@ void Application::initializeGL() {
_voxelHideShowThread.initialize(_enableProcessVoxelsThread);
_particleEditSender.initialize(_enableProcessVoxelsThread);
_modelEditSender.initialize(_enableProcessVoxelsThread);
-
+
if (_enableProcessVoxelsThread) {
qDebug("Voxel parsing thread created.");
}
@@ -1251,7 +1251,7 @@ void Application::dropEvent(QDropEvent *event) {
void Application::sendPingPackets() {
QByteArray pingPacket = NodeList::getInstance()->constructPingPacket();
- controlledBroadcastToNodes(pingPacket, NodeSet()
+ controlledBroadcastToNodes(pingPacket, NodeSet()
<< NodeType::VoxelServer << NodeType::ParticleServer << NodeType::ModelServer
<< NodeType::AudioMixer << NodeType::AvatarMixer
<< NodeType::MetavoxelServer);
@@ -1262,7 +1262,7 @@ void Application::timer() {
if (Menu::getInstance()->isOptionChecked(MenuOption::TestPing)) {
sendPingPackets();
}
-
+
float diffTime = (float)_timerStart.nsecsElapsed() / 1000000000.0f;
_fps = (float)_frameCount / diffTime;
@@ -1666,7 +1666,7 @@ void Application::init() {
connect(_rearMirrorTools, SIGNAL(restoreView()), SLOT(restoreMirrorView()));
connect(_rearMirrorTools, SIGNAL(shrinkView()), SLOT(shrinkMirrorView()));
connect(_rearMirrorTools, SIGNAL(resetView()), SLOT(resetSensors()));
-
+
// set up our audio reflector
_audioReflector.setMyAvatar(getAvatar());
_audioReflector.setVoxels(_voxels.getTree());
@@ -1675,7 +1675,7 @@ void Application::init() {
connect(getAudio(), &Audio::processInboundAudio, &_audioReflector, &AudioReflector::processInboundAudio,Qt::DirectConnection);
connect(getAudio(), &Audio::processLocalAudio, &_audioReflector, &AudioReflector::processLocalAudio,Qt::DirectConnection);
- connect(getAudio(), &Audio::preProcessOriginalInboundAudio, &_audioReflector,
+ connect(getAudio(), &Audio::preProcessOriginalInboundAudio, &_audioReflector,
&AudioReflector::preProcessOriginalInboundAudio,Qt::DirectConnection);
// save settings when avatar changes
@@ -1790,7 +1790,7 @@ void Application::updateMyAvatarLookAtPosition() {
PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()");
FaceTracker* tracker = getActiveFaceTracker();
-
+
bool isLookingAtSomeone = false;
glm::vec3 lookAtSpot;
if (_myCamera.getMode() == CAMERA_MODE_MIRROR) {
@@ -1825,7 +1825,7 @@ void Application::updateMyAvatarLookAtPosition() {
glm::distance(_mouseRayOrigin, _myAvatar->getHead()->calculateAverageEyePosition()));
lookAtSpot = _mouseRayOrigin + _mouseRayDirection * qMax(minEyeDistance, distance);
*/
-
+
}
//
// Deflect the eyes a bit to match the detected Gaze from 3D camera if active
@@ -1845,7 +1845,7 @@ void Application::updateMyAvatarLookAtPosition() {
eyePitch * pitchSign * deflection, eyeYaw * deflection, 0.0f))) *
glm::inverse(_myCamera.getRotation()) * (lookAtSpot - origin);
}
-
+
_myAvatar->getHead()->setLookAtPosition(lookAtSpot);
}
@@ -1897,7 +1897,7 @@ void Application::updateCamera(float deltaTime) {
PerformanceWarning warn(showWarnings, "Application::updateCamera()");
if (!OculusManager::isConnected() && !TV3DManager::isConnected() &&
- Menu::getInstance()->isOptionChecked(MenuOption::OffAxisProjection)) {
+ Menu::getInstance()->isOptionChecked(MenuOption::OffAxisProjection)) {
FaceTracker* tracker = getActiveFaceTracker();
if (tracker) {
const float EYE_OFFSET_SCALE = 0.025f;
@@ -2453,7 +2453,7 @@ void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) {
// disable specular lighting for ground and voxels
glMaterialfv(GL_FRONT, GL_SPECULAR, NO_SPECULAR_COLOR);
-
+
// draw the audio reflector overlay
_audioReflector.render();
@@ -2621,7 +2621,7 @@ void Application::displayOverlay() {
const float LOG2_LOUDNESS_FLOOR = 11.f;
float audioLevel = 0.f;
float loudness = _audio.getLastInputLoudness() + 1.f;
-
+
_trailingAudioLoudness = AUDIO_METER_AVERAGING * _trailingAudioLoudness + (1.f - AUDIO_METER_AVERAGING) * loudness;
float log2loudness = log(_trailingAudioLoudness) / LOG2;
@@ -2634,7 +2634,7 @@ void Application::displayOverlay() {
audioLevel = AUDIO_METER_SCALE_WIDTH;
}
bool isClipping = ((_audio.getTimeSinceLastClip() > 0.f) && (_audio.getTimeSinceLastClip() < CLIPPING_INDICATOR_TIME));
-
+
if ((_audio.getTimeSinceLastClip() > 0.f) && (_audio.getTimeSinceLastClip() < CLIPPING_INDICATOR_TIME)) {
const float MAX_MAGNITUDE = 0.7f;
float magnitude = MAX_MAGNITUDE * (1 - _audio.getTimeSinceLastClip() / CLIPPING_INDICATOR_TIME);
@@ -2740,7 +2740,7 @@ void Application::displayOverlay() {
// give external parties a change to hook in
emit renderingOverlay();
-
+
_overlays.render2D();
glPopMatrix();
@@ -2816,7 +2816,7 @@ void Application::renderRearViewMirror(const QRect& region, bool billboard) {
// save absolute translations
glm::vec3 absoluteSkeletonTranslation = _myAvatar->getSkeletonModel().getTranslation();
glm::vec3 absoluteFaceTranslation = _myAvatar->getHead()->getFaceModel().getTranslation();
-
+
// get the eye positions relative to the neck and use them to set the face translation
glm::vec3 leftEyePosition, rightEyePosition;
_myAvatar->getHead()->getFaceModel().setTranslation(glm::vec3());
@@ -3082,7 +3082,7 @@ void Application::uploadModel(ModelType modelType) {
thread->connect(uploader, SIGNAL(destroyed()), SLOT(quit()));
thread->connect(thread, SIGNAL(finished()), SLOT(deleteLater()));
uploader->connect(thread, SIGNAL(started()), SLOT(send()));
-
+
thread->start();
}
@@ -3099,28 +3099,28 @@ void Application::updateWindowTitle(){
}
void Application::updateLocationInServer() {
-
+
AccountManager& accountManager = AccountManager::getInstance();
-
+
if (accountManager.isLoggedIn()) {
-
+
static QJsonObject lastLocationObject;
-
+
// construct a QJsonObject given the user's current address information
QJsonObject updatedLocationObject;
-
+
QJsonObject addressObject;
addressObject.insert("position", QString(createByteArray(_myAvatar->getPosition())));
addressObject.insert("orientation", QString(createByteArray(glm::degrees(safeEulerAngles(_myAvatar->getOrientation())))));
addressObject.insert("domain", NodeList::getInstance()->getDomainHandler().getHostname());
-
+
updatedLocationObject.insert("address", addressObject);
-
+
if (updatedLocationObject != lastLocationObject) {
-
+
accountManager.authenticatedRequest("/api/v1/users/address", QNetworkAccessManager::PutOperation,
JSONCallbackParameters(), QJsonDocument(updatedLocationObject).toJson());
-
+
lastLocationObject = updatedLocationObject;
}
}
@@ -3145,7 +3145,7 @@ void Application::domainChanged(const QString& domainHostname) {
// reset the voxels renderer
_voxels.killLocalVoxels();
-
+
// reset the auth URL for OAuth web view handler
OAuthWebViewHandler::getInstance().clearLastAuthorizationURL();
}
@@ -3366,7 +3366,7 @@ void Application::loadScripts() {
loadScript(string);
}
}
-
+
QMutexLocker locker(&_settingsMutex);
_settings->endArray();
}
@@ -3592,9 +3592,12 @@ void Application::loadScriptURLDialog() {
void Application::toggleLogDialog() {
if (! _logDialog) {
_logDialog = new LogDialog(_glWidget, getLogger());
- _logDialog->show();
+ }
+
+ if (_logDialog->isVisible()) {
+ _logDialog->hide();
} else {
- _logDialog->close();
+ _logDialog->show();
}
}
@@ -3628,7 +3631,7 @@ void Application::parseVersionXml() {
QObject* sender = QObject::sender();
QXmlStreamReader xml(qobject_cast(sender));
-
+
while (!xml.atEnd() && !xml.hasError()) {
if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == operatingSystem) {
while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == operatingSystem)) {
@@ -3645,7 +3648,7 @@ void Application::parseVersionXml() {
}
xml.readNext();
}
-
+
if (!shouldSkipVersion(latestVersion) && applicationVersion() != latestVersion) {
new UpdateDialog(_glWidget, releaseNotes, latestVersion, downloadUrl);
}
@@ -3697,24 +3700,24 @@ void Application::urlGoTo(int argc, const char * constArgv[]) {
} else if (urlParts.count() > 1) {
// if url has 2 or more parts, the first one is domain name
QString domain = urlParts[0];
-
+
// second part is either a destination coordinate or
// a place name
QString destination = urlParts[1];
-
+
// any third part is an avatar orientation.
QString orientation = urlParts.count() > 2 ? urlParts[2] : QString();
-
+
Menu::goToDomain(domain);
-
+
// goto either @user, #place, or x-xx,y-yy,z-zz
// style co-ordinate.
Menu::goTo(destination);
-
+
if (!orientation.isEmpty()) {
// location orientation
Menu::goToOrientation(orientation);
}
- }
+ }
}
}
diff --git a/interface/src/Application.h b/interface/src/Application.h
index 5460093cbd..1968ef4fee 100644
--- a/interface/src/Application.h
+++ b/interface/src/Application.h
@@ -217,6 +217,7 @@ public:
QNetworkAccessManager* getNetworkAccessManager() { return _networkAccessManager; }
GeometryCache* getGeometryCache() { return &_geometryCache; }
+ AnimationCache* getAnimationCache() { return &_animationCache; }
TextureCache* getTextureCache() { return &_textureCache; }
GlowEffect* getGlowEffect() { return &_glowEffect; }
ControllerScriptingInterface* getControllerScriptingInterface() { return &_controllerScriptingInterface; }
diff --git a/interface/src/FileLogger.cpp b/interface/src/FileLogger.cpp
index c4e75b21b2..cb3d43925d 100644
--- a/interface/src/FileLogger.cpp
+++ b/interface/src/FileLogger.cpp
@@ -23,7 +23,7 @@ const QString LOGS_DIRECTORY = "Logs";
FileLogger::FileLogger(QObject* parent) :
AbstractLoggerInterface(parent),
- _logData(NULL)
+ _logData("")
{
setExtraDebugging(false);
@@ -36,7 +36,7 @@ FileLogger::FileLogger(QObject* parent) :
void FileLogger::addMessage(QString message) {
QMutexLocker locker(&_mutex);
emit logReceived(message);
- _logData.append(message);
+ _logData += message;
QFile file(_fileName);
if (file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
diff --git a/interface/src/FileLogger.h b/interface/src/FileLogger.h
index 5da86044ab..3dbbfd26cd 100644
--- a/interface/src/FileLogger.h
+++ b/interface/src/FileLogger.h
@@ -22,11 +22,11 @@ public:
FileLogger(QObject* parent = NULL);
virtual void addMessage(QString);
- virtual QStringList getLogData() { return _logData; };
+ virtual QString getLogData() { return _logData; }
virtual void locateLog();
private:
- QStringList _logData;
+ QString _logData;
QString _fileName;
QMutex _mutex;
diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp
index c8bf194b25..96bfa106f4 100644
--- a/interface/src/Menu.cpp
+++ b/interface/src/Menu.cpp
@@ -37,6 +37,7 @@
#include "Menu.h"
#include "scripting/MenuScriptingInterface.h"
#include "Util.h"
+#include "ui/AnimationsDialog.h"
#include "ui/AttachmentsDialog.h"
#include "ui/InfoView.h"
#include "ui/MetavoxelEditor.h"
@@ -193,6 +194,7 @@ Menu::Menu() :
QAction::PreferencesRole);
addActionToQMenuAndActionHash(editMenu, MenuOption::Attachments, 0, this, SLOT(editAttachments()));
+ addActionToQMenuAndActionHash(editMenu, MenuOption::Animations, 0, this, SLOT(editAnimations()));
addDisabledActionAndSeparator(editMenu, "Physics");
QObject* avatar = appInstance->getAvatar();
@@ -863,6 +865,15 @@ void Menu::editAttachments() {
}
}
+void Menu::editAnimations() {
+ if (!_animationsDialog) {
+ _animationsDialog = new AnimationsDialog();
+ _animationsDialog->show();
+ } else {
+ _animationsDialog->close();
+ }
+}
+
void Menu::goToDomain(const QString newDomain) {
if (NodeList::getInstance()->getDomainHandler().getHostname() != newDomain) {
// send a node kill request, indicating to other clients that they should play the "disappeared" effect
diff --git a/interface/src/Menu.h b/interface/src/Menu.h
index 9043825b72..b12f989ed6 100644
--- a/interface/src/Menu.h
+++ b/interface/src/Menu.h
@@ -64,6 +64,7 @@ struct ViewFrustumOffset {
class QSettings;
+class AnimationsDialog;
class AttachmentsDialog;
class BandwidthDialog;
class LodToolsDialog;
@@ -176,6 +177,7 @@ private slots:
void aboutApp();
void editPreferences();
void editAttachments();
+ void editAnimations();
void goToDomainDialog();
void goToLocation();
void nameLocation();
@@ -260,6 +262,7 @@ private:
QAction* _loginAction;
QPointer _preferencesDialog;
QPointer _attachmentsDialog;
+ QPointer _animationsDialog;
QAction* _chatAction;
QString _snapshotsLocation;
};
@@ -270,6 +273,7 @@ namespace MenuOption {
const QString AllowOculusCameraModeChange = "Allow Oculus Camera Mode Change (Nausea)";
const QString AlternateIK = "Alternate IK";
const QString AmbientOcclusion = "Ambient Occlusion";
+ const QString Animations = "Animations...";
const QString Atmosphere = "Atmosphere";
const QString Attachments = "Attachments...";
const QString AudioNoiseReduction = "Audio Noise Reduction";
diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index d0adebb7e2..85c1734e56 100644
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -428,6 +428,47 @@ void MyAvatar::setGravity(const glm::vec3& gravity) {
}
}
+AnimationHandlePointer MyAvatar::addAnimationHandle() {
+ AnimationHandlePointer handle = _skeletonModel.createAnimationHandle();
+ handle->setLoop(true);
+ handle->start();
+ _animationHandles.append(handle);
+ return handle;
+}
+
+void MyAvatar::removeAnimationHandle(const AnimationHandlePointer& handle) {
+ handle->stop();
+ _animationHandles.removeOne(handle);
+}
+
+void MyAvatar::startAnimation(const QString& url, float fps, float priority, bool loop, const QStringList& maskedJoints) {
+ if (QThread::currentThread() != thread()) {
+ QMetaObject::invokeMethod(this, "startAnimation", Q_ARG(const QString&, url),
+ Q_ARG(float, fps), Q_ARG(float, priority), Q_ARG(bool, loop), Q_ARG(const QStringList&, maskedJoints));
+ return;
+ }
+ AnimationHandlePointer handle = _skeletonModel.createAnimationHandle();
+ handle->setURL(url);
+ handle->setFPS(fps);
+ handle->setPriority(priority);
+ handle->setLoop(loop);
+ handle->setMaskedJoints(maskedJoints);
+ handle->start();
+}
+
+void MyAvatar::stopAnimation(const QString& url) {
+ if (QThread::currentThread() != thread()) {
+ QMetaObject::invokeMethod(this, "stopAnimation", Q_ARG(const QString&, url));
+ return;
+ }
+ foreach (const AnimationHandlePointer& handle, _skeletonModel.getRunningAnimations()) {
+ if (handle->getURL() == url) {
+ handle->stop();
+ return;
+ }
+ }
+}
+
void MyAvatar::saveData(QSettings* settings) {
settings->beginGroup("Avatar");
@@ -466,6 +507,17 @@ void MyAvatar::saveData(QSettings* settings) {
}
settings->endArray();
+ settings->beginWriteArray("animationHandles");
+ for (int i = 0; i < _animationHandles.size(); i++) {
+ settings->setArrayIndex(i);
+ const AnimationHandlePointer& pointer = _animationHandles.at(i);
+ settings->setValue("url", pointer->getURL());
+ settings->setValue("fps", pointer->getFPS());
+ settings->setValue("priority", pointer->getPriority());
+ settings->setValue("maskedJoints", pointer->getMaskedJoints());
+ }
+ settings->endArray();
+
settings->setValue("displayName", _displayName);
settings->endGroup();
@@ -516,6 +568,23 @@ void MyAvatar::loadData(QSettings* settings) {
settings->endArray();
setAttachmentData(attachmentData);
+ int animationCount = settings->beginReadArray("animationHandles");
+ while (_animationHandles.size() > animationCount) {
+ _animationHandles.takeLast()->stop();
+ }
+ while (_animationHandles.size() < animationCount) {
+ addAnimationHandle();
+ }
+ for (int i = 0; i < animationCount; i++) {
+ settings->setArrayIndex(i);
+ const AnimationHandlePointer& handle = _animationHandles.at(i);
+ handle->setURL(settings->value("url").toUrl());
+ handle->setFPS(loadSetting(settings, "fps", 30.0f));
+ handle->setPriority(loadSetting(settings, "priority", 1.0f));
+ handle->setMaskedJoints(settings->value("maskedJoints").toStringList());
+ }
+ settings->endArray();
+
setDisplayName(settings->value("displayName").toString());
settings->endGroup();
@@ -1546,3 +1615,4 @@ void MyAvatar::applyCollision(const glm::vec3& contactPoint, const glm::vec3& pe
getHead()->addLeanDeltas(sideways, forward);
}
}
+
diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h
index 2e75ac984d..9d6f22264f 100644
--- a/interface/src/avatar/MyAvatar.h
+++ b/interface/src/avatar/MyAvatar.h
@@ -62,6 +62,17 @@ public:
glm::vec3 getUprightHeadPosition() const;
bool getShouldRenderLocally() const { return _shouldRender; }
+ const QList& getAnimationHandles() const { return _animationHandles; }
+ AnimationHandlePointer addAnimationHandle();
+ void removeAnimationHandle(const AnimationHandlePointer& handle);
+
+ /// Allows scripts to run animations.
+ Q_INVOKABLE void startAnimation(const QString& url, float fps = 30.0f,
+ float priority = 1.0f, bool loop = false, const QStringList& maskedJoints = QStringList());
+
+ /// Stops an animation as identified by a URL.
+ Q_INVOKABLE void stopAnimation(const QString& url);
+
// get/set avatar data
void saveData(QSettings* settings);
void loadData(QSettings* settings);
@@ -151,6 +162,8 @@ private:
bool _billboardValid;
float _oculusYawOffset;
+ QList _animationHandles;
+
// private methods
void updateOrientation(float deltaTime);
void updateMotorFromKeyboard(float deltaTime, bool walking);
diff --git a/interface/src/renderer/GeometryCache.cpp b/interface/src/renderer/GeometryCache.cpp
index ec68c87a76..b5bd63ab87 100644
--- a/interface/src/renderer/GeometryCache.cpp
+++ b/interface/src/renderer/GeometryCache.cpp
@@ -404,6 +404,22 @@ QSharedPointer NetworkGeometry::getLODOrFallback(float distance
return lod;
}
+uint qHash(const QWeakPointer& animation, uint seed = 0) {
+ return qHash(animation.data(), seed);
+}
+
+QVector NetworkGeometry::getJointMappings(const AnimationPointer& animation) {
+ QVector mappings = _jointMappings.value(animation);
+ if (mappings.isEmpty() && isLoaded() && animation && animation->isLoaded()) {
+ const FBXGeometry& animationGeometry = animation->getGeometry();
+ for (int i = 0; i < animationGeometry.joints.size(); i++) {
+ mappings.append(_geometry.jointIndices.value(animationGeometry.joints.at(i).name) - 1);
+ }
+ _jointMappings.insert(animation, mappings);
+ }
+ return mappings;
+}
+
void NetworkGeometry::setLoadPriority(const QPointer& owner, float priority) {
Resource::setLoadPriority(owner, priority);
diff --git a/interface/src/renderer/GeometryCache.h b/interface/src/renderer/GeometryCache.h
index deecfd56c5..41bedc5e05 100644
--- a/interface/src/renderer/GeometryCache.h
+++ b/interface/src/renderer/GeometryCache.h
@@ -22,6 +22,8 @@
#include
+#include
+
class Model;
class NetworkGeometry;
class NetworkMesh;
@@ -90,6 +92,8 @@ public:
const FBXGeometry& getFBXGeometry() const { return _geometry; }
const QVector& getMeshes() const { return _meshes; }
+ QVector getJointMappings(const AnimationPointer& animation);
+
virtual void setLoadPriority(const QPointer& owner, float priority);
virtual void setLoadPriorities(const QHash, float>& priorities);
virtual void clearLoadPriority(const QPointer& owner);
@@ -117,6 +121,8 @@ private:
QVector _meshes;
QWeakPointer _lodParent;
+
+ QHash, QVector > _jointMappings;
};
/// The state associated with a single mesh part.
diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp
index 90ae9e5e46..900d7ff951 100644
--- a/interface/src/renderer/Model.cpp
+++ b/interface/src/renderer/Model.cpp
@@ -118,6 +118,7 @@ QVector Model::createJointStates(const FBXGeometry& geometry)
JointState state;
state.translation = joint.translation;
state.rotation = joint.rotation;
+ state.animationDisabled = false;
jointStates.append(state);
}
@@ -435,8 +436,12 @@ Extents Model::getMeshExtents() const {
return Extents();
}
const Extents& extents = _geometry->getFBXGeometry().meshExtents;
- glm::vec3 scale = _scale * _geometry->getFBXGeometry().fstScaled;
- Extents scaledExtents = { extents.minimum * scale, extents.maximum * scale };
+
+ // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which
+ // is captured in the offset matrix
+ glm::vec3 minimum = glm::vec3(_geometry->getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0));
+ glm::vec3 maximum = glm::vec3(_geometry->getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0));
+ Extents scaledExtents = { minimum * _scale, maximum * _scale };
return scaledExtents;
}
@@ -447,9 +452,12 @@ Extents Model::getUnscaledMeshExtents() const {
const Extents& extents = _geometry->getFBXGeometry().meshExtents;
- // even though our caller asked for "unscaled" we need to include any fst scaling
- float scale = _geometry->getFBXGeometry().fstScaled;
- Extents scaledExtents = { extents.minimum * scale, extents.maximum * scale };
+ // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which
+ // is captured in the offset matrix
+ glm::vec3 minimum = glm::vec3(_geometry->getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0));
+ glm::vec3 maximum = glm::vec3(_geometry->getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0));
+ Extents scaledExtents = { minimum, maximum };
+
return scaledExtents;
}
@@ -594,6 +602,16 @@ QStringList Model::getJointNames() const {
return isActive() ? _geometry->getFBXGeometry().getJointNames() : QStringList();
}
+uint qHash(const WeakAnimationHandlePointer& handle, uint seed) {
+ return qHash(handle.data(), seed);
+}
+
+AnimationHandlePointer Model::createAnimationHandle() {
+ AnimationHandlePointer handle(new AnimationHandle(this));
+ handle->_self = handle;
+ _animationHandles.insert(handle);
+ return handle;
+}
void Model::clearShapes() {
for (int i = 0; i < _jointShapes.size(); ++i) {
@@ -1006,6 +1024,11 @@ void Model::simulate(float deltaTime, bool fullUpdate) {
}
void Model::simulateInternal(float deltaTime) {
+ // update animations
+ foreach (const AnimationHandlePointer& handle, _runningAnimations) {
+ handle->simulate(deltaTime);
+ }
+
// NOTE: this is a recursive call that walks all attachments, and their attachments
// update the world space transforms for all joints
for (int i = 0; i < _jointStates.size(); i++) {
@@ -1013,9 +1036,8 @@ void Model::simulateInternal(float deltaTime) {
}
_shapesAreDirty = true;
- const FBXGeometry& geometry = _geometry->getFBXGeometry();
-
// update the attachment transforms and simulate them
+ const FBXGeometry& geometry = _geometry->getFBXGeometry();
for (int i = 0; i < _attachments.size(); i++) {
const FBXAttachment& attachment = geometry.attachments.at(i);
Model* model = _attachments.at(i);
@@ -1186,6 +1208,7 @@ bool Model::setJointRotation(int jointIndex, const glm::quat& rotation, bool fro
state.rotation = state.rotation * glm::inverse(state.combinedRotation) * rotation *
glm::inverse(fromBind ? _geometry->getFBXGeometry().joints.at(jointIndex).inverseBindRotation :
_geometry->getFBXGeometry().joints.at(jointIndex).inverseDefaultRotation);
+ state.animationDisabled = true;
return true;
}
@@ -1218,6 +1241,7 @@ bool Model::restoreJointPosition(int jointIndex, float percent) {
const FBXJoint& joint = geometry.joints.at(index);
state.rotation = safeMix(state.rotation, joint.rotation, percent);
state.translation = glm::mix(state.translation, joint.translation, percent);
+ state.animationDisabled = false;
}
return true;
}
@@ -1251,6 +1275,7 @@ void Model::applyRotationDelta(int jointIndex, const glm::quat& delta, bool cons
glm::quat newRotation = glm::quat(glm::clamp(eulers, joint.rotationMin, joint.rotationMax));
state.combinedRotation = state.combinedRotation * glm::inverse(state.rotation) * newRotation;
state.rotation = newRotation;
+ state.animationDisabled = true;
}
const int BALL_SUBDIVISIONS = 10;
@@ -1426,6 +1451,16 @@ void Model::deleteGeometry() {
_meshStates.clear();
clearShapes();
+ for (QSet::iterator it = _animationHandles.begin(); it != _animationHandles.end(); ) {
+ AnimationHandlePointer handle = it->toStrongRef();
+ if (handle) {
+ handle->_jointMappings.clear();
+ it++;
+ } else {
+ it = _animationHandles.erase(it);
+ }
+ }
+
if (_geometry) {
_geometry->clearLoadPriority(this);
}
@@ -1621,3 +1656,117 @@ void Model::renderMeshes(float alpha, RenderMode mode, bool translucent) {
activeProgram->release();
}
}
+
+void AnimationHandle::setURL(const QUrl& url) {
+ if (_url != url) {
+ _animation = Application::getInstance()->getAnimationCache()->getAnimation(_url = url);
+ _jointMappings.clear();
+ }
+}
+
+static void insertSorted(QList& handles, const AnimationHandlePointer& handle) {
+ for (QList::iterator it = handles.begin(); it != handles.end(); it++) {
+ if (handle->getPriority() < (*it)->getPriority()) {
+ handles.insert(it, handle);
+ return;
+ }
+ }
+ handles.append(handle);
+}
+
+void AnimationHandle::setPriority(float priority) {
+ if (_priority != priority) {
+ _priority = priority;
+ if (_running) {
+ _model->_runningAnimations.removeOne(_self);
+ insertSorted(_model->_runningAnimations, _self);
+ }
+ }
+}
+
+void AnimationHandle::setMaskedJoints(const QStringList& maskedJoints) {
+ _maskedJoints = maskedJoints;
+ _jointMappings.clear();
+}
+
+void AnimationHandle::setRunning(bool running) {
+ if ((_running = running)) {
+ if (!_model->_runningAnimations.contains(_self)) {
+ insertSorted(_model->_runningAnimations, _self);
+ }
+ _frameIndex = 0.0f;
+
+ } else {
+ _model->_runningAnimations.removeOne(_self);
+ }
+}
+
+AnimationHandle::AnimationHandle(Model* model) :
+ QObject(model),
+ _model(model),
+ _fps(30.0f),
+ _priority(1.0f),
+ _loop(false),
+ _running(false) {
+}
+
+void AnimationHandle::simulate(float deltaTime) {
+ _frameIndex += deltaTime * _fps;
+
+ // update the joint mappings if necessary/possible
+ if (_jointMappings.isEmpty()) {
+ if (_model->isActive()) {
+ _jointMappings = _model->getGeometry()->getJointMappings(_animation);
+ }
+ if (_jointMappings.isEmpty()) {
+ return;
+ }
+ if (!_maskedJoints.isEmpty()) {
+ const FBXGeometry& geometry = _model->getGeometry()->getFBXGeometry();
+ for (int i = 0; i < _jointMappings.size(); i++) {
+ int& mapping = _jointMappings[i];
+ if (mapping != -1 && _maskedJoints.contains(geometry.joints.at(mapping).name)) {
+ mapping = -1;
+ }
+ }
+ }
+ }
+
+ const FBXGeometry& animationGeometry = _animation->getGeometry();
+ if (animationGeometry.animationFrames.isEmpty()) {
+ stop();
+ return;
+ }
+ int ceilFrameIndex = (int)glm::ceil(_frameIndex);
+ if (!_loop && ceilFrameIndex >= animationGeometry.animationFrames.size()) {
+ // passed the end; apply the last frame
+ const FBXAnimationFrame& frame = animationGeometry.animationFrames.last();
+ for (int i = 0; i < _jointMappings.size(); i++) {
+ int mapping = _jointMappings.at(i);
+ if (mapping != -1) {
+ Model::JointState& state = _model->_jointStates[mapping];
+ if (!state.animationDisabled) {
+ state.rotation = frame.rotations.at(i);
+ }
+ }
+ }
+ stop();
+ return;
+ }
+ // blend between the closest two frames
+ const FBXAnimationFrame& ceilFrame = animationGeometry.animationFrames.at(
+ ceilFrameIndex % animationGeometry.animationFrames.size());
+ const FBXAnimationFrame& floorFrame = animationGeometry.animationFrames.at(
+ (int)glm::floor(_frameIndex) % animationGeometry.animationFrames.size());
+ float frameFraction = glm::fract(_frameIndex);
+ for (int i = 0; i < _jointMappings.size(); i++) {
+ int mapping = _jointMappings.at(i);
+ if (mapping != -1) {
+ Model::JointState& state = _model->_jointStates[mapping];
+ if (!state.animationDisabled) {
+ state.rotation = safeMix(floorFrame.rotations.at(i), ceilFrame.rotations.at(i), frameFraction);
+ }
+ }
+ }
+}
+
diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h
index 5b2839baa2..59ec50cac1 100644
--- a/interface/src/renderer/Model.h
+++ b/interface/src/renderer/Model.h
@@ -12,18 +12,25 @@
#ifndef hifi_Model_h
#define hifi_Model_h
+#include
#include
#include
#include
+#include
+
#include "GeometryCache.h"
#include "InterfaceConfig.h"
#include "ProgramObject.h"
#include "TextureCache.h"
+class AnimationHandle;
class Shape;
+typedef QSharedPointer AnimationHandlePointer;
+typedef QWeakPointer WeakAnimationHandlePointer;
+
/// A generic 3D model displaying geometry loaded from a URL.
class Model : public QObject {
Q_OBJECT
@@ -185,6 +192,10 @@ public:
QStringList getJointNames() const;
+ AnimationHandlePointer createAnimationHandle();
+
+ const QList& getRunningAnimations() const { return _runningAnimations; }
+
void clearShapes();
void rebuildShapes();
void resetShapePositions();
@@ -243,6 +254,7 @@ protected:
glm::quat rotation; // rotation relative to parent
glm::mat4 transform; // rotation to world frame + translation in model frame
glm::quat combinedRotation; // rotation from joint local to world frame
+ bool animationDisabled; // if true, animations do not affect this joint
};
bool _shapesAreDirty;
@@ -299,6 +311,8 @@ protected:
private:
+ friend class AnimationHandle;
+
void applyNextGeometry();
void deleteGeometry();
void renderMeshes(float alpha, RenderMode mode, bool translucent);
@@ -322,6 +336,10 @@ private:
QVector _attachments;
+ QSet _animationHandles;
+
+ QList _runningAnimations;
+
static ProgramObject _program;
static ProgramObject _normalMapProgram;
static ProgramObject _specularMapProgram;
@@ -357,4 +375,52 @@ Q_DECLARE_METATYPE(QPointer)
Q_DECLARE_METATYPE(QWeakPointer)
Q_DECLARE_METATYPE(QVector)
+/// Represents a handle to a model animation.
+class AnimationHandle : public QObject {
+ Q_OBJECT
+
+public:
+
+ void setURL(const QUrl& url);
+ const QUrl& getURL() const { return _url; }
+
+ void setFPS(float fps) { _fps = fps; }
+ float getFPS() const { return _fps; }
+
+ void setPriority(float priority);
+ float getPriority() const { return _priority; }
+
+ void setLoop(bool loop) { _loop = loop; }
+ bool getLoop() const { return _loop; }
+
+ void setMaskedJoints(const QStringList& maskedJoints);
+ const QStringList& getMaskedJoints() const { return _maskedJoints; }
+
+ void setRunning(bool running);
+ bool isRunning() const { return _running; }
+
+ void start() { setRunning(true); }
+ void stop() { setRunning(false); }
+
+private:
+
+ friend class Model;
+
+ AnimationHandle(Model* model);
+
+ void simulate(float deltaTime);
+
+ Model* _model;
+ WeakAnimationHandlePointer _self;
+ AnimationPointer _animation;
+ QUrl _url;
+ float _fps;
+ float _priority;
+ bool _loop;
+ QStringList _maskedJoints;
+ bool _running;
+ QVector _jointMappings;
+ float _frameIndex;
+};
+
#endif // hifi_Model_h
diff --git a/interface/src/ui/AnimationsDialog.cpp b/interface/src/ui/AnimationsDialog.cpp
new file mode 100644
index 0000000000..29837f67be
--- /dev/null
+++ b/interface/src/ui/AnimationsDialog.cpp
@@ -0,0 +1,158 @@
+//
+// AnimationsDialog.cpp
+// interface/src/ui
+//
+// Created by Andrzej Kapolka on 5/19/14.
+// Copyright 2014 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "AnimationsDialog.h"
+#include "Application.h"
+
+AnimationsDialog::AnimationsDialog() :
+ QDialog(Application::getInstance()->getWindow()) {
+
+ setWindowTitle("Edit Animations");
+ setAttribute(Qt::WA_DeleteOnClose);
+
+ QVBoxLayout* layout = new QVBoxLayout();
+ setLayout(layout);
+
+ QScrollArea* area = new QScrollArea();
+ layout->addWidget(area);
+ area->setWidgetResizable(true);
+ QWidget* container = new QWidget();
+ container->setLayout(_animations = new QVBoxLayout());
+ container->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
+ area->setWidget(container);
+ _animations->addStretch(1);
+
+ foreach (const AnimationHandlePointer& handle, Application::getInstance()->getAvatar()->getAnimationHandles()) {
+ _animations->insertWidget(_animations->count() - 1, new AnimationPanel(this, handle));
+ }
+
+ QPushButton* newAnimation = new QPushButton("New Animation");
+ connect(newAnimation, SIGNAL(clicked(bool)), SLOT(addAnimation()));
+ layout->addWidget(newAnimation);
+
+ QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok);
+ layout->addWidget(buttons);
+ connect(buttons, SIGNAL(accepted()), SLOT(deleteLater()));
+ _ok = buttons->button(QDialogButtonBox::Ok);
+
+ setMinimumSize(600, 600);
+}
+
+void AnimationsDialog::setVisible(bool visible) {
+ QDialog::setVisible(visible);
+
+ // un-default the OK button
+ if (visible) {
+ _ok->setDefault(false);
+ }
+}
+
+void AnimationsDialog::addAnimation() {
+ _animations->insertWidget(_animations->count() - 1, new AnimationPanel(
+ this, Application::getInstance()->getAvatar()->addAnimationHandle()));
+}
+
+AnimationPanel::AnimationPanel(AnimationsDialog* dialog, const AnimationHandlePointer& handle) :
+ _dialog(dialog),
+ _handle(handle),
+ _applying(false) {
+ setFrameStyle(QFrame::StyledPanel);
+
+ QFormLayout* layout = new QFormLayout();
+ layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
+ setLayout(layout);
+
+ QHBoxLayout* urlBox = new QHBoxLayout();
+ layout->addRow("URL:", urlBox);
+ urlBox->addWidget(_url = new QLineEdit(handle->getURL().toString()), 1);
+ connect(_url, SIGNAL(returnPressed()), SLOT(updateHandle()));
+ QPushButton* chooseURL = new QPushButton("Choose");
+ urlBox->addWidget(chooseURL);
+ connect(chooseURL, SIGNAL(clicked(bool)), SLOT(chooseURL()));
+
+ layout->addRow("FPS:", _fps = new QDoubleSpinBox());
+ _fps->setSingleStep(0.01);
+ _fps->setMaximum(FLT_MAX);
+ _fps->setValue(handle->getFPS());
+ connect(_fps, SIGNAL(valueChanged(double)), SLOT(updateHandle()));
+
+ layout->addRow("Priority:", _priority = new QDoubleSpinBox());
+ _priority->setSingleStep(0.01);
+ _priority->setMinimum(-FLT_MAX);
+ _priority->setMaximum(FLT_MAX);
+ _priority->setValue(handle->getPriority());
+ connect(_priority, SIGNAL(valueChanged(double)), SLOT(updateHandle()));
+
+ QHBoxLayout* maskedJointBox = new QHBoxLayout();
+ layout->addRow("Masked Joints:", maskedJointBox);
+ maskedJointBox->addWidget(_maskedJoints = new QLineEdit(handle->getMaskedJoints().join(", ")), 1);
+ connect(_maskedJoints, SIGNAL(returnPressed()), SLOT(updateHandle()));
+ maskedJointBox->addWidget(_chooseMaskedJoints = new QPushButton("Choose"));
+ connect(_chooseMaskedJoints, SIGNAL(clicked(bool)), SLOT(chooseMaskedJoints()));
+
+ QPushButton* remove = new QPushButton("Delete");
+ layout->addRow(remove);
+ connect(remove, SIGNAL(clicked(bool)), SLOT(removeHandle()));
+}
+
+void AnimationPanel::chooseURL() {
+ QString directory = Application::getInstance()->lockSettings()->value("animation_directory").toString();
+ Application::getInstance()->unlockSettings();
+ QString filename = QFileDialog::getOpenFileName(this, "Choose Animation", directory, "Animation files (*.fbx)");
+ if (filename.isEmpty()) {
+ return;
+ }
+ Application::getInstance()->lockSettings()->setValue("animation_directory", QFileInfo(filename).path());
+ Application::getInstance()->unlockSettings();
+ _url->setText(QUrl::fromLocalFile(filename).toString());
+ emit _url->returnPressed();
+}
+
+void AnimationPanel::chooseMaskedJoints() {
+ QMenu menu;
+ QStringList maskedJoints = _handle->getMaskedJoints();
+ foreach (const QString& jointName, Application::getInstance()->getAvatar()->getJointNames()) {
+ QAction* action = menu.addAction(jointName);
+ action->setCheckable(true);
+ action->setChecked(maskedJoints.contains(jointName));
+ }
+ QAction* action = menu.exec(_chooseMaskedJoints->mapToGlobal(QPoint(0, 0)));
+ if (action) {
+ if (action->isChecked()) {
+ maskedJoints.append(action->text());
+ } else {
+ maskedJoints.removeOne(action->text());
+ }
+ _handle->setMaskedJoints(maskedJoints);
+ _maskedJoints->setText(maskedJoints.join(", "));
+ }
+}
+
+void AnimationPanel::updateHandle() {
+ _handle->setURL(_url->text());
+ _handle->setFPS(_fps->value());
+ _handle->setPriority(_priority->value());
+ _handle->setMaskedJoints(_maskedJoints->text().split(QRegExp("\\s*,\\s*")));
+}
+
+void AnimationPanel::removeHandle() {
+ Application::getInstance()->getAvatar()->removeAnimationHandle(_handle);
+ deleteLater();
+}
diff --git a/interface/src/ui/AnimationsDialog.h b/interface/src/ui/AnimationsDialog.h
new file mode 100644
index 0000000000..7693a1da97
--- /dev/null
+++ b/interface/src/ui/AnimationsDialog.h
@@ -0,0 +1,72 @@
+//
+// AnimationsDialog.h
+// interface/src/ui
+//
+// Created by Andrzej Kapolka on 5/19/14.
+// Copyright 2014 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#ifndef hifi_AnimationsDialog_h
+#define hifi_AnimationsDialog_h
+
+#include
+#include
+
+#include "avatar/MyAvatar.h"
+
+class QDoubleSpinner;
+class QLineEdit;
+class QPushButton;
+class QVBoxLayout;
+
+/// Allows users to edit the avatar animations.
+class AnimationsDialog : public QDialog {
+ Q_OBJECT
+
+public:
+
+ AnimationsDialog();
+
+ virtual void setVisible(bool visible);
+
+private slots:
+
+ void addAnimation();
+
+private:
+
+ QVBoxLayout* _animations;
+ QPushButton* _ok;
+};
+
+/// A panel controlling a single animation.
+class AnimationPanel : public QFrame {
+ Q_OBJECT
+
+public:
+
+ AnimationPanel(AnimationsDialog* dialog, const AnimationHandlePointer& handle);
+
+private slots:
+
+ void chooseURL();
+ void chooseMaskedJoints();
+ void updateHandle();
+ void removeHandle();
+
+private:
+
+ AnimationsDialog* _dialog;
+ AnimationHandlePointer _handle;
+ QLineEdit* _url;
+ QDoubleSpinBox* _fps;
+ QDoubleSpinBox* _priority;
+ QLineEdit* _maskedJoints;
+ QPushButton* _chooseMaskedJoints;
+ bool _applying;
+};
+
+#endif // hifi_AnimationsDialog_h
diff --git a/interface/src/ui/LogDialog.cpp b/interface/src/ui/LogDialog.cpp
index 5db704b230..7136ed2d25 100644
--- a/interface/src/ui/LogDialog.cpp
+++ b/interface/src/ui/LogDialog.cpp
@@ -57,6 +57,8 @@ LogDialog::LogDialog(QWidget* parent, AbstractLoggerInterface* logger) : QDialog
resize(INITIAL_WIDTH, static_cast(screen.height() * INITIAL_HEIGHT_RATIO));
move(screen.center() - rect().center());
setMinimumWidth(MINIMAL_WIDTH);
+
+ connect(_logger, SIGNAL(logReceived(QString)), this, SLOT(appendLogLine(QString)), Qt::QueuedConnection);
}
LogDialog::~LogDialog() {
@@ -105,7 +107,6 @@ void LogDialog::initControls() {
}
void LogDialog::showEvent(QShowEvent*) {
- connect(_logger, SIGNAL(logReceived(QString)), this, SLOT(appendLogLine(QString)), Qt::QueuedConnection);
showLogData();
}
@@ -122,7 +123,6 @@ void LogDialog::appendLogLine(QString logLine) {
if (logLine.contains(_searchTerm, Qt::CaseInsensitive)) {
_logTextBox->appendPlainText(logLine.simplified());
}
- _logTextBox->ensureCursorVisible();
}
}
@@ -146,10 +146,8 @@ void LogDialog::handleSearchTextChanged(const QString searchText) {
void LogDialog::showLogData() {
_logTextBox->clear();
- QStringList _logData = _logger->getLogData();
- for (int i = 0; i < _logData.size(); ++i) {
- appendLogLine(_logData[i]);
- }
+ _logTextBox->insertPlainText(_logger->getLogData());
+ _logTextBox->ensureCursorVisible();
}
KeywordHighlighter::KeywordHighlighter(QTextDocument *parent) : QSyntaxHighlighter(parent), keywordFormat() {
diff --git a/interface/src/ui/ModelsBrowser.cpp b/interface/src/ui/ModelsBrowser.cpp
index f65829a8ac..4296a096a0 100644
--- a/interface/src/ui/ModelsBrowser.cpp
+++ b/interface/src/ui/ModelsBrowser.cpp
@@ -351,6 +351,7 @@ bool ModelHandler::parseHeaders(QNetworkReply* reply) {
QList items = _model.findItems(QFileInfo(reply->url().toString()).baseName());
if (items.isEmpty() || items.first()->text() == DO_NOT_MODIFY_TAG) {
+ _lock.unlock();
return false;
}
diff --git a/interface/src/ui/NodeBounds.cpp b/interface/src/ui/NodeBounds.cpp
index 735dc66ddf..c4139f39c5 100644
--- a/interface/src/ui/NodeBounds.cpp
+++ b/interface/src/ui/NodeBounds.cpp
@@ -133,7 +133,6 @@ void NodeBounds::draw() {
glTranslatef(selectedCenter.x, selectedCenter.y, selectedCenter.z);
glScalef(selectedScale, selectedScale, selectedScale);
- NodeType_t selectedNodeType = selectedNode->getType();
float red, green, blue;
getColorForNodeType(selectedNode->getType(), red, green, blue);
@@ -225,7 +224,6 @@ void NodeBounds::drawOverlay() {
const int FONT = 2;
const int PADDING = 10;
const int MOUSE_OFFSET = 10;
- const int BACKGROUND_OFFSET_Y = -20;
const int BACKGROUND_BEVEL = 3;
int mouseX = application->getMouseX(),
diff --git a/interface/src/voxels/VoxelPacketProcessor.cpp b/interface/src/voxels/VoxelPacketProcessor.cpp
index 8576f14b16..c0f27264f6 100644
--- a/interface/src/voxels/VoxelPacketProcessor.cpp
+++ b/interface/src/voxels/VoxelPacketProcessor.cpp
@@ -61,6 +61,24 @@ void VoxelPacketProcessor::processPacket(const SharedNodePointer& sendingNode, c
} // fall through to piggyback message
voxelPacketType = packetTypeForPacket(mutablePacket);
+ PacketVersion packetVersion = mutablePacket[1];
+ PacketVersion expectedVersion = versionForPacketType(voxelPacketType);
+
+ // check version of piggyback packet against expected version
+ if (packetVersion != expectedVersion) {
+ static QMultiMap versionDebugSuppressMap;
+
+ QUuid senderUUID = uuidFromPacketHeader(packet);
+ if (!versionDebugSuppressMap.contains(senderUUID, voxelPacketType)) {
+ qDebug() << "Packet version mismatch on" << voxelPacketType << "- Sender"
+ << senderUUID << "sent" << (int)packetVersion << "but"
+ << (int)expectedVersion << "expected.";
+
+ versionDebugSuppressMap.insert(senderUUID, voxelPacketType);
+ }
+ return; // bail since piggyback version doesn't match
+ }
+
if (Menu::getInstance()->isOptionChecked(MenuOption::Voxels)) {
app->trackIncomingVoxelPacket(mutablePacket, sendingNode, wasStatsPacket);
diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp
index 18e647ca24..37a03bcdee 100644
--- a/libraries/fbx/src/FBXReader.cpp
+++ b/libraries/fbx/src/FBXReader.cpp
@@ -1399,7 +1399,6 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping)
// get offset transform from mapping
float offsetScale = mapping.value("scale", 1.0f).toFloat();
- geometry.fstScaled = offsetScale;
glm::quat offsetRotation = glm::quat(glm::radians(glm::vec3(mapping.value("rx").toFloat(),
mapping.value("ry").toFloat(), mapping.value("rz").toFloat())));
geometry.offset = glm::translate(glm::vec3(mapping.value("tx").toFloat(), mapping.value("ty").toFloat(),
@@ -1443,7 +1442,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping)
}
// figure the number of animation frames from the curves
- int frameCount = 0;
+ int frameCount = 1;
foreach (const AnimationCurve& curve, animationCurves) {
frameCount = qMax(frameCount, curve.values.size());
}
diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h
index 2aabdab6fa..38251e4065 100644
--- a/libraries/fbx/src/FBXReader.h
+++ b/libraries/fbx/src/FBXReader.h
@@ -211,8 +211,6 @@ public:
Extents bindExtents;
Extents meshExtents;
- float fstScaled;
-
QVector animationFrames;
QVector attachments;
diff --git a/libraries/models/src/ModelItem.h b/libraries/models/src/ModelItem.h
index 847e58e7c2..838dbc0fc8 100644
--- a/libraries/models/src/ModelItem.h
+++ b/libraries/models/src/ModelItem.h
@@ -56,6 +56,7 @@ const QString MODEL_DEFAULT_ANIMATION_URL("");
const float MODEL_DEFAULT_ANIMATION_FPS = 30.0f;
const PacketVersion VERSION_MODELS_HAVE_ANIMATION = 1;
+const PacketVersion VERSION_ROOT_ELEMENT_HAS_DATA = 2;
/// A collection of properties of a model item used in the scripting API. Translates between the actual properties of a model
/// and a JavaScript style hash/QScriptValue storing a set of properties. Used in scripting to set/get the complete set of
diff --git a/libraries/models/src/ModelTree.h b/libraries/models/src/ModelTree.h
index 10ef62c0a0..b8df1c1a22 100644
--- a/libraries/models/src/ModelTree.h
+++ b/libraries/models/src/ModelTree.h
@@ -41,6 +41,7 @@ public:
virtual int processEditPacketData(PacketType packetType, const unsigned char* packetData, int packetLength,
const unsigned char* editData, int maxLength, const SharedNodePointer& senderNode);
+ virtual bool rootElementHasData() const { return true; }
virtual void update();
void storeModel(const ModelItem& model, const SharedNodePointer& senderNode = SharedNodePointer());
diff --git a/libraries/models/src/ModelTreeElement.cpp b/libraries/models/src/ModelTreeElement.cpp
index 2f57818044..3caaf3a14c 100644
--- a/libraries/models/src/ModelTreeElement.cpp
+++ b/libraries/models/src/ModelTreeElement.cpp
@@ -95,6 +95,11 @@ bool ModelTreeElement::bestFitModelBounds(const ModelItem& model) const {
if (_box.contains(clampedMin) && _box.contains(clampedMax)) {
int childForMinimumPoint = getMyChildContainingPoint(clampedMin);
int childForMaximumPoint = getMyChildContainingPoint(clampedMax);
+
+ // if this is a really small box, then it's close enough!
+ if (_box.getScale() <= SMALLEST_REASONABLE_OCTREE_ELEMENT_SCALE) {
+ return true;
+ }
// If I contain both the minimum and maximum point, but two different children of mine
// contain those points, then I am the best fit for that model
if (childForMinimumPoint != childForMaximumPoint) {
@@ -324,6 +329,14 @@ bool ModelTreeElement::removeModelWithID(uint32_t id) {
int ModelTreeElement::readElementDataFromBuffer(const unsigned char* data, int bytesLeftToRead,
ReadBitstreamToTreeParams& args) {
+ // If we're the root, but this bitstream doesn't support root elements with data, then
+ // return without reading any bytes
+ if (this == _myTree->getRoot() && args.bitstreamVersion < VERSION_ROOT_ELEMENT_HAS_DATA) {
+ qDebug() << "ROOT ELEMENT: no root data for "
+ "bitstreamVersion=" << (int)args.bitstreamVersion << " bytesLeftToRead=" << bytesLeftToRead;
+ return 0;
+ }
+
const unsigned char* dataAt = data;
int bytesRead = 0;
uint16_t numberOfModels = 0;
diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp
index 547768ec48..aad2cfb386 100644
--- a/libraries/networking/src/AccountManager.cpp
+++ b/libraries/networking/src/AccountManager.cpp
@@ -136,7 +136,13 @@ void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager::
QNetworkRequest authenticatedRequest;
QUrl requestURL = _authURL;
- requestURL.setPath(path);
+
+ if (path.startsWith("/")) {
+ requestURL.setPath(path);
+ } else {
+ requestURL.setPath("/" + path);
+ }
+
requestURL.setQuery("access_token=" + _accountInfo.getAccessToken().token);
authenticatedRequest.setUrl(requestURL);
diff --git a/libraries/networking/src/Assignment.cpp b/libraries/networking/src/Assignment.cpp
index 925ed2930f..fd6ecd4602 100644
--- a/libraries/networking/src/Assignment.cpp
+++ b/libraries/networking/src/Assignment.cpp
@@ -63,7 +63,8 @@ Assignment::Assignment(Assignment::Command command, Assignment::Type type, const
_pool(pool),
_location(location),
_payload(),
- _isStatic(false)
+ _isStatic(false),
+ _walletUUID()
{
if (_command == Assignment::CreateCommand) {
// this is a newly created assignment, generate a random UUID
@@ -74,7 +75,8 @@ Assignment::Assignment(Assignment::Command command, Assignment::Type type, const
Assignment::Assignment(const QByteArray& packet) :
_pool(),
_location(GlobalLocation),
- _payload()
+ _payload(),
+ _walletUUID()
{
PacketType packetType = packetTypeForPacket(packet);
@@ -104,6 +106,7 @@ Assignment::Assignment(const Assignment& otherAssignment) {
_location = otherAssignment._location;
_pool = otherAssignment._pool;
_payload = otherAssignment._payload;
+ _walletUUID = otherAssignment._walletUUID;
}
Assignment& Assignment::operator=(const Assignment& rhsAssignment) {
@@ -121,6 +124,7 @@ void Assignment::swap(Assignment& otherAssignment) {
swap(_location, otherAssignment._location);
swap(_pool, otherAssignment._pool);
swap(_payload, otherAssignment._payload);
+ swap(_walletUUID, otherAssignment._walletUUID);
}
const char* Assignment::getTypeName() const {
@@ -156,6 +160,10 @@ QDebug operator<<(QDebug debug, const Assignment &assignment) {
QDataStream& operator<<(QDataStream &out, const Assignment& assignment) {
out << (quint8) assignment._type << assignment._uuid << assignment._pool << assignment._payload;
+ if (assignment._command == Assignment::RequestCommand) {
+ out << assignment._walletUUID;
+ }
+
return out;
}
@@ -166,5 +174,9 @@ QDataStream& operator>>(QDataStream &in, Assignment& assignment) {
in >> assignment._uuid >> assignment._pool >> assignment._payload;
+ if (assignment._command == Assignment::RequestCommand) {
+ in >> assignment._walletUUID;
+ }
+
return in;
}
diff --git a/libraries/networking/src/Assignment.h b/libraries/networking/src/Assignment.h
index 1d97b08bb8..3898b84787 100644
--- a/libraries/networking/src/Assignment.h
+++ b/libraries/networking/src/Assignment.h
@@ -81,6 +81,9 @@ public:
void setIsStatic(bool isStatic) { _isStatic = isStatic; }
bool isStatic() const { return _isStatic; }
+ void setWalletUUID(const QUuid& walletUUID) { _walletUUID = walletUUID; }
+ const QUuid& getWalletUUID() const { return _walletUUID; }
+
const char* getTypeName() const;
// implement parseData to return 0 so we can be a subclass of NodeData
@@ -98,6 +101,7 @@ protected:
Assignment::Location _location; /// the location of the assignment, allows a domain to preferentially use local ACs
QByteArray _payload; /// an optional payload attached to this assignment, a maximum for 1024 bytes will be packed
bool _isStatic; /// defines if this assignment needs to be re-queued in the domain-server if it stops being fulfilled
+ QUuid _walletUUID; /// the UUID for the wallet that should be paid for this assignment
};
#endif // hifi_Assignment_h
diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp
index 5692023ab1..b5a23f6b99 100644
--- a/libraries/networking/src/LimitedNodeList.cpp
+++ b/libraries/networking/src/LimitedNodeList.cpp
@@ -152,10 +152,11 @@ void LimitedNodeList::changeSendSocketBufferSize(int numSendBytes) {
bool LimitedNodeList::packetVersionAndHashMatch(const QByteArray& packet) {
PacketType checkType = packetTypeForPacket(packet);
- if (packet[1] != versionForPacketType(checkType)
+ int numPacketTypeBytes = numBytesArithmeticCodingFromBuffer(packet.data());
+
+ if (packet[numPacketTypeBytes] != versionForPacketType(checkType)
&& checkType != PacketTypeStunResponse) {
PacketType mismatchType = packetTypeForPacket(packet);
- int numPacketTypeBytes = numBytesArithmeticCodingFromBuffer(packet.data());
static QMultiMap versionDebugSuppressMap;
diff --git a/libraries/networking/src/PacketHeaders.cpp b/libraries/networking/src/PacketHeaders.cpp
index b9eee6e0c9..0f0b57635c 100644
--- a/libraries/networking/src/PacketHeaders.cpp
+++ b/libraries/networking/src/PacketHeaders.cpp
@@ -53,8 +53,6 @@ PacketVersion versionForPacketType(PacketType type) {
return 1;
case PacketTypeEnvironmentData:
return 1;
- case PacketTypeParticleData:
- return 1;
case PacketTypeDomainList:
case PacketTypeDomainListRequest:
return 3;
@@ -66,8 +64,10 @@ PacketVersion versionForPacketType(PacketType type) {
return 1;
case PacketTypeOctreeStats:
return 1;
+ case PacketTypeParticleData:
+ return 1;
case PacketTypeModelData:
- return 1;
+ return 2;
default:
return 0;
}
diff --git a/libraries/networking/src/PacketHeaders.h b/libraries/networking/src/PacketHeaders.h
index d5b1e8301c..9c764f9f02 100644
--- a/libraries/networking/src/PacketHeaders.h
+++ b/libraries/networking/src/PacketHeaders.h
@@ -47,7 +47,7 @@ enum PacketType {
PacketTypeVoxelSet,
PacketTypeVoxelSetDestructive,
PacketTypeVoxelErase,
- PacketTypeOctreeStats,
+ PacketTypeOctreeStats, // 26
PacketTypeJurisdiction,
PacketTypeJurisdictionRequest,
PacketTypeParticleQuery,
@@ -62,7 +62,7 @@ enum PacketType {
PacketTypeDomainServerRequireDTLS,
PacketTypeNodeJsonStats,
PacketTypeModelQuery,
- PacketTypeModelData,
+ PacketTypeModelData, // 41
PacketTypeModelAddOrEdit,
PacketTypeModelErase,
PacketTypeModelAddResponse,
diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp
index 3042626a51..fd0cbcff7c 100644
--- a/libraries/octree/src/Octree.cpp
+++ b/libraries/octree/src/Octree.cpp
@@ -301,6 +301,13 @@ int Octree::readElementData(OctreeElement* destinationElement, const unsigned ch
}
}
}
+
+ // if this is the root, and there is more data to read, allow it to read it's element data...
+ if (destinationElement == _rootElement && rootElementHasData() && (bytesLeftToRead - bytesRead) > 0) {
+ // tell the element to read the subsequent data
+ bytesRead += _rootElement->readElementDataFromBuffer(nodeData + bytesRead, bytesLeftToRead - bytesRead, args);
+ }
+
return bytesRead;
}
@@ -1524,7 +1531,22 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElement* element,
}
} // end keepDiggingDeeper
- // At this point all our BitMasks are complete... so let's output them to see how they compare...
+ // If we made it this far, then we've written all of our child data... if this element is the root
+ // element, then we also allow the root element to write out it's data...
+ if (continueThisLevel && element == _rootElement && rootElementHasData()) {
+ int bytesBeforeChild = packetData->getUncompressedSize();
+ continueThisLevel = element->appendElementData(packetData, params);
+ int bytesAfterChild = packetData->getUncompressedSize();
+
+ if (continueThisLevel) {
+ bytesAtThisLevel += (bytesAfterChild - bytesBeforeChild); // keep track of byte count for this child
+
+ if (params.stats) {
+ params.stats->colorSent(element);
+ }
+ }
+ }
+
// if we were unable to fit this level in our packet, then rewind and add it to the element bag for
// sending later...
if (continueThisLevel) {
diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h
index 1e71884faa..81a9823dd5 100644
--- a/libraries/octree/src/Octree.h
+++ b/libraries/octree/src/Octree.h
@@ -211,6 +211,7 @@ public:
const unsigned char* editData, int maxLength, const SharedNodePointer& sourceNode) { return 0; }
virtual bool recurseChildrenWithData() const { return true; }
+ virtual bool rootElementHasData() const { return false; }
virtual void update() { }; // nothing to do by default
diff --git a/libraries/octree/src/OctreeElement.cpp b/libraries/octree/src/OctreeElement.cpp
index 0462a3b53d..90938ddff3 100644
--- a/libraries/octree/src/OctreeElement.cpp
+++ b/libraries/octree/src/OctreeElement.cpp
@@ -1412,6 +1412,11 @@ OctreeElement* OctreeElement::getOrCreateChildElementContaining(const AABox& box
if (!child) {
child = addChildAtIndex(childIndex);
}
+
+ // if we've made a really small child, then go ahead and use that one.
+ if (child->getScale() <= SMALLEST_REASONABLE_OCTREE_ELEMENT_SCALE) {
+ return child;
+ }
// Now that we have the child to recurse down, let it answer the original question...
return child->getOrCreateChildElementContaining(box);
diff --git a/libraries/octree/src/OctreeElement.h b/libraries/octree/src/OctreeElement.h
index 2485e49797..3b056fa789 100644
--- a/libraries/octree/src/OctreeElement.h
+++ b/libraries/octree/src/OctreeElement.h
@@ -32,6 +32,8 @@ class OctreePacketData;
class ReadBitstreamToTreeParams;
class VoxelSystem;
+const float SMALLEST_REASONABLE_OCTREE_ELEMENT_SCALE = (1.0f / TREE_SCALE) / 10000.0f; // 1/10,000th of a meter
+
// Callers who want delete hook callbacks should implement this class
class OctreeElementDeleteHook {
public:
diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp
index d20f4276f1..e8ab59ce2d 100644
--- a/libraries/shared/src/HifiConfigVariantMap.cpp
+++ b/libraries/shared/src/HifiConfigVariantMap.cpp
@@ -41,19 +41,19 @@ QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringL
nextKeyIndex = argumentList.indexOf(dashedKeyRegex, keyIndex + 1);
- if (nextKeyIndex == 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());
} else {
- int maxIndex = (nextKeyIndex == -1) ? argumentList.size() : nextKeyIndex;
+ int maxIndex = (nextKeyIndex == -1) ? argumentList.size() - 1: 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];
}