From eb48342d07da948743c91626a9d0e98ba640dcd5 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 18 Nov 2015 09:10:04 -0800 Subject: [PATCH 01/99] add inversiona nd spatialkey --- examples/toybox/bubblewand/createWand.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/toybox/bubblewand/createWand.js b/examples/toybox/bubblewand/createWand.js index 4f4bc39e2c..a0e6ce5918 100644 --- a/examples/toybox/bubblewand/createWand.js +++ b/examples/toybox/bubblewand/createWand.js @@ -43,5 +43,14 @@ var wand = Entities.addEntity({ //must be enabled to be grabbable in the physics engine collisionsWillMove: true, compoundShapeURL: WAND_COLLISION_SHAPE, - script: WAND_SCRIPT_URL + script: WAND_SCRIPT_URL, + userData:JSON.stringify({ + grabbableKey:{ + invertSolidWhileHeld:true, + spatialKey:{ + relativePosition:{x:0,y:1,z:0}, + relativeRotation:Quat.fromPitchYawRollDegrees(0,90,0) + } + } + }) }); \ No newline at end of file From de953ce14d4da3bbad14e151b1e5317456c62edb Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 18 Nov 2015 10:41:53 -0800 Subject: [PATCH 02/99] better spatialkey --- examples/toybox/bubblewand/createWand.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/toybox/bubblewand/createWand.js b/examples/toybox/bubblewand/createWand.js index a0e6ce5918..faea908c81 100644 --- a/examples/toybox/bubblewand/createWand.js +++ b/examples/toybox/bubblewand/createWand.js @@ -12,8 +12,8 @@ Script.include("../../libraries/utils.js"); -var WAND_MODEL = 'http://hifi-public.s3.amazonaws.com/models/bubblewand/wand.fbx'; -var WAND_COLLISION_SHAPE = 'http://hifi-public.s3.amazonaws.com/models/bubblewand/actual_no_top_collision_hull.obj'; +var WAND_MODEL = 'http://hifi-content.s3.amazonaws.com/james/bubblewand/wand.fbx'; +var WAND_COLLISION_SHAPE = 'http://hifi-content.s3.amazonaws.com/james/bubblewand/wand_collision_hull.obj'; var WAND_SCRIPT_URL = Script.resolvePath("wand.js"); @@ -48,8 +48,8 @@ var wand = Entities.addEntity({ grabbableKey:{ invertSolidWhileHeld:true, spatialKey:{ - relativePosition:{x:0,y:1,z:0}, - relativeRotation:Quat.fromPitchYawRollDegrees(0,90,0) + relativePosition:{x:0,y:0.1,z:0}, + relativeRotation:Quat.fromPitchYawRollDegrees(0,0,90) } } }) From d3ee0d4998e19a2368177b363cee6e52641b43f1 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 18 Nov 2015 10:44:24 -0800 Subject: [PATCH 03/99] handle bubble solidity --- examples/toybox/bubblewand/wand.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/toybox/bubblewand/wand.js b/examples/toybox/bubblewand/wand.js index 8036d9ead6..354a88c43f 100644 --- a/examples/toybox/bubblewand/wand.js +++ b/examples/toybox/bubblewand/wand.js @@ -113,6 +113,7 @@ Entities.editEntity(this.currentBubble, { velocity: velocity, lifetime: lifetime, + ignoreForCollisions:false, gravity: this.randomizeBubbleGravity() }); @@ -162,7 +163,7 @@ position: this.getWandTipPosition(properties), dimensions: BUBBLE_INITIAL_DIMENSIONS, collisionsWillMove: false, - ignoreForCollisions: false, + ignoreForCollisions: true, linearDamping: BUBBLE_LINEAR_DAMPING, shapeType: "sphere" }); From 5f3e3ed9d1c760c9489fa0befe1a730151ced109 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 18 Nov 2015 11:06:48 -0800 Subject: [PATCH 04/99] cleanup --- examples/toybox/bubblewand/createWand.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/toybox/bubblewand/createWand.js b/examples/toybox/bubblewand/createWand.js index faea908c81..6b4f9717ec 100644 --- a/examples/toybox/bubblewand/createWand.js +++ b/examples/toybox/bubblewand/createWand.js @@ -44,12 +44,16 @@ var wand = Entities.addEntity({ collisionsWillMove: true, compoundShapeURL: WAND_COLLISION_SHAPE, script: WAND_SCRIPT_URL, - userData:JSON.stringify({ - grabbableKey:{ - invertSolidWhileHeld:true, - spatialKey:{ - relativePosition:{x:0,y:0.1,z:0}, - relativeRotation:Quat.fromPitchYawRollDegrees(0,0,90) + userData: JSON.stringify({ + grabbableKey: { + invertSolidWhileHeld: true, + spatialKey: { + relativePosition: { + x: 0, + y: 0.1, + z: 0 + }, + relativeRotation: Quat.fromPitchYawRollDegrees(0, 0, 90) } } }) From 12e2178dd3f2deabba02dc5ce57d70184cc9726b Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 18 Nov 2015 11:28:07 -0800 Subject: [PATCH 05/99] cleanup --- examples/toybox/bubblewand/wand.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/examples/toybox/bubblewand/wand.js b/examples/toybox/bubblewand/wand.js index 354a88c43f..f429ef6d55 100644 --- a/examples/toybox/bubblewand/wand.js +++ b/examples/toybox/bubblewand/wand.js @@ -12,7 +12,7 @@ /*global MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, randFloat, randInt */ -(function () { +(function() { Script.include("../../libraries/utils.js"); @@ -58,23 +58,23 @@ BubbleWand.prototype = { timePassed: null, currentBubble: null, - preload: function (entityID) { + preload: function(entityID) { this.entityID = entityID; }, - getWandTipPosition: function (properties) { + getWandTipPosition: function(properties) { //the tip of the wand is going to be in a different place than the center, so we move in space relative to the model to find that position var upVector = Quat.getUp(properties.rotation); var upOffset = Vec3.multiply(upVector, WAND_TIP_OFFSET); var wandTipPosition = Vec3.sum(properties.position, upOffset); return wandTipPosition; }, - addCollisionsToBubbleAfterCreation: function (bubble) { + addCollisionsToBubbleAfterCreation: function(bubble) { //if the bubble collide immediately, we get weird effects. so we add collisions after release Entities.editEntity(bubble, { collisionsWillMove: true }); }, - randomizeBubbleGravity: function () { + randomizeBubbleGravity: function() { //change up the gravity a little bit for variation in floating effects var randomNumber = randFloat(BUBBLE_GRAVITY_MIN, BUBBLE_GRAVITY_MAX); var gravity = { @@ -84,7 +84,7 @@ }; return gravity; }, - growBubbleWithWandVelocity: function (properties, deltaTime) { + growBubbleWithWandVelocity: function(properties, deltaTime) { //get the wand and tip position for calculations var wandPosition = properties.position; this.getWandTipPosition(properties); @@ -113,7 +113,6 @@ Entities.editEntity(this.currentBubble, { velocity: velocity, lifetime: lifetime, - ignoreForCollisions:false, gravity: this.randomizeBubbleGravity() }); @@ -146,7 +145,7 @@ dimensions: dimensions }); }, - createBubbleAtTipOfWand: function () { + createBubbleAtTipOfWand: function() { //create a new bubble at the tip of the wand var properties = Entities.getEntityProperties(this.entityID, ["position", "rotation"]); @@ -169,18 +168,17 @@ }); }, - startNearGrab: function () { + startNearGrab: function() { //create a bubble to grow at the start of the grab if (this.currentBubble === null) { this.createBubbleAtTipOfWand(); } }, - continueNearGrab: function () { + continueNearGrab: function() { var deltaTime = checkInterval(); //only get the properties that we need var properties = Entities.getEntityProperties(this.entityID, ["position", "rotation"]); - var wandTipPosition = this.getWandTipPosition(properties); //update the bubble to stay with the wand tip @@ -190,7 +188,7 @@ this.growBubbleWithWandVelocity(properties, deltaTime); }, - releaseGrab: function () { + releaseGrab: function() { //delete the current buble and reset state when the wand is released Entities.deleteEntity(this.currentBubble); this.currentBubble = null; From 2fe9af54d4c579d64c850a7d991e746dfb8a279f Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 18 Nov 2015 13:59:32 -0800 Subject: [PATCH 06/99] Add textures property to polyline --- .../src/RenderablePolyLineEntityItem.cpp | 17 ++++++++++++----- .../src/RenderablePolyLineEntityItem.h | 8 ++++++-- libraries/entities/src/PolyLineEntityItem.cpp | 8 +++++++- libraries/entities/src/PolyLineEntityItem.h | 11 ++++++++++- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 32418199b8..76cf4fac3d 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -36,7 +36,6 @@ PolyLineEntityItem(entityItemID, properties) { gpu::PipelinePointer RenderablePolyLineEntityItem::_pipeline; gpu::Stream::FormatPointer RenderablePolyLineEntityItem::_format; -gpu::TexturePointer RenderablePolyLineEntityItem::_texture; int32_t RenderablePolyLineEntityItem::PAINTSTROKE_GPU_SLOT; void RenderablePolyLineEntityItem::createPipeline() { @@ -44,9 +43,6 @@ void RenderablePolyLineEntityItem::createPipeline() { static const int COLOR_OFFSET = 24; static const int TEXTURE_OFFSET = 28; - auto textureCache = DependencyManager::get(); - QString path = PathUtils::resourcesPath() + "images/paintStroke.png"; - _texture = textureCache->getImageTexture(path); _format.reset(new gpu::Stream::Format()); _format->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); _format->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), NORMAL_OFFSET); @@ -132,6 +128,13 @@ void RenderablePolyLineEntityItem::render(RenderArgs* args) { createPipeline(); } + if (!_texture || _texturesChangedFlag) { + auto textureCache = DependencyManager::get(); + QString path = _textures.isEmpty() ? PathUtils::resourcesPath() + "images/paintStroke.png" : _textures; + _texture = textureCache->getTexture(QUrl(path)); + _texturesChangedFlag = false; + } + PerformanceTimer perfTimer("RenderablePolyLineEntityItem::render"); Q_ASSERT(getType() == EntityTypes::PolyLine); @@ -147,7 +150,11 @@ void RenderablePolyLineEntityItem::render(RenderArgs* args) { batch.setModelTransform(transform); batch.setPipeline(_pipeline); - batch.setResourceTexture(PAINTSTROKE_GPU_SLOT, _texture); + if (_texture->isLoaded()) { + batch.setResourceTexture(PAINTSTROKE_GPU_SLOT, _texture->getGPUTexture()); + } else { + batch.setResourceTexture(PAINTSTROKE_GPU_SLOT, args->_whiteTexture); + } batch.setInputFormat(_format); batch.setInputBuffer(0, _verticesBuffer, 0, _format->getChannels().at(0)._stride); diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h index c8a47cce0c..618f8c66a6 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h @@ -12,10 +12,13 @@ #ifndef hifi_RenderablePolyLineEntityItem_h #define hifi_RenderablePolyLineEntityItem_h + #include +#include #include #include "RenderableEntityItem.h" -#include +#include + #include @@ -29,9 +32,10 @@ public: SIMPLE_RENDERABLE(); + NetworkTexturePointer _texture; + static gpu::PipelinePointer _pipeline; static gpu::Stream::FormatPointer _format; - static gpu::TexturePointer _texture; static int32_t PAINTSTROKE_GPU_SLOT; protected: diff --git a/libraries/entities/src/PolyLineEntityItem.cpp b/libraries/entities/src/PolyLineEntityItem.cpp index f1be431ce8..45c967f78d 100644 --- a/libraries/entities/src/PolyLineEntityItem.cpp +++ b/libraries/entities/src/PolyLineEntityItem.cpp @@ -37,7 +37,8 @@ _pointsChanged(true), _points(QVector(0.0f)), _vertices(QVector(0.0f)), _normals(QVector(0.0f)), -_strokeWidths(QVector(0.0f)) +_strokeWidths(QVector(0.0f)), +_textures("") { _type = EntityTypes::PolyLine; _created = properties.getCreated(); @@ -56,6 +57,7 @@ EntityItemProperties PolyLineEntityItem::getProperties(EntityPropertyFlags desir COPY_ENTITY_PROPERTY_TO_PROPERTIES(linePoints, getLinePoints); COPY_ENTITY_PROPERTY_TO_PROPERTIES(normals, getNormals); COPY_ENTITY_PROPERTY_TO_PROPERTIES(strokeWidths, getStrokeWidths); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(textures, getTextures); properties._glowLevel = getGlowLevel(); properties._glowLevelChanged = false; @@ -72,6 +74,7 @@ bool PolyLineEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(linePoints, setLinePoints); SET_ENTITY_PROPERTY_FROM_PROPERTIES(normals, setNormals); SET_ENTITY_PROPERTY_FROM_PROPERTIES(strokeWidths, setStrokeWidths); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(textures, setTextures); if (somethingChanged) { bool wantDebug = false; @@ -196,6 +199,7 @@ int PolyLineEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* da READ_ENTITY_PROPERTY(PROP_LINE_POINTS, QVector, setLinePoints); READ_ENTITY_PROPERTY(PROP_NORMALS, QVector, setNormals); READ_ENTITY_PROPERTY(PROP_STROKE_WIDTHS, QVector, setStrokeWidths); + READ_ENTITY_PROPERTY(PROP_TEXTURES, QString, setTextures); return bytesRead; } @@ -209,6 +213,7 @@ EntityPropertyFlags PolyLineEntityItem::getEntityProperties(EncodeBitstreamParam requestedProperties += PROP_LINE_POINTS; requestedProperties += PROP_NORMALS; requestedProperties += PROP_STROKE_WIDTHS; + requestedProperties += PROP_TEXTURES; return requestedProperties; } @@ -228,6 +233,7 @@ void PolyLineEntityItem::appendSubclassData(OctreePacketData* packetData, Encode APPEND_ENTITY_PROPERTY(PROP_LINE_POINTS, getLinePoints()); APPEND_ENTITY_PROPERTY(PROP_NORMALS, getNormals()); APPEND_ENTITY_PROPERTY(PROP_STROKE_WIDTHS, getStrokeWidths()); + APPEND_ENTITY_PROPERTY(PROP_TEXTURES, getTextures()); } void PolyLineEntityItem::debugDump() const { diff --git a/libraries/entities/src/PolyLineEntityItem.h b/libraries/entities/src/PolyLineEntityItem.h index 86a1dfb6e0..9e9d3f9013 100644 --- a/libraries/entities/src/PolyLineEntityItem.h +++ b/libraries/entities/src/PolyLineEntityItem.h @@ -67,7 +67,14 @@ class PolyLineEntityItem : public EntityItem { bool setStrokeWidths(const QVector& strokeWidths); const QVector& getStrokeWidths() const{ return _strokeWidths; } - + + const QString& getTextures() const { return _textures; } + void setTextures(const QString& textures) { + if (_textures != textures) { + _textures = textures; + _texturesChangedFlag = true; + } + } virtual ShapeType getShapeType() const { return SHAPE_TYPE_LINE; } @@ -90,6 +97,8 @@ class PolyLineEntityItem : public EntityItem { QVector _vertices; QVector _normals; QVector _strokeWidths; + QString _textures; + bool _texturesChangedFlag { false }; mutable QReadWriteLock _quadReadWriteLock; }; From 3d0afc3c5ff0910d2b92b5ee9843d844d4315696 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 18 Nov 2015 14:00:00 -0800 Subject: [PATCH 07/99] Update entities version number for polyline change --- libraries/networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index f5c66617a8..2411ee23ac 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -41,7 +41,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_ENTITIES_PARTICLES_ADDITIVE_BLENDING; + return VERSION_ENTITIES_POLYLINE_TEXTURE; case PacketType::AvatarData: case PacketType::BulkAvatarData: default: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index e0a847dcc6..23df89b3d7 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -159,5 +159,6 @@ const PacketVersion VERSION_ENTITIES_ANIMATION_PROPERTIES_GROUP = 46; const PacketVersion VERSION_ENTITIES_KEYLIGHT_PROPERTIES_GROUP = 47; const PacketVersion VERSION_ENTITIES_KEYLIGHT_PROPERTIES_GROUP_BIS = 48; const PacketVersion VERSION_ENTITIES_PARTICLES_ADDITIVE_BLENDING = 49; +const PacketVersion VERSION_ENTITIES_POLYLINE_TEXTURE = 50; #endif // hifi_PacketHeaders_h From 2f3cf82202b3418456916ae22d8616242770cc8e Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Wed, 18 Nov 2015 14:22:02 -0800 Subject: [PATCH 08/99] don't apply out of order edits to entities that have been deleted --- libraries/entities/src/EntityItem.cpp | 9 +++++++ libraries/entities/src/EntityTree.cpp | 4 ++++ libraries/entities/src/EntityTree.h | 25 +++++++++++++++++--- libraries/entities/src/EntityTreeElement.cpp | 19 ++++++++++----- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 100f6dfe22..75bc26a560 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -500,6 +500,15 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef } } + // before proceeding, check to see if this is an entity that we know has been deleted, which + // might happen in the case of out-of-order and/or recorvered packets, if we've deleted the entity + // we can confidently ignore this packet + EntityTreePointer tree = getTree(); + if (tree && tree->isDeletedEntity(_id)) { + qDebug() << "Recieved packet for previously deleted entity [" << _id << "] ignoring. (inside " << __FUNCTION__ << ")"; + ignoreServerPacket = true; + } + if (ignoreServerPacket) { overwriteLocalData = false; #ifdef WANT_DEBUG diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index dc7c19056a..9eac14e4b4 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -68,6 +68,7 @@ void EntityTree::eraseAllOctreeElements(bool createNewRoot) { Octree::eraseAllOctreeElements(createNewRoot); resetClientEditStats(); + clearDeletedEntities(); } bool EntityTree::handlesEditPacketType(PacketType packetType) const { @@ -398,6 +399,9 @@ void EntityTree::processRemovedEntities(const DeleteEntityOperator& theOperator) // set up the deleted entities ID QWriteLocker locker(&_recentlyDeletedEntitiesLock); _recentlyDeletedEntityItemIDs.insert(deletedAt, theEntity->getEntityItemID()); + } else { + // on the client side, we also remember that we deleted this entity, we don't care about the time + trackDeletedEntity(theEntity->getEntityItemID()); } if (_simulation) { diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 645e3f4f76..d1e0462f64 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -228,6 +228,11 @@ public: EntityTreePointer getThisPointer() { return std::static_pointer_cast(shared_from_this()); } + bool isDeletedEntity(const QUuid& id) { + QReadLocker locker(&_deletedEntitiesLock); + return _deletedEntityItemIDs.contains(id); + } + signals: void deletingEntity(const EntityItemID& entityID); void addingEntity(const EntityItemID& entityID); @@ -235,7 +240,7 @@ signals: void newCollisionSoundURL(const QUrl& url); void clearingEntities(); -private: +protected: void processRemovedEntities(const DeleteEntityOperator& theOperator); bool updateEntityWithElement(EntityItemPointer entity, const EntityItemProperties& properties, @@ -252,8 +257,22 @@ private: QReadWriteLock _newlyCreatedHooksLock; QVector _newlyCreatedHooks; - mutable QReadWriteLock _recentlyDeletedEntitiesLock; - QMultiMap _recentlyDeletedEntityItemIDs; + mutable QReadWriteLock _recentlyDeletedEntitiesLock; /// lock of server side recent deletes + QMultiMap _recentlyDeletedEntityItemIDs; /// server side recent deletes + + mutable QReadWriteLock _deletedEntitiesLock; /// lock of client side recent deletes + QSet _deletedEntityItemIDs; /// client side recent deletes + + void clearDeletedEntities() { + QWriteLocker locker(&_deletedEntitiesLock); + _deletedEntityItemIDs.clear(); + } + + void trackDeletedEntity(const QUuid& id) { + QWriteLocker locker(&_deletedEntitiesLock); + _deletedEntityItemIDs << id; + } + EntityItemFBXService* _fbxService; QHash _entityToElementMap; diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 7ada138d02..02552ef488 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -894,12 +894,19 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int entityItem = EntityTypes::constructEntityItem(dataAt, bytesLeftToRead, args); if (entityItem) { bytesForThisEntity = entityItem->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args); - addEntityItem(entityItem); // add this new entity to this elements entities - entityItemID = entityItem->getEntityItemID(); - _myTree->setContainingElement(entityItemID, getThisPointer()); - _myTree->postAddEntity(entityItem); - if (entityItem->getCreated() == UNKNOWN_CREATED_TIME) { - entityItem->recordCreationTime(); + + // don't add if we've recently deleted.... + if (!_myTree->isDeletedEntity(entityItem->getID())) { + addEntityItem(entityItem); // add this new entity to this elements entities + entityItemID = entityItem->getEntityItemID(); + _myTree->setContainingElement(entityItemID, getThisPointer()); + _myTree->postAddEntity(entityItem); + if (entityItem->getCreated() == UNKNOWN_CREATED_TIME) { + entityItem->recordCreationTime(); + } + } else { + qDebug() << "Recieved packet for previously deleted entity [" << + entityItem->getID() << "] ignoring. (inside " << __FUNCTION__ << ")"; } } } From 6719092094fb4a1ba8fde4ea38cd23a87e6e9324 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Wed, 18 Nov 2015 14:56:31 -0800 Subject: [PATCH 09/99] add an example of an entityScript that subscribes to messages --- .../messagesReceiverEntityExample.js | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 examples/entityScripts/messagesReceiverEntityExample.js diff --git a/examples/entityScripts/messagesReceiverEntityExample.js b/examples/entityScripts/messagesReceiverEntityExample.js new file mode 100644 index 0000000000..c10319e1cc --- /dev/null +++ b/examples/entityScripts/messagesReceiverEntityExample.js @@ -0,0 +1,56 @@ +// +// messagesReceiverEntityExample.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 11/18/15. +// Copyright 2015 High Fidelity, Inc. +// +// This is an example of an entity script which when assigned to an entity, will detect when the entity is being grabbed by the hydraGrab script +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function () { + + var _this; + + var messageReceived = function (channel, message, senderID) { + print("message received on channel:" + channel + ", message:" + message + ", senderID:" + senderID); + }; + + // this is the "constructor" for the entity as a JS object we don't do much here, but we do want to remember + // our this object, so we can access it in cases where we're called without a this (like in the case of various global signals) + MessagesReceiver = function () { + _this = this; + }; + + MessagesReceiver.prototype = { + + // preload() will be called when the entity has become visible (or known) to the interface + // it gives us a chance to set our local JavaScript object up. In this case it means: + // * remembering our entityID, so we can access it in cases where we're called without an entityID + // * unsubscribing from messages + // * connectingf to the messageReceived signal + preload: function (entityID) { + this.entityID = entityID; + + print("---- subscribing ----"); + Messages.subscribe("example"); + Messages.messageReceived.connect(messageReceived); + }, + + // unload() will be called when the entity has become no longer known to the interface + // it gives us a chance to clean up our local JavaScript object. In this case it means: + // * unsubscribing from messages + // * disconnecting from the messageReceived signal + unload: function (entityID) { + print("---- unsubscribing ----"); + Messages.unsubscribe("example"); + Messages.messageReceived.disconnect(messageReceived); + }, + }; + + // entity scripts always need to return a newly constructed object of our type + return new MessagesReceiver(); +}) From f4cf1c0291b1f3b202570c0fd67c694e131b2036 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 18 Nov 2015 17:33:22 -0800 Subject: [PATCH 10/99] only broadcast removed node messages to interested nodes --- domain-server/src/DomainServer.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index d7bcec2431..653f0e84cd 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1835,14 +1835,25 @@ void DomainServer::processNodeDisconnectRequestPacket(QSharedPointer p qDebug() << "Received a disconnect request from node with UUID" << nodeUUID; - if (limitedNodeList->killNodeWithUUID(nodeUUID)) { + // we want to check what type this node was before going to kill it so that we can avoid sending the RemovedNode + // packet to nodes that don't care about this type + auto node = limitedNodeList->nodeWithUUID(nodeUUID); + + if (node) { + auto nodeType = node->getType(); + limitedNodeList->killNodeWithUUID(nodeUUID); + static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID); removedNodePacket->reset(); removedNodePacket->write(nodeUUID.toRfc4122()); // broadcast out the DomainServerRemovedNode message - limitedNodeList->eachNode([&limitedNodeList](const SharedNodePointer& otherNode){ + limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool { + // only send the removed node packet to nodes that care about the type of node this was + auto nodeLinkedData = dynamic_cast(otherNode->getLinkedData()); + return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType); + }, [&limitedNodeList](const SharedNodePointer& otherNode){ limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode); }); } From 3fba1f8445422e3a639dc4a93c7c0fb776ffd8fd Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 18 Nov 2015 17:46:32 -0800 Subject: [PATCH 11/99] better variable naming for nodeToKill --- domain-server/src/DomainServer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 653f0e84cd..b4243ef8a0 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1837,10 +1837,10 @@ void DomainServer::processNodeDisconnectRequestPacket(QSharedPointer p // we want to check what type this node was before going to kill it so that we can avoid sending the RemovedNode // packet to nodes that don't care about this type - auto node = limitedNodeList->nodeWithUUID(nodeUUID); + auto nodeToKill = limitedNodeList->nodeWithUUID(nodeUUID); - if (node) { - auto nodeType = node->getType(); + if (nodeToKill) { + auto nodeType = nodeToKill->getType(); limitedNodeList->killNodeWithUUID(nodeUUID); static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID); From 271387f96e0a1947cf62f19603cfae4312067747 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 18 Nov 2015 18:03:31 -0800 Subject: [PATCH 12/99] use a unique_ptr for linkedData in Node --- assignment-client/src/AssignmentClientMonitor.cpp | 6 ++++-- assignment-client/src/audio/AudioMixer.cpp | 3 ++- assignment-client/src/avatars/AvatarMixer.cpp | 3 ++- assignment-client/src/entities/EntityServer.cpp | 4 ++-- assignment-client/src/entities/EntityServer.h | 4 +++- assignment-client/src/octree/OctreeServer.cpp | 6 +++--- assignment-client/src/octree/OctreeServer.h | 4 +++- domain-server/src/DomainServer.cpp | 4 +++- libraries/networking/src/Node.cpp | 5 ----- libraries/networking/src/Node.h | 8 ++++---- 10 files changed, 26 insertions(+), 21 deletions(-) diff --git a/assignment-client/src/AssignmentClientMonitor.cpp b/assignment-client/src/AssignmentClientMonitor.cpp index 7b3d5695e1..763945fa3e 100644 --- a/assignment-client/src/AssignmentClientMonitor.cpp +++ b/assignment-client/src/AssignmentClientMonitor.cpp @@ -9,6 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include @@ -227,8 +228,9 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer p matchingNode = DependencyManager::get()->addOrUpdateNode (senderID, NodeType::Unassigned, senderSockAddr, senderSockAddr, false, false); - childData = new AssignmentClientChildData(Assignment::Type::AllTypes); - matchingNode->setLinkedData(childData); + auto childData = std::unique_ptr + { new AssignmentClientChildData(Assignment::Type::AllTypes) }; + matchingNode->setLinkedData(std::move(childData)); } else { // tell unknown assignment-client child to exit. qDebug() << "Asking unknown child at" << senderSockAddr << "to exit."; diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 1d8908845f..5ca9f46f10 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -652,7 +653,7 @@ void AudioMixer::run() { nodeList->addNodeTypeToInterestSet(NodeType::Agent); nodeList->linkedDataCreateCallback = [](Node* node) { - node->setLinkedData(new AudioMixerClientData()); + node->setLinkedData(std::unique_ptr { new AudioMixerClientData }); }; // wait until we have the domain-server settings, otherwise we bail diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 1e17467c3b..1198767944 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -519,7 +520,7 @@ void AvatarMixer::run() { nodeList->addNodeTypeToInterestSet(NodeType::Agent); nodeList->linkedDataCreateCallback = [] (Node* node) { - node->setLinkedData(new AvatarMixerClientData()); + node->setLinkedData(std::unique_ptr { new AvatarMixerClientData }); }; // setup the timer that will be fired on the broadcast thread diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index 2fafaa6731..8b1524972f 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -46,8 +46,8 @@ void EntityServer::handleEntityPacket(QSharedPointer packet, SharedNod } } -OctreeQueryNode* EntityServer::createOctreeQueryNode() { - return new EntityNodeData(); +std::unique_ptr EntityServer::createOctreeQueryNode() { + return std::unique_ptr { new EntityNodeData() }; } OctreePointer EntityServer::createTree() { diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 065834cbc2..eeb87f9ef1 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -14,6 +14,8 @@ #include "../octree/OctreeServer.h" +#include + #include "EntityItem.h" #include "EntityServerConsts.h" #include "EntityTree.h" @@ -26,7 +28,7 @@ public: ~EntityServer(); // Subclasses must implement these methods - virtual OctreeQueryNode* createOctreeQueryNode() override ; + virtual std::unique_ptr createOctreeQueryNode() override ; virtual char getMyNodeType() const override { return NodeType::EntityServer; } virtual PacketType getMyQueryMessageType() const override { return PacketType::EntityQuery; } virtual const char* getMyServerName() const override { return MODEL_SERVER_NAME; } diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 84749bd975..d1b604f97f 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -1108,9 +1108,9 @@ void OctreeServer::run() { #endif nodeList->linkedDataCreateCallback = [] (Node* node) { - OctreeQueryNode* newQueryNodeData = _instance->createOctreeQueryNode(); - newQueryNodeData->init(); - node->setLinkedData(newQueryNodeData); + auto queryNodeData = _instance->createOctreeQueryNode(); + queryNodeData->init(); + node->setLinkedData(std::move(queryNodeData)); }; srand((unsigned)time(0)); diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index b8e4a5c261..e3083dea07 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -12,6 +12,8 @@ #ifndef hifi_OctreeServer_h #define hifi_OctreeServer_h +#include + #include #include #include @@ -61,7 +63,7 @@ public: quint64 getLoadElapsedTime() const { return (_persistThread) ? _persistThread->getLoadElapsedTime() : 0; } // Subclasses must implement these methods - virtual OctreeQueryNode* createOctreeQueryNode() = 0; + virtual std::unique_ptr createOctreeQueryNode() = 0; virtual char getMyNodeType() const = 0; virtual PacketType getMyQueryMessageType() const = 0; virtual const char* getMyServerName() const = 0; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index d7bcec2431..4f866c965f 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -11,6 +11,8 @@ #include "DomainServer.h" +#include + #include #include #include @@ -1640,7 +1642,7 @@ void DomainServer::refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& void DomainServer::nodeAdded(SharedNodePointer node) { // we don't use updateNodeWithData, so add the DomainServerNodeData to the node here - node->setLinkedData(new DomainServerNodeData()); + node->setLinkedData(std::unique_ptr { new DomainServerNodeData() }); } void DomainServer::nodeKilled(SharedNodePointer node) { diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 243dca78e2..918bd5b972 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -48,7 +48,6 @@ Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, NetworkPeer(uuid, publicSocket, localSocket, parent), _type(type), _connectionSecret(connectionSecret), - _linkedData(NULL), _isAlive(true), _pingMs(-1), // "Uninitialized" _clockSkewUsec(0), @@ -61,10 +60,6 @@ Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, setType(_type); } -Node::~Node() { - delete _linkedData; -} - void Node::setType(char type) { _type = type; diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 38d6678ba5..2b35516787 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -12,6 +12,7 @@ #ifndef hifi_Node_h #define hifi_Node_h +#include #include #include @@ -34,7 +35,6 @@ public: const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, bool canAdjustLocks, bool canRez, const QUuid& connectionSecret = QUuid(), QObject* parent = 0); - ~Node(); bool operator==(const Node& otherNode) const { return _uuid == otherNode._uuid; } bool operator!=(const Node& otherNode) const { return !(*this == otherNode); } @@ -45,8 +45,8 @@ public: const QUuid& getConnectionSecret() const { return _connectionSecret; } void setConnectionSecret(const QUuid& connectionSecret) { _connectionSecret = connectionSecret; } - NodeData* getLinkedData() const { return _linkedData; } - void setLinkedData(NodeData* linkedData) { _linkedData = linkedData; } + NodeData* getLinkedData() const { return _linkedData.get(); } + void setLinkedData(std::unique_ptr linkedData) { _linkedData = std::move(linkedData); } bool isAlive() const { return _isAlive; } void setAlive(bool isAlive) { _isAlive = isAlive; } @@ -75,7 +75,7 @@ private: NodeType_t _type; QUuid _connectionSecret; - NodeData* _linkedData; + std::unique_ptr _linkedData; bool _isAlive; int _pingMs; int _clockSkewUsec; From b4f9c4158fe423e94fa73a9dd776ad61c38da9dc Mon Sep 17 00:00:00 2001 From: AlessandroSigna Date: Thu, 19 Nov 2015 09:44:25 -0800 Subject: [PATCH 13/99] cleanup directory + bug fix --- examples/entityScripts/recordingMaster.js | 1 - .../entityScripts/synchronizerEntityScript.js | 90 -------------- examples/entityScripts/synchronizerMaster.js | 117 ------------------ 3 files changed, 208 deletions(-) delete mode 100644 examples/entityScripts/synchronizerEntityScript.js delete mode 100644 examples/entityScripts/synchronizerMaster.js diff --git a/examples/entityScripts/recordingMaster.js b/examples/entityScripts/recordingMaster.js index 680d364eb1..27f84f44c2 100644 --- a/examples/entityScripts/recordingMaster.js +++ b/examples/entityScripts/recordingMaster.js @@ -27,7 +27,6 @@ var toolBar = null; var recordIcon; var isRecording = false; var channel = "groupRecordingChannel"; -Messages.subscribe(channel); setupToolBar(); function setupToolBar() { diff --git a/examples/entityScripts/synchronizerEntityScript.js b/examples/entityScripts/synchronizerEntityScript.js deleted file mode 100644 index 82dd954381..0000000000 --- a/examples/entityScripts/synchronizerEntityScript.js +++ /dev/null @@ -1,90 +0,0 @@ -// -// synchronizerEntityScript.js -// examples/entityScripts -// -// Created by Alessandro Signa on 11/12/15. -// Copyright 2015 High Fidelity, Inc. -// - -// This script shows how to create a synchronized event between avatars trhough an entity. -// It works using the entity's userData: the master change its value and every client checks it every frame -// This entity prints a message when the event starts and when it ends. -// The client running synchronizerMaster.js is the event master and it decides when the event starts/ends by pressing a button. -// All the avatars in the area when the master presses the button will receive a message. -// - -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - - - - -(function() { - var insideArea = false; - var isJoiningTheEvent = false; - var _this; - - - - function ParamsEntity() { - _this = this; - return; - } - - - ParamsEntity.prototype = { - update: function(){ - var userData = JSON.parse(Entities.getEntityProperties(_this.entityID, ["userData"]).userData); - var valueToCheck = userData.myKey.valueToCheck; - if(valueToCheck && !isJoiningTheEvent){ - _this.sendMessage(); - }else if((!valueToCheck && isJoiningTheEvent) || (isJoiningTheEvent && !insideArea)){ - _this.stopMessage(); - } - }, - preload: function(entityID) { - print('entity loaded') - this.entityID = entityID; - Script.update.connect(_this.update); - }, - enterEntity: function(entityID) { - print("enterEntity("+entityID+")"); - var userData = JSON.parse(Entities.getEntityProperties(_this.entityID, ["userData"]).userData); - var valueToCheck = userData.myKey.valueToCheck; - if(!valueToCheck){ - //i'm in the area in time (before the event starts) - insideArea = true; - } - change(entityID); - }, - leaveEntity: function(entityID) { - print("leaveEntity("+entityID+")"); - Entities.editEntity(entityID, { color: { red: 255, green: 190, blue: 20} }); - insideArea = false; - }, - - sendMessage: function(myID){ - if(insideArea && !isJoiningTheEvent){ - print("The event started"); - isJoiningTheEvent = true; - } - }, - - stopMessage: function(myID){ - if(isJoiningTheEvent){ - print("The event ended"); - isJoiningTheEvent = false; - } - }, - clean: function(entityID) { - Script.update.disconnect(_this.update); - } - } - - function change(entityID) { - Entities.editEntity(entityID, { color: { red: 255, green: 100, blue: 220} }); - } - - - return new ParamsEntity(); -}); diff --git a/examples/entityScripts/synchronizerMaster.js b/examples/entityScripts/synchronizerMaster.js deleted file mode 100644 index 8b6c8c2b8b..0000000000 --- a/examples/entityScripts/synchronizerMaster.js +++ /dev/null @@ -1,117 +0,0 @@ -// -// synchronizerMaster.js -// examples/entityScripts -// -// Created by Alessandro Signa on 11/12/15. -// Copyright 2015 High Fidelity, Inc. -// -// Run this script to spawn a box (synchronizer) and drive the start/end of the event for anyone who is inside the box -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - -var PARAMS_SCRIPT_URL = Script.resolvePath('synchronizerEntityScript.js'); - - -HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; -Script.include("../libraries/toolBars.js"); -Script.include("../libraries/utils.js"); - - - -var rotation = Quat.safeEulerAngles(Camera.getOrientation()); -rotation = Quat.fromPitchYawRollDegrees(0, rotation.y, 0); -var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1, Quat.getFront(rotation))); - -var TOOL_ICON_URL = HIFI_PUBLIC_BUCKET + "images/tools/"; -var ALPHA_ON = 1.0; -var ALPHA_OFF = 0.7; -var COLOR_TOOL_BAR = { red: 0, green: 0, blue: 0 }; - -var toolBar = null; -var recordIcon; - - - -var isHappening = false; - -var testEntity = Entities.addEntity({ - name: 'paramsTestEntity', - dimensions: { - x: 2, - y: 1, - z: 2 - }, - type: 'Box', - position: center, - color: { - red: 255, - green: 255, - blue: 255 - }, - visible: true, - ignoreForCollisions: true, - script: PARAMS_SCRIPT_URL, - - userData: JSON.stringify({ - myKey: { - valueToCheck: false - } - }) -}); - - -setupToolBar(); - -function setupToolBar() { - if (toolBar != null) { - print("Multiple calls to setupToolBar()"); - return; - } - Tool.IMAGE_HEIGHT /= 2; - Tool.IMAGE_WIDTH /= 2; - - toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL); //put the button in the up-left corner - - toolBar.setBack(COLOR_TOOL_BAR, ALPHA_OFF); - - recordIcon = toolBar.addTool({ - imageURL: TOOL_ICON_URL + "recording-record.svg", - subImage: { x: 0, y: 0, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, - x: 0, y: 0, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT, - alpha: MyAvatar.isPlaying() ? ALPHA_OFF : ALPHA_ON, - visible: true - }, true, isHappening); - -} - -function mousePressEvent(event) { - clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); - if (recordIcon === toolBar.clicked(clickedOverlay, false)) { - if (!isHappening) { - print("I'm the event master. I want the event starts"); - isHappening = true; - setEntityCustomData("myKey", testEntity, {valueToCheck: true}); - - } else { - print("I want the event stops"); - isHappening = false; - setEntityCustomData("myKey", testEntity, {valueToCheck: false}); - - } - } -} - - -function cleanup() { - toolBar.cleanup(); - Entities.callEntityMethod(testEntity, 'clean'); //have to call this before deleting to avoid the JSON warnings - Entities.deleteEntity(testEntity); -} - - - - Script.scriptEnding.connect(cleanup); - Controller.mousePressEvent.connect(mousePressEvent); \ No newline at end of file From b7f501c0b3aee31c9fba328b536e235b90cb2fac Mon Sep 17 00:00:00 2001 From: "U-GAPOS\\andrew" Date: Thu, 19 Nov 2015 10:42:53 -0800 Subject: [PATCH 14/99] use glm::quat_cast instead of extractRotation() --- interface/src/avatar/MyAvatar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 1c151bcd3f..989a81fc0c 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -428,7 +428,7 @@ void MyAvatar::updateHMDFollowVelocity() { } if (_followSpeed > 0.0f) { // to compute new velocity we must rotate offset into the world-frame - glm::quat sensorToWorldRotation = extractRotation(_sensorToWorldMatrix); + glm::quat sensorToWorldRotation = glm::normalize(glm::quat_cast(_sensorToWorldMatrix)); _followVelocity = _followSpeed * glm::normalize(sensorToWorldRotation * offset); } } From 28e349f421225fb3ec085f86a29291705b9b1f23 Mon Sep 17 00:00:00 2001 From: "U-GAPOS\\andrew" Date: Thu, 19 Nov 2015 10:43:40 -0800 Subject: [PATCH 15/99] remeasure sensor after recentering --- plugins/oculus/src/OculusBaseDisplayPlugin.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index 4c80b9a51d..7fd956a08f 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -29,6 +29,7 @@ glm::mat4 OculusBaseDisplayPlugin::getProjection(Eye eye, const glm::mat4& baseP void OculusBaseDisplayPlugin::resetSensors() { #if (OVR_MAJOR_VERSION >= 6) ovr_RecenterPose(_hmd); + preRender(); #endif } From 2a05ec650b0bd1a878a620115b4c7876552fa7f0 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 19 Nov 2015 14:17:48 -0800 Subject: [PATCH 16/99] fix locking in AvatarHashMap/AvatarManager --- interface/src/avatar/AvatarManager.cpp | 62 ++++++++++++++----------- interface/src/avatar/AvatarManager.h | 4 +- libraries/avatars/src/AvatarHashMap.cpp | 59 ++++++++++++++--------- libraries/avatars/src/AvatarHashMap.h | 5 +- 4 files changed, 78 insertions(+), 52 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 4b9bfd21a3..ae4c978877 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -119,6 +119,8 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { PerformanceTimer perfTimer("otherAvatars"); // simulate avatars + QWriteLocker writeLock(&_hashLock); + AvatarHash::iterator avatarIterator = _avatarHash.begin(); while (avatarIterator != _avatarHash.end()) { auto avatar = std::dynamic_pointer_cast(avatarIterator.value()); @@ -128,10 +130,9 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { // DO NOT update or fade out uninitialized Avatars ++avatarIterator; } else if (avatar->shouldDie()) { - removeAvatarMotionState(avatar); - _avatarFades.push_back(avatarIterator.value()); - QWriteLocker locker(&_hashLock); + auto removedAvatar = avatarIterator.value(); avatarIterator = _avatarHash.erase(avatarIterator); + handleRemovedAvatar(removedAvatar); } else { avatar->startUpdate(); avatar->simulate(deltaTime); @@ -139,6 +140,8 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { ++avatarIterator; } } + + writeLock.unlock(); // simulate avatar fades simulateAvatarFades(deltaTime); @@ -173,8 +176,8 @@ AvatarSharedPointer AvatarManager::newSharedAvatar() { } // virtual -AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { - auto avatar = std::dynamic_pointer_cast(AvatarHashMap::addAvatar(sessionUUID, mixerWeakPointer)); +AvatarSharedPointer AvatarManager::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { + auto avatar = std::dynamic_pointer_cast(AvatarHashMap::newOrExistingAvatar(sessionUUID, mixerWeakPointer)); render::ScenePointer scene = qApp->getMain3DScene(); render::PendingChanges pendingChanges; if (DependencyManager::get()->shouldRenderAvatars()) { @@ -200,20 +203,28 @@ void AvatarManager::removeAvatarMotionState(AvatarSharedPointer avatar) { // virtual void AvatarManager::removeAvatar(const QUuid& sessionUUID) { - AvatarHash::iterator avatarIterator = _avatarHash.find(sessionUUID); - if (avatarIterator != _avatarHash.end()) { - std::shared_ptr avatar = std::dynamic_pointer_cast(avatarIterator.value()); - if (avatar != _myAvatar && avatar->isInitialized()) { - removeAvatarMotionState(avatar); - _avatarFades.push_back(avatarIterator.value()); - QWriteLocker locker(&_hashLock); - _avatarHash.erase(avatarIterator); - } + QWriteLocker locker(&_hashLock); + + auto removedAvatar = _avatarHash.take(sessionUUID); + if (removedAvatar) { + handleRemovedAvatar(removedAvatar); } } +void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar) { + qDebug() << "Removed avatar with UUID" << uuidStringWithoutCurlyBraces(removedAvatar->getSessionUUID()) + << "from AvatarManager"; + + AvatarHashMap::handleRemovedAvatar(removedAvatar); + + removeAvatarMotionState(removedAvatar); + _avatarFades.push_back(removedAvatar); +} + void AvatarManager::clearOtherAvatars() { // clear any avatars that came from an avatar-mixer + QWriteLocker locker(&_hashLock); + AvatarHash::iterator avatarIterator = _avatarHash.begin(); while (avatarIterator != _avatarHash.end()) { auto avatar = std::static_pointer_cast(avatarIterator.value()); @@ -221,10 +232,10 @@ void AvatarManager::clearOtherAvatars() { // don't remove myAvatar or uninitialized avatars from the list ++avatarIterator; } else { - removeAvatarMotionState(avatar); - _avatarFades.push_back(avatarIterator.value()); - QWriteLocker locker(&_hashLock); + auto removedAvatar = avatarIterator.value(); avatarIterator = _avatarHash.erase(avatarIterator); + + handleRemovedAvatar(removedAvatar); } } _myAvatar->clearLookAtTargetAvatar(); @@ -318,9 +329,11 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents } void AvatarManager::updateAvatarPhysicsShape(const QUuid& id) { - AvatarHash::iterator avatarItr = _avatarHash.find(id); - if (avatarItr != _avatarHash.end()) { - auto avatar = std::static_pointer_cast(avatarItr.value()); + auto avatarData = findAvatar(id); + + if (avatarData) { + auto avatar = std::dynamic_pointer_cast(avatarData); + AvatarMotionState* motionState = avatar->getMotionState(); if (motionState) { motionState->addDirtyFlags(Simulation::DIRTY_SHAPE); @@ -363,11 +376,6 @@ AvatarSharedPointer AvatarManager::getAvatarBySessionID(const QUuid& sessionID) if (sessionID == _myAvatar->getSessionUUID()) { return std::static_pointer_cast(_myAvatar); } - QReadLocker locker(&_hashLock); - auto iter = _avatarHash.find(sessionID); - if (iter != _avatarHash.end()) { - return iter.value(); - } else { - return AvatarSharedPointer(); - } + + return findAvatar(sessionID); } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index fa0593368b..71f579abc2 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -77,9 +77,11 @@ private: // virtual overrides virtual AvatarSharedPointer newSharedAvatar(); - virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); + virtual AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); void removeAvatarMotionState(AvatarSharedPointer avatar); + virtual void removeAvatar(const QUuid& sessionUUID); + virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar); QVector _avatarFades; std::shared_ptr _myAvatar; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 342f995de7..cc83ddf678 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -42,18 +42,30 @@ AvatarSharedPointer AvatarHashMap::newSharedAvatar() { return std::make_shared(); } -AvatarSharedPointer AvatarHashMap::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { - qCDebug(avatars) << "Adding avatar with sessionUUID " << sessionUUID << "to AvatarHashMap."; - - AvatarSharedPointer avatar = newSharedAvatar(); - avatar->setSessionUUID(sessionUUID); - avatar->setOwningAvatarMixer(mixerWeakPointer); +AvatarSharedPointer AvatarHashMap::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { QWriteLocker locker(&_hashLock); - _avatarHash.insert(sessionUUID, avatar); - emit avatarAddedEvent(sessionUUID); + + auto avatar = _avatarHash.value(sessionUUID); + + if (!avatar) { + qCDebug(avatars) << "Adding avatar with sessionUUID " << sessionUUID << "to AvatarHashMap."; + + avatar = newSharedAvatar(); + avatar->setSessionUUID(sessionUUID); + avatar->setOwningAvatarMixer(mixerWeakPointer); + + _avatarHash.insert(sessionUUID, avatar); + emit avatarAddedEvent(sessionUUID); + } + return avatar; } +AvatarSharedPointer AvatarHashMap::findAvatar(const QUuid& sessionUUID) { + QReadLocker locker(&_hashLock); + return _avatarHash.value(sessionUUID); +} + void AvatarHashMap::processAvatarDataPacket(QSharedPointer packet, SharedNodePointer sendingNode) { // enumerate over all of the avatars in this packet @@ -66,10 +78,7 @@ void AvatarHashMap::processAvatarDataPacket(QSharedPointer packet, Sha QByteArray byteArray = packet->readWithoutCopy(packet->bytesLeftToRead()); if (sessionUUID != _lastOwnerSessionUUID) { - AvatarSharedPointer avatar = _avatarHash.value(sessionUUID); - if (!avatar) { - avatar = addAvatar(sessionUUID, sendingNode); - } + auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); // have the matching (or new) avatar parse the data from the packet int bytesRead = avatar->parseDataFromBuffer(byteArray); @@ -97,10 +106,8 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer packet, identityStream >> sessionUUID >> faceMeshURL >> skeletonURL >> attachmentData >> displayName; // mesh URL for a UUID, find avatar in our list - AvatarSharedPointer avatar = _avatarHash.value(sessionUUID); - if (!avatar) { - avatar = addAvatar(sessionUUID, sendingNode); - } + auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); + if (avatar->getFaceModelURL() != faceMeshURL) { avatar->setFaceModelURL(faceMeshURL); } @@ -122,10 +129,7 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer packet, void AvatarHashMap::processAvatarBillboardPacket(QSharedPointer packet, SharedNodePointer sendingNode) { QUuid sessionUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - AvatarSharedPointer avatar = _avatarHash.value(sessionUUID); - if (!avatar) { - avatar = addAvatar(sessionUUID, sendingNode); - } + auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); QByteArray billboard = packet->read(packet->bytesLeftToRead()); if (avatar->getBillboard() != billboard) { @@ -137,13 +141,22 @@ void AvatarHashMap::processKillAvatar(QSharedPointer packet, SharedNod // read the node id QUuid sessionUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); removeAvatar(sessionUUID); - } void AvatarHashMap::removeAvatar(const QUuid& sessionUUID) { QWriteLocker locker(&_hashLock); - _avatarHash.remove(sessionUUID); - emit avatarRemovedEvent(sessionUUID); + + auto removedAvatar = _avatarHash.take(sessionUUID); + + if (removedAvatar) { + handleRemovedAvatar(removedAvatar); + } +} + +void AvatarHashMap::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar) { + qDebug() << "Removed avatar with UUID" << uuidStringWithoutCurlyBraces(removedAvatar->getSessionUUID()) + << "from AvatarHashMap"; + emit avatarRemovedEvent(removedAvatar->getSessionUUID()); } void AvatarHashMap::sessionUUIDChanged(const QUuid& sessionUUID, const QUuid& oldUUID) { diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index a125784cd8..0f230a7022 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -54,8 +54,11 @@ protected: AvatarHashMap(); virtual AvatarSharedPointer newSharedAvatar(); - virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); + virtual AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); + virtual AvatarSharedPointer findAvatar(const QUuid& sessionUUID); // uses a QReadLocker on the hashLock virtual void removeAvatar(const QUuid& sessionUUID); + + virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar); AvatarHash _avatarHash; // "Case-based safety": Most access to the _avatarHash is on the same thread. Write access is protected by a write-lock. From b122dad7eab3b28936409c6ef2315eefbfdb6609 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 19 Nov 2015 15:16:23 -0800 Subject: [PATCH 17/99] Add begin and middle textures to InfiniteLine --- examples/libraries/line.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/libraries/line.js b/examples/libraries/line.js index c21bf2f3ad..d5e5c83028 100644 --- a/examples/libraries/line.js +++ b/examples/libraries/line.js @@ -19,10 +19,11 @@ var MAX_LINE_LENGTH = 40; // This must be 2 or greater; var DEFAULT_STROKE_WIDTH = 0.1; var DEFAULT_LIFETIME = 20; var DEFAULT_COLOR = { red: 255, green: 255, blue: 255 }; -var PolyLine = function(position, color, lifetime) { +var PolyLine = function(position, color, lifetime, texture) { this.position = position; this.color = color; this.lifetime = lifetime === undefined ? DEFAULT_LIFETIME : lifetime; + this.texture = texture ? texture : ""; this.points = [ ]; this.strokeWidths = [ @@ -37,7 +38,8 @@ var PolyLine = function(position, color, lifetime) { strokeWidths: this.strokeWidths, dimensions: LINE_DIMENSIONS, color: color, - lifetime: lifetime + lifetime: lifetime, + textures: this.texture }); }; @@ -98,26 +100,29 @@ PolyLine.prototype.destroy = function() { // InfiniteLine -InfiniteLine = function(position, color, lifetime) { +InfiniteLine = function(position, color, lifetime, textureBegin, textureMiddle) { this.position = position; this.color = color; this.lifetime = lifetime === undefined ? DEFAULT_LIFETIME : lifetime; this.lines = []; this.size = 0; + + this.textureBegin = textureBegin ? textureBegin : ""; + this.textureMiddle = textureMiddle ? textureMiddle : ""; }; InfiniteLine.prototype.enqueuePoint = function(position, strokeWidth) { var currentLine; if (this.lines.length == 0) { - currentLine = new PolyLine(position, this.color, this.lifetime); + currentLine = new PolyLine(position, this.color, this.lifetime, this.textureBegin); this.lines.push(currentLine); } else { currentLine = this.lines[this.lines.length - 1]; } if (currentLine.isFull()) { - var newLine = new PolyLine(currentLine.getLastPoint(), this.color, this.lifetime); + var newLine = new PolyLine(currentLine.getLastPoint(), this.color, this.lifetime, this.textureMiddle); newLine.enqueuePoint(currentLine.getLastPoint(), strokeWidth); this.lines.push(newLine); currentLine = newLine; From 9e31614fce4379adf4e5675d54a7e5c40af174fc Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 19 Nov 2015 15:16:46 -0800 Subject: [PATCH 18/99] Fix textures property not being sent for polyline --- libraries/entities/src/EntityItemProperties.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 4f5d256969..78a4f3e8b6 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -432,6 +432,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_POINTS, linePoints); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NORMALS, normals); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_STROKE_WIDTHS, strokeWidths); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures); } // Sitting properties support @@ -1011,6 +1012,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_LINE_POINTS, properties.getLinePoints()); APPEND_ENTITY_PROPERTY(PROP_NORMALS, properties.getNormals()); APPEND_ENTITY_PROPERTY(PROP_STROKE_WIDTHS, properties.getStrokeWidths()); + APPEND_ENTITY_PROPERTY(PROP_TEXTURES, properties.getTextures()); } APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, properties.getMarketplaceID()); @@ -1287,6 +1289,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LINE_POINTS, QVector, setLinePoints); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_NORMALS, QVector, setNormals); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_STROKE_WIDTHS, QVector, setStrokeWidths); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_TEXTURES, QString, setTextures); } READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_MARKETPLACE_ID, QString, setMarketplaceID); From 5140a480a15556bbd7b52ea77810fef74dd6d396 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 19 Nov 2015 15:35:41 -0800 Subject: [PATCH 19/99] fix for recursive write then read lock --- interface/src/avatar/Avatar.cpp | 2 +- interface/src/avatar/AvatarManager.cpp | 32 +++++++++++--------------- interface/src/avatar/AvatarManager.h | 2 +- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index b979334383..bfa32b8223 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1187,7 +1187,7 @@ void Avatar::computeShapeInfo(ShapeInfo& shapeInfo) { // virtual void Avatar::rebuildSkeletonBody() { - DependencyManager::get()->updateAvatarPhysicsShape(getSessionUUID()); + DependencyManager::get()->updateAvatarPhysicsShape(this); } glm::vec3 Avatar::getLeftPalmPosition() { diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index ae4c978877..af5937d3eb 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -328,25 +328,19 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents } } -void AvatarManager::updateAvatarPhysicsShape(const QUuid& id) { - auto avatarData = findAvatar(id); - - if (avatarData) { - auto avatar = std::dynamic_pointer_cast(avatarData); - - AvatarMotionState* motionState = avatar->getMotionState(); - if (motionState) { - motionState->addDirtyFlags(Simulation::DIRTY_SHAPE); - } else { - ShapeInfo shapeInfo; - avatar->computeShapeInfo(shapeInfo); - btCollisionShape* shape = ObjectMotionState::getShapeManager()->getShape(shapeInfo); - if (shape) { - AvatarMotionState* motionState = new AvatarMotionState(avatar.get(), shape); - avatar->setMotionState(motionState); - _motionStatesToAdd.insert(motionState); - _avatarMotionStates.insert(motionState); - } +void AvatarManager::updateAvatarPhysicsShape(Avatar* avatar) { + AvatarMotionState* motionState = avatar->getMotionState(); + if (motionState) { + motionState->addDirtyFlags(Simulation::DIRTY_SHAPE); + } else { + ShapeInfo shapeInfo; + avatar->computeShapeInfo(shapeInfo); + btCollisionShape* shape = ObjectMotionState::getShapeManager()->getShape(shapeInfo); + if (shape) { + AvatarMotionState* motionState = new AvatarMotionState(avatar, shape); + avatar->setMotionState(motionState); + _motionStatesToAdd.insert(motionState); + _avatarMotionStates.insert(motionState); } } } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 71f579abc2..35e37656d8 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -63,7 +63,7 @@ public: void handleOutgoingChanges(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); - void updateAvatarPhysicsShape(const QUuid& id); + void updateAvatarPhysicsShape(Avatar* avatar); public slots: void setShouldShowReceiveStats(bool shouldShowReceiveStats) { _shouldShowReceiveStats = shouldShowReceiveStats; } From 6398a922c656316c001c3990cb71f1f450cb9776 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 19 Nov 2015 15:43:29 -0800 Subject: [PATCH 20/99] leverage COW for AvatarHash --- interface/src/avatar/AvatarManager.cpp | 14 ++++++-------- interface/src/avatar/MyAvatar.cpp | 6 ++---- libraries/avatars/src/AvatarHashMap.cpp | 4 ---- libraries/avatars/src/AvatarHashMap.h | 2 +- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index af5937d3eb..4a69e9010a 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -119,10 +119,10 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { PerformanceTimer perfTimer("otherAvatars"); // simulate avatars - QWriteLocker writeLock(&_hashLock); + auto hashCopy = _avatarHash; - AvatarHash::iterator avatarIterator = _avatarHash.begin(); - while (avatarIterator != _avatarHash.end()) { + AvatarHash::iterator avatarIterator = hashCopy.begin(); + while (avatarIterator != hashCopy.end()) { auto avatar = std::dynamic_pointer_cast(avatarIterator.value()); if (avatar == _myAvatar || !avatar->isInitialized()) { @@ -130,9 +130,8 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { // DO NOT update or fade out uninitialized Avatars ++avatarIterator; } else if (avatar->shouldDie()) { - auto removedAvatar = avatarIterator.value(); - avatarIterator = _avatarHash.erase(avatarIterator); - handleRemovedAvatar(removedAvatar); + removeAvatar(avatarIterator.key()); + ++avatarIterator; } else { avatar->startUpdate(); avatar->simulate(deltaTime); @@ -140,8 +139,6 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { ++avatarIterator; } } - - writeLock.unlock(); // simulate avatar fades simulateAvatarFades(deltaTime); @@ -329,6 +326,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents } void AvatarManager::updateAvatarPhysicsShape(Avatar* avatar) { + qDebug() << "Update physics state called for" << avatar; AvatarMotionState* motionState = avatar->getMotionState(); if (motionState) { motionState->addDirtyFlags(Simulation::DIRTY_SHAPE); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 1c151bcd3f..6521a0c891 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -982,10 +982,8 @@ void MyAvatar::updateLookAtTargetAvatar() { const float KEEP_LOOKING_AT_CURRENT_ANGLE_FACTOR = 1.3f; const float GREATEST_LOOKING_AT_DISTANCE = 10.0f; - AvatarHash hash; - DependencyManager::get()->withAvatarHash([&] (const AvatarHash& locked) { - hash = locked; // make a shallow copy and operate on that, to minimize lock time - }); + AvatarHash hash = DependencyManager::get()->getCopy(); + foreach (const AvatarSharedPointer& avatarPointer, hash) { auto avatar = static_pointer_cast(avatarPointer); bool isCurrentTarget = avatar->getIsLookAtTarget(); diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index cc83ddf678..1da9abacdd 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -22,10 +22,6 @@ AvatarHashMap::AvatarHashMap() { connect(DependencyManager::get().data(), &NodeList::uuidChanged, this, &AvatarHashMap::sessionUUIDChanged); } -void AvatarHashMap::withAvatarHash(std::function callback) { - QReadLocker locker(&_hashLock); - callback(_avatarHash); -} bool AvatarHashMap::isAvatarInRange(const glm::vec3& position, const float range) { QReadLocker locker(&_hashLock); foreach(const AvatarSharedPointer& sharedAvatar, _avatarHash) { diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 0f230a7022..c3c9160f12 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -31,7 +31,7 @@ class AvatarHashMap : public QObject, public Dependency { SINGLETON_DEPENDENCY public: - void withAvatarHash(std::function); + AvatarHash getCopy() { return _avatarHash; } int size() { return _avatarHash.size(); } signals: From e187aaedcbf60fb72f5d475da2e373d63db6ccf6 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 19 Nov 2015 15:56:37 -0800 Subject: [PATCH 21/99] Load recorded clips from URLs, not file paths --- libraries/recording/CMakeLists.txt | 2 +- .../recording/src/recording/ClipCache.cpp | 40 +++++ libraries/recording/src/recording/ClipCache.h | 57 ++++++ .../recording/src/recording/impl/FileClip.cpp | 137 +-------------- .../recording/src/recording/impl/FileClip.h | 27 +-- .../src/recording/impl/PointerClip.cpp | 163 ++++++++++++++++++ .../src/recording/impl/PointerClip.h | 61 +++++++ .../src/RecordingScriptingInterface.cpp | 18 +- .../src/RecordingScriptingInterface.h | 4 +- 9 files changed, 338 insertions(+), 171 deletions(-) create mode 100644 libraries/recording/src/recording/ClipCache.cpp create mode 100644 libraries/recording/src/recording/ClipCache.h create mode 100644 libraries/recording/src/recording/impl/PointerClip.cpp create mode 100644 libraries/recording/src/recording/impl/PointerClip.h diff --git a/libraries/recording/CMakeLists.txt b/libraries/recording/CMakeLists.txt index a0beae4496..b42a4018f8 100644 --- a/libraries/recording/CMakeLists.txt +++ b/libraries/recording/CMakeLists.txt @@ -4,6 +4,6 @@ set(TARGET_NAME recording) setup_hifi_library(Script) # use setup_hifi_library macro to setup our project and link appropriate Qt modules -link_hifi_libraries(shared) +link_hifi_libraries(shared networking) GroupSources("src/recording") diff --git a/libraries/recording/src/recording/ClipCache.cpp b/libraries/recording/src/recording/ClipCache.cpp new file mode 100644 index 0000000000..fb09245bf9 --- /dev/null +++ b/libraries/recording/src/recording/ClipCache.cpp @@ -0,0 +1,40 @@ +// +// Created by Bradley Austin Davis on 2015/11/19 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "ClipCache.h" +#include "impl/PointerClip.h" + +using namespace recording; +NetworkClipLoader::NetworkClipLoader(const QUrl& url, bool delayLoad) + : Resource(url, delayLoad), _clip(std::make_shared(url)) +{ + +} + + +void NetworkClip::init(const QByteArray& clipData) { + _clipData = clipData; + PointerClip::init((uchar*)_clipData.data(), _clipData.size()); +} + +void NetworkClipLoader::downloadFinished(const QByteArray& data) { + _clip->init(data); +} + +ClipCache& ClipCache::instance() { + static ClipCache _instance; + return _instance; +} + +NetworkClipLoaderPointer ClipCache::getClipLoader(const QUrl& url) { + return ResourceCache::getResource(url, QUrl(), false, nullptr).staticCast(); +} + +QSharedPointer ClipCache::createResource(const QUrl& url, const QSharedPointer& fallback, bool delayLoad, const void* extra) { + return QSharedPointer(new NetworkClipLoader(url, delayLoad), &Resource::allReferencesCleared); +} + diff --git a/libraries/recording/src/recording/ClipCache.h b/libraries/recording/src/recording/ClipCache.h new file mode 100644 index 0000000000..c72d45648d --- /dev/null +++ b/libraries/recording/src/recording/ClipCache.h @@ -0,0 +1,57 @@ +// +// Created by Bradley Austin Davis on 2015/11/19 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once +#ifndef hifi_Recording_ClipCache_h +#define hifi_Recording_ClipCache_h + +#include + +#include "Forward.h" +#include "impl/PointerClip.h" + +namespace recording { + +class NetworkClip : public PointerClip { +public: + using Pointer = std::shared_ptr; + + NetworkClip(const QUrl& url) : _url(url) {} + virtual void init(const QByteArray& clipData); + virtual QString getName() const override { return _url.toString(); } + +private: + QByteArray _clipData; + QUrl _url; +}; + +class NetworkClipLoader : public Resource { +public: + NetworkClipLoader(const QUrl& url, bool delayLoad); + virtual void downloadFinished(const QByteArray& data) override; + ClipPointer getClip() { return _clip; } + bool completed() { return _failedToLoad || isLoaded(); } + +private: + const NetworkClip::Pointer _clip; +}; + +using NetworkClipLoaderPointer = QSharedPointer; + +class ClipCache : public ResourceCache { +public: + static ClipCache& instance(); + + NetworkClipLoaderPointer getClipLoader(const QUrl& url); + +protected: + virtual QSharedPointer createResource(const QUrl& url, const QSharedPointer& fallback, bool delayLoad, const void* extra) override; +}; + +} + +#endif diff --git a/libraries/recording/src/recording/impl/FileClip.cpp b/libraries/recording/src/recording/impl/FileClip.cpp index fcc22452e0..ce2705a76c 100644 --- a/libraries/recording/src/recording/impl/FileClip.cpp +++ b/libraries/recording/src/recording/impl/FileClip.cpp @@ -23,63 +23,6 @@ using namespace recording; -static const qint64 MINIMUM_FRAME_SIZE = sizeof(FrameType) + sizeof(Frame::Time) + sizeof(FrameSize); -static const QString FRAME_TYPE_MAP = QStringLiteral("frameTypes"); -static const QString FRAME_COMREPSSION_FLAG = QStringLiteral("compressed"); - -using FrameTranslationMap = QMap; - -FrameTranslationMap parseTranslationMap(const QJsonDocument& doc) { - FrameTranslationMap results; - auto headerObj = doc.object(); - if (headerObj.contains(FRAME_TYPE_MAP)) { - auto frameTypeObj = headerObj[FRAME_TYPE_MAP].toObject(); - auto currentFrameTypes = Frame::getFrameTypes(); - for (auto frameTypeName : frameTypeObj.keys()) { - qDebug() << frameTypeName; - if (!currentFrameTypes.contains(frameTypeName)) { - continue; - } - FrameType currentTypeEnum = currentFrameTypes[frameTypeName]; - FrameType storedTypeEnum = static_cast(frameTypeObj[frameTypeName].toInt()); - results[storedTypeEnum] = currentTypeEnum; - } - } - return results; -} - - -FileFrameHeaderList parseFrameHeaders(uchar* const start, const qint64& size) { - FileFrameHeaderList results; - auto current = start; - auto end = current + size; - // Read all the frame headers - // FIXME move to Frame::readHeader? - while (end - current >= MINIMUM_FRAME_SIZE) { - FileFrameHeader header; - memcpy(&(header.type), current, sizeof(FrameType)); - current += sizeof(FrameType); - memcpy(&(header.timeOffset), current, sizeof(Frame::Time)); - current += sizeof(Frame::Time); - memcpy(&(header.size), current, sizeof(FrameSize)); - current += sizeof(FrameSize); - header.fileOffset = current - start; - if (end - current < header.size) { - current = end; - break; - } - current += header.size; - results.push_back(header); - } - qDebug() << "Parsed source data into " << results.size() << " frames"; -// int i = 0; -// for (const auto& frameHeader : results) { -// qDebug() << "Frame " << i++ << " time " << frameHeader.timeOffset << " Type " << frameHeader.type; -// } - return results; -} - - FileClip::FileClip(const QString& fileName) : _file(fileName) { auto size = _file.size(); qDebug() << "Opening file of size: " << size; @@ -88,58 +31,8 @@ FileClip::FileClip(const QString& fileName) : _file(fileName) { qCWarning(recordingLog) << "Unable to open file " << fileName; return; } - _map = _file.map(0, size, QFile::MapPrivateOption); - if (!_map) { - qCWarning(recordingLog) << "Unable to map file " << fileName; - return; - } - - auto parsedFrameHeaders = parseFrameHeaders(_map, size); - - // Verify that at least one frame exists and that the first frame is a header - if (0 == parsedFrameHeaders.size()) { - qWarning() << "No frames found, invalid file"; - return; - } - - // Grab the file header - { - auto fileHeaderFrameHeader = *parsedFrameHeaders.begin(); - parsedFrameHeaders.pop_front(); - if (fileHeaderFrameHeader.type != Frame::TYPE_HEADER) { - qWarning() << "Missing header frame, invalid file"; - return; - } - - QByteArray fileHeaderData((char*)_map + fileHeaderFrameHeader.fileOffset, fileHeaderFrameHeader.size); - _fileHeader = QJsonDocument::fromBinaryData(fileHeaderData); - } - - // Check for compression - { - _compressed = _fileHeader.object()[FRAME_COMREPSSION_FLAG].toBool(); - } - - // Find the type enum translation map and fix up the frame headers - { - FrameTranslationMap translationMap = parseTranslationMap(_fileHeader); - if (translationMap.empty()) { - qWarning() << "Header missing frame type map, invalid file"; - return; - } - qDebug() << translationMap; - - // Update the loaded headers with the frame data - _frames.reserve(parsedFrameHeaders.size()); - for (auto& frameHeader : parsedFrameHeaders) { - if (!translationMap.contains(frameHeader.type)) { - continue; - } - frameHeader.type = translationMap[frameHeader.type]; - _frames.push_back(frameHeader); - } - } - + auto mappedFile = _file.map(0, size, QFile::MapPrivateOption); + init(mappedFile, size); } @@ -228,31 +121,9 @@ bool FileClip::write(const QString& fileName, Clip::Pointer clip) { FileClip::~FileClip() { Locker lock(_mutex); - _file.unmap(_map); - _map = nullptr; + _file.unmap(_data); if (_file.isOpen()) { _file.close(); } -} - -// Internal only function, needs no locking -FrameConstPointer FileClip::readFrame(size_t frameIndex) const { - FramePointer result; - if (frameIndex < _frames.size()) { - result = std::make_shared(); - const auto& header = _frames[frameIndex]; - result->type = header.type; - result->timeOffset = header.timeOffset; - if (header.size) { - result->data.insert(0, reinterpret_cast(_map)+header.fileOffset, header.size); - if (_compressed) { - result->data = qUncompress(result->data); - } - } - } - return result; -} - -void FileClip::addFrame(FrameConstPointer) { - throw std::runtime_error("File clips are read only"); + reset(); } diff --git a/libraries/recording/src/recording/impl/FileClip.h b/libraries/recording/src/recording/impl/FileClip.h index f103a9aca6..71ae414721 100644 --- a/libraries/recording/src/recording/impl/FileClip.h +++ b/libraries/recording/src/recording/impl/FileClip.h @@ -10,27 +10,13 @@ #ifndef hifi_Recording_Impl_FileClip_h #define hifi_Recording_Impl_FileClip_h -#include "ArrayClip.h" - -#include +#include "PointerClip.h" #include -#include - -#include "../Frame.h" namespace recording { -struct FileFrameHeader : public FrameHeader { - FrameType type; - Frame::Time timeOffset; - uint16_t size; - quint64 fileOffset; -}; - -using FileFrameHeaderList = std::list; - -class FileClip : public ArrayClip { +class FileClip : public PointerClip { public: using Pointer = std::shared_ptr; @@ -38,20 +24,11 @@ public: virtual ~FileClip(); virtual QString getName() const override; - virtual void addFrame(FrameConstPointer) override; - - const QJsonDocument& getHeader() { - return _fileHeader; - } static bool write(const QString& filePath, Clip::Pointer clip); private: - virtual FrameConstPointer readFrame(size_t index) const override; - QJsonDocument _fileHeader; QFile _file; - uchar* _map { nullptr }; - bool _compressed { true }; }; } diff --git a/libraries/recording/src/recording/impl/PointerClip.cpp b/libraries/recording/src/recording/impl/PointerClip.cpp new file mode 100644 index 0000000000..48132c066d --- /dev/null +++ b/libraries/recording/src/recording/impl/PointerClip.cpp @@ -0,0 +1,163 @@ +// +// Created by Bradley Austin Davis 2015/11/04 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "PointerClip.h" + +#include + +#include +#include +#include + +#include + +#include "../Frame.h" +#include "../Logging.h" +#include "BufferClip.h" + + +using namespace recording; + +const QString PointerClip::FRAME_TYPE_MAP = QStringLiteral("frameTypes"); +const QString PointerClip::FRAME_COMREPSSION_FLAG = QStringLiteral("compressed"); + +using FrameTranslationMap = QMap; + +FrameTranslationMap parseTranslationMap(const QJsonDocument& doc) { + FrameTranslationMap results; + auto headerObj = doc.object(); + if (headerObj.contains(PointerClip::FRAME_TYPE_MAP)) { + auto frameTypeObj = headerObj[PointerClip::FRAME_TYPE_MAP].toObject(); + auto currentFrameTypes = Frame::getFrameTypes(); + for (auto frameTypeName : frameTypeObj.keys()) { + qDebug() << frameTypeName; + if (!currentFrameTypes.contains(frameTypeName)) { + continue; + } + FrameType currentTypeEnum = currentFrameTypes[frameTypeName]; + FrameType storedTypeEnum = static_cast(frameTypeObj[frameTypeName].toInt()); + results[storedTypeEnum] = currentTypeEnum; + } + } + return results; +} + + +PointerFrameHeaderList parseFrameHeaders(uchar* const start, const size_t& size) { + PointerFrameHeaderList results; + auto current = start; + auto end = current + size; + // Read all the frame headers + // FIXME move to Frame::readHeader? + while (end - current >= PointerClip::MINIMUM_FRAME_SIZE) { + PointerFrameHeader header; + memcpy(&(header.type), current, sizeof(FrameType)); + current += sizeof(FrameType); + memcpy(&(header.timeOffset), current, sizeof(Frame::Time)); + current += sizeof(Frame::Time); + memcpy(&(header.size), current, sizeof(FrameSize)); + current += sizeof(FrameSize); + header.fileOffset = current - start; + if (end - current < header.size) { + current = end; + break; + } + current += header.size; + results.push_back(header); + } + qDebug() << "Parsed source data into " << results.size() << " frames"; +// int i = 0; +// for (const auto& frameHeader : results) { +// qDebug() << "Frame " << i++ << " time " << frameHeader.timeOffset << " Type " << frameHeader.type; +// } + return results; +} + +void PointerClip::reset() { + _frames.clear(); + _data = nullptr; + _size = 0; + _header = QJsonDocument(); +} + +void PointerClip::init(uchar* data, size_t size) { + reset(); + + _data = data; + _size = size; + + auto parsedFrameHeaders = parseFrameHeaders(data, size); + // Verify that at least one frame exists and that the first frame is a header + if (0 == parsedFrameHeaders.size()) { + qWarning() << "No frames found, invalid file"; + reset(); + return; + } + + // Grab the file header + { + auto fileHeaderFrameHeader = *parsedFrameHeaders.begin(); + parsedFrameHeaders.pop_front(); + if (fileHeaderFrameHeader.type != Frame::TYPE_HEADER) { + qWarning() << "Missing header frame, invalid file"; + reset(); + return; + } + + QByteArray fileHeaderData((char*)_data + fileHeaderFrameHeader.fileOffset, fileHeaderFrameHeader.size); + _header = QJsonDocument::fromBinaryData(fileHeaderData); + } + + // Check for compression + { + _compressed = _header.object()[FRAME_COMREPSSION_FLAG].toBool(); + } + + // Find the type enum translation map and fix up the frame headers + { + FrameTranslationMap translationMap = parseTranslationMap(_header); + if (translationMap.empty()) { + qWarning() << "Header missing frame type map, invalid file"; + reset(); + return; + } + + // Update the loaded headers with the frame data + _frames.reserve(parsedFrameHeaders.size()); + for (auto& frameHeader : parsedFrameHeaders) { + if (!translationMap.contains(frameHeader.type)) { + continue; + } + frameHeader.type = translationMap[frameHeader.type]; + _frames.push_back(frameHeader); + } + } + +} + +// Internal only function, needs no locking +FrameConstPointer PointerClip::readFrame(size_t frameIndex) const { + FramePointer result; + if (frameIndex < _frames.size()) { + result = std::make_shared(); + const auto& header = _frames[frameIndex]; + result->type = header.type; + result->timeOffset = header.timeOffset; + if (header.size) { + result->data.insert(0, reinterpret_cast(_data)+header.fileOffset, header.size); + if (_compressed) { + result->data = qUncompress(result->data); + } + } + } + return result; +} + +void PointerClip::addFrame(FrameConstPointer) { + throw std::runtime_error("Pointer clips are read only, use duplicate to create a read/write clip"); +} diff --git a/libraries/recording/src/recording/impl/PointerClip.h b/libraries/recording/src/recording/impl/PointerClip.h new file mode 100644 index 0000000000..5a7a3499fe --- /dev/null +++ b/libraries/recording/src/recording/impl/PointerClip.h @@ -0,0 +1,61 @@ +// +// Created by Bradley Austin Davis 2015/11/05 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_Recording_Impl_PointerClip_h +#define hifi_Recording_Impl_PointerClip_h + +#include "ArrayClip.h" + +#include + +#include + +#include "../Frame.h" + +namespace recording { + +struct PointerFrameHeader : public FrameHeader { + FrameType type; + Frame::Time timeOffset; + uint16_t size; + quint64 fileOffset; +}; + +using PointerFrameHeaderList = std::list; + +class PointerClip : public ArrayClip { +public: + using Pointer = std::shared_ptr; + + PointerClip() {}; + PointerClip(uchar* data, size_t size) { init(data, size); } + + void init(uchar* data, size_t size); + virtual void addFrame(FrameConstPointer) override; + const QJsonDocument& getHeader() const { + return _header; + } + + // FIXME move to frame? + static const qint64 MINIMUM_FRAME_SIZE = sizeof(FrameType) + sizeof(Frame::Time) + sizeof(FrameSize); + static const QString FRAME_TYPE_MAP; + static const QString FRAME_COMREPSSION_FLAG; + +protected: + void reset(); + virtual FrameConstPointer readFrame(size_t index) const override; + QJsonDocument _header; + uchar* _data { nullptr }; + size_t _size { 0 }; + bool _compressed { true }; +}; + +} + +#endif diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index 33e67e1b43..d82d471d79 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -8,14 +8,15 @@ #include "RecordingScriptingInterface.h" -#include +#include +#include +#include #include #include #include #include -#include -#include +#include #include "ScriptEngineLogging.h" @@ -43,20 +44,17 @@ float RecordingScriptingInterface::playerLength() const { return _player->length(); } -void RecordingScriptingInterface::loadRecording(const QString& filename) { +void RecordingScriptingInterface::loadRecording(const QString& url) { using namespace recording; if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "loadRecording", Qt::BlockingQueuedConnection, - Q_ARG(QString, filename)); + Q_ARG(QString, url)); return; } - ClipPointer clip = Clip::fromFile(filename); - if (!clip) { - qWarning() << "Unable to load clip data from " << filename; - } - _player->queueClip(clip); + // FIXME make blocking and force off main thread? + _player->queueClip(ClipCache::instance().getClipLoader(url)->getClip()); } void RecordingScriptingInterface::startPlaying() { diff --git a/libraries/script-engine/src/RecordingScriptingInterface.h b/libraries/script-engine/src/RecordingScriptingInterface.h index 483ead3ca3..3834089177 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.h +++ b/libraries/script-engine/src/RecordingScriptingInterface.h @@ -12,7 +12,7 @@ #include #include -#include +#include #include #include @@ -25,7 +25,7 @@ public: RecordingScriptingInterface(); public slots: - void loadRecording(const QString& filename); + void loadRecording(const QString& url); void startPlaying(); void pausePlayer(); From 40397add433f25c0dfa4baf39b711adf2dd673bc Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 19 Nov 2015 16:15:21 -0800 Subject: [PATCH 22/99] fix for extra adds in AvatarManager --- interface/src/avatar/AvatarManager.cpp | 24 +++++++++++------------- interface/src/avatar/AvatarManager.h | 2 +- libraries/avatars/src/AvatarHashMap.cpp | 22 ++++++++++++++-------- libraries/avatars/src/AvatarHashMap.h | 3 ++- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 4a69e9010a..a64263aec2 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -123,7 +123,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { AvatarHash::iterator avatarIterator = hashCopy.begin(); while (avatarIterator != hashCopy.end()) { - auto avatar = std::dynamic_pointer_cast(avatarIterator.value()); + auto avatar = std::static_pointer_cast(avatarIterator.value()); if (avatar == _myAvatar || !avatar->isInitialized()) { // DO NOT update _myAvatar! Its update has already been done earlier in the main loop. @@ -169,19 +169,21 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { } AvatarSharedPointer AvatarManager::newSharedAvatar() { - return AvatarSharedPointer(std::make_shared(std::make_shared())); + return std::make_shared(std::make_shared()); } -// virtual -AvatarSharedPointer AvatarManager::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { - auto avatar = std::dynamic_pointer_cast(AvatarHashMap::newOrExistingAvatar(sessionUUID, mixerWeakPointer)); +AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { + auto newAvatar = AvatarHashMap::addAvatar(sessionUUID, mixerWeakPointer); + auto rawRenderableAvatar = std::static_pointer_cast(newAvatar); + render::ScenePointer scene = qApp->getMain3DScene(); render::PendingChanges pendingChanges; if (DependencyManager::get()->shouldRenderAvatars()) { - avatar->addToScene(avatar, scene, pendingChanges); + rawRenderableAvatar->addToScene(rawRenderableAvatar, scene, pendingChanges); } scene->enqueuePendingChanges(pendingChanges); - return avatar; + + return newAvatar; } // protected @@ -209,9 +211,6 @@ void AvatarManager::removeAvatar(const QUuid& sessionUUID) { } void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar) { - qDebug() << "Removed avatar with UUID" << uuidStringWithoutCurlyBraces(removedAvatar->getSessionUUID()) - << "from AvatarManager"; - AvatarHashMap::handleRemovedAvatar(removedAvatar); removeAvatarMotionState(removedAvatar); @@ -326,7 +325,6 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents } void AvatarManager::updateAvatarPhysicsShape(Avatar* avatar) { - qDebug() << "Update physics state called for" << avatar; AvatarMotionState* motionState = avatar->getMotionState(); if (motionState) { motionState->addDirtyFlags(Simulation::DIRTY_SHAPE); @@ -346,7 +344,7 @@ void AvatarManager::updateAvatarPhysicsShape(Avatar* avatar) { void AvatarManager::updateAvatarRenderStatus(bool shouldRenderAvatars) { if (DependencyManager::get()->shouldRenderAvatars()) { for (auto avatarData : _avatarHash) { - auto avatar = std::dynamic_pointer_cast(avatarData); + auto avatar = std::static_pointer_cast(avatarData); render::ScenePointer scene = qApp->getMain3DScene(); render::PendingChanges pendingChanges; avatar->addToScene(avatar, scene, pendingChanges); @@ -354,7 +352,7 @@ void AvatarManager::updateAvatarRenderStatus(bool shouldRenderAvatars) { } } else { for (auto avatarData : _avatarHash) { - auto avatar = std::dynamic_pointer_cast(avatarData); + auto avatar = std::static_pointer_cast(avatarData); render::ScenePointer scene = qApp->getMain3DScene(); render::PendingChanges pendingChanges; avatar->removeFromScene(avatar, scene, pendingChanges); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 35e37656d8..f911dacc4d 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -77,7 +77,7 @@ private: // virtual overrides virtual AvatarSharedPointer newSharedAvatar(); - virtual AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); + virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); void removeAvatarMotionState(AvatarSharedPointer avatar); virtual void removeAvatar(const QUuid& sessionUUID); diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 1da9abacdd..8f2ea82373 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -38,20 +38,26 @@ AvatarSharedPointer AvatarHashMap::newSharedAvatar() { return std::make_shared(); } +AvatarSharedPointer AvatarHashMap::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { + qCDebug(avatars) << "Adding avatar with sessionUUID " << sessionUUID << "to AvatarHashMap."; + + auto avatar = newSharedAvatar(); + avatar->setSessionUUID(sessionUUID); + avatar->setOwningAvatarMixer(mixerWeakPointer); + + _avatarHash.insert(sessionUUID, avatar); + emit avatarAddedEvent(sessionUUID); + + return avatar; +} + AvatarSharedPointer AvatarHashMap::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { QWriteLocker locker(&_hashLock); auto avatar = _avatarHash.value(sessionUUID); if (!avatar) { - qCDebug(avatars) << "Adding avatar with sessionUUID " << sessionUUID << "to AvatarHashMap."; - - avatar = newSharedAvatar(); - avatar->setSessionUUID(sessionUUID); - avatar->setOwningAvatarMixer(mixerWeakPointer); - - _avatarHash.insert(sessionUUID, avatar); - emit avatarAddedEvent(sessionUUID); + avatar = addAvatar(sessionUUID, mixerWeakPointer); } return avatar; diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index c3c9160f12..c2fecf5bd9 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -54,7 +54,8 @@ protected: AvatarHashMap(); virtual AvatarSharedPointer newSharedAvatar(); - virtual AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); + virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); + AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); virtual AvatarSharedPointer findAvatar(const QUuid& sessionUUID); // uses a QReadLocker on the hashLock virtual void removeAvatar(const QUuid& sessionUUID); From 3f89d73fc651c5553ceb6956912f03b013517080 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 19 Nov 2015 16:15:34 -0800 Subject: [PATCH 23/99] add grasp to equip and example --- examples/controllers/handControllerGrab.js | 237 +++++++++++++------ examples/example/avatarcontrol/graspHands.js | 68 ++++++ 2 files changed, 239 insertions(+), 66 deletions(-) create mode 100644 examples/example/avatarcontrol/graspHands.js diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index dd3a9a4b96..abacb819ff 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -37,9 +37,21 @@ var BUMPER_ON_VALUE = 0.5; var DISTANCE_HOLDING_RADIUS_FACTOR = 5; // multiplied by distance between hand and object var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did -var NO_INTERSECT_COLOR = { red: 10, green: 10, blue: 255}; // line color when pick misses -var INTERSECT_COLOR = { red: 250, green: 10, blue: 10}; // line color when pick hits -var LINE_ENTITY_DIMENSIONS = { x: 1000, y: 1000,z: 1000}; +var NO_INTERSECT_COLOR = { + red: 10, + green: 10, + blue: 255 +}; // line color when pick misses +var INTERSECT_COLOR = { + red: 250, + green: 10, + blue: 10 +}; // line color when pick hits +var LINE_ENTITY_DIMENSIONS = { + x: 1000, + y: 1000, + z: 1000 +}; var LINE_LENGTH = 500; var PICK_MAX_DISTANCE = 500; // max length of pick-ray @@ -84,12 +96,13 @@ var ACTION_TTL_REFRESH = 5; var PICKS_PER_SECOND_PER_HAND = 5; var MSECS_PER_SEC = 1000.0; var GRABBABLE_PROPERTIES = ["position", - "rotation", - "gravity", - "ignoreForCollisions", - "collisionsWillMove", - "locked", - "name"]; + "rotation", + "gravity", + "ignoreForCollisions", + "collisionsWillMove", + "locked", + "name" +]; var GRABBABLE_DATA_KEY = "grabbableKey"; // shared with grab.js @@ -100,7 +113,7 @@ var DEFAULT_GRABBABLE_DATA = { invertSolidWhileHeld: false }; -var disabledHand ='none'; +var disabledHand = 'none'; // states for the state machine @@ -125,40 +138,40 @@ var STATE_EQUIP_SPRING = 16; function stateToName(state) { switch (state) { - case STATE_OFF: - return "off"; - case STATE_SEARCHING: - return "searching"; - case STATE_DISTANCE_HOLDING: - return "distance_holding"; - case STATE_CONTINUE_DISTANCE_HOLDING: - return "continue_distance_holding"; - case STATE_NEAR_GRABBING: - return "near_grabbing"; - case STATE_CONTINUE_NEAR_GRABBING: - return "continue_near_grabbing"; - case STATE_NEAR_TRIGGER: - return "near_trigger"; - case STATE_CONTINUE_NEAR_TRIGGER: - return "continue_near_trigger"; - case STATE_FAR_TRIGGER: - return "far_trigger"; - case STATE_CONTINUE_FAR_TRIGGER: - return "continue_far_trigger"; - case STATE_RELEASE: - return "release"; - case STATE_EQUIP_SEARCHING: - return "equip_searching"; - case STATE_EQUIP: - return "equip"; - case STATE_CONTINUE_EQUIP_BD: - return "continue_equip_bd"; - case STATE_CONTINUE_EQUIP: - return "continue_equip"; - case STATE_WAITING_FOR_BUMPER_RELEASE: - return "waiting_for_bumper_release"; - case STATE_EQUIP_SPRING: - return "state_equip_spring"; + case STATE_OFF: + return "off"; + case STATE_SEARCHING: + return "searching"; + case STATE_DISTANCE_HOLDING: + return "distance_holding"; + case STATE_CONTINUE_DISTANCE_HOLDING: + return "continue_distance_holding"; + case STATE_NEAR_GRABBING: + return "near_grabbing"; + case STATE_CONTINUE_NEAR_GRABBING: + return "continue_near_grabbing"; + case STATE_NEAR_TRIGGER: + return "near_trigger"; + case STATE_CONTINUE_NEAR_TRIGGER: + return "continue_near_trigger"; + case STATE_FAR_TRIGGER: + return "far_trigger"; + case STATE_CONTINUE_FAR_TRIGGER: + return "continue_far_trigger"; + case STATE_RELEASE: + return "release"; + case STATE_EQUIP_SEARCHING: + return "equip_searching"; + case STATE_EQUIP: + return "equip"; + case STATE_CONTINUE_EQUIP_BD: + return "continue_equip_bd"; + case STATE_CONTINUE_EQUIP: + return "continue_equip"; + case STATE_WAITING_FOR_BUMPER_RELEASE: + return "waiting_for_bumper_release"; + case STATE_EQUIP_SPRING: + return "state_equip_spring"; } return "unknown"; @@ -188,6 +201,12 @@ function entityIsGrabbedByOther(entityID) { } + +//make sure to clean this up when the script ends so we don't get stuck. +Script.scriptEnding.connect(function() { + MyController.endHandGrasp(); +}) + function MyController(hand) { this.hand = hand; if (this.hand === RIGHT_HAND) { @@ -211,8 +230,17 @@ function MyController(hand) { this.rawTriggerValue = 0; this.rawBumperValue = 0; - this.offsetPosition = { x: 0.0, y: 0.0, z: 0.0 }; - this.offsetRotation = { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }; + this.offsetPosition = { + x: 0.0, + y: 0.0, + z: 0.0 + }; + this.offsetRotation = { + x: 0.0, + y: 0.0, + z: 0.0, + w: 1.0 + }; var _this = this; @@ -277,7 +305,7 @@ function MyController(hand) { this.state = newState; } - this.debugLine = function(closePoint, farPoint, color){ + this.debugLine = function(closePoint, farPoint, color) { Entities.addEntity({ type: "Line", name: "Grab Debug Entity", @@ -321,16 +349,16 @@ function MyController(hand) { this.pointer = null; }; - this.triggerPress = function (value) { + this.triggerPress = function(value) { _this.rawTriggerValue = value; }; - this.bumperPress = function (value) { + this.bumperPress = function(value) { _this.rawBumperValue = value; }; - this.updateSmoothedTrigger = function () { + this.updateSmoothedTrigger = function() { var triggerValue = this.rawTriggerValue; // smooth out trigger value this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) + @@ -401,7 +429,7 @@ function MyController(hand) { this.lastPickTime = now; } - for (var index=0; index < pickRays.length; ++index) { + for (var index = 0; index < pickRays.length; ++index) { var pickRay = pickRays[index]; var directionNormalized = Vec3.normalize(pickRay.direction); var directionBacked = Vec3.multiply(directionNormalized, PICK_BACKOFF_DISTANCE); @@ -466,10 +494,9 @@ function MyController(hand) { } return; } - } else if (! entityIsGrabbedByOther(intersection.entityID)) { + } else if (!entityIsGrabbedByOther(intersection.entityID)) { // don't allow two people to distance grab the same object - if (intersection.properties.collisionsWillMove - && !intersection.properties.locked) { + if (intersection.properties.collisionsWillMove && !intersection.properties.locked) { // the hand is far from the intersected object. go into distance-holding mode this.grabbedEntity = intersection.entityID; if (typeof grabbableData.spatialKey !== 'undefined' && this.state == STATE_EQUIP_SEARCHING) { @@ -494,10 +521,18 @@ function MyController(hand) { Entities.addEntity({ type: "Sphere", name: "Grab Debug Entity", - dimensions: {x: GRAB_RADIUS, y: GRAB_RADIUS, z: GRAB_RADIUS}, + dimensions: { + x: GRAB_RADIUS, + y: GRAB_RADIUS, + z: GRAB_RADIUS + }, visible: true, position: handPosition, - color: { red: 0, green: 255, blue: 0}, + color: { + red: 0, + green: 255, + blue: 0 + }, lifetime: 0.1 }); } @@ -604,6 +639,7 @@ function MyController(hand) { } else { Entities.callEntityMethod(this.grabbedEntity, "setLeftHand"); } + Entities.callEntityMethod(this.grabbedEntity, "setHand", [this.hand]); Entities.callEntityMethod(this.grabbedEntity, "startDistantGrab"); } @@ -639,7 +675,7 @@ function MyController(hand) { // the action was set up on a previous call. update the targets. var radius = Math.max(Vec3.distance(this.currentObjectPosition, handControllerPosition) * - DISTANCE_HOLDING_RADIUS_FACTOR, DISTANCE_HOLDING_RADIUS_FACTOR); + DISTANCE_HOLDING_RADIUS_FACTOR, DISTANCE_HOLDING_RADIUS_FACTOR); // how far did avatar move this timestep? var currentPosition = MyAvatar.position; var avatarDeltaPosition = Vec3.subtract(currentPosition, this.currentAvatarPosition); @@ -688,9 +724,9 @@ function MyController(hand) { // this doubles hand rotation var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, - handRotation, - DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), - Quat.inverse(this.handPreviousRotation)); + handRotation, + DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), + Quat.inverse(this.handPreviousRotation)); this.handPreviousRotation = handRotation; this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); @@ -773,6 +809,8 @@ function MyController(hand) { this.setState(STATE_CONTINUE_NEAR_GRABBING); } else { // equipping + Entities.callEntityMethod(this.grabbedEntity, "startEquip", [JSON.stringify(this.hand)]); + this.startHandGrasp(); this.setState(STATE_CONTINUE_EQUIP_BD); } @@ -781,6 +819,9 @@ function MyController(hand) { } else { Entities.callEntityMethod(this.grabbedEntity, "setLeftHand"); } + + Entities.callEntityMethod(this.grabbedEntity, "setHand", [this.hand]); + Entities.callEntityMethod(this.grabbedEntity, "startNearGrab"); } @@ -807,6 +848,7 @@ function MyController(hand) { } if (this.state == STATE_CONTINUE_NEAR_GRABBING && this.bumperSqueezed()) { this.setState(STATE_CONTINUE_EQUIP_BD); + Entities.callEntityMethod(this.grabbedEntity, "startEquip", [JSON.stringify(this.hand)]); return; } @@ -827,6 +869,10 @@ function MyController(hand) { this.currentObjectTime = now; Entities.callEntityMethod(this.grabbedEntity, "continueNearGrab"); + if (this.state === STATE_CONTINUE_EQUIP_BD) { + Entities.callEntityMethod(this.grabbedEntity, "continueEquip"); + } + if (this.actionTimeout - now < ACTION_TTL_REFRESH * MSEC_PER_SEC) { // if less than a 5 seconds left, refresh the actions ttl Entities.updateAction(this.grabbedEntity, this.actionID, { @@ -846,6 +892,8 @@ function MyController(hand) { if (this.bumperReleased()) { this.setState(STATE_RELEASE); Entities.callEntityMethod(this.grabbedEntity, "releaseGrab"); + Entities.callEntityMethod(this.grabbedEntity, "unequip"); + this.endHandGrasp(); } }; @@ -856,8 +904,17 @@ function MyController(hand) { var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); // use a spring to pull the object to where it will be when equipped - var relativeRotation = { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }; - var relativePosition = { x: 0.0, y: 0.0, z: 0.0 }; + var relativeRotation = { + x: 0.0, + y: 0.0, + z: 0.0, + w: 1.0 + }; + var relativePosition = { + x: 0.0, + y: 0.0, + z: 0.0 + }; if (grabbableData.spatialKey.relativePosition) { relativePosition = grabbableData.spatialKey.relativePosition; } @@ -913,6 +970,9 @@ function MyController(hand) { } else { Entities.callEntityMethod(this.grabbedEntity, "setLeftHand"); } + + Entities.callEntityMethod(this.grabbedEntity, "setHand", [this.hand]); + Entities.callEntityMethod(this.grabbedEntity, "startNearTrigger"); this.setState(STATE_CONTINUE_NEAR_TRIGGER); }; @@ -929,6 +989,7 @@ function MyController(hand) { } else { Entities.callEntityMethod(this.grabbedEntity, "setLeftHand"); } + Entities.callEntityMethod(this.grabbedEntity, "setHand", [this.hand]); Entities.callEntityMethod(this.grabbedEntity, "startFarTrigger"); this.setState(STATE_CONTINUE_FAR_TRIGGER); }; @@ -1040,7 +1101,7 @@ function MyController(hand) { this.release = function() { - if(this.hand !== disabledHand){ + if (this.hand !== disabledHand) { //release the disabled hand when we let go with the main one disabledHand = 'none'; } @@ -1075,9 +1136,15 @@ function MyController(hand) { data["gravity"] = grabbedProperties.gravity; data["ignoreForCollisions"] = grabbedProperties.ignoreForCollisions; data["collisionsWillMove"] = grabbedProperties.collisionsWillMove; - var whileHeldProperties = {gravity: {x:0, y:0, z:0}}; + var whileHeldProperties = { + gravity: { + x: 0, + y: 0, + z: 0 + } + }; if (invertSolidWhileHeld) { - whileHeldProperties["ignoreForCollisions"] = ! grabbedProperties.ignoreForCollisions; + whileHeldProperties["ignoreForCollisions"] = !grabbedProperties.ignoreForCollisions; } Entities.editEntity(entityID, whileHeldProperties); } @@ -1103,6 +1170,44 @@ function MyController(hand) { } setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); }; + + + //this is our handler, where we do the actual work of changing animation settings + this.graspHand = function(animationProperties) { + var result = {}; + //full alpha on overlay for this hand + //set grab to true + //set idle to false + //full alpha on the blend btw open and grab + if (_this.hand === RIGHT_HAND) { + result['rightHandOverlayAlpha'] = 1.0; + result['isRightHandGrab'] = true; + result['isRightHandIdle'] = false; + result['rightHandGrabBlend'] = 1.0; + } else if (_this.hand === LEFT_HAND) { + result['leftHandOverlayAlpha'] = 1.0; + result['isLeftHandGrab'] = true; + result['isLeftHandIdle'] = false; + result['leftHandGrabBlend'] = 1.0; + } + //return an object with our updated settings + return result; + } + + this.graspHandler = null + this.startHandGrasp = function() { + if (this.hand === RIGHT_HAND) { + this.graspHandler = MyAvatar.addAnimationStateHandler(this.graspHand, ['isRightHandGrab']); + } else if (this.hand === LEFT_HAND) { + this.graspHandler = MyAvatar.addAnimationStateHandler(this.graspHand, ['isLeftHandGrab']); + } + } + + this.endHandGrasp = function() { + // Tell the animation system we don't need any more callbacks. + MyAvatar.removeAnimationStateHandler(this.graspHandler); + } + } var rightController = new MyController(RIGHT_HAND); @@ -1132,4 +1237,4 @@ function cleanup() { } Script.scriptEnding.connect(cleanup); -Script.update.connect(update); +Script.update.connect(update); \ No newline at end of file diff --git a/examples/example/avatarcontrol/graspHands.js b/examples/example/avatarcontrol/graspHands.js new file mode 100644 index 0000000000..266ffe6866 --- /dev/null +++ b/examples/example/avatarcontrol/graspHands.js @@ -0,0 +1,68 @@ +// graspHands.js +// +// Created by James B. Pollack @imgntn -- 11/19/2015 +// Copyright 2015 High Fidelity, Inc. +// +// Shows how to use the animation API to grasp an Avatar's hands. +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +//choose a hand. set it programatically if you'd like +var handToGrasp = 'LEFT_HAND'; + +//this is our handler, where we do the actual work of changing animation settings +function graspHand(animationProperties) { + var result = {}; + //full alpha on overlay for this hand + //set grab to true + //set idle to false + //full alpha on the blend btw open and grab + if (handToGrasp === 'RIGHT_HAND') { + result['rightHandOverlayAlpha'] = 1.0; + result['isRightHandGrab'] = true; + result['isRightHandIdle'] = false; + result['rightHandGrabBlend'] = 1.0; + } else if (handToGrasp === 'LEFT_HAND') { + result['leftHandOverlayAlpha'] = 1.0; + result['isLeftHandGrab'] = true; + result['isLeftHandIdle'] = false; + result['leftHandGrabBlend'] = 1.0; + } + //return an object with our updated settings + return result; +} + +//keep a reference to this so we can clear it +var handler; + +//register our handler with the animation system +function startHandGrasp() { + if (handToGrasp === 'RIGHT_HAND') { + handler = MyAvatar.addAnimationStateHandler(graspHand, ['isRightHandGrab']); + } else if (handToGrasp === 'LEFT_HAND') { + handler = MyAvatar.addAnimationStateHandler(graspHand, ['isLeftHandGrab']); + } +} + +function endHandGrasp() { + // Tell the animation system we don't need any more callbacks. + MyAvatar.removeAnimationStateHandler(handler); +} + +//make sure to clean this up when the script ends so we don't get stuck. +Script.scriptEnding.connect(function() { + Script.clearInterval(graspInterval); + endHandGrasp(); +}) + +//set an interval and toggle grasping +var isGrasping = false; +var graspInterval = Script.setInterval(function() { + if (isGrasping === false) { + startHandGrasp(); + isGrasping = true; + } else { + endHandGrasp(); + isGrasping = false + } +}, 1000) \ No newline at end of file From 6f76f0cbc04e2a4ba36a361b8ebf43ba779c9ba2 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 19 Nov 2015 16:22:54 -0800 Subject: [PATCH 24/99] release grasp at cleanup --- examples/controllers/handControllerGrab.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index abacb819ff..f8a2eeefa5 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -200,13 +200,6 @@ function entityIsGrabbedByOther(entityID) { return false; } - - -//make sure to clean this up when the script ends so we don't get stuck. -Script.scriptEnding.connect(function() { - MyController.endHandGrasp(); -}) - function MyController(hand) { this.hand = hand; if (this.hand === RIGHT_HAND) { @@ -1122,6 +1115,7 @@ function MyController(hand) { this.cleanup = function() { this.release(); + this.endHandGrasp(); }; this.activateEntity = function(entityID, grabbedProperties) { From 430cb7876dd99cfb7832620b2da65e40f0bf004f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 19 Nov 2015 16:22:56 -0800 Subject: [PATCH 25/99] remove a couple of read lockers for AvatarHashMap --- interface/src/avatar/AvatarManager.cpp | 1 - libraries/avatars/src/AvatarHashMap.cpp | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index a64263aec2..bd45561b38 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -256,7 +256,6 @@ QVector AvatarManager::getLocalLights() const { } QVector AvatarManager::getAvatarIdentifiers() { - QReadLocker locker(&_hashLock); return _avatarHash.keys().toVector(); } AvatarData* AvatarManager::getAvatar(QUuid avatarID) { diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 8f2ea82373..035d29f344 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -23,8 +23,8 @@ AvatarHashMap::AvatarHashMap() { } bool AvatarHashMap::isAvatarInRange(const glm::vec3& position, const float range) { - QReadLocker locker(&_hashLock); - foreach(const AvatarSharedPointer& sharedAvatar, _avatarHash) { + auto hashCopy = _avatarHash; + foreach(const AvatarSharedPointer& sharedAvatar, hashCopy) { glm::vec3 avatarPosition = sharedAvatar->getPosition(); float distance = glm::distance(avatarPosition, position); if (distance < range) { From 22f5d4df6dece4e1c9f3c42d1cdced28e4b2dfa9 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 19 Nov 2015 16:25:33 -0800 Subject: [PATCH 26/99] change signature of copy return --- interface/src/avatar/MyAvatar.cpp | 2 +- libraries/avatars/src/AvatarHashMap.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 6521a0c891..a2102bd010 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -982,7 +982,7 @@ void MyAvatar::updateLookAtTargetAvatar() { const float KEEP_LOOKING_AT_CURRENT_ANGLE_FACTOR = 1.3f; const float GREATEST_LOOKING_AT_DISTANCE = 10.0f; - AvatarHash hash = DependencyManager::get()->getCopy(); + AvatarHash hash = DependencyManager::get()->getHashCopy(); foreach (const AvatarSharedPointer& avatarPointer, hash) { auto avatar = static_pointer_cast(avatarPointer); diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index c2fecf5bd9..7795072ec2 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -31,7 +31,7 @@ class AvatarHashMap : public QObject, public Dependency { SINGLETON_DEPENDENCY public: - AvatarHash getCopy() { return _avatarHash; } + AvatarHash getHashCopy() { return _avatarHash; } int size() { return _avatarHash.size(); } signals: From f981fb9f4607bc08aff512d78011a1012e488ad7 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Thu, 19 Nov 2015 18:27:58 -0600 Subject: [PATCH 27/99] Initial stack manager commit --- cmake/modules/FindQuaZip.cmake | 32 + stack-manager/CMakeLists.txt | 80 ++ stack-manager/assets/assignment-run.svg | 22 + stack-manager/assets/assignment-stop.svg | 27 + stack-manager/assets/icon.icns | Bin 0 -> 72888 bytes stack-manager/assets/icon.ico | Bin 0 -> 370070 bytes stack-manager/assets/icon.png | Bin 0 -> 14209 bytes stack-manager/assets/logo-larger.png | Bin 0 -> 2536 bytes stack-manager/assets/server-start.svg | 57 ++ stack-manager/assets/server-stop.svg | 53 ++ stack-manager/content-sets/content-sets.html | 64 ++ stack-manager/content-sets/content-sets.json | 27 + stack-manager/src/AppDelegate.cpp | 776 +++++++++++++++++++ stack-manager/src/AppDelegate.h | 89 +++ stack-manager/src/BackgroundProcess.cpp | 125 +++ stack-manager/src/BackgroundProcess.h | 48 ++ stack-manager/src/DownloadManager.cpp | 164 ++++ stack-manager/src/DownloadManager.h | 52 ++ stack-manager/src/Downloader.cpp | 133 ++++ stack-manager/src/Downloader.h | 45 ++ stack-manager/src/GlobalData.cpp | 84 ++ stack-manager/src/GlobalData.h | 75 ++ stack-manager/src/StackManagerVersion.h.in | 16 + stack-manager/src/main.cpp | 15 + stack-manager/src/resources.qrc | 9 + stack-manager/src/ui/AssignmentWidget.cpp | 61 ++ stack-manager/src/ui/AssignmentWidget.h | 37 + stack-manager/src/ui/LogViewer.cpp | 63 ++ stack-manager/src/ui/LogViewer.h | 31 + stack-manager/src/ui/MainWindow.cpp | 329 ++++++++ stack-manager/src/ui/MainWindow.h | 67 ++ stack-manager/src/ui/SvgButton.cpp | 32 + stack-manager/src/ui/SvgButton.h | 33 + stack-manager/windows_icon.rc | 1 + 34 files changed, 2647 insertions(+) create mode 100644 cmake/modules/FindQuaZip.cmake create mode 100644 stack-manager/CMakeLists.txt create mode 100644 stack-manager/assets/assignment-run.svg create mode 100644 stack-manager/assets/assignment-stop.svg create mode 100644 stack-manager/assets/icon.icns create mode 100644 stack-manager/assets/icon.ico create mode 100644 stack-manager/assets/icon.png create mode 100644 stack-manager/assets/logo-larger.png create mode 100644 stack-manager/assets/server-start.svg create mode 100644 stack-manager/assets/server-stop.svg create mode 100644 stack-manager/content-sets/content-sets.html create mode 100644 stack-manager/content-sets/content-sets.json create mode 100644 stack-manager/src/AppDelegate.cpp create mode 100644 stack-manager/src/AppDelegate.h create mode 100644 stack-manager/src/BackgroundProcess.cpp create mode 100644 stack-manager/src/BackgroundProcess.h create mode 100644 stack-manager/src/DownloadManager.cpp create mode 100644 stack-manager/src/DownloadManager.h create mode 100644 stack-manager/src/Downloader.cpp create mode 100644 stack-manager/src/Downloader.h create mode 100644 stack-manager/src/GlobalData.cpp create mode 100644 stack-manager/src/GlobalData.h create mode 100644 stack-manager/src/StackManagerVersion.h.in create mode 100644 stack-manager/src/main.cpp create mode 100644 stack-manager/src/resources.qrc create mode 100644 stack-manager/src/ui/AssignmentWidget.cpp create mode 100644 stack-manager/src/ui/AssignmentWidget.h create mode 100644 stack-manager/src/ui/LogViewer.cpp create mode 100644 stack-manager/src/ui/LogViewer.h create mode 100644 stack-manager/src/ui/MainWindow.cpp create mode 100644 stack-manager/src/ui/MainWindow.h create mode 100644 stack-manager/src/ui/SvgButton.cpp create mode 100644 stack-manager/src/ui/SvgButton.h create mode 100644 stack-manager/windows_icon.rc diff --git a/cmake/modules/FindQuaZip.cmake b/cmake/modules/FindQuaZip.cmake new file mode 100644 index 0000000000..85dda71684 --- /dev/null +++ b/cmake/modules/FindQuaZip.cmake @@ -0,0 +1,32 @@ +# +# FindQuaZip.h +# StackManagerQt/cmake/modules +# +# Created by Mohammed Nafees. +# Copyright (c) 2014 High Fidelity. All rights reserved. +# + +# QUAZIP_FOUND - QuaZip library was found +# QUAZIP_INCLUDE_DIR - Path to QuaZip include dir +# QUAZIP_INCLUDE_DIRS - Path to QuaZip and zlib include dir (combined from QUAZIP_INCLUDE_DIR + ZLIB_INCLUDE_DIR) +# QUAZIP_LIBRARIES - List of QuaZip libraries +# QUAZIP_ZLIB_INCLUDE_DIR - The include dir of zlib headers + + +IF (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) + SET(QUAZIP_FOUND TRUE) +ELSE (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) + SET(QUAZIP_SEARCH_DIRS "$ENV{HIFI_LIB_DIR}/QuaZip") + IF (WIN32) + FIND_PATH(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES include/quazip HINTS ${QUAZIP_SEARCH_DIRS}) + FIND_LIBRARY(QUAZIP_LIBRARIES NAMES quazip PATH_SUFFIXES lib HINTS ${QUAZIP_SEARCH_DIRS}) + ELSEIF(APPLE) + FIND_PATH(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES include/quazip HINTS ${QUAZIP_SEARCH_DIRS}) + FIND_LIBRARY(QUAZIP_LIBRARIES NAMES quazip PATH_SUFFIXES lib HINTS ${QUAZIP_SEARCH_DIRS}) + ELSE () + FIND_PATH(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES quazip HINTS ${QUAZIP_SEARCH_DIRS}) + FIND_LIBRARY(QUAZIP_LIBRARIES NAMES quazip-qt5 PATH_SUFFIXES lib HINTS ${QUAZIP_SEARCH_DIRS}) + ENDIF () + INCLUDE(FindPackageHandleStandardArgs) + find_package_handle_standard_args(QUAZIP DEFAULT_MSG QUAZIP_INCLUDE_DIRS QUAZIP_LIBRARIES) +ENDIF (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) diff --git a/stack-manager/CMakeLists.txt b/stack-manager/CMakeLists.txt new file mode 100644 index 0000000000..01dfc1c73f --- /dev/null +++ b/stack-manager/CMakeLists.txt @@ -0,0 +1,80 @@ +cmake_minimum_required(VERSION 2.8.11) + +if (POLICY CMP0028) + cmake_policy(SET CMP0028 OLD) +endif () + +set(TARGET_NAME "StackManager") + +project(${TARGET_NAME}) + +set(CMAKE_AUTOMOC ON) + +if (NOT QT_CMAKE_PREFIX_PATH) + set(QT_CMAKE_PREFIX_PATH $ENV{QT_CMAKE_PREFIX_PATH}) +endif () + +set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${QT_CMAKE_PREFIX_PATH}) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/") + +find_package(Qt5Widgets REQUIRED) +find_package(Qt5Gui REQUIRED) +find_package(Qt5Svg REQUIRED) +find_package(Qt5Core REQUIRED) +find_package(Qt5Network REQUIRED) +find_package(Qt5WebKitWidgets REQUIRED) +find_package(QuaZip REQUIRED) + +if (WIN32) + find_package(ZLIB REQUIRED) +endif () + +include_directories( + ${QUAZIP_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} + src + src/ui + ${PROJECT_BINARY_DIR}/includes +) + +if (DEFINED ENV{JOB_ID}) + set(PR_BUILD "false") + set(BUILD_SEQ $ENV{JOB_ID}) + set(BASE_URL "http://s3.amazonaws.com/hifi-public") +else () + set(BUILD_SEQ "dev") + if (DEFINED ENV{PR_NUMBER}) + set(PR_BUILD "true") + set(BASE_URL "http://s3.amazonaws.com/hifi-public/pr-builds/$ENV{PR_NUMBER}") + else () + set(PR_BUILD "false") + set(BASE_URL "http://s3.amazonaws.com/hifi-public") + endif () +endif () + +configure_file(src/StackManagerVersion.h.in "${PROJECT_BINARY_DIR}/includes/StackManagerVersion.h") + +file(GLOB SRCS "src/*.cpp" "src/ui/*.cpp") +file(GLOB HEADERS "src/*.h" "src/ui/*.h" "${PROJECT_BINARY_DIR}/includes/*.h") +file(GLOB QT_RES_FILES "src/*.qrc") +qt5_add_resources(QT_RES "${QT_RES_FILES}") +set(SM_SRCS ${QT_RES} ${SRCS} ${HEADERS}) + +if (APPLE) + set(CMAKE_OSX_DEPLOYMENT_TARGET 10.8) + set(MACOSX_BUNDLE_BUNDLE_NAME "Stack Manager") + set(MACOSX_BUNDLE_GUI_IDENTIFIER io.highfidelity.StackManager) + set(MACOSX_BUNDLE_ICON_FILE icon.icns) + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/assets/icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + set(SM_SRCS ${SM_SRCS} "${CMAKE_CURRENT_SOURCE_DIR}/assets/icon.icns") + add_executable(${TARGET_NAME} MACOSX_BUNDLE ${SM_SRCS}) +else () + if (WIN32) + add_executable(${TARGET_NAME} WIN32 ${SM_SRCS} windows_icon.rc) + else () + add_executable(${TARGET_NAME} ${SM_SRCS}) + endif () +endif () + +target_link_libraries(${TARGET_NAME} Qt5::Core Qt5::Gui Qt5::Svg Qt5::Network Qt5::Widgets Qt5::WebKitWidgets ${QUAZIP_LIBRARIES} ${ZLIB_LIBRARIES}) diff --git a/stack-manager/assets/assignment-run.svg b/stack-manager/assets/assignment-run.svg new file mode 100644 index 0000000000..4005d58fbf --- /dev/null +++ b/stack-manager/assets/assignment-run.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/stack-manager/assets/assignment-stop.svg b/stack-manager/assets/assignment-stop.svg new file mode 100644 index 0000000000..ecc1b190c4 --- /dev/null +++ b/stack-manager/assets/assignment-stop.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + diff --git a/stack-manager/assets/icon.icns b/stack-manager/assets/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..711c7f63809c42ed7b9ac196513ffe9275d337e5 GIT binary patch literal 72888 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 literal 0 HcmV?d00001 diff --git a/stack-manager/assets/server-start.svg b/stack-manager/assets/server-start.svg new file mode 100644 index 0000000000..e95d2f3a25 --- /dev/null +++ b/stack-manager/assets/server-start.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stack-manager/assets/server-stop.svg b/stack-manager/assets/server-stop.svg new file mode 100644 index 0000000000..13bf8f3067 --- /dev/null +++ b/stack-manager/assets/server-stop.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stack-manager/content-sets/content-sets.html b/stack-manager/content-sets/content-sets.html new file mode 100644 index 0000000000..05c5f4d6f7 --- /dev/null +++ b/stack-manager/content-sets/content-sets.html @@ -0,0 +1,64 @@ + + + + High Fidelity Stack Manager Content Sets + + + + + + + + + + + + + + +
+
+
+
Click on the name of one of the content sets below to replace your local content with that set.
+
Note that the content set you choose may change the index path ('/') in your domain-server settings.
+
+
+
+ +
+
+ + + + diff --git a/stack-manager/content-sets/content-sets.json b/stack-manager/content-sets/content-sets.json new file mode 100644 index 0000000000..250c40adba --- /dev/null +++ b/stack-manager/content-sets/content-sets.json @@ -0,0 +1,27 @@ +{ + "floating-island": { + "name": "Floating Island", + "description": "Start your galactic empire with this floating island and small oasis. Build it up and share it with your friends.", + "path": "/1064.2,75.6,915.1/0.0000127922,0.71653,0.0000684642,0.697556" + }, + "low-poly-floating-island": { + "name": "Low-poly Floating Island", + "description": "Impressionism with polygons. If you want your virtual island to be nothing but a beautiful painting, this is the aesthetic for you.", + "path": "/8216.88,580.568,8264.03/-0.000192036,-0.838296,-0.000124955,0.545216" + }, + "mid-century-modern-living-room": { + "name": "Mid-century Modern Living Room", + "description": "Timeless, mid-century modern beauty. Notice the classic Eames Recliner and the beautiful built-in shelving.", + "path": "/8206.22,22.8716,8210.47/1.61213e-06,0.814919,1.44589e-06,0.579575" + }, + "bar" : { + "name": "The Bar", + "description": "A sexy club scene to plan your parties and live shows.", + "path": "/1048.52,9.5386,1005.7/-0.0000565125,-0.395713,-0.000131155,0.918374" + }, + "space": { + "name": "Space", + "description": "Vast, empty, nothingness. A completely clean slate for you to start building anything you desire.", + "path": "/1000,100,100" + } +} diff --git a/stack-manager/src/AppDelegate.cpp b/stack-manager/src/AppDelegate.cpp new file mode 100644 index 0000000000..ea9310a2d8 --- /dev/null +++ b/stack-manager/src/AppDelegate.cpp @@ -0,0 +1,776 @@ +// +// AppDelegate.cpp +// StackManagerQt/src +// +// Created by Mohammed Nafees on 06/27/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include + +#include "AppDelegate.h" +#include "BackgroundProcess.h" +#include "GlobalData.h" +#include "DownloadManager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const QString HIGH_FIDELITY_API_URL = "https://metaverse.highfidelity.com/api/v1"; + +const QString CHECK_BUILDS_URL = "https://highfidelity.com/builds.xml"; + +// Use a custom User-Agent to avoid ModSecurity filtering, e.g. by hosting providers. +const QByteArray HIGH_FIDELITY_USER_AGENT = "Mozilla/5.0 (HighFidelity)"; + +const int VERSION_CHECK_INTERVAL_MS = 86400000; // a day + +const int WAIT_FOR_CHILD_MSECS = 5000; + +void signalHandler(int param) { + AppDelegate* app = AppDelegate::getInstance(); + + app->quit(); +} + +static QTextStream* outStream = NULL; + +void myMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { + Q_UNUSED(context); + + QString dateTime = QDateTime::currentDateTime().toString("dd/MM/yyyy hh:mm:ss"); + QString txt = QString("[%1] ").arg(dateTime); + + //in this function, you can write the message to any stream! + switch (type) { + case QtDebugMsg: + fprintf(stdout, "Debug: %s\n", qPrintable(msg)); + txt += msg; + break; + case QtWarningMsg: + fprintf(stdout, "Warning: %s\n", qPrintable(msg)); + txt += msg; + break; + case QtCriticalMsg: + fprintf(stdout, "Critical: %s\n", qPrintable(msg)); + txt += msg; + break; + case QtFatalMsg: + fprintf(stdout, "Fatal: %s\n", qPrintable(msg)); + txt += msg; + } + + if (outStream) { + *outStream << txt << endl; + } +} + +AppDelegate::AppDelegate(int argc, char* argv[]) : + QApplication(argc, argv), + _qtReady(false), + _dsReady(false), + _dsResourcesReady(false), + _acReady(false), + _domainServerProcess(NULL), + _acMonitorProcess(NULL), + _domainServerName("localhost") +{ + // be a signal handler for SIGTERM so we can stop child processes if we get it + signal(SIGTERM, signalHandler); + + // look for command-line options + parseCommandLine(); + + setApplicationName("Stack Manager"); + setOrganizationName("High Fidelity"); + setOrganizationDomain("io.highfidelity.StackManager"); + + QFile* logFile = new QFile("last_run_log", this); + if (!logFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qDebug() << "Failed to open log file. Will not be able to write STDOUT/STDERR to file."; + } else { + outStream = new QTextStream(logFile); + } + + + qInstallMessageHandler(myMessageHandler); + _domainServerProcess = new BackgroundProcess(GlobalData::getInstance().getDomainServerExecutablePath(), this); + _acMonitorProcess = new BackgroundProcess(GlobalData::getInstance().getAssignmentClientExecutablePath(), this); + + _manager = new QNetworkAccessManager(this); + + _window = new MainWindow(); + + createExecutablePath(); + downloadLatestExecutablesAndRequirements(); + + _checkVersionTimer.setInterval(0); + connect(&_checkVersionTimer, SIGNAL(timeout()), this, SLOT(checkVersion())); + _checkVersionTimer.start(); + + connect(this, &QApplication::aboutToQuit, this, &AppDelegate::stopStack); +} + +AppDelegate::~AppDelegate() { + QHash::iterator it = _scriptProcesses.begin(); + + qDebug() << "Stopping scripted assignment-client processes prior to quit."; + while (it != _scriptProcesses.end()) { + BackgroundProcess* backgroundProcess = it.value(); + + // remove from the script processes hash + it = _scriptProcesses.erase(it); + + // make sure the process is dead + backgroundProcess->terminate(); + backgroundProcess->waitForFinished(); + backgroundProcess->deleteLater(); + } + + qDebug() << "Stopping domain-server process prior to quit."; + _domainServerProcess->terminate(); + _domainServerProcess->waitForFinished(); + + qDebug() << "Stopping assignment-client process prior to quit."; + _acMonitorProcess->terminate(); + _acMonitorProcess->waitForFinished(); + + _domainServerProcess->deleteLater(); + _acMonitorProcess->deleteLater(); + + _window->deleteLater(); + + delete outStream; + outStream = NULL; +} + +void AppDelegate::parseCommandLine() { + QCommandLineParser parser; + parser.setApplicationDescription("High Fidelity Stack Manager"); + parser.addHelpOption(); + + const QCommandLineOption helpOption = parser.addHelpOption(); + + const QCommandLineOption hifiBuildDirectoryOption("b", "Path to build of hifi", "build-directory"); + parser.addOption(hifiBuildDirectoryOption); + + if (!parser.parse(QCoreApplication::arguments())) { + qCritical() << parser.errorText() << endl; + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (parser.isSet(helpOption)) { + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (parser.isSet(hifiBuildDirectoryOption)) { + const QString hifiBuildDirectory = parser.value(hifiBuildDirectoryOption); + qDebug() << "hifiBuildDirectory=" << hifiBuildDirectory << "\n"; + GlobalData::getInstance().setHifiBuildDirectory(hifiBuildDirectory); + } +} + +void AppDelegate::toggleStack(bool start) { + toggleDomainServer(start); + toggleAssignmentClientMonitor(start); + toggleScriptedAssignmentClients(start); + emit stackStateChanged(start); +} + +void AppDelegate::toggleDomainServer(bool start) { + + if (start) { + _domainServerProcess->start(QStringList()); + + _window->getLogsWidget()->addTab(_domainServerProcess->getLogViewer(), "Domain Server"); + + if (_domainServerID.isEmpty()) { + // after giving the domain server some time to set up, ask for its ID + QTimer::singleShot(1000, this, SLOT(requestDomainServerID())); + } + } else { + _domainServerProcess->terminate(); + _domainServerProcess->waitForFinished(WAIT_FOR_CHILD_MSECS); + _domainServerProcess->kill(); + } +} + +void AppDelegate::toggleAssignmentClientMonitor(bool start) { + if (start) { + _acMonitorProcess->start(QStringList() << "--min" << "5"); + _window->getLogsWidget()->addTab(_acMonitorProcess->getLogViewer(), "Assignment Clients"); + } else { + _acMonitorProcess->terminate(); + _acMonitorProcess->waitForFinished(WAIT_FOR_CHILD_MSECS); + _acMonitorProcess->kill(); + } +} + +void AppDelegate::toggleScriptedAssignmentClients(bool start) { + foreach(BackgroundProcess* scriptProcess, _scriptProcesses) { + if (start) { + scriptProcess->start(scriptProcess->getLastArgList()); + } else { + scriptProcess->terminate(); + scriptProcess->waitForFinished(WAIT_FOR_CHILD_MSECS); + scriptProcess->kill(); + } + } +} + +int AppDelegate::startScriptedAssignment(const QUuid& scriptID, const QString& pool) { + + BackgroundProcess* scriptProcess = _scriptProcesses.value(scriptID); + + if (!scriptProcess) { + QStringList argList = QStringList() << "-t" << "2"; + if (!pool.isEmpty()) { + argList << "--pool" << pool; + } + + scriptProcess = new BackgroundProcess(GlobalData::getInstance().getAssignmentClientExecutablePath(), + this); + + scriptProcess->start(argList); + + qint64 processID = scriptProcess->processId(); + _scriptProcesses.insert(scriptID, scriptProcess); + + _window->getLogsWidget()->addTab(scriptProcess->getLogViewer(), "Scripted Assignment " + + QString::number(processID)); + } else { + scriptProcess->QProcess::start(); + } + + return scriptProcess->processId(); +} + +void AppDelegate::stopScriptedAssignment(BackgroundProcess* backgroundProcess) { + _window->getLogsWidget()->removeTab(_window->getLogsWidget()->indexOf(backgroundProcess->getLogViewer())); + backgroundProcess->terminate(); + backgroundProcess->waitForFinished(WAIT_FOR_CHILD_MSECS); + backgroundProcess->kill(); +} + +void AppDelegate::stopScriptedAssignment(const QUuid& scriptID) { + BackgroundProcess* processValue = _scriptProcesses.take(scriptID); + if (processValue) { + stopScriptedAssignment(processValue); + } +} + + +void AppDelegate::requestDomainServerID() { + // ask the domain-server for its ID so we can update the accessible name + emit domainAddressChanged(); + QUrl domainIDURL = GlobalData::getInstance().getDomainServerBaseUrl() + "/id"; + + qDebug() << "Requesting domain server ID from" << domainIDURL.toString(); + + QNetworkReply* idReply = _manager->get(QNetworkRequest(domainIDURL)); + + connect(idReply, &QNetworkReply::finished, this, &AppDelegate::handleDomainIDReply); +} + +const QString AppDelegate::getServerAddress() const { + return "hifi://" + _domainServerName; +} + +void AppDelegate::handleDomainIDReply() { + QNetworkReply* reply = qobject_cast(sender()); + + if (reply->error() == QNetworkReply::NoError + && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + _domainServerID = QString(reply->readAll()); + + if (!_domainServerID.isEmpty()) { + + if (!QUuid(_domainServerID).isNull()) { + qDebug() << "The domain server ID is" << _domainServerID; + qDebug() << "Asking High Fidelity API for associated domain name."; + + // fire off a request to high fidelity API to see if this domain exists with them + QUrl domainGetURL = HIGH_FIDELITY_API_URL + "/domains/" + _domainServerID; + QNetworkReply* domainGetReply = _manager->get(QNetworkRequest(domainGetURL)); + connect(domainGetReply, &QNetworkReply::finished, this, &AppDelegate::handleDomainGetReply); + } else { + emit domainServerIDMissing(); + } + } + } else { + qDebug() << "Error getting domain ID from domain-server - " + << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() + << reply->errorString(); + } +} + +void AppDelegate::handleDomainGetReply() { + QNetworkReply* reply = qobject_cast(sender()); + + if (reply->error() == QNetworkReply::NoError + && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + QJsonDocument responseDocument = QJsonDocument::fromJson(reply->readAll()); + + QJsonObject domainObject = responseDocument.object()["domain"].toObject(); + + const QString DOMAIN_NAME_KEY = "name"; + const QString DOMAIN_OWNER_PLACES_KEY = "owner_places"; + + if (domainObject.contains(DOMAIN_NAME_KEY)) { + _domainServerName = domainObject[DOMAIN_NAME_KEY].toString(); + } else if (domainObject.contains(DOMAIN_OWNER_PLACES_KEY)) { + QJsonArray ownerPlaces = domainObject[DOMAIN_OWNER_PLACES_KEY].toArray(); + if (ownerPlaces.size() > 0) { + _domainServerName = ownerPlaces[0].toObject()[DOMAIN_NAME_KEY].toString(); + } + } + + qDebug() << "This domain server's name is" << _domainServerName << "- updating address link."; + + emit domainAddressChanged(); + } +} + +void AppDelegate::changeDomainServerIndexPath(const QString& newPath) { + if (!newPath.isEmpty()) { + QString pathsJSON = "{\"paths\": { \"/\": { \"viewpoint\": \"%1\" }}}"; + + QNetworkRequest settingsRequest(GlobalData::getInstance().getDomainServerBaseUrl() + "/settings.json"); + settingsRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QNetworkReply* settingsReply = _manager->post(settingsRequest, pathsJSON.arg(newPath).toLocal8Bit()); + connect(settingsReply, &QNetworkReply::finished, this, &AppDelegate::handleChangeIndexPathResponse); + } +} + +void AppDelegate::handleChangeIndexPathResponse() { + QNetworkReply* reply = qobject_cast(sender()); + + if (reply->error() == QNetworkReply::NoError + && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + + qDebug() << "Successfully changed index path in domain-server."; + emit indexPathChangeResponse(true); + } else { + qDebug() << "Error changing domain-server index path-" << reply->errorString(); + emit indexPathChangeResponse(false); + } +} + +void AppDelegate::downloadContentSet(const QUrl& contentSetURL) { + // make sure this link was an svo + if (contentSetURL.path().endsWith(".svo")) { + // setup a request for this content set + QNetworkRequest contentRequest(contentSetURL); + QNetworkReply* contentReply = _manager->get(contentRequest); + connect(contentReply, &QNetworkReply::finished, this, &AppDelegate::handleContentSetDownloadFinished); + } +} + +void AppDelegate::handleContentSetDownloadFinished() { + QNetworkReply* reply = qobject_cast(sender()); + + if (reply->error() == QNetworkReply::NoError + && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + + QString modelFilename = GlobalData::getInstance().getClientsResourcesPath() + "models.svo"; + + // write the model file + QFile modelFile(modelFilename); + modelFile.open(QIODevice::WriteOnly); + + // stop the base assignment clients before we try to write the new content + toggleAssignmentClientMonitor(false); + + if (modelFile.write(reply->readAll()) == -1) { + qDebug() << "Error writing content set to" << modelFilename; + modelFile.close(); + toggleAssignmentClientMonitor(true); + } else { + qDebug() << "Wrote new content set to" << modelFilename; + modelFile.close(); + + // restart the assignment-client + toggleAssignmentClientMonitor(true); + + emit contentSetDownloadResponse(true); + + // did we have a path in the query? + // if so when we need to set the DS index path to that path + QUrlQuery svoQuery(reply->url().query()); + changeDomainServerIndexPath(svoQuery.queryItemValue("path")); + + emit domainAddressChanged(); + + return; + } + } + + // if we failed we need to emit our signal with a fail + emit contentSetDownloadResponse(false); + emit domainAddressChanged(); +} + +void AppDelegate::onFileSuccessfullyInstalled(const QUrl& url) { + if (url == GlobalData::getInstance().getRequirementsURL()) { + _qtReady = true; + } else if (url == GlobalData::getInstance().getAssignmentClientURL()) { + _acReady = true; + } else if (url == GlobalData::getInstance().getDomainServerURL()) { + _dsReady = true; + } else if (url == GlobalData::getInstance().getDomainServerResourcesURL()) { + _dsResourcesReady = true; + } + + if (_qtReady && _acReady && _dsReady && _dsResourcesReady) { + _window->setRequirementsLastChecked(QDateTime::currentDateTime().toString()); + _window->show(); + toggleStack(true); + } +} + +void AppDelegate::createExecutablePath() { + QDir launchDir(GlobalData::getInstance().getClientsLaunchPath()); + QDir resourcesDir(GlobalData::getInstance().getClientsResourcesPath()); + QDir logsDir(GlobalData::getInstance().getLogsPath()); + if (!launchDir.exists()) { + if (QDir().mkpath(launchDir.absolutePath())) { + qDebug() << "Successfully created directory: " + << launchDir.absolutePath(); + } else { + qCritical() << "Failed to create directory: " + << launchDir.absolutePath(); + } + } + if (!resourcesDir.exists()) { + if (QDir().mkpath(resourcesDir.absolutePath())) { + qDebug() << "Successfully created directory: " + << resourcesDir.absolutePath(); + } else { + qCritical() << "Failed to create directory: " + << resourcesDir.absolutePath(); + } + } + if (!logsDir.exists()) { + if (QDir().mkpath(logsDir.absolutePath())) { + qDebug() << "Successfully created directory: " + << logsDir.absolutePath(); + } else { + qCritical() << "Failed to create directory: " + << logsDir.absolutePath(); + } + } +} + +void AppDelegate::downloadLatestExecutablesAndRequirements() { + // Check if Qt is already installed + if (GlobalData::getInstance().getPlatform() == "mac") { + if (QDir(GlobalData::getInstance().getClientsLaunchPath() + "QtCore.framework").exists()) { + _qtReady = true; + } + } else if (GlobalData::getInstance().getPlatform() == "win") { + if (QFileInfo(GlobalData::getInstance().getClientsLaunchPath() + "Qt5Core.dll").exists()) { + _qtReady = true; + } + } else { // linux + if (QFileInfo(GlobalData::getInstance().getClientsLaunchPath() + "libQt5Core.so.5").exists()) { + _qtReady = true; + } + } + + + QFile reqZipFile(GlobalData::getInstance().getRequirementsZipPath()); + QByteArray reqZipData; + if (reqZipFile.open(QIODevice::ReadOnly)) { + reqZipData = reqZipFile.readAll(); + reqZipFile.close(); + } + QFile resZipFile(GlobalData::getInstance().getDomainServerResourcesZipPath()); + QByteArray resZipData; + if (resZipFile.open(QIODevice::ReadOnly)) { + resZipData = resZipFile.readAll(); + resZipFile.close(); + } + + QDir resourcesDir(GlobalData::getInstance().getClientsResourcesPath()); + if (!(resourcesDir.entryInfoList(QDir::AllEntries).size() < 3)) { + _dsResourcesReady = true; + } + + // if the user has set hifiBuildDirectory, don't attempt to download the domain-server or assignement-client + if (GlobalData::getInstance().isGetHifiBuildDirectorySet()) { + _dsReady = true; + _acReady = true; + } else { + QByteArray dsData; + QFile dsFile(GlobalData::getInstance().getDomainServerExecutablePath()); + if (dsFile.open(QIODevice::ReadOnly)) { + dsData = dsFile.readAll(); + dsFile.close(); + } + QByteArray acData; + QFile acFile(GlobalData::getInstance().getAssignmentClientExecutablePath()); + if (acFile.open(QIODevice::ReadOnly)) { + acData = acFile.readAll(); + acFile.close(); + } + + QNetworkRequest acReq(QUrl(GlobalData::getInstance().getAssignmentClientMD5URL())); + QNetworkReply* acReply = _manager->get(acReq); + QEventLoop acLoop; + connect(acReply, SIGNAL(finished()), &acLoop, SLOT(quit())); + acLoop.exec(); + QByteArray acMd5Data = acReply->readAll().trimmed(); + if (GlobalData::getInstance().getPlatform() == "win") { + // fix for reading the MD5 hash from Windows-generated + // binary data of the MD5 hash + QTextStream stream(acMd5Data); + stream >> acMd5Data; + } + + // fix for Mac and Linux network accessibility + if (acMd5Data.size() == 0) { + // network is not accessible + qDebug() << "Could not connect to the internet."; + _window->show(); + return; + } + + qDebug() << "AC MD5: " << acMd5Data; + if (acMd5Data.toLower() == QCryptographicHash::hash(acData, QCryptographicHash::Md5).toHex()) { + _acReady = true; + } + + + QNetworkRequest dsReq(QUrl(GlobalData::getInstance().getDomainServerMD5URL())); + QNetworkReply* dsReply = _manager->get(dsReq); + QEventLoop dsLoop; + connect(dsReply, SIGNAL(finished()), &dsLoop, SLOT(quit())); + dsLoop.exec(); + QByteArray dsMd5Data = dsReply->readAll().trimmed(); + if (GlobalData::getInstance().getPlatform() == "win") { + // fix for reading the MD5 hash from Windows generated + // binary data of the MD5 hash + QTextStream stream(dsMd5Data); + stream >> dsMd5Data; + } + qDebug() << "DS MD5: " << dsMd5Data; + if (dsMd5Data.toLower() == QCryptographicHash::hash(dsData, QCryptographicHash::Md5).toHex()) { + _dsReady = true; + } + } + + if (_qtReady) { + // check MD5 of requirements.zip only if Qt is found + QNetworkRequest reqZipReq(QUrl(GlobalData::getInstance().getRequirementsMD5URL())); + QNetworkReply* reqZipReply = _manager->get(reqZipReq); + QEventLoop reqZipLoop; + connect(reqZipReply, SIGNAL(finished()), &reqZipLoop, SLOT(quit())); + reqZipLoop.exec(); + QByteArray reqZipMd5Data = reqZipReply->readAll().trimmed(); + if (GlobalData::getInstance().getPlatform() == "win") { + // fix for reading the MD5 hash from Windows generated + // binary data of the MD5 hash + QTextStream stream(reqZipMd5Data); + stream >> reqZipMd5Data; + } + qDebug() << "Requirements ZIP MD5: " << reqZipMd5Data; + if (reqZipMd5Data.toLower() != QCryptographicHash::hash(reqZipData, QCryptographicHash::Md5).toHex()) { + _qtReady = false; + } + } + + if (_dsResourcesReady) { + // check MD5 of resources.zip only if Domain Server + // resources are installed + QNetworkRequest resZipReq(QUrl(GlobalData::getInstance().getDomainServerResourcesMD5URL())); + QNetworkReply* resZipReply = _manager->get(resZipReq); + QEventLoop resZipLoop; + connect(resZipReply, SIGNAL(finished()), &resZipLoop, SLOT(quit())); + resZipLoop.exec(); + QByteArray resZipMd5Data = resZipReply->readAll().trimmed(); + if (GlobalData::getInstance().getPlatform() == "win") { + // fix for reading the MD5 hash from Windows generated + // binary data of the MD5 hash + QTextStream stream(resZipMd5Data); + stream >> resZipMd5Data; + } + qDebug() << "Domain Server Resources ZIP MD5: " << resZipMd5Data; + if (resZipMd5Data.toLower() != QCryptographicHash::hash(resZipData, QCryptographicHash::Md5).toHex()) { + _dsResourcesReady = false; + } + } + + DownloadManager* downloadManager = 0; + if (!_qtReady || !_acReady || !_dsReady || !_dsResourcesReady) { + // initialise DownloadManager + downloadManager = new DownloadManager(_manager); + downloadManager->setWindowModality(Qt::ApplicationModal); + connect(downloadManager, SIGNAL(fileSuccessfullyInstalled(QUrl)), + SLOT(onFileSuccessfullyInstalled(QUrl))); + downloadManager->show(); + } else { + _window->setRequirementsLastChecked(QDateTime::currentDateTime().toString()); + _window->show(); + toggleStack(true); + } + + if (!_qtReady) { + downloadManager->downloadFile(GlobalData::getInstance().getRequirementsURL()); + } + + if (!_acReady) { + downloadManager->downloadFile(GlobalData::getInstance().getAssignmentClientURL()); + } + + if (!_dsReady) { + downloadManager->downloadFile(GlobalData::getInstance().getDomainServerURL()); + } + + if (!_dsResourcesReady) { + downloadManager->downloadFile(GlobalData::getInstance().getDomainServerResourcesURL()); + } +} + +void AppDelegate::checkVersion() { + QNetworkRequest latestVersionRequest((QUrl(CHECK_BUILDS_URL))); + latestVersionRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + latestVersionRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); + QNetworkReply* reply = _manager->get(latestVersionRequest); + connect(reply, &QNetworkReply::finished, this, &AppDelegate::parseVersionXml); + + _checkVersionTimer.setInterval(VERSION_CHECK_INTERVAL_MS); + _checkVersionTimer.start(); +} + +struct VersionInformation { + QString version; + QUrl downloadUrl; + QString timeStamp; + QString releaseNotes; +}; + +void AppDelegate::parseVersionXml() { + +#ifdef Q_OS_WIN32 + QString operatingSystem("windows"); +#endif + +#ifdef Q_OS_MAC + QString operatingSystem("mac"); +#endif + +#ifdef Q_OS_LINUX + QString operatingSystem("ubuntu"); +#endif + + QNetworkReply* sender = qobject_cast(QObject::sender()); + QXmlStreamReader xml(sender); + + QHash projectVersions; + + while (!xml.atEnd() && !xml.hasError()) { + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "project") { + QString projectName = ""; + foreach(const QXmlStreamAttribute &attr, xml.attributes()) { + if (attr.name().toString() == "name") { + projectName = attr.value().toString(); + break; + } + } + while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name().toString() == "project")) { + if (projectName != "") { + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "platform") { + QString platformName = ""; + foreach(const QXmlStreamAttribute &attr, xml.attributes()) { + if (attr.name().toString() == "name") { + platformName = attr.value().toString(); + break; + } + } + int latestVersion = 0; + VersionInformation latestVersionInformation; + while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name().toString() == "platform")) { + if (platformName == operatingSystem) { + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "build") { + VersionInformation buildVersionInformation; + while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name().toString() == "build")) { + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "version") { + xml.readNext(); + buildVersionInformation.version = xml.text().toString(); + } + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "url") { + xml.readNext(); + buildVersionInformation.downloadUrl = QUrl(xml.text().toString()); + } + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "timestamp") { + xml.readNext(); + buildVersionInformation.timeStamp = xml.text().toString(); + } + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "note") { + xml.readNext(); + if (buildVersionInformation.releaseNotes != "") { + buildVersionInformation.releaseNotes += "\n"; + } + buildVersionInformation.releaseNotes += xml.text().toString(); + } + xml.readNext(); + } + if (latestVersion < buildVersionInformation.version.toInt()) { + latestVersionInformation = buildVersionInformation; + latestVersion = buildVersionInformation.version.toInt(); + } + } + } + xml.readNext(); + } + if (latestVersion>0) { + projectVersions[projectName] = latestVersionInformation; + } + } + } + xml.readNext(); + } + } + xml.readNext(); + } + +#ifdef WANT_DEBUG + qDebug() << "parsed projects for OS" << operatingSystem; + QHashIterator projectVersion(projectVersions); + while (projectVersion.hasNext()) { + projectVersion.next(); + qDebug() << "project:" << projectVersion.key(); + qDebug() << "version:" << projectVersion.value().version; + qDebug() << "downloadUrl:" << projectVersion.value().downloadUrl.toString(); + qDebug() << "timeStamp:" << projectVersion.value().timeStamp; + qDebug() << "releaseNotes:" << projectVersion.value().releaseNotes; + } +#endif + + if (projectVersions.contains("stackmanager")) { + VersionInformation latestVersion = projectVersions["stackmanager"]; + if (QCoreApplication::applicationVersion() != latestVersion.version && QCoreApplication::applicationVersion() != "dev") { + _window->setUpdateNotification("There is an update available. Please download and install version " + latestVersion.version + "."); + _window->update(); + } + } + + sender->deleteLater(); +} diff --git a/stack-manager/src/AppDelegate.h b/stack-manager/src/AppDelegate.h new file mode 100644 index 0000000000..1d4728b7ce --- /dev/null +++ b/stack-manager/src/AppDelegate.h @@ -0,0 +1,89 @@ +// +// AppDelegate.h +// StackManagerQt/src +// +// Created by Mohammed Nafees on 06/27/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_AppDelegate_h +#define hifi_AppDelegate_h + + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MainWindow.h" + +class BackgroundProcess; + +class AppDelegate : public QApplication +{ + Q_OBJECT +public: + static AppDelegate* getInstance() { return static_cast(QCoreApplication::instance()); } + + AppDelegate(int argc, char* argv[]); + ~AppDelegate(); + + void toggleStack(bool start); + void toggleDomainServer(bool start); + void toggleAssignmentClientMonitor(bool start); + void toggleScriptedAssignmentClients(bool start); + + int startScriptedAssignment(const QUuid& scriptID, const QString& pool = QString()); + void stopScriptedAssignment(BackgroundProcess* backgroundProcess); + void stopScriptedAssignment(const QUuid& scriptID); + + void stopStack() { toggleStack(false); } + + const QString getServerAddress() const; +public slots: + void downloadContentSet(const QUrl& contentSetURL); +signals: + void domainServerIDMissing(); + void domainAddressChanged(); + void contentSetDownloadResponse(bool wasSuccessful); + void indexPathChangeResponse(bool wasSuccessful); + void stackStateChanged(bool isOn); +private slots: + void onFileSuccessfullyInstalled(const QUrl& url); + void requestDomainServerID(); + void handleDomainIDReply(); + void handleDomainGetReply(); + void handleChangeIndexPathResponse(); + void handleContentSetDownloadFinished(); + void checkVersion(); + void parseVersionXml(); + +private: + void parseCommandLine(); + void createExecutablePath(); + void downloadLatestExecutablesAndRequirements(); + + void changeDomainServerIndexPath(const QString& newPath); + + QNetworkAccessManager* _manager; + bool _qtReady; + bool _dsReady; + bool _dsResourcesReady; + bool _acReady; + BackgroundProcess* _domainServerProcess; + BackgroundProcess* _acMonitorProcess; + QHash _scriptProcesses; + + QString _domainServerID; + QString _domainServerName; + + QTimer _checkVersionTimer; + + MainWindow* _window; +}; + +#endif diff --git a/stack-manager/src/BackgroundProcess.cpp b/stack-manager/src/BackgroundProcess.cpp new file mode 100644 index 0000000000..e5d0452a83 --- /dev/null +++ b/stack-manager/src/BackgroundProcess.cpp @@ -0,0 +1,125 @@ +// +// BackgroundProcess.cpp +// StackManagerQt/src +// +// Created by Mohammed Nafees on 07/03/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "BackgroundProcess.h" +#include "GlobalData.h" + +#include +#include +#include +#include +#include +#include +#include + +const int LOG_CHECK_INTERVAL_MS = 500; + +const QString DATETIME_FORMAT = "yyyy-MM-dd_hh.mm.ss"; +const QString LOGS_DIRECTORY = "/Logs/"; + +BackgroundProcess::BackgroundProcess(const QString& program, QObject *parent) : + QProcess(parent), + _program(program), + _stdoutFilePos(0), + _stderrFilePos(0) +{ + _logViewer = new LogViewer; + + connect(this, SIGNAL(started()), SLOT(processStarted())); + connect(this, SIGNAL(error(QProcess::ProcessError)), SLOT(processError())); + + _logFilePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + _logFilePath.append(LOGS_DIRECTORY); + QDir logDir(_logFilePath); + if (!logDir.exists(_logFilePath)) { + logDir.mkpath(_logFilePath); + } + + _logTimer.setInterval(LOG_CHECK_INTERVAL_MS); + _logTimer.setSingleShot(false); + connect(&_logTimer, SIGNAL(timeout()), this, SLOT(receivedStandardError())); + connect(&_logTimer, SIGNAL(timeout()), this, SLOT(receivedStandardOutput())); + connect(this, SIGNAL(started()), &_logTimer, SLOT(start())); + + setWorkingDirectory(GlobalData::getInstance().getClientsLaunchPath()); +} + +void BackgroundProcess::start(const QStringList& arguments) { + QDateTime now = QDateTime::currentDateTime(); + QString nowString = now.toString(DATETIME_FORMAT); + QFileInfo programFile(_program); + QString baseFilename = _logFilePath + programFile.completeBaseName(); + _stdoutFilename = QString("%1_stdout_%2.txt").arg(baseFilename, nowString); + _stderrFilename = QString("%1_stderr_%2.txt").arg(baseFilename, nowString); + + qDebug() << "stdout for " << _program << " being written to: " << _stdoutFilename; + qDebug() << "stderr for " << _program << " being written to: " << _stderrFilename; + + // reset the stdout and stderr file positions + _stdoutFilePos = 0; + _stderrFilePos = 0; + + // clear our LogViewer + _logViewer->clear(); + + // reset our output and error files + setStandardOutputFile(_stdoutFilename); + setStandardErrorFile(_stderrFilename); + + _lastArgList = arguments; + + QProcess::start(_program, arguments); +} + +void BackgroundProcess::processStarted() { + qDebug() << "process " << _program << " started."; +} + +void BackgroundProcess::processError() { + qDebug() << "process error for" << _program << "-" << errorString(); +} + +void BackgroundProcess::receivedStandardOutput() { + QString output; + + QFile file(_stdoutFilename); + + if (!file.open(QIODevice::ReadOnly)) return; + + if (file.size() > _stdoutFilePos) { + file.seek(_stdoutFilePos); + output = file.readAll(); + _stdoutFilePos = file.pos(); + } + + file.close(); + + if (!output.isEmpty() && !output.isNull()) { + _logViewer->appendStandardOutput(output); + } +} + +void BackgroundProcess::receivedStandardError() { + QString output; + + QFile file(_stderrFilename); + + if (!file.open(QIODevice::ReadOnly)) return; + + if (file.size() > _stderrFilePos) { + file.seek(_stderrFilePos); + output = file.readAll(); + _stderrFilePos = file.pos(); + } + + file.close(); + + if (!output.isEmpty() && !output.isNull()) { + _logViewer->appendStandardError(output); + } +} diff --git a/stack-manager/src/BackgroundProcess.h b/stack-manager/src/BackgroundProcess.h new file mode 100644 index 0000000000..1b3ff85758 --- /dev/null +++ b/stack-manager/src/BackgroundProcess.h @@ -0,0 +1,48 @@ +// +// BackgroundProcess.h +// StackManagerQt/src +// +// Created by Mohammed Nafees on 07/03/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_BackgroundProcess_h +#define hifi_BackgroundProcess_h + +#include "LogViewer.h" + +#include +#include +#include + +class BackgroundProcess : public QProcess +{ + Q_OBJECT +public: + BackgroundProcess(const QString& program, QObject* parent = 0); + + LogViewer* getLogViewer() { return _logViewer; } + + const QStringList& getLastArgList() const { return _lastArgList; } + + void start(const QStringList& arguments); + +private slots: + void processStarted(); + void processError(); + void receivedStandardOutput(); + void receivedStandardError(); + +private: + QString _program; + QStringList _lastArgList; + QString _logFilePath; + LogViewer* _logViewer; + QTimer _logTimer; + QString _stdoutFilename; + QString _stderrFilename; + qint64 _stdoutFilePos; + qint64 _stderrFilePos; +}; + +#endif diff --git a/stack-manager/src/DownloadManager.cpp b/stack-manager/src/DownloadManager.cpp new file mode 100644 index 0000000000..f97ba1f680 --- /dev/null +++ b/stack-manager/src/DownloadManager.cpp @@ -0,0 +1,164 @@ +// +// DownloadManager.cpp +// StackManagerQt/src +// +// Created by Mohammed Nafees on 07/09/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "DownloadManager.h" +#include "GlobalData.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DownloadManager::DownloadManager(QNetworkAccessManager* manager, QWidget* parent) : + QWidget(parent), + _manager(manager) +{ + setBaseSize(500, 250); + + QVBoxLayout* layout = new QVBoxLayout; + layout->setContentsMargins(10, 10, 10, 10); + QLabel* label = new QLabel; + label->setText("Download Manager"); + label->setStyleSheet("font-size: 19px;"); + label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + label->setAlignment(Qt::AlignCenter); + layout->addWidget(label); + + _table = new QTableWidget; + _table->setEditTriggers(QTableWidget::NoEditTriggers); + _table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + _table->setColumnCount(3); + _table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + _table->setHorizontalHeaderLabels(QStringList() << "Name" << "Progress" << "Status"); + layout->addWidget(_table); + + setLayout(layout); +} + +DownloadManager::~DownloadManager() { + _downloaderHash.clear(); +} + +void DownloadManager::downloadFile(const QUrl& url) { + for (int i = 0; i < _downloaderHash.size(); ++i) { + if (_downloaderHash.keys().at(i)->getUrl() == url) { + qDebug() << "Downloader for URL " << url << " already initialised."; + return; + } + } + + Downloader* downloader = new Downloader(url); + connect(downloader, SIGNAL(downloadCompleted(QUrl)), SLOT(onDownloadCompleted(QUrl))); + connect(downloader, SIGNAL(downloadStarted(Downloader*,QUrl)), + SLOT(onDownloadStarted(Downloader*,QUrl))); + connect(downloader, SIGNAL(downloadFailed(QUrl)), SLOT(onDownloadFailed(QUrl))); + connect(downloader, SIGNAL(downloadProgress(QUrl,int)), SLOT(onDownloadProgress(QUrl,int))); + connect(downloader, SIGNAL(installingFiles(QUrl)), SLOT(onInstallingFiles(QUrl))); + connect(downloader, SIGNAL(filesSuccessfullyInstalled(QUrl)), SLOT(onFilesSuccessfullyInstalled(QUrl))); + connect(downloader, SIGNAL(filesInstallationFailed(QUrl)), SLOT(onFilesInstallationFailed(QUrl))); + downloader->start(_manager); +} + +void DownloadManager::onDownloadStarted(Downloader* downloader, const QUrl& url) { + int rowIndex = _table->rowCount(); + _table->setRowCount(rowIndex + 1); + QTableWidgetItem* nameItem = new QTableWidgetItem(QFileInfo(url.toString()).fileName()); + _table->setItem(rowIndex, 0, nameItem); + QProgressBar* progressBar = new QProgressBar; + _table->setCellWidget(rowIndex, 1, progressBar); + QTableWidgetItem* statusItem = new QTableWidgetItem; + if (QFile(QDir::toNativeSeparators(GlobalData::getInstance().getClientsLaunchPath() + "/" + QFileInfo(url.toString()).fileName())).exists()) { + statusItem->setText("Updating"); + } else { + statusItem->setText("Downloading"); + } + _table->setItem(rowIndex, 2, statusItem); + _downloaderHash.insert(downloader, rowIndex); +} + +void DownloadManager::onDownloadCompleted(const QUrl& url) { + _table->item(downloaderRowIndexForUrl(url), 2)->setText("Download Complete"); +} + +void DownloadManager::onDownloadProgress(const QUrl& url, int percentage) { + qobject_cast(_table->cellWidget(downloaderRowIndexForUrl(url), 1))->setValue(percentage); +} + +void DownloadManager::onDownloadFailed(const QUrl& url) { + _table->item(downloaderRowIndexForUrl(url), 2)->setText("Download Failed"); + _downloaderHash.remove(downloaderForUrl(url)); +} + +void DownloadManager::onInstallingFiles(const QUrl& url) { + _table->item(downloaderRowIndexForUrl(url), 2)->setText("Installing"); +} + +void DownloadManager::onFilesSuccessfullyInstalled(const QUrl& url) { + _table->item(downloaderRowIndexForUrl(url), 2)->setText("Successfully Installed"); + _downloaderHash.remove(downloaderForUrl(url)); + emit fileSuccessfullyInstalled(url); + if (_downloaderHash.size() == 0) { + close(); + } +} + +void DownloadManager::onFilesInstallationFailed(const QUrl& url) { + _table->item(downloaderRowIndexForUrl(url), 2)->setText("Installation Failed"); + _downloaderHash.remove(downloaderForUrl(url)); +} + +void DownloadManager::closeEvent(QCloseEvent*) { + if (_downloaderHash.size() > 0) { + QMessageBox msgBox; + msgBox.setText("There are active downloads that need to be installed for the proper functioning of Stack Manager. Do you want to stop the downloads and exit?"); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + int ret = msgBox.exec(); + switch (ret) { + case QMessageBox::Yes: + qApp->quit(); + break; + case QMessageBox::No: + msgBox.close(); + break; + } + } +} + +int DownloadManager::downloaderRowIndexForUrl(const QUrl& url) { + QHash::const_iterator i = _downloaderHash.constBegin(); + while (i != _downloaderHash.constEnd()) { + if (i.key()->getUrl() == url) { + return i.value(); + } else { + ++i; + } + } + + return -1; +} + +Downloader* DownloadManager::downloaderForUrl(const QUrl& url) { + QHash::const_iterator i = _downloaderHash.constBegin(); + while (i != _downloaderHash.constEnd()) { + if (i.key()->getUrl() == url) { + return i.key(); + } else { + ++i; + } + } + + return NULL; +} diff --git a/stack-manager/src/DownloadManager.h b/stack-manager/src/DownloadManager.h new file mode 100644 index 0000000000..1cef0ae118 --- /dev/null +++ b/stack-manager/src/DownloadManager.h @@ -0,0 +1,52 @@ +// +// DownloadManager.h +// StackManagerQt/src +// +// Created by Mohammed Nafees on 07/09/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_DownloadManager_h +#define hifi_DownloadManager_h + +#include +#include +#include +#include +#include + +#include "Downloader.h" + +class DownloadManager : public QWidget { + Q_OBJECT +public: + DownloadManager(QNetworkAccessManager* manager, QWidget* parent = 0); + ~DownloadManager(); + + void downloadFile(const QUrl& url); + +private slots: + void onDownloadStarted(Downloader* downloader, const QUrl& url); + void onDownloadCompleted(const QUrl& url); + void onDownloadProgress(const QUrl& url, int percentage); + void onDownloadFailed(const QUrl& url); + void onInstallingFiles(const QUrl& url); + void onFilesSuccessfullyInstalled(const QUrl& url); + void onFilesInstallationFailed(const QUrl& url); + +protected: + void closeEvent(QCloseEvent*); + +signals: + void fileSuccessfullyInstalled(const QUrl& url); + +private: + QTableWidget* _table; + QNetworkAccessManager* _manager; + QHash _downloaderHash; + + int downloaderRowIndexForUrl(const QUrl& url); + Downloader* downloaderForUrl(const QUrl& url); +}; + +#endif diff --git a/stack-manager/src/Downloader.cpp b/stack-manager/src/Downloader.cpp new file mode 100644 index 0000000000..42ae8ba091 --- /dev/null +++ b/stack-manager/src/Downloader.cpp @@ -0,0 +1,133 @@ +// +// Downloader.cpp +// StackManagerQt/src +// +// Created by Mohammed Nafees on 07/09/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "Downloader.h" +#include "GlobalData.h" + +#include +#include + +#include +#include +#include +#include +#include + +Downloader::Downloader(const QUrl& url, QObject* parent) : + QObject(parent) +{ + _url = url; +} + +void Downloader::start(QNetworkAccessManager* manager) { + qDebug() << "Downloader::start() for URL - " << _url; + QNetworkRequest req(_url); + QNetworkReply* reply = manager->get(req); + emit downloadStarted(this, _url); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(error(QNetworkReply::NetworkError))); + connect(reply, SIGNAL(downloadProgress(qint64,qint64)), SLOT(downloadProgress(qint64,qint64))); + connect(reply, SIGNAL(finished()), SLOT(downloadFinished())); +} + +void Downloader::error(QNetworkReply::NetworkError error) { + QNetworkReply* reply = qobject_cast(sender()); + qDebug() << reply->errorString(); + reply->deleteLater(); +} + +void Downloader::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { + int percentage = bytesReceived*100/bytesTotal; + emit downloadProgress(_url, percentage); +} + +void Downloader::downloadFinished() { + qDebug() << "Downloader::downloadFinished() for URL - " << _url; + QNetworkReply* reply = qobject_cast(sender()); + if (reply->error() != QNetworkReply::NoError) { + qDebug() << reply->errorString(); + emit downloadFailed(_url); + return; + } + emit downloadCompleted(_url); + + QString fileName = QFileInfo(_url.toString()).fileName(); + QString fileDir = GlobalData::getInstance().getClientsLaunchPath(); + QString filePath = fileDir + fileName; + + QFile file(filePath); + + // remove file if already exists + if (file.exists()) { + file.remove(); + } + + if (file.open(QIODevice::WriteOnly)) { + if (fileName == "assignment-client" || fileName == "assignment-client.exe" || + fileName == "domain-server" || fileName == "domain-server.exe") { + file.setPermissions(QFile::ExeOwner | QFile::ReadOwner | QFile::WriteOwner); + } else { + file.setPermissions(QFile::ReadOwner | QFile::WriteOwner); + } + emit installingFiles(_url); + file.write(reply->readAll()); + bool error = false; + file.close(); + + if (fileName.endsWith(".zip")) { // we need to unzip the file now + QuaZip zip(QFileInfo(file).absoluteFilePath()); + if (zip.open(QuaZip::mdUnzip)) { + QuaZipFile zipFile(&zip); + for(bool f = zip.goToFirstFile(); f; f = zip.goToNextFile()) { + if (zipFile.open(QIODevice::ReadOnly)) { + QFile newFile(QDir::toNativeSeparators(fileDir + "/" + zipFile.getActualFileName())); + if (zipFile.getActualFileName().endsWith("/")) { + QDir().mkpath(QFileInfo(newFile).absolutePath()); + zipFile.close(); + continue; + } + + // remove file if already exists + if (newFile.exists()) { + newFile.remove(); + } + + if (newFile.open(QIODevice::WriteOnly)) { + newFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner); + newFile.write(zipFile.readAll()); + newFile.close(); + } else { + error = true; + qDebug() << "Could not open archive file for writing: " << zip.getCurrentFileName(); + emit filesInstallationFailed(_url); + break; + } + } else { + error = true; + qDebug() << "Could not open archive file: " << zip.getCurrentFileName(); + emit filesInstallationFailed(_url); + break; + } + zipFile.close(); + } + zip.close(); + } else { + error = true; + emit filesInstallationFailed(_url); + qDebug() << "Could not open zip file for extraction."; + } + } + if (!error) + emit filesSuccessfullyInstalled(_url); + } else { + emit filesInstallationFailed(_url); + qDebug() << "Could not open file: " << filePath; + } + reply->deleteLater(); +} + + diff --git a/stack-manager/src/Downloader.h b/stack-manager/src/Downloader.h new file mode 100644 index 0000000000..f5b02214e0 --- /dev/null +++ b/stack-manager/src/Downloader.h @@ -0,0 +1,45 @@ +// +// Downloader.h +// StackManagerQt/src +// +// Created by Mohammed Nafees on 07/09/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_Downloader_h +#define hifi_Downloader_h + +#include +#include +#include +#include + +class Downloader : public QObject +{ + Q_OBJECT +public: + explicit Downloader(const QUrl& url, QObject* parent = 0); + + const QUrl& getUrl() { return _url; } + + void start(QNetworkAccessManager* manager); + +private slots: + void error(QNetworkReply::NetworkError error); + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void downloadFinished(); + +signals: + void downloadStarted(Downloader* downloader, const QUrl& url); + void downloadCompleted(const QUrl& url); + void downloadProgress(const QUrl& url, int percentage); + void downloadFailed(const QUrl& url); + void installingFiles(const QUrl& url); + void filesSuccessfullyInstalled(const QUrl& url); + void filesInstallationFailed(const QUrl& url); + +private: + QUrl _url; +}; + +#endif diff --git a/stack-manager/src/GlobalData.cpp b/stack-manager/src/GlobalData.cpp new file mode 100644 index 0000000000..ecc5ed520d --- /dev/null +++ b/stack-manager/src/GlobalData.cpp @@ -0,0 +1,84 @@ +// +// GlobalData.cpp +// StackManagerQt/src +// +// Created by Mohammed Nafees on 6/25/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "GlobalData.h" +#include "StackManagerVersion.h" + +#include +#include +#include +#include + +GlobalData& GlobalData::getInstance() { + static GlobalData staticInstance; + return staticInstance; +} + +GlobalData::GlobalData() { + QString urlBase = URL_BASE; +#if defined Q_OS_OSX + _platform = "mac"; +#elif defined Q_OS_WIN32 + _platform = "win"; +#elif defined Q_OS_LINUX + _platform = "linux"; +#endif + + _resourcePath = "resources/"; + _assignmentClientExecutable = "assignment-client"; + _domainServerExecutable = "domain-server"; + QString applicationSupportDirectory = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + if (PR_BUILD) { + applicationSupportDirectory += "/pr-binaries"; + } + + _clientsLaunchPath = QDir::toNativeSeparators(applicationSupportDirectory + "/"); + _clientsResourcePath = QDir::toNativeSeparators(applicationSupportDirectory + "/" + _resourcePath); + + _assignmentClientExecutablePath = QDir::toNativeSeparators(_clientsLaunchPath + _assignmentClientExecutable); + if (_platform == "win") { + _assignmentClientExecutablePath.append(".exe"); + } + _domainServerExecutablePath = QDir::toNativeSeparators(_clientsLaunchPath + _domainServerExecutable); + if (_platform == "win") { + _domainServerExecutablePath.append(".exe"); + } + + _requirementsURL = urlBase + "/binaries/" + _platform + "/requirements/requirements.zip"; + _requirementsZipPath = _clientsLaunchPath + "requirements.zip"; + _requirementsMD5URL = urlBase + "/binaries/" + _platform + "/requirements/requirements.md5"; + _assignmentClientURL = urlBase + "/binaries/" + _platform + "/assignment-client" + (_platform == "win" ? "/assignment-client.exe" : "/assignment-client"); + _domainServerResourcesURL = urlBase + "/binaries/" + _platform + "/domain-server/resources.zip"; + _domainServerResourcesZipPath = _clientsLaunchPath + "resources.zip"; + _domainServerResourcesMD5URL = urlBase + "/binaries/" + _platform + "/domain-server/resources.md5"; + _domainServerURL = urlBase + "/binaries/" + _platform + "/domain-server" + (_platform == "win" ? "/domain-server.exe" : "/domain-server"); + + _assignmentClientMD5URL = urlBase + "/binaries/" + _platform + "/assignment-client/assignment-client.md5"; + _domainServerMD5URL = urlBase + "/binaries/" + _platform + "/domain-server/domain-server.md5"; + + _defaultDomain = "localhost"; + _logsPath = QDir::toNativeSeparators(_clientsLaunchPath + "logs/"); + _availableAssignmentTypes.insert("audio-mixer", 0); + _availableAssignmentTypes.insert("avatar-mixer", 1); + _availableAssignmentTypes.insert("entity-server", 6); + + // allow user to override path to binaries so that they can run their own builds + _hifiBuildDirectory = ""; + + _domainServerBaseUrl = "http://localhost:40100"; +} + + +void GlobalData::setHifiBuildDirectory(const QString hifiBuildDirectory) { + _hifiBuildDirectory = hifiBuildDirectory; + _clientsLaunchPath = QDir::toNativeSeparators(_hifiBuildDirectory + "/assignment-client/"); + _clientsResourcePath = QDir::toNativeSeparators(_clientsLaunchPath + "/" + _resourcePath); + _logsPath = QDir::toNativeSeparators(_clientsLaunchPath + "logs/"); + _assignmentClientExecutablePath = QDir::toNativeSeparators(_clientsLaunchPath + _assignmentClientExecutable); + _domainServerExecutablePath = QDir::toNativeSeparators(_hifiBuildDirectory + "/domain-server/" + _domainServerExecutable); +} diff --git a/stack-manager/src/GlobalData.h b/stack-manager/src/GlobalData.h new file mode 100644 index 0000000000..58c9a93526 --- /dev/null +++ b/stack-manager/src/GlobalData.h @@ -0,0 +1,75 @@ +// +// GlobalData.h +// StackManagerQt/src +// +// Created by Mohammed Nafees on 6/25/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_GlobalData_h +#define hifi_GlobalData_h + +#include +#include + +class GlobalData { +public: + static GlobalData& getInstance(); + + QString getPlatform() { return _platform; } + QString getClientsLaunchPath() { return _clientsLaunchPath; } + QString getClientsResourcesPath() { return _clientsResourcePath; } + QString getAssignmentClientExecutablePath() { return _assignmentClientExecutablePath; } + QString getDomainServerExecutablePath() { return _domainServerExecutablePath; } + QString getRequirementsURL() { return _requirementsURL; } + QString getRequirementsZipPath() { return _requirementsZipPath; } + QString getRequirementsMD5URL() { return _requirementsMD5URL; } + QString getAssignmentClientURL() { return _assignmentClientURL; } + QString getAssignmentClientMD5URL() { return _assignmentClientMD5URL; } + QString getDomainServerURL() { return _domainServerURL; } + QString getDomainServerResourcesURL() { return _domainServerResourcesURL; } + QString getDomainServerResourcesZipPath() { return _domainServerResourcesZipPath; } + QString getDomainServerResourcesMD5URL() { return _domainServerResourcesMD5URL; } + QString getDomainServerMD5URL() { return _domainServerMD5URL; } + QString getDefaultDomain() { return _defaultDomain; } + QString getLogsPath() { return _logsPath; } + QHash getAvailableAssignmentTypes() { return _availableAssignmentTypes; } + + void setHifiBuildDirectory(const QString hifiBuildDirectory); + bool isGetHifiBuildDirectorySet() { return _hifiBuildDirectory != ""; } + + void setDomainServerBaseUrl(const QString domainServerBaseUrl) { _domainServerBaseUrl = domainServerBaseUrl; } + QString getDomainServerBaseUrl() { return _domainServerBaseUrl; } + +private: + GlobalData(); + + QString _platform; + QString _clientsLaunchPath; + QString _clientsResourcePath; + QString _assignmentClientExecutablePath; + QString _domainServerExecutablePath; + QString _requirementsURL; + QString _requirementsZipPath; + QString _requirementsMD5URL; + QString _assignmentClientURL; + QString _assignmentClientMD5URL; + QString _domainServerURL; + QString _domainServerResourcesURL; + QString _domainServerResourcesZipPath; + QString _domainServerResourcesMD5URL; + QString _domainServerMD5URL; + QString _defaultDomain; + QString _logsPath; + QString _hifiBuildDirectory; + + QString _resourcePath; + QString _assignmentClientExecutable; + QString _domainServerExecutable; + + QHash _availableAssignmentTypes; + + QString _domainServerBaseUrl; +}; + +#endif diff --git a/stack-manager/src/StackManagerVersion.h.in b/stack-manager/src/StackManagerVersion.h.in new file mode 100644 index 0000000000..402e19a056 --- /dev/null +++ b/stack-manager/src/StackManagerVersion.h.in @@ -0,0 +1,16 @@ +// +// StackManagerVersion.h +// StackManagerQt +// +// Created by Kai Ludwig on 02/16/15. +// Copyright 2015 High Fidelity, Inc. +// +// Declaration of version and build data +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +const QString BUILD_VERSION = "@BUILD_SEQ@"; +const QString URL_BASE = "@BASE_URL@"; +const bool PR_BUILD = @PR_BUILD@; diff --git a/stack-manager/src/main.cpp b/stack-manager/src/main.cpp new file mode 100644 index 0000000000..b5b715c75a --- /dev/null +++ b/stack-manager/src/main.cpp @@ -0,0 +1,15 @@ +// +// main.cpp +// StackManagerQt/src +// +// Created by Mohammed Nafees on 06/27/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "AppDelegate.h" + +int main(int argc, char* argv[]) +{ + AppDelegate app(argc, argv); + return app.exec(); +} diff --git a/stack-manager/src/resources.qrc b/stack-manager/src/resources.qrc new file mode 100644 index 0000000000..508edcc728 --- /dev/null +++ b/stack-manager/src/resources.qrc @@ -0,0 +1,9 @@ + + + ../assets/logo-larger.png + ../assets/assignment-run.svg + ../assets/assignment-stop.svg + ../assets/server-start.svg + ../assets/server-stop.svg + + diff --git a/stack-manager/src/ui/AssignmentWidget.cpp b/stack-manager/src/ui/AssignmentWidget.cpp new file mode 100644 index 0000000000..51fe067eb3 --- /dev/null +++ b/stack-manager/src/ui/AssignmentWidget.cpp @@ -0,0 +1,61 @@ +// +// AssignmentWidget.cpp +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 10/18/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "AssignmentWidget.h" + +#include +#include + +#include "AppDelegate.h" + +AssignmentWidget::AssignmentWidget(QWidget* parent) : + QWidget(parent), + _processID(0), + _isRunning(false), + _scriptID(QUuid::createUuid()) +{ + setFont(QFont("sans-serif")); + + QHBoxLayout* layout = new QHBoxLayout; + + _runButton = new SvgButton(this); + _runButton->setFixedSize(59, 32); + _runButton->setSvgImage(":/assignment-run.svg"); + _runButton->setCheckable(true); + _runButton->setChecked(false); + + QLabel* label = new QLabel; + label->setText("Pool ID"); + + _poolIDLineEdit = new QLineEdit; + _poolIDLineEdit->setPlaceholderText("Optional"); + + layout->addWidget(_runButton, 5); + layout->addWidget(label); + layout->addWidget(_poolIDLineEdit); + + setLayout(layout); + + connect(_runButton, &QPushButton::clicked, this, &AssignmentWidget::toggleRunningState); +} + +void AssignmentWidget::toggleRunningState() { + if (_isRunning && _processID > 0) { + AppDelegate::getInstance()->stopScriptedAssignment(_scriptID); + _runButton->setSvgImage(":/assignment-run.svg"); + update(); + _poolIDLineEdit->setEnabled(true); + _isRunning = false; + } else { + _processID = AppDelegate::getInstance()->startScriptedAssignment(_scriptID, _poolIDLineEdit->text()); + _runButton->setSvgImage(":/assignment-stop.svg"); + update(); + _poolIDLineEdit->setEnabled(false); + _isRunning = true; + } +} diff --git a/stack-manager/src/ui/AssignmentWidget.h b/stack-manager/src/ui/AssignmentWidget.h new file mode 100644 index 0000000000..3e52d7f1af --- /dev/null +++ b/stack-manager/src/ui/AssignmentWidget.h @@ -0,0 +1,37 @@ +// +// AssignmentWidget.h +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 10/18/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_AssignmentWidget_h +#define hifi_AssignmentWidget_h + +#include +#include +#include + +#include "SvgButton.h" + +class AssignmentWidget : public QWidget +{ + Q_OBJECT +public: + AssignmentWidget(QWidget* parent = 0); + + bool isRunning() { return _isRunning; } + +public slots: + void toggleRunningState(); + +private: + int _processID; + bool _isRunning; + SvgButton* _runButton; + QLineEdit* _poolIDLineEdit; + QUuid _scriptID; +}; + +#endif diff --git a/stack-manager/src/ui/LogViewer.cpp b/stack-manager/src/ui/LogViewer.cpp new file mode 100644 index 0000000000..12b6f33f88 --- /dev/null +++ b/stack-manager/src/ui/LogViewer.cpp @@ -0,0 +1,63 @@ +// +// LogViewer.cpp +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 07/10/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "LogViewer.h" +#include "GlobalData.h" + +#include +#include +#include + +LogViewer::LogViewer(QWidget* parent) : + QWidget(parent) +{ + QVBoxLayout* layout = new QVBoxLayout; + QLabel* outputLabel = new QLabel; + outputLabel->setText("Standard Output:"); + outputLabel->setStyleSheet("font-size: 13pt;"); + + layout->addWidget(outputLabel); + + _outputView = new QTextEdit; + _outputView->setUndoRedoEnabled(false); + _outputView->setReadOnly(true); + + layout->addWidget(_outputView); + + QLabel* errorLabel = new QLabel; + errorLabel->setText("Standard Error:"); + errorLabel->setStyleSheet("font-size: 13pt;"); + + layout->addWidget(errorLabel); + + _errorView = new QTextEdit; + _errorView->setUndoRedoEnabled(false); + _errorView->setReadOnly(true); + + layout->addWidget(_errorView); + setLayout(layout); +} + +void LogViewer::clear() { + _outputView->clear(); + _errorView->clear(); +} + +void LogViewer::appendStandardOutput(const QString& output) { + QTextCursor cursor = _outputView->textCursor(); + cursor.movePosition(QTextCursor::End); + cursor.insertText(output); + _outputView->ensureCursorVisible(); +} + +void LogViewer::appendStandardError(const QString& error) { + QTextCursor cursor = _errorView->textCursor(); + cursor.movePosition(QTextCursor::End); + cursor.insertText(error); + _errorView->ensureCursorVisible(); +} diff --git a/stack-manager/src/ui/LogViewer.h b/stack-manager/src/ui/LogViewer.h new file mode 100644 index 0000000000..b4321cc886 --- /dev/null +++ b/stack-manager/src/ui/LogViewer.h @@ -0,0 +1,31 @@ +// +// LogViewer.h +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 07/10/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_LogViewer_h +#define hifi_LogViewer_h + +#include +#include + +class LogViewer : public QWidget +{ + Q_OBJECT +public: + explicit LogViewer(QWidget* parent = 0); + + void clear(); + + void appendStandardOutput(const QString& output); + void appendStandardError(const QString& error); + +private: + QTextEdit* _outputView; + QTextEdit* _errorView; +}; + +#endif diff --git a/stack-manager/src/ui/MainWindow.cpp b/stack-manager/src/ui/MainWindow.cpp new file mode 100644 index 0000000000..59551933f4 --- /dev/null +++ b/stack-manager/src/ui/MainWindow.cpp @@ -0,0 +1,329 @@ +// +// MainWindow.cpp +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 10/17/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "MainWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AppDelegate.h" +#include "AssignmentWidget.h" +#include "GlobalData.h" +#include "StackManagerVersion.h" + +const int GLOBAL_X_PADDING = 55; +const int TOP_Y_PADDING = 25; +const int REQUIREMENTS_TEXT_TOP_MARGIN = 19; +//const int HORIZONTAL_RULE_TOP_MARGIN = 25; + +const int BUTTON_PADDING_FIX = -5; + +//const int ASSIGNMENT_LAYOUT_RESIZE_FACTOR = 56; +//const int ASSIGNMENT_LAYOUT_WIDGET_STRETCH = 0; + +const QColor lightGrayColor = QColor(205, 205, 205); +const QColor darkGrayColor = QColor(84, 84, 84); +const QColor redColor = QColor(189, 54, 78); +const QColor greenColor = QColor(3, 150, 126); + +const QString SHARE_BUTTON_COPY_LINK_TEXT = "Copy link"; + +MainWindow::MainWindow() : + QWidget(), + _domainServerRunning(false), + _startServerButton(NULL), + _stopServerButton(NULL), + _serverAddressLabel(NULL), + _viewLogsButton(NULL), + _settingsButton(NULL), + _copyLinkButton(NULL), + _contentSetButton(NULL), + _logsWidget(NULL), + _localHttpPortSharedMem(NULL) +{ + // Set build version + QCoreApplication::setApplicationVersion(BUILD_VERSION); + + setWindowTitle("High Fidelity Stack Manager (build " + QCoreApplication::applicationVersion() + ")"); + const int WINDOW_FIXED_WIDTH = 640; + const int WINDOW_INITIAL_HEIGHT = 170; + + if (GlobalData::getInstance().getPlatform() == "win") { + const int windowsYCoord = 30; + setGeometry(qApp->desktop()->availableGeometry().width() / 2 - WINDOW_FIXED_WIDTH / 2, windowsYCoord, + WINDOW_FIXED_WIDTH, WINDOW_INITIAL_HEIGHT); + } else if (GlobalData::getInstance().getPlatform() == "linux") { + const int linuxYCoord = 30; + setGeometry(qApp->desktop()->availableGeometry().width() / 2 - WINDOW_FIXED_WIDTH / 2, linuxYCoord, + WINDOW_FIXED_WIDTH, WINDOW_INITIAL_HEIGHT + 40); + } else { + const int unixYCoord = 0; + setGeometry(qApp->desktop()->availableGeometry().width() / 2 - WINDOW_FIXED_WIDTH / 2, unixYCoord, + WINDOW_FIXED_WIDTH, WINDOW_INITIAL_HEIGHT); + } + setFixedWidth(WINDOW_FIXED_WIDTH); + setMaximumHeight(qApp->desktop()->availableGeometry().height()); + setWindowFlags(Qt::CustomizeWindowHint | Qt::WindowTitleHint | + Qt::WindowMinimizeButtonHint | Qt::WindowCloseButtonHint); + setMouseTracking(true); + setStyleSheet("font-family: 'Helvetica', 'Arial', 'sans-serif';"); + + const int SERVER_BUTTON_HEIGHT = 47; + + _startServerButton = new SvgButton(this); + + QPixmap scaledStart(":/server-start.svg"); + scaledStart.scaledToHeight(SERVER_BUTTON_HEIGHT); + + _startServerButton->setGeometry((width() / 2.0f) - (scaledStart.width() / 2.0f), + TOP_Y_PADDING, + scaledStart.width(), + scaledStart.height()); + _startServerButton->setSvgImage(":/server-start.svg"); + + _stopServerButton = new SvgButton(this); + _stopServerButton->setSvgImage(":/server-stop.svg"); + _stopServerButton->setGeometry(GLOBAL_X_PADDING, TOP_Y_PADDING, + scaledStart.width(), scaledStart.height()); + + const int SERVER_ADDRESS_LABEL_LEFT_MARGIN = 20; + const int SERVER_ADDRESS_LABEL_TOP_MARGIN = 17; + _serverAddressLabel = new QLabel(this); + _serverAddressLabel->move(_stopServerButton->geometry().right() + SERVER_ADDRESS_LABEL_LEFT_MARGIN, + TOP_Y_PADDING + SERVER_ADDRESS_LABEL_TOP_MARGIN); + _serverAddressLabel->setOpenExternalLinks(true); + + const int SECONDARY_BUTTON_ROW_TOP_MARGIN = 10; + + int secondaryButtonY = _stopServerButton->geometry().bottom() + SECONDARY_BUTTON_ROW_TOP_MARGIN; + + _viewLogsButton = new QPushButton("View logs", this); + _viewLogsButton->adjustSize(); + _viewLogsButton->setGeometry(GLOBAL_X_PADDING + BUTTON_PADDING_FIX, secondaryButtonY, + _viewLogsButton->width(), _viewLogsButton->height()); + + _settingsButton = new QPushButton("Settings", this); + _settingsButton->adjustSize(); + _settingsButton->setGeometry(_viewLogsButton->geometry().right(), secondaryButtonY, + _settingsButton->width(), _settingsButton->height()); + + _copyLinkButton = new QPushButton(SHARE_BUTTON_COPY_LINK_TEXT, this); + _copyLinkButton->adjustSize(); + _copyLinkButton->setGeometry(_settingsButton->geometry().right(), secondaryButtonY, + _copyLinkButton->width(), _copyLinkButton->height()); + + // add the drop down for content sets + _contentSetButton = new QPushButton("Get content set", this); + _contentSetButton->adjustSize(); + _contentSetButton->setGeometry(_copyLinkButton->geometry().right(), secondaryButtonY, + _contentSetButton->width(), _contentSetButton->height()); + + const QSize logsWidgetSize = QSize(500, 500); + _logsWidget = new QTabWidget; + _logsWidget->setUsesScrollButtons(true); + _logsWidget->setElideMode(Qt::ElideMiddle); + _logsWidget->setWindowFlags(Qt::CustomizeWindowHint | Qt::WindowTitleHint | + Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint); + _logsWidget->resize(logsWidgetSize); + + connect(_startServerButton, &QPushButton::clicked, this, &MainWindow::toggleDomainServerButton); + connect(_stopServerButton, &QPushButton::clicked, this, &MainWindow::toggleDomainServerButton); + connect(_copyLinkButton, &QPushButton::clicked, this, &MainWindow::handleCopyLinkButton); + connect(_contentSetButton, &QPushButton::clicked, this, &MainWindow::showContentSetPage); + connect(_viewLogsButton, &QPushButton::clicked, _logsWidget, &QTabWidget::show); + connect(_settingsButton, &QPushButton::clicked, this, &MainWindow::openSettings); + + AppDelegate* app = AppDelegate::getInstance(); + // update the current server address label and change it if the AppDelegate says the address has changed + updateServerAddressLabel(); + connect(app, &AppDelegate::domainAddressChanged, this, &MainWindow::updateServerAddressLabel); + connect(app, &AppDelegate::domainAddressChanged, this, &MainWindow::updateServerBaseUrl); + + // handle response for content set download + connect(app, &AppDelegate::contentSetDownloadResponse, this, &MainWindow::handleContentSetDownloadResponse); + + // handle response for index path change + connect(app, &AppDelegate::indexPathChangeResponse, this, &MainWindow::handleIndexPathChangeResponse); + + // handle stack state change + connect(app, &AppDelegate::stackStateChanged, this, &MainWindow::toggleContent); + + toggleContent(false); + +} + +void MainWindow::updateServerAddressLabel() { + AppDelegate* app = AppDelegate::getInstance(); + + _serverAddressLabel->setText("

Accessible at: " + "getServerAddress() + "\">" + "" + app->getServerAddress() + + "

"); + _serverAddressLabel->adjustSize(); +} + +void MainWindow::updateServerBaseUrl() { + quint16 localPort; + + if (getLocalServerPortFromSharedMemory("domain-server.local-http-port", _localHttpPortSharedMem, localPort)) { + GlobalData::getInstance().setDomainServerBaseUrl(QString("http://localhost:") + QString::number(localPort)); + } +} + + +void MainWindow::handleCopyLinkButton() { + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(AppDelegate::getInstance()->getServerAddress()); +} + +void MainWindow::showContentSetPage() { + const QString CONTENT_SET_HTML_URL = "http://hifi-public.s3.amazonaws.com/content-sets/content-sets.html"; + + // show a QWebView for the content set page + QWebView* contentSetWebView = new QWebView(); + contentSetWebView->setUrl(CONTENT_SET_HTML_URL); + + // have the widget delete on close + contentSetWebView->setAttribute(Qt::WA_DeleteOnClose); + + // setup the page viewport to be the right size + const QSize CONTENT_SET_VIEWPORT_SIZE = QSize(800, 480); + contentSetWebView->resize(CONTENT_SET_VIEWPORT_SIZE); + + // have our app delegate handle a click on one of the content sets + contentSetWebView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); + connect(contentSetWebView->page(), &QWebPage::linkClicked, AppDelegate::getInstance(), &AppDelegate::downloadContentSet); + connect(contentSetWebView->page(), &QWebPage::linkClicked, contentSetWebView, &QWebView::close); + + contentSetWebView->show(); +} + +void MainWindow::handleContentSetDownloadResponse(bool wasSuccessful) { + if (wasSuccessful) { + QMessageBox::information(this, "New content set", + "Your new content set has been downloaded and your assignment-clients have been restarted."); + } else { + QMessageBox::information(this, "Error", "There was a problem downloading that content set. Please try again!"); + } +} + +void MainWindow::handleIndexPathChangeResponse(bool wasSuccessful) { + if (!wasSuccessful) { + QString errorMessage = "The content set was downloaded successfully but there was a problem changing your \ + domain-server index path.\n\nIf you want users to jump to the new content set when they come to your domain \ + please try and re-download the content set."; + QMessageBox::information(this, "Error", errorMessage); + } +} + +void MainWindow::setRequirementsLastChecked(const QString& lastCheckedDateTime) { + _requirementsLastCheckedDateTime = lastCheckedDateTime; +} + +void MainWindow::setUpdateNotification(const QString& updateNotification) { + _updateNotification = updateNotification; +} + +void MainWindow::toggleContent(bool isRunning) { + _stopServerButton->setVisible(isRunning); + _startServerButton->setVisible(!isRunning); + _domainServerRunning = isRunning; + _serverAddressLabel->setVisible(isRunning); + _viewLogsButton->setVisible(isRunning); + _settingsButton->setVisible(isRunning); + _copyLinkButton->setVisible(isRunning); + _contentSetButton->setVisible(isRunning); + update(); +} + +void MainWindow::paintEvent(QPaintEvent *) { + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QFont font("Helvetica"); + font.insertSubstitutions("Helvetica", QStringList() << "Arial" << "sans-serif"); + + int currentY = (_domainServerRunning ? _viewLogsButton->geometry().bottom() : _startServerButton->geometry().bottom()) + + REQUIREMENTS_TEXT_TOP_MARGIN; + + if (!_updateNotification.isEmpty()) { + font.setBold(true); + font.setUnderline(false); + if (GlobalData::getInstance().getPlatform() == "linux") { + font.setPointSize(14); + } + painter.setFont(font); + painter.setPen(redColor); + + QString updateNotificationString = ">>> " + _updateNotification + " <<<"; + float fontWidth = QFontMetrics(font).width(updateNotificationString) + GLOBAL_X_PADDING; + + painter.drawText(QRectF(_domainServerRunning ? ((width() - fontWidth) / 2.0f) : GLOBAL_X_PADDING, + currentY, + fontWidth, + QFontMetrics(font).height()), + updateNotificationString); + } + else if (!_requirementsLastCheckedDateTime.isEmpty()) { + font.setBold(false); + font.setUnderline(false); + if (GlobalData::getInstance().getPlatform() == "linux") { + font.setPointSize(14); + } + painter.setFont(font); + painter.setPen(darkGrayColor); + + QString requirementsString = "Requirements are up to date as of " + _requirementsLastCheckedDateTime; + float fontWidth = QFontMetrics(font).width(requirementsString); + + painter.drawText(QRectF(_domainServerRunning ? GLOBAL_X_PADDING : ((width() - fontWidth)/ 2.0f), + currentY, + fontWidth, + QFontMetrics(font).height()), + "Requirements are up to date as of " + _requirementsLastCheckedDateTime); + } +} + +void MainWindow::toggleDomainServerButton() { + AppDelegate::getInstance()->toggleStack(!_domainServerRunning); +} + +void MainWindow::openSettings() { + QDesktopServices::openUrl(QUrl(GlobalData::getInstance().getDomainServerBaseUrl() + "/settings/")); +} + + +// XXX this code is duplicate of LimitedNodeList::getLocalServerPortFromSharedMemory +bool MainWindow::getLocalServerPortFromSharedMemory(const QString key, QSharedMemory*& sharedMem, quint16& localPort) { + if (!sharedMem) { + sharedMem = new QSharedMemory(key, this); + + if (!sharedMem->attach(QSharedMemory::ReadOnly)) { + qWarning() << "Could not attach to shared memory at key" << key; + } + } + + if (sharedMem->isAttached()) { + sharedMem->lock(); + memcpy(&localPort, sharedMem->data(), sizeof(localPort)); + sharedMem->unlock(); + return true; + } + + return false; +} diff --git a/stack-manager/src/ui/MainWindow.h b/stack-manager/src/ui/MainWindow.h new file mode 100644 index 0000000000..6b6669ce3a --- /dev/null +++ b/stack-manager/src/ui/MainWindow.h @@ -0,0 +1,67 @@ +// +// MainWindow.h +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 10/17/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_MainWindow_h +#define hifi_MainWindow_h + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "SvgButton.h" + +class MainWindow : public QWidget { + Q_OBJECT +public: + MainWindow(); + + void setRequirementsLastChecked(const QString& lastCheckedDateTime); + void setUpdateNotification(const QString& updateNotification); + QTabWidget* getLogsWidget() { return _logsWidget; } + bool getLocalServerPortFromSharedMemory(const QString key, QSharedMemory*& sharedMem, quint16& localPort); + +protected: + virtual void paintEvent(QPaintEvent*); + +private slots: + void toggleDomainServerButton(); + void openSettings(); + void updateServerAddressLabel(); + void updateServerBaseUrl(); + void handleCopyLinkButton(); + void showContentSetPage(); + + void handleContentSetDownloadResponse(bool wasSuccessful); + void handleIndexPathChangeResponse(bool wasSuccessful); +private: + void toggleContent(bool isRunning); + + bool _domainServerRunning; + + QString _requirementsLastCheckedDateTime; + QString _updateNotification; + SvgButton* _startServerButton; + SvgButton* _stopServerButton; + QLabel* _serverAddressLabel; + QPushButton* _viewLogsButton; + QPushButton* _settingsButton; + QPushButton* _copyLinkButton; + QPushButton* _contentSetButton; + QTabWidget* _logsWidget; + + QSharedMemory* _localHttpPortSharedMem; // memory shared with domain server +}; + +#endif diff --git a/stack-manager/src/ui/SvgButton.cpp b/stack-manager/src/ui/SvgButton.cpp new file mode 100644 index 0000000000..0d646ff0d1 --- /dev/null +++ b/stack-manager/src/ui/SvgButton.cpp @@ -0,0 +1,32 @@ +// +// SvgButton.cpp +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 10/20/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "SvgButton.h" + +#include +#include +#include + +SvgButton::SvgButton(QWidget* parent) : + QAbstractButton(parent) +{ +} + +void SvgButton::enterEvent(QEvent*) { + setCursor(QCursor(Qt::PointingHandCursor)); +} + +void SvgButton::setSvgImage(const QString& svg) { + _svgImage = svg; +} + +void SvgButton::paintEvent(QPaintEvent*) { + QPainter painter(this); + QSvgRenderer renderer(_svgImage); + renderer.render(&painter); +} diff --git a/stack-manager/src/ui/SvgButton.h b/stack-manager/src/ui/SvgButton.h new file mode 100644 index 0000000000..b4d44631ec --- /dev/null +++ b/stack-manager/src/ui/SvgButton.h @@ -0,0 +1,33 @@ +// +// SvgButton.h +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 10/20/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_SvgButton_h +#define hifi_SvgButton_h + +#include +#include +#include + +class SvgButton : public QAbstractButton +{ + Q_OBJECT +public: + explicit SvgButton(QWidget* parent = 0); + + void setSvgImage(const QString& svg); + +protected: + virtual void enterEvent(QEvent*); + virtual void paintEvent(QPaintEvent*); + +private: + QString _svgImage; + +}; + +#endif diff --git a/stack-manager/windows_icon.rc b/stack-manager/windows_icon.rc new file mode 100644 index 0000000000..125e4c19db --- /dev/null +++ b/stack-manager/windows_icon.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "assets/icon.ico" \ No newline at end of file From f80a765a296caa309f9484f07c12ed3de5f8985e Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 19 Nov 2015 17:26:59 -0800 Subject: [PATCH 28/99] add Assets.uploadData() and Assets.downloadData() --- examples/example/assetsExample.js | 11 ++++ libraries/networking/src/AssetClient.cpp | 62 ++++++++++++++++++++ libraries/networking/src/AssetClient.h | 13 ++++ libraries/script-engine/src/ScriptEngine.cpp | 3 + libraries/script-engine/src/ScriptEngine.h | 3 + 5 files changed, 92 insertions(+) create mode 100644 examples/example/assetsExample.js diff --git a/examples/example/assetsExample.js b/examples/example/assetsExample.js new file mode 100644 index 0000000000..decebbcfa3 --- /dev/null +++ b/examples/example/assetsExample.js @@ -0,0 +1,11 @@ +var data = "this is some data"; +var extension = "txt"; +var uploadedFile; + +Assets.uploadData(data, extension, function (url) { + print("data uploaded to:" + url); + uploadedFile = url; + Assets.downloadData(url, function (data) { + print("data downloaded from:" + url + " the data is:" + data); + }); +}); diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 6a1b46340c..75b4ca04e8 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -365,3 +365,65 @@ void AssetClient::handleNodeKilled(SharedNodePointer node) { } } } + +void AssetScriptingInterface::uploadData(QString data, QString extension, QScriptValue callback) { + QByteArray dataByteArray = data.toUtf8(); + auto upload = DependencyManager::get()->createUpload(dataByteArray, extension); + QObject::connect(upload, &AssetUpload::finished, this, [callback, extension](AssetUpload* upload, const QString& hash) mutable { + if (callback.isFunction()) { + QString url = "atp://" + hash + "." + extension; + QScriptValueList args { url }; + callback.call(QScriptValue(), args); + } + }); + + // start the upload now + upload->start(); +} + +void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) { + const QString ATP_SCHEME { "atp://" }; + + if (!urlString.startsWith(ATP_SCHEME)) { + return; + } + + // Make request to atp + auto path = urlString.right(urlString.length() - ATP_SCHEME.length()); + auto parts = path.split(".", QString::SkipEmptyParts); + auto hash = parts.length() > 0 ? parts[0] : ""; + auto extension = parts.length() > 1 ? parts[1] : ""; + + if (hash.length() != SHA256_HASH_HEX_LENGTH) { + return; + } + + auto assetClient = DependencyManager::get(); + auto assetRequest = assetClient->createRequest(hash, extension); + + if (!assetRequest) { + return; + } + + _pendingRequests << assetRequest; + + connect(assetRequest, &AssetRequest::finished, [this, callback](AssetRequest* request) mutable { + Q_ASSERT(request->getState() == AssetRequest::Finished); + + if (request->getError() == AssetRequest::Error::NoError) { + if (callback.isFunction()) { + QString data = QString::fromUtf8(request->getData()); + QScriptValueList args { data }; + callback.call(QScriptValue(), args); + } + } + + request->deleteLater(); + _pendingRequests.remove(request); + }); + + assetRequest->start(); +} + + + diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index 22933ea30b..f1bb210614 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -14,6 +14,7 @@ #define hifi_AssetClient_h #include +#include #include @@ -21,6 +22,7 @@ #include "LimitedNodeList.h" #include "NLPacket.h" #include "Node.h" +#include "ResourceCache.h" class AssetRequest; class AssetUpload; @@ -68,4 +70,15 @@ private: friend class AssetUpload; }; + +class AssetScriptingInterface : public QObject { + Q_OBJECT +public: + Q_INVOKABLE void uploadData(QString data, QString extension, QScriptValue callback); + Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete); +protected: + QSet _pendingRequests; +}; + + #endif diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 1b0fb80a05..5326090723 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -381,6 +381,9 @@ void ScriptEngine::init() { auto recordingInterface = DependencyManager::get(); registerGlobalObject("Recording", recordingInterface.data()); + + registerGlobalObject("Assets", &_assetScriptingInterface); + } void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) { diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index c957b0c3b4..1412ba7aaf 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -195,6 +196,8 @@ private: ArrayBufferClass* _arrayBufferClass; + AssetScriptingInterface _assetScriptingInterface; + QHash _registeredHandlers; void forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHanderArgs); Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success); From 7f8274f58f928d92ed7813716c5696819dd3f939 Mon Sep 17 00:00:00 2001 From: Chris Collins Date: Thu, 19 Nov 2015 18:08:12 -0800 Subject: [PATCH 29/99] removed omnitool removed omnitool --- examples/defaultScripts.js | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/defaultScripts.js b/examples/defaultScripts.js index 6efe6edef3..f06af70fb3 100644 --- a/examples/defaultScripts.js +++ b/examples/defaultScripts.js @@ -19,4 +19,3 @@ Script.load("controllers/handControllerGrab.js"); Script.load("grab.js"); Script.load("directory.js"); Script.load("dialTone.js"); -Script.load("libraries/omniTool.js"); From 5c6cd9b06c2f653830f7bc51f0d4386e84b0f66e Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 19 Nov 2015 18:53:12 -0800 Subject: [PATCH 30/99] fix thread safety and crash with no asset server --- libraries/networking/src/AssetClient.cpp | 31 +++++++++++++++--------- libraries/networking/src/AssetClient.h | 1 + 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 75b4ca04e8..4dc08962c5 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -369,16 +369,18 @@ void AssetClient::handleNodeKilled(SharedNodePointer node) { void AssetScriptingInterface::uploadData(QString data, QString extension, QScriptValue callback) { QByteArray dataByteArray = data.toUtf8(); auto upload = DependencyManager::get()->createUpload(dataByteArray, extension); - QObject::connect(upload, &AssetUpload::finished, this, [callback, extension](AssetUpload* upload, const QString& hash) mutable { - if (callback.isFunction()) { - QString url = "atp://" + hash + "." + extension; - QScriptValueList args { url }; - callback.call(QScriptValue(), args); - } - }); + if (upload) { + QObject::connect(upload, &AssetUpload::finished, this, [callback, extension](AssetUpload* upload, const QString& hash) mutable { + if (callback.isFunction()) { + QString url = "atp://" + hash + "." + extension; + QScriptValueList args { url }; + callback.call(QScriptValue(), args); + } + }); - // start the upload now - upload->start(); + // start the upload now + upload->start(); + } } void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) { @@ -405,7 +407,10 @@ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callb return; } - _pendingRequests << assetRequest; + { + QWriteLocker locker(&_lock); + _pendingRequests << assetRequest; + } connect(assetRequest, &AssetRequest::finished, [this, callback](AssetRequest* request) mutable { Q_ASSERT(request->getState() == AssetRequest::Finished); @@ -419,7 +424,11 @@ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callb } request->deleteLater(); - _pendingRequests.remove(request); + + { + QWriteLocker locker(&_lock); + _pendingRequests.remove(request); + } }); assetRequest->start(); diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index f1bb210614..a194db225b 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -77,6 +77,7 @@ public: Q_INVOKABLE void uploadData(QString data, QString extension, QScriptValue callback); Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete); protected: + mutable QReadWriteLock _lock; QSet _pendingRequests; }; From fb90ffec5c6fdebb9997c507819e65be9455c766 Mon Sep 17 00:00:00 2001 From: EdgarPironti Date: Tue, 17 Nov 2015 19:42:18 -0800 Subject: [PATCH 31/99] Migration of AC playback --- examples/acScripts/playbackAgents.js | 141 +++++++++++++++ examples/acScripts/playbackMaster.js | 259 +++++++++++++++++++++++++++ 2 files changed, 400 insertions(+) create mode 100644 examples/acScripts/playbackAgents.js create mode 100644 examples/acScripts/playbackMaster.js diff --git a/examples/acScripts/playbackAgents.js b/examples/acScripts/playbackAgents.js new file mode 100644 index 0000000000..9912e00010 --- /dev/null +++ b/examples/acScripts/playbackAgents.js @@ -0,0 +1,141 @@ +// +// playbackAgents.js +// acScripts +// +// Created by Edgar Pironti on 11/17/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// Set the following variables to the values needed +var channel = "PlaybackChannel1"; +var clip_url = null; +var playFromCurrentLocation = true; +var useDisplayName = true; +var useAttachments = true; +var useAvatarModel = true; + +// ID of the agent. Two agents can't have the same ID. +var id = 0; + +// Set position/orientation/scale here if playFromCurrentLocation is true +Avatar.position = { x:0, y: 0, z: 0 }; +Avatar.orientation = Quat.fromPitchYawRollDegrees(0, 0, 0); +Avatar.scale = 1.0; + +var totalTime = 0; +var subscribed = false; +var WAIT_FOR_AUDIO_MIXER = 1; + +// Script. DO NOT MODIFY BEYOND THIS LINE. +var DO_NOTHING = 0; +var PLAY = 1; +var PLAY_LOOP = 2; +var STOP = 3; +var SHOW = 4; +var HIDE = 5; +var LOAD = 6; + +Recording.setPlayFromCurrentLocation(playFromCurrentLocation); +Recording.setPlayerUseDisplayName(useDisplayName); +Recording.setPlayerUseAttachments(useAttachments); +Recording.setPlayerUseHeadModel(false); +Recording.setPlayerUseSkeletonModel(useAvatarModel); + +function getAction(channel, message, senderID) { + + if(subscribed) { + var command = JSON.parse(message); + print("I'm the agent " + id + " and I received this: ID: " + command.id_key + " Action: " + command.action_key + " URL: " + command.clip_url_key); + + if (command.id_key == id || command.id_key == -1) { + if (command.action_key === 6) + clip_url = command.clip_url_key; + + // If the id is -1 (broadcast) and the action is 6, in the url should be the performance file + // with all the clips recorded in a session (not just the single clip url). + // It has to be computed here in order to retrieve the url for the single agent. + // Checking the id we can assign the correct url to the correct agent. + + action = command.action_key; + } else { + action = DO_NOTHING; + } + + switch(action) { + case PLAY: + print("Play"); + if (!Agent.isAvatar) { + Agent.isAvatar = true; + } + if (!Recording.isPlaying()) { + Recording.startPlaying(); + } + Recording.setPlayerLoop(false); + break; + case PLAY_LOOP: + print("Play loop"); + if (!Agent.isAvatar) { + Agent.isAvatar = true; + } + if (!Recording.isPlaying()) { + Recording.startPlaying(); + } + Recording.setPlayerLoop(true); + break; + case STOP: + print("Stop"); + if (Recording.isPlaying()) { + Recording.stopPlaying(); + } + break; + case SHOW: + print("Show"); + if (!Agent.isAvatar) { + Agent.isAvatar = true; + } + break; + case HIDE: + print("Hide"); + if (Recording.isPlaying()) { + Recording.stopPlaying(); + } + Agent.isAvatar = false; + break; + case LOAD: + print("Load"); + if(clip_url !== null) { + Recording.loadRecording(clip_url); + } + break; + case DO_NOTHING: + break; + default: + print("Unknown action: " + action); + break; + + } + + if (Recording.isPlaying()) { + Recording.play(); + } + } +} + + +function update(deltaTime) { + + totalTime += deltaTime; + + if (totalTime > WAIT_FOR_AUDIO_MIXER && !subscribed) { + Messages.subscribe(channel); + subscribed = true; + print("I'm the agent and I am ready to receive!") + } +} + +Script.update.connect(update); +Messages.messageReceived.connect(getAction); + diff --git a/examples/acScripts/playbackMaster.js b/examples/acScripts/playbackMaster.js new file mode 100644 index 0000000000..f1f1b44768 --- /dev/null +++ b/examples/acScripts/playbackMaster.js @@ -0,0 +1,259 @@ +// +// playbackMaster.js +// acScripts +// +// Created by Edgar Pironti on 11/17/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; + + +var ac_number = 1; // This is the default number of ACs. Their ID need to be unique and between 0 (included) and ac_number (excluded) +var names = new Array(); // It is possible to specify the name of the ACs in this array. ACs names ordered by IDs (Default name is "ACx", x = ID + 1)) +var channel = "PlaybackChannel1"; +var subscribed = false; +var clip_url = null; +var input_text = null; + +// Script. DO NOT MODIFY BEYOND THIS LINE. +Script.include("../libraries/toolBars.js"); + +var DO_NOTHING = 0; +var PLAY = 1; +var PLAY_LOOP = 2; +var STOP = 3; +var SHOW = 4; +var HIDE = 5; +var LOAD = 6; + +var windowDimensions = Controller.getViewportDimensions(); +var TOOL_ICON_URL = HIFI_PUBLIC_BUCKET + "images/tools/"; +var ALPHA_ON = 1.0; +var ALPHA_OFF = 0.7; +var COLOR_TOOL_BAR = { red: 0, green: 0, blue: 0 }; +var COLOR_MASTER = { red: 0, green: 0, blue: 0 }; +var TEXT_HEIGHT = 12; +var TEXT_MARGIN = 3; + +var toolBars = new Array(); +var nameOverlays = new Array(); +var onOffIcon = new Array(); +var playIcon = new Array(); +var playLoopIcon = new Array(); +var stopIcon = new Array(); +var loadIcon = new Array(); + +setupPlayback(); + +function setupPlayback() { + ac_number = Window.prompt("Insert number of agents: ","1"); + if (ac_number === "" || ac_number === null) + ac_number = 1; + Messages.subscribe(channel); + subscribed = true; + setupToolBars(); +} + +function setupToolBars() { + if (toolBars.length > 0) { + print("Multiple calls to Recorder.js:setupToolBars()"); + return; + } + Tool.IMAGE_HEIGHT /= 2; + Tool.IMAGE_WIDTH /= 2; + + for (i = 0; i <= ac_number; i++) { + toolBars.push(new ToolBar(0, 0, ToolBar.HORIZONTAL)); + toolBars[i].setBack((i == ac_number) ? COLOR_MASTER : COLOR_TOOL_BAR, ALPHA_OFF); + + onOffIcon.push(toolBars[i].addTool({ + imageURL: TOOL_ICON_URL + "ac-on-off.svg", + subImage: { x: 0, y: 0, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, + x: 0, y: 0, + width: Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT, + alpha: ALPHA_ON, + visible: true + }, true, true)); + + playIcon[i] = toolBars[i].addTool({ + imageURL: TOOL_ICON_URL + "play.svg", + subImage: { x: 0, y: 0, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, + width: Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT, + alpha: ALPHA_OFF, + visible: true + }, false); + + var playLoopWidthFactor = 1.65; + playLoopIcon[i] = toolBars[i].addTool({ + imageURL: TOOL_ICON_URL + "play-and-loop.svg", + subImage: { x: 0, y: 0, width: playLoopWidthFactor * Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, + width: playLoopWidthFactor * Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT, + alpha: ALPHA_OFF, + visible: true + }, false); + + stopIcon[i] = toolBars[i].addTool({ + imageURL: TOOL_ICON_URL + "recording-stop.svg", + width: Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT, + alpha: ALPHA_OFF, + visible: true + }, false); + + loadIcon[i] = toolBars[i].addTool({ + imageURL: TOOL_ICON_URL + "recording-upload.svg", + width: Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT, + alpha: ALPHA_OFF, + visible: true + }, false); + + nameOverlays.push(Overlays.addOverlay("text", { + backgroundColor: { red: 0, green: 0, blue: 0 }, + font: { size: TEXT_HEIGHT }, + text: (i == ac_number) ? "Master" : i + ". " + + ((i < names.length) ? names[i] : + "AC" + i), + x: 0, y: 0, + width: toolBars[i].width + ToolBar.SPACING, + height: TEXT_HEIGHT + TEXT_MARGIN, + leftMargin: TEXT_MARGIN, + topMargin: TEXT_MARGIN, + alpha: ALPHA_OFF, + backgroundAlpha: ALPHA_OFF, + visible: true + })); + } +} + +function sendCommand(id, action) { + + if (action === SHOW) { + toolBars[id].selectTool(onOffIcon[id], false); + toolBars[id].setAlpha(ALPHA_ON, playIcon[id]); + toolBars[id].setAlpha(ALPHA_ON, playLoopIcon[id]); + toolBars[id].setAlpha(ALPHA_ON, stopIcon[id]); + toolBars[id].setAlpha(ALPHA_ON, loadIcon[id]); + } else if (action === HIDE) { + toolBars[id].selectTool(onOffIcon[id], true); + toolBars[id].setAlpha(ALPHA_OFF, playIcon[id]); + toolBars[id].setAlpha(ALPHA_OFF, playLoopIcon[id]); + toolBars[id].setAlpha(ALPHA_OFF, stopIcon[id]); + toolBars[id].setAlpha(ALPHA_OFF, loadIcon[id]); + } else if (toolBars[id].toolSelected(onOffIcon[id])) { + return; + } + + if (id == (toolBars.length - 1)) + id = -1; // Master command becomes broadcast. + + var message = { + id_key: id, + action_key: action, + clip_url_key: clip_url + }; + + if(subscribed){ + Messages.sendMessage(channel, JSON.stringify(message)); + print("Message sent!"); + } +} + +function mousePressEvent(event) { + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + + // Check master control + var i = toolBars.length - 1; + if (onOffIcon[i] === toolBars[i].clicked(clickedOverlay, false)) { + if (toolBars[i].toolSelected(onOffIcon[i])) { + sendCommand(i, SHOW); + } else { + sendCommand(i, HIDE); + } + } else if (playIcon[i] === toolBars[i].clicked(clickedOverlay, false)) { + sendCommand(i, PLAY); + } else if (playLoopIcon[i] === toolBars[i].clicked(clickedOverlay, false)) { + sendCommand(i, PLAY_LOOP); + } else if (stopIcon[i] === toolBars[i].clicked(clickedOverlay, false)) { + sendCommand(i, STOP); + } else if (loadIcon[i] === toolBars[i].clicked(clickedOverlay, false)) { + input_text = Window.prompt("Insert the url of the clip: ",""); + if (!(input_text === "" || input_text === null)) { + clip_url = input_text; + sendCommand(i, LOAD); + } + } else { + // Check individual controls + for (i = 0; i < ac_number; i++) { + if (onOffIcon[i] === toolBars[i].clicked(clickedOverlay, false)) { + if (toolBars[i].toolSelected(onOffIcon[i], false)) { + sendCommand(i, SHOW); + } else { + sendCommand(i, HIDE); + } + } else if (playIcon[i] === toolBars[i].clicked(clickedOverlay, false)) { + sendCommand(i, PLAY); + } else if (playLoopIcon[i] === toolBars[i].clicked(clickedOverlay, false)) { + sendCommand(i, PLAY_LOOP); + } else if (stopIcon[i] === toolBars[i].clicked(clickedOverlay, false)) { + sendCommand(i, STOP); + } else if (loadIcon[i] === toolBars[i].clicked(clickedOverlay, false)) { + input_text = Window.prompt("Insert the url of the clip: ",""); + if (!(input_text === "" || input_text === null)) { + clip_url = input_text; + sendCommand(i, LOAD); + } + } else { + + } + } + } +} + +function moveUI() { + var textSize = TEXT_HEIGHT + 2 * TEXT_MARGIN; + var relative = { x: 70, y: 75 + (ac_number) * (Tool.IMAGE_HEIGHT + ToolBar.SPACING + textSize) }; + + for (i = 0; i <= ac_number; i++) { + toolBars[i].move(relative.x, + windowDimensions.y - relative.y + + i * (Tool.IMAGE_HEIGHT + ToolBar.SPACING + textSize)); + + Overlays.editOverlay(nameOverlays[i], { + x: toolBars[i].x - ToolBar.SPACING, + y: toolBars[i].y - textSize + }); + } +} + +function update() { + var newDimensions = Controller.getViewportDimensions(); + if (windowDimensions.x != newDimensions.x || + windowDimensions.y != newDimensions.y) { + windowDimensions = newDimensions; + moveUI(); + } +} + +function scriptEnding() { + for (i = 0; i <= ac_number; i++) { + toolBars[i].cleanup(); + Overlays.deleteOverlay(nameOverlays[i]); + } + + if(subscribed) + Messages.unsubscribe(channel); +} + +Controller.mousePressEvent.connect(mousePressEvent); +Script.update.connect(update); +Script.scriptEnding.connect(scriptEnding); + +moveUI(); \ No newline at end of file From d75209cb3783c071c18d097db199345612e86ea2 Mon Sep 17 00:00:00 2001 From: EdgarPironti Date: Wed, 18 Nov 2015 12:30:39 -0800 Subject: [PATCH 32/99] Fixes to clip_url overwriting --- examples/acScripts/playbackAgents.js | 2 ++ examples/acScripts/playbackMaster.js | 1 + 2 files changed, 3 insertions(+) diff --git a/examples/acScripts/playbackAgents.js b/examples/acScripts/playbackAgents.js index 9912e00010..93b563d497 100644 --- a/examples/acScripts/playbackAgents.js +++ b/examples/acScripts/playbackAgents.js @@ -60,6 +60,8 @@ function getAction(channel, message, senderID) { // Checking the id we can assign the correct url to the correct agent. action = command.action_key; + print("That command was for me!"); + print("My clip is: " + clip_url); } else { action = DO_NOTHING; } diff --git a/examples/acScripts/playbackMaster.js b/examples/acScripts/playbackMaster.js index f1f1b44768..e3448c0256 100644 --- a/examples/acScripts/playbackMaster.js +++ b/examples/acScripts/playbackMaster.js @@ -163,6 +163,7 @@ function sendCommand(id, action) { if(subscribed){ Messages.sendMessage(channel, JSON.stringify(message)); print("Message sent!"); + clip_url = null; } } From e267926725fa9e03f6b398398eb3212946d3588b Mon Sep 17 00:00:00 2001 From: EdgarPironti Date: Thu, 19 Nov 2015 20:03:27 -0800 Subject: [PATCH 33/99] JSON computing --- examples/acScripts/playbackAgents.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/examples/acScripts/playbackAgents.js b/examples/acScripts/playbackAgents.js index 93b563d497..c50c727277 100644 --- a/examples/acScripts/playbackAgents.js +++ b/examples/acScripts/playbackAgents.js @@ -51,13 +51,25 @@ function getAction(channel, message, senderID) { print("I'm the agent " + id + " and I received this: ID: " + command.id_key + " Action: " + command.action_key + " URL: " + command.clip_url_key); if (command.id_key == id || command.id_key == -1) { - if (command.action_key === 6) + if (command.action_key === 6) { clip_url = command.clip_url_key; - - // If the id is -1 (broadcast) and the action is 6, in the url should be the performance file - // with all the clips recorded in a session (not just the single clip url). - // It has to be computed here in order to retrieve the url for the single agent. - // Checking the id we can assign the correct url to the correct agent. + + // If the id is -1 (broadcast) and the action is 6, in the url should be the performance file + // with all the clips recorded in a session (not just the single clip url). + // It has to be computed here in order to retrieve the url for the single agent. + // Checking the id we can assign the correct url to the correct agent. + + if (command.id_key == -1) { + Assets.downloadData(clip_url, function (data) { + var myJSONObject = JSON.parse(data); + var hash = myJSONObject.results[id].hashATP; + }); + + Assets.downloadData(hash, function (data) { + clip_url = JSON.parse(data); + }); + } + } action = command.action_key; print("That command was for me!"); From 5adcbcaf5f91228fcd6881f143c3a39e2f389752 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 19 Nov 2015 20:23:50 -0800 Subject: [PATCH 34/99] Support writing a clip to a qbytearray --- libraries/recording/src/recording/Clip.cpp | 82 ++++++++++++++++++- libraries/recording/src/recording/Clip.h | 6 ++ .../recording/src/recording/impl/FileClip.cpp | 64 +-------------- .../src/recording/impl/PointerClip.cpp | 7 +- .../src/recording/impl/PointerClip.h | 3 - 5 files changed, 90 insertions(+), 72 deletions(-) diff --git a/libraries/recording/src/recording/Clip.cpp b/libraries/recording/src/recording/Clip.cpp index abe66ccb2e..1451724f23 100644 --- a/libraries/recording/src/recording/Clip.cpp +++ b/libraries/recording/src/recording/Clip.cpp @@ -13,6 +13,11 @@ #include "impl/FileClip.h" #include "impl/BufferClip.h" +#include +#include +#include +#include + using namespace recording; Clip::Pointer Clip::fromFile(const QString& filePath) { @@ -27,6 +32,15 @@ void Clip::toFile(const QString& filePath, const Clip::ConstPointer& clip) { FileClip::write(filePath, clip->duplicate()); } +QByteArray Clip::toBuffer(const Clip::ConstPointer& clip) { + QBuffer buffer; + if (buffer.open(QFile::Truncate | QFile::WriteOnly)) { + clip->duplicate()->write(buffer); + buffer.close(); + } + return buffer.data(); +} + Clip::Pointer Clip::newClip() { return std::make_shared(); } @@ -37,4 +51,70 @@ void Clip::seek(float offset) { float Clip::position() const { return Frame::frameTimeToSeconds(positionFrameTime()); -}; +} + +// FIXME move to frame? +bool writeFrame(QIODevice& output, const Frame& frame, bool compressed = true) { + if (frame.type == Frame::TYPE_INVALID) { + qWarning() << "Attempting to write invalid frame"; + return true; + } + + auto written = output.write((char*)&(frame.type), sizeof(FrameType)); + if (written != sizeof(FrameType)) { + return false; + } + //qDebug() << "Writing frame with time offset " << frame.timeOffset; + written = output.write((char*)&(frame.timeOffset), sizeof(Frame::Time)); + if (written != sizeof(Frame::Time)) { + return false; + } + QByteArray frameData = frame.data; + if (compressed) { + frameData = qCompress(frameData); + } + + uint16_t dataSize = frameData.size(); + written = output.write((char*)&dataSize, sizeof(FrameSize)); + if (written != sizeof(uint16_t)) { + return false; + } + + if (dataSize != 0) { + written = output.write(frameData); + if (written != dataSize) { + return false; + } + } + return true; +} + +const QString Clip::FRAME_TYPE_MAP = QStringLiteral("frameTypes"); +const QString Clip::FRAME_COMREPSSION_FLAG = QStringLiteral("compressed"); + +bool Clip::write(QIODevice& output) { + auto frameTypes = Frame::getFrameTypes(); + QJsonObject frameTypeObj; + for (const auto& frameTypeName : frameTypes.keys()) { + frameTypeObj[frameTypeName] = frameTypes[frameTypeName]; + } + + QJsonObject rootObject; + rootObject.insert(FRAME_TYPE_MAP, frameTypeObj); + // Always mark new files as compressed + rootObject.insert(FRAME_COMREPSSION_FLAG, true); + QByteArray headerFrameData = QJsonDocument(rootObject).toBinaryData(); + // Never compress the header frame + if (!writeFrame(output, Frame({ Frame::TYPE_HEADER, 0, headerFrameData }), false)) { + return false; + } + + seek(0); + + for (auto frame = nextFrame(); frame; frame = nextFrame()) { + if (!writeFrame(output, *frame)) { + return false; + } + } + return true; +} diff --git a/libraries/recording/src/recording/Clip.h b/libraries/recording/src/recording/Clip.h index 722fadf0b2..1fdad39da7 100644 --- a/libraries/recording/src/recording/Clip.h +++ b/libraries/recording/src/recording/Clip.h @@ -47,10 +47,16 @@ public: virtual void skipFrame() = 0; virtual void addFrame(FrameConstPointer) = 0; + bool write(QIODevice& output); + static Pointer fromFile(const QString& filePath); static void toFile(const QString& filePath, const ConstPointer& clip); + static QByteArray toBuffer(const ConstPointer& clip); static Pointer newClip(); + static const QString FRAME_TYPE_MAP; + static const QString FRAME_COMREPSSION_FLAG; + protected: friend class WrapperClip; using Mutex = std::recursive_mutex; diff --git a/libraries/recording/src/recording/impl/FileClip.cpp b/libraries/recording/src/recording/impl/FileClip.cpp index ce2705a76c..b153f5aa4a 100644 --- a/libraries/recording/src/recording/impl/FileClip.cpp +++ b/libraries/recording/src/recording/impl/FileClip.cpp @@ -11,8 +11,6 @@ #include #include -#include -#include #include @@ -40,41 +38,7 @@ QString FileClip::getName() const { return _file.fileName(); } -// FIXME move to frame? -bool writeFrame(QIODevice& output, const Frame& frame, bool compressed = true) { - if (frame.type == Frame::TYPE_INVALID) { - qWarning() << "Attempting to write invalid frame"; - return true; - } - auto written = output.write((char*)&(frame.type), sizeof(FrameType)); - if (written != sizeof(FrameType)) { - return false; - } - //qDebug() << "Writing frame with time offset " << frame.timeOffset; - written = output.write((char*)&(frame.timeOffset), sizeof(Frame::Time)); - if (written != sizeof(Frame::Time)) { - return false; - } - QByteArray frameData = frame.data; - if (compressed) { - frameData = qCompress(frameData); - } - - uint16_t dataSize = frameData.size(); - written = output.write((char*)&dataSize, sizeof(FrameSize)); - if (written != sizeof(uint16_t)) { - return false; - } - - if (dataSize != 0) { - written = output.write(frameData); - if (written != dataSize) { - return false; - } - } - return true; -} bool FileClip::write(const QString& fileName, Clip::Pointer clip) { // FIXME need to move this to a different thread @@ -90,33 +54,7 @@ bool FileClip::write(const QString& fileName, Clip::Pointer clip) { } Finally closer([&] { outputFile.close(); }); - { - auto frameTypes = Frame::getFrameTypes(); - QJsonObject frameTypeObj; - for (const auto& frameTypeName : frameTypes.keys()) { - frameTypeObj[frameTypeName] = frameTypes[frameTypeName]; - } - - QJsonObject rootObject; - rootObject.insert(FRAME_TYPE_MAP, frameTypeObj); - // Always mark new files as compressed - rootObject.insert(FRAME_COMREPSSION_FLAG, true); - QByteArray headerFrameData = QJsonDocument(rootObject).toBinaryData(); - // Never compress the header frame - if (!writeFrame(outputFile, Frame({ Frame::TYPE_HEADER, 0, headerFrameData }), false)) { - return false; - } - - } - - clip->seek(0); - for (auto frame = clip->nextFrame(); frame; frame = clip->nextFrame()) { - if (!writeFrame(outputFile, *frame)) { - return false; - } - } - outputFile.close(); - return true; + return clip->write(outputFile); } FileClip::~FileClip() { diff --git a/libraries/recording/src/recording/impl/PointerClip.cpp b/libraries/recording/src/recording/impl/PointerClip.cpp index 48132c066d..6f74391b4b 100644 --- a/libraries/recording/src/recording/impl/PointerClip.cpp +++ b/libraries/recording/src/recording/impl/PointerClip.cpp @@ -23,16 +23,13 @@ using namespace recording; -const QString PointerClip::FRAME_TYPE_MAP = QStringLiteral("frameTypes"); -const QString PointerClip::FRAME_COMREPSSION_FLAG = QStringLiteral("compressed"); - using FrameTranslationMap = QMap; FrameTranslationMap parseTranslationMap(const QJsonDocument& doc) { FrameTranslationMap results; auto headerObj = doc.object(); - if (headerObj.contains(PointerClip::FRAME_TYPE_MAP)) { - auto frameTypeObj = headerObj[PointerClip::FRAME_TYPE_MAP].toObject(); + if (headerObj.contains(Clip::FRAME_TYPE_MAP)) { + auto frameTypeObj = headerObj[Clip::FRAME_TYPE_MAP].toObject(); auto currentFrameTypes = Frame::getFrameTypes(); for (auto frameTypeName : frameTypeObj.keys()) { qDebug() << frameTypeName; diff --git a/libraries/recording/src/recording/impl/PointerClip.h b/libraries/recording/src/recording/impl/PointerClip.h index 5a7a3499fe..f5c0dd6bc4 100644 --- a/libraries/recording/src/recording/impl/PointerClip.h +++ b/libraries/recording/src/recording/impl/PointerClip.h @@ -44,9 +44,6 @@ public: // FIXME move to frame? static const qint64 MINIMUM_FRAME_SIZE = sizeof(FrameType) + sizeof(Frame::Time) + sizeof(FrameSize); - static const QString FRAME_TYPE_MAP; - static const QString FRAME_COMREPSSION_FLAG; - protected: void reset(); virtual FrameConstPointer readFrame(size_t index) const override; From c56a3b6d2228540b12c5c7ed40c33e39b0f9dd74 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Fri, 20 Nov 2015 09:56:10 -0600 Subject: [PATCH 35/99] Checkpoint - turning quazip into external --- CMakeLists.txt | 2 + cmake/externals/quazip/CMakeLists.txt | 25 +++++++++++ cmake/macros/TargetQuazip.cmake | 13 ++++++ cmake/modules/FindQuaZip.cmake | 34 +++++++-------- stack-manager/CMakeLists.txt | 60 +++++++-------------------- stack-manager/src/BackgroundProcess.h | 2 +- stack-manager/src/Downloader.cpp | 4 +- 7 files changed, 75 insertions(+), 65 deletions(-) create mode 100644 cmake/externals/quazip/CMakeLists.txt create mode 100644 cmake/macros/TargetQuazip.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index d1c69e4f6b..e5807146ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,6 +202,8 @@ if (NOT ANDROID) set_target_properties(ice-server PROPERTIES FOLDER "Apps") add_subdirectory(interface) set_target_properties(interface PROPERTIES FOLDER "Apps") + add_subdirectory(stack-manager) + set_target_properties(stack-manager PROPERTIES FOLER "Apps") add_subdirectory(tests) add_subdirectory(plugins) add_subdirectory(tools) diff --git a/cmake/externals/quazip/CMakeLists.txt b/cmake/externals/quazip/CMakeLists.txt new file mode 100644 index 0000000000..061511d3dc --- /dev/null +++ b/cmake/externals/quazip/CMakeLists.txt @@ -0,0 +1,25 @@ +set(EXTERNAL_NAME quazip) + +include(ExternalProject) +ExternalProject_Add( + ${EXTERNAL_NAME} + URL https://s3-us-west-1.amazonaws.com/hifi-production/dependencies/quazip-0.6.2.zip + URL_MD5 514851970f1a14d815bdc3ad6267af4d + BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_PREFIX_PATH=$ENV{QT_CMAKE_PREFIX_PATH} + LOG_DOWNLOAD 1 + LOG_CONFIGURE 1 + LOG_BUILD 1 +) + +# Hide this external target (for ide users) +set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") + +ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) + +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE PATH "List of QuaZip include directories") + +if (APPLE) + set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${INSTALL_DIR}/lib/libquazip.1.0.0.dylib CACHE FILEPATH "List of QuaZip libraries") +endif () diff --git a/cmake/macros/TargetQuazip.cmake b/cmake/macros/TargetQuazip.cmake new file mode 100644 index 0000000000..1a06ab612a --- /dev/null +++ b/cmake/macros/TargetQuazip.cmake @@ -0,0 +1,13 @@ +# +# Copyright 2015 High Fidelity, Inc. +# Created by Leonardo Murillo on 2015/11/20 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# +macro(TARGET_QUAZIP) + add_dependency_external_projects(quazip) + find_package(QUAZIP REQUIRED) + target_include_directories(${TARGET_NAME} PUBLIC ${QUAZIP_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) +endmacro() diff --git a/cmake/modules/FindQuaZip.cmake b/cmake/modules/FindQuaZip.cmake index 85dda71684..5fbc8e1ac4 100644 --- a/cmake/modules/FindQuaZip.cmake +++ b/cmake/modules/FindQuaZip.cmake @@ -12,21 +12,21 @@ # QUAZIP_LIBRARIES - List of QuaZip libraries # QUAZIP_ZLIB_INCLUDE_DIR - The include dir of zlib headers +include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") +hifi_library_search_hints("quazip") -IF (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) - SET(QUAZIP_FOUND TRUE) -ELSE (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) - SET(QUAZIP_SEARCH_DIRS "$ENV{HIFI_LIB_DIR}/QuaZip") - IF (WIN32) - FIND_PATH(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES include/quazip HINTS ${QUAZIP_SEARCH_DIRS}) - FIND_LIBRARY(QUAZIP_LIBRARIES NAMES quazip PATH_SUFFIXES lib HINTS ${QUAZIP_SEARCH_DIRS}) - ELSEIF(APPLE) - FIND_PATH(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES include/quazip HINTS ${QUAZIP_SEARCH_DIRS}) - FIND_LIBRARY(QUAZIP_LIBRARIES NAMES quazip PATH_SUFFIXES lib HINTS ${QUAZIP_SEARCH_DIRS}) - ELSE () - FIND_PATH(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES quazip HINTS ${QUAZIP_SEARCH_DIRS}) - FIND_LIBRARY(QUAZIP_LIBRARIES NAMES quazip-qt5 PATH_SUFFIXES lib HINTS ${QUAZIP_SEARCH_DIRS}) - ENDIF () - INCLUDE(FindPackageHandleStandardArgs) - find_package_handle_standard_args(QUAZIP DEFAULT_MSG QUAZIP_INCLUDE_DIRS QUAZIP_LIBRARIES) -ENDIF (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) +if (WIN32) + find_path(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES include/quazip HINTS ${QUAZIP_SEARCH_DIRS}) +# find_library(QUAZIP_LIBRARIES NAMES quazip PATH_SUFFIXES lib HINTS ${QUAZIP_SEARCH_DIRS}) +elseif (APPLE) + find_path(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES include/quazip HINTS ${QUAZIP_SEARCH_DIRS}) +# find_library(QUAZIP_LIBRARIES NAMES quazip PATH_SUFFIXES lib HINTS ${QUAZIP_SEARCH_DIRS}) +else () + find_path(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES quazip HINTS ${QUAZIP_SEARCH_DIRS}) +# find_library(QUAZIP_LIBRARIES NAMES quazip-qt5 PATH_SUFFIXES lib HINTS ${QUAZIP_SEARCH_DIRS}) +endif () + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(QUAZIP DEFAULT_MSG QUAZIP_INCLUDE_DIRS) + +mark_as_advanced(QUAZIP_INCLUDE_DIRS QUAZIP_SEARCH_DIRS) diff --git a/stack-manager/CMakeLists.txt b/stack-manager/CMakeLists.txt index 01dfc1c73f..a4071295e1 100644 --- a/stack-manager/CMakeLists.txt +++ b/stack-manager/CMakeLists.txt @@ -1,43 +1,10 @@ -cmake_minimum_required(VERSION 2.8.11) - -if (POLICY CMP0028) - cmake_policy(SET CMP0028 OLD) -endif () - -set(TARGET_NAME "StackManager") - -project(${TARGET_NAME}) - -set(CMAKE_AUTOMOC ON) - -if (NOT QT_CMAKE_PREFIX_PATH) - set(QT_CMAKE_PREFIX_PATH $ENV{QT_CMAKE_PREFIX_PATH}) -endif () - -set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${QT_CMAKE_PREFIX_PATH}) - -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/") - -find_package(Qt5Widgets REQUIRED) -find_package(Qt5Gui REQUIRED) -find_package(Qt5Svg REQUIRED) -find_package(Qt5Core REQUIRED) -find_package(Qt5Network REQUIRED) -find_package(Qt5WebKitWidgets REQUIRED) -find_package(QuaZip REQUIRED) +set(TARGET_NAME "stack-manager") +setup_hifi_project(Widgets Gui Svg Core Network WebKitWidgets) if (WIN32) find_package(ZLIB REQUIRED) endif () -include_directories( - ${QUAZIP_INCLUDE_DIRS} - ${ZLIB_INCLUDE_DIRS} - src - src/ui - ${PROJECT_BINARY_DIR}/includes -) - if (DEFINED ENV{JOB_ID}) set(PR_BUILD "false") set(BUILD_SEQ $ENV{JOB_ID}) @@ -54,12 +21,17 @@ else () endif () configure_file(src/StackManagerVersion.h.in "${PROJECT_BINARY_DIR}/includes/StackManagerVersion.h") +include_directories( + ${PROJECT_BINARY_DIR}/includes + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/src/ui + ${QUAZIP_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} +) -file(GLOB SRCS "src/*.cpp" "src/ui/*.cpp") -file(GLOB HEADERS "src/*.h" "src/ui/*.h" "${PROJECT_BINARY_DIR}/includes/*.h") -file(GLOB QT_RES_FILES "src/*.qrc") -qt5_add_resources(QT_RES "${QT_RES_FILES}") -set(SM_SRCS ${QT_RES} ${SRCS} ${HEADERS}) +target_quazip() + +target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) if (APPLE) set(CMAKE_OSX_DEPLOYMENT_TARGET 10.8) @@ -68,13 +40,11 @@ if (APPLE) set(MACOSX_BUNDLE_ICON_FILE icon.icns) set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/assets/icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set(SM_SRCS ${SM_SRCS} "${CMAKE_CURRENT_SOURCE_DIR}/assets/icon.icns") - add_executable(${TARGET_NAME} MACOSX_BUNDLE ${SM_SRCS}) +# add_executable(${TARGET_NAME} MACOSX_BUNDLE ${SM_SRCS}) else () if (WIN32) - add_executable(${TARGET_NAME} WIN32 ${SM_SRCS} windows_icon.rc) +# add_executable(${TARGET_NAME} WIN32 ${SM_SRCS} windows_icon.rc) else () - add_executable(${TARGET_NAME} ${SM_SRCS}) +# add_executable(${TARGET_NAME} ${SM_SRCS}) endif () endif () - -target_link_libraries(${TARGET_NAME} Qt5::Core Qt5::Gui Qt5::Svg Qt5::Network Qt5::Widgets Qt5::WebKitWidgets ${QUAZIP_LIBRARIES} ${ZLIB_LIBRARIES}) diff --git a/stack-manager/src/BackgroundProcess.h b/stack-manager/src/BackgroundProcess.h index 1b3ff85758..fd652ba73e 100644 --- a/stack-manager/src/BackgroundProcess.h +++ b/stack-manager/src/BackgroundProcess.h @@ -9,7 +9,7 @@ #ifndef hifi_BackgroundProcess_h #define hifi_BackgroundProcess_h -#include "LogViewer.h" +#include #include #include diff --git a/stack-manager/src/Downloader.cpp b/stack-manager/src/Downloader.cpp index 42ae8ba091..db3b397b96 100644 --- a/stack-manager/src/Downloader.cpp +++ b/stack-manager/src/Downloader.cpp @@ -9,8 +9,8 @@ #include "Downloader.h" #include "GlobalData.h" -#include -#include +#include +#include #include #include From 6f3d818121d2cf3edafc7dabd28bbe5680860ede Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Fri, 20 Nov 2015 10:56:47 -0600 Subject: [PATCH 36/99] Checkpoint - fixing ID in dylib --- cmake/externals/quazip/CMakeLists.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cmake/externals/quazip/CMakeLists.txt b/cmake/externals/quazip/CMakeLists.txt index 061511d3dc..e6a8a0fa2a 100644 --- a/cmake/externals/quazip/CMakeLists.txt +++ b/cmake/externals/quazip/CMakeLists.txt @@ -6,14 +6,17 @@ ExternalProject_Add( URL https://s3-us-west-1.amazonaws.com/hifi-production/dependencies/quazip-0.6.2.zip URL_MD5 514851970f1a14d815bdc3ad6267af4d BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build - CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_PREFIX_PATH=$ENV{QT_CMAKE_PREFIX_PATH} + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_PREFIX_PATH=$ENV{QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=/lib LOG_DOWNLOAD 1 LOG_CONFIGURE 1 LOG_BUILD 1 ) # Hide this external target (for ide users) -set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") +set_target_properties(${EXTERNAL_NAME} PROPERTIES + FOLDER "hidden/externals" + INSTALL_NAME_DIR ${INSTALL_DIR}/lib + BUILD_WITH_INSTALL_RPATH True) ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) @@ -21,5 +24,5 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE PATH "List of QuaZip include directories") if (APPLE) - set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${INSTALL_DIR}/lib/libquazip.1.0.0.dylib CACHE FILEPATH "List of QuaZip libraries") + set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${INSTALL_DIR}/lib/libquazip.1.0.0.dylib CACHE PATH "List of QuaZip libraries") endif () From abf169ebd9fe7328883efc07a6af6dad7e599ced Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 Nov 2015 09:03:21 -0800 Subject: [PATCH 37/99] fix senderID in messages, dry up code --- .../src/messages/MessagesMixer.cpp | 43 +++------------ libraries/networking/src/MessagesClient.cpp | 55 ++++++++++++------- libraries/networking/src/MessagesClient.h | 12 ++-- 3 files changed, 51 insertions(+), 59 deletions(-) diff --git a/assignment-client/src/messages/MessagesMixer.cpp b/assignment-client/src/messages/MessagesMixer.cpp index d3662f3fb5..a811631617 100644 --- a/assignment-client/src/messages/MessagesMixer.cpp +++ b/assignment-client/src/messages/MessagesMixer.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -24,9 +25,7 @@ const QString MESSAGES_MIXER_LOGGING_NAME = "messages-mixer"; MessagesMixer::MessagesMixer(NLPacket& packet) : ThreadedAssignment(packet) { - // make sure we hear about node kills so we can tell the other nodes connect(DependencyManager::get().data(), &NodeList::nodeKilled, this, &MessagesMixer::nodeKilled); - auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerMessageListener(PacketType::MessagesData, this, "handleMessages"); packetReceiver.registerMessageListener(PacketType::MessagesSubscribe, this, "handleMessagesSubscribe"); @@ -45,42 +44,20 @@ void MessagesMixer::nodeKilled(SharedNodePointer killedNode) { void MessagesMixer::handleMessages(QSharedPointer packetList, SharedNodePointer senderNode) { Q_ASSERT(packetList->getType() == PacketType::MessagesData); - QByteArray packetData = packetList->getMessage(); - QBuffer packet{ &packetData }; - packet.open(QIODevice::ReadOnly); + QString channel, message; + QUuid senderID; + MessagesClient::decodeMessagesPacket(packetList, channel, message, senderID); + Q_ASSERT(senderNode->getUUID() == senderID); // NOTE: do we want to reject messages that come from bogus senders - quint16 channelLength; - packet.read(reinterpret_cast(&channelLength), sizeof(channelLength)); - auto channelData = packet.read(channelLength); - QString channel = QString::fromUtf8(channelData); - - quint16 messageLength; - packet.read(reinterpret_cast(&messageLength), sizeof(messageLength)); - auto messageData = packet.read(messageLength); - QString message = QString::fromUtf8(messageData); - auto nodeList = DependencyManager::get(); nodeList->eachMatchingNode( [&](const SharedNodePointer& node)->bool { - return node->getType() == NodeType::Agent && node->getActiveSocket() && _channelSubscribers[channel].contains(node->getUUID()); }, [&](const SharedNodePointer& node) { - - auto packetList = NLPacketList::create(PacketType::MessagesData, QByteArray(), true, true); - - auto channelUtf8 = channel.toUtf8(); - quint16 channelLength = channelUtf8.length(); - packetList->writePrimitive(channelLength); - packetList->write(channelUtf8); - - auto messageUtf8 = message.toUtf8(); - quint16 messageLength = messageUtf8.length(); - packetList->writePrimitive(messageLength); - packetList->write(messageUtf8); - + auto packetList = MessagesClient::encodeMessagesPacket(channel, message, senderID); nodeList->sendPacketList(std::move(packetList), *node); }); } @@ -107,15 +84,13 @@ void MessagesMixer::sendStatsPacket() { QJsonObject statsObject; QJsonObject messagesObject; auto nodeList = DependencyManager::get(); + // add stats for each listerner nodeList->eachNode([&](const SharedNodePointer& node) { QJsonObject messagesStats; - - // add the key to ask the domain-server for a username replacement, if it has it messagesStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidStringWithoutCurlyBraces(node->getUUID()); messagesStats["outbound_kbps"] = node->getOutboundBandwidth(); messagesStats["inbound_kbps"] = node->getInboundBandwidth(); - messagesObject[uuidStringWithoutCurlyBraces(node->getUUID())] = messagesStats; }); @@ -125,10 +100,6 @@ void MessagesMixer::sendStatsPacket() { void MessagesMixer::run() { ThreadedAssignment::commonInit(MESSAGES_MIXER_LOGGING_NAME, NodeType::MessagesMixer); - auto nodeList = DependencyManager::get(); nodeList->addNodeTypeToInterestSet(NodeType::Agent); - - // The messages-mixer currently does currently have any domain settings. If it did, they would be - // synchronously grabbed here. } diff --git a/libraries/networking/src/MessagesClient.cpp b/libraries/networking/src/MessagesClient.cpp index 2e8b3bbb09..fc1729b4c5 100644 --- a/libraries/networking/src/MessagesClient.cpp +++ b/libraries/networking/src/MessagesClient.cpp @@ -36,7 +36,7 @@ void MessagesClient::init() { } } -void MessagesClient::handleMessagesPacket(QSharedPointer packetList, SharedNodePointer senderNode) { +void MessagesClient::decodeMessagesPacket(QSharedPointer packetList, QString& channel, QString& message, QUuid& senderID) { QByteArray packetData = packetList->getMessage(); QBuffer packet{ &packetData }; packet.open(QIODevice::ReadOnly); @@ -44,38 +44,55 @@ void MessagesClient::handleMessagesPacket(QSharedPointer packetLis quint16 channelLength; packet.read(reinterpret_cast(&channelLength), sizeof(channelLength)); auto channelData = packet.read(channelLength); - QString channel = QString::fromUtf8(channelData); + channel = QString::fromUtf8(channelData); quint16 messageLength; packet.read(reinterpret_cast(&messageLength), sizeof(messageLength)); auto messageData = packet.read(messageLength); - QString message = QString::fromUtf8(messageData); + message = QString::fromUtf8(messageData); - emit messageReceived(channel, message, senderNode->getUUID()); + QByteArray bytesSenderID = packet.read(NUM_BYTES_RFC4122_UUID); + senderID = QUuid::fromRfc4122(bytesSenderID); } -void MessagesClient::sendMessage(const QString& channel, const QString& message) { +std::unique_ptr MessagesClient::encodeMessagesPacket(QString channel, QString message, QUuid senderID) { + auto packetList = NLPacketList::create(PacketType::MessagesData, QByteArray(), true, true); + + auto channelUtf8 = channel.toUtf8(); + quint16 channelLength = channelUtf8.length(); + packetList->writePrimitive(channelLength); + packetList->write(channelUtf8); + + auto messageUtf8 = message.toUtf8(); + quint16 messageLength = messageUtf8.length(); + packetList->writePrimitive(messageLength); + packetList->write(messageUtf8); + + packetList->write(senderID.toRfc4122()); + + return packetList; +} + + +void MessagesClient::handleMessagesPacket(QSharedPointer packetList, SharedNodePointer senderNode) { + QString channel, message; + QUuid senderID; + decodeMessagesPacket(packetList, channel, message, senderID); + emit messageReceived(channel, message, senderID); +} + +void MessagesClient::sendMessage(QString channel, QString message) { auto nodeList = DependencyManager::get(); SharedNodePointer messagesMixer = nodeList->soloNodeOfType(NodeType::MessagesMixer); if (messagesMixer) { - auto packetList = NLPacketList::create(PacketType::MessagesData, QByteArray(), true, true); - - auto channelUtf8 = channel.toUtf8(); - quint16 channelLength = channelUtf8.length(); - packetList->writePrimitive(channelLength); - packetList->write(channelUtf8); - - auto messageUtf8 = message.toUtf8(); - quint16 messageLength = messageUtf8.length(); - packetList->writePrimitive(messageLength); - packetList->write(messageUtf8); - + QUuid senderID = nodeList->getSessionUUID(); + auto packetList = encodeMessagesPacket(channel, message, senderID); nodeList->sendPacketList(std::move(packetList), *messagesMixer); } } -void MessagesClient::subscribe(const QString& channel) { +void MessagesClient::subscribe(QString channel) { _subscribedChannels << channel; auto nodeList = DependencyManager::get(); SharedNodePointer messagesMixer = nodeList->soloNodeOfType(NodeType::MessagesMixer); @@ -87,7 +104,7 @@ void MessagesClient::subscribe(const QString& channel) { } } -void MessagesClient::unsubscribe(const QString& channel) { +void MessagesClient::unsubscribe(QString channel) { _subscribedChannels.remove(channel); auto nodeList = DependencyManager::get(); SharedNodePointer messagesMixer = nodeList->soloNodeOfType(NodeType::MessagesMixer); diff --git a/libraries/networking/src/MessagesClient.h b/libraries/networking/src/MessagesClient.h index a1ae4cb5ba..d5d94f62c4 100644 --- a/libraries/networking/src/MessagesClient.h +++ b/libraries/networking/src/MessagesClient.h @@ -28,12 +28,16 @@ public: Q_INVOKABLE void init(); - Q_INVOKABLE void sendMessage(const QString& channel, const QString& message); - Q_INVOKABLE void subscribe(const QString& channel); - Q_INVOKABLE void unsubscribe(const QString& channel); + Q_INVOKABLE void sendMessage(QString channel, QString message); + Q_INVOKABLE void subscribe(QString channel); + Q_INVOKABLE void unsubscribe(QString channel); + + static void decodeMessagesPacket(QSharedPointer packetList, QString& channel, QString& message, QUuid& senderID); + static std::unique_ptr encodeMessagesPacket(QString channel, QString message, QUuid senderID); + signals: - void messageReceived(const QString& channel, const QString& message, const QUuid& senderUUID); + void messageReceived(QString channel, QString message, QUuid senderUUID); private slots: void handleMessagesPacket(QSharedPointer packetList, SharedNodePointer senderNode); From 8ba6dfe721cc9c0369e0ceec677daba77a3dfd1b Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 Nov 2015 09:29:36 -0800 Subject: [PATCH 38/99] add deprication warning about binary SVOs --- libraries/octree/src/Octree.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index fe92fe7745..6f73be360f 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1841,6 +1841,7 @@ bool Octree::readFromStream(unsigned long streamLength, QDataStream& inputStream bool Octree::readSVOFromStream(unsigned long streamLength, QDataStream& inputStream) { + qWarning() << "SVO file format depricated. Support for reading SVO files is no longer support and will be removed soon."; bool fileOk = false; @@ -2062,6 +2063,8 @@ void Octree::writeToJSONFile(const char* fileName, OctreeElementPointer element, } void Octree::writeToSVOFile(const char* fileName, OctreeElementPointer element) { + qWarning() << "SVO file format depricated. Support for reading SVO files is no longer support and will be removed soon."; + std::ofstream file(fileName, std::ios::out|std::ios::binary); if(file.is_open()) { From 0e5e33446e0fcf26d746c6b0734cfc0c1c85614e Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 Nov 2015 09:57:30 -0800 Subject: [PATCH 39/99] handle old protocol case --- libraries/networking/src/MessagesClient.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/MessagesClient.cpp b/libraries/networking/src/MessagesClient.cpp index fc1729b4c5..95b61cf8d7 100644 --- a/libraries/networking/src/MessagesClient.cpp +++ b/libraries/networking/src/MessagesClient.cpp @@ -52,7 +52,11 @@ void MessagesClient::decodeMessagesPacket(QSharedPointer packetLis message = QString::fromUtf8(messageData); QByteArray bytesSenderID = packet.read(NUM_BYTES_RFC4122_UUID); - senderID = QUuid::fromRfc4122(bytesSenderID); + if (bytesSenderID.length() == NUM_BYTES_RFC4122_UUID) { + senderID = QUuid::fromRfc4122(bytesSenderID); + } else { + senderID = QUuid::QUuid(); // packet was missing UUID use default instead + } } std::unique_ptr MessagesClient::encodeMessagesPacket(QString channel, QString message, QUuid senderID) { From e530aa6545b925f35e1651a9c5def71f9f0bc38d Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 Nov 2015 09:58:28 -0800 Subject: [PATCH 40/99] don't assert on sender mismatch --- assignment-client/src/messages/MessagesMixer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/assignment-client/src/messages/MessagesMixer.cpp b/assignment-client/src/messages/MessagesMixer.cpp index a811631617..684c5f262c 100644 --- a/assignment-client/src/messages/MessagesMixer.cpp +++ b/assignment-client/src/messages/MessagesMixer.cpp @@ -47,7 +47,6 @@ void MessagesMixer::handleMessages(QSharedPointer packetList, Shar QString channel, message; QUuid senderID; MessagesClient::decodeMessagesPacket(packetList, channel, message, senderID); - Q_ASSERT(senderNode->getUUID() == senderID); // NOTE: do we want to reject messages that come from bogus senders auto nodeList = DependencyManager::get(); From 19d3d80ff97ec6e2aafff260fcc14b47482beaff Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 20 Nov 2015 11:03:52 -0800 Subject: [PATCH 41/99] fixes for some incorrect assumptions --- interface/src/avatar/AvatarManager.cpp | 10 +++++++++- libraries/avatars/src/AvatarHashMap.cpp | 2 +- libraries/avatars/src/AvatarHashMap.h | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index bd45561b38..c17590c4ac 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -110,16 +110,22 @@ void AvatarManager::updateMyAvatar(float deltaTime) { } void AvatarManager::updateOtherAvatars(float deltaTime) { + // lock the hash for read to check the size + QReadLocker lock(&_hashLock); + if (_avatarHash.size() < 2 && _avatarFades.isEmpty()) { return; } + + lock.unlock(); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateAvatars()"); PerformanceTimer perfTimer("otherAvatars"); // simulate avatars - auto hashCopy = _avatarHash; + auto hashCopy = getHashCopy(); AvatarHash::iterator avatarIterator = hashCopy.begin(); while (avatarIterator != hashCopy.end()) { @@ -256,8 +262,10 @@ QVector AvatarManager::getLocalLights() const { } QVector AvatarManager::getAvatarIdentifiers() { + QReadLocker lock(&_hashLock); return _avatarHash.keys().toVector(); } + AvatarData* AvatarManager::getAvatar(QUuid avatarID) { QReadLocker locker(&_hashLock); return _avatarHash[avatarID].get(); // Non-obvious: A bogus avatarID answers your own avatar. diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 035d29f344..c195ab4c32 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -23,7 +23,7 @@ AvatarHashMap::AvatarHashMap() { } bool AvatarHashMap::isAvatarInRange(const glm::vec3& position, const float range) { - auto hashCopy = _avatarHash; + auto hashCopy = getHashCopy(); foreach(const AvatarSharedPointer& sharedAvatar, hashCopy) { glm::vec3 avatarPosition = sharedAvatar->getPosition(); float distance = glm::distance(avatarPosition, position); diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 7795072ec2..5881b779a1 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -31,7 +31,7 @@ class AvatarHashMap : public QObject, public Dependency { SINGLETON_DEPENDENCY public: - AvatarHash getHashCopy() { return _avatarHash; } + AvatarHash getHashCopy() { QReadLocker lock(&_hashLock); return _avatarHash; } int size() { return _avatarHash.size(); } signals: From 9b5bfd45bc4a822ff365d646b4d65c449655f649 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 20 Nov 2015 11:04:46 -0800 Subject: [PATCH 42/99] change lock back to locker to remove change --- interface/src/avatar/AvatarManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index c17590c4ac..769b1d56a2 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -262,7 +262,7 @@ QVector AvatarManager::getLocalLights() const { } QVector AvatarManager::getAvatarIdentifiers() { - QReadLocker lock(&_hashLock); + QReadLocker locker(&_hashLock); return _avatarHash.keys().toVector(); } From caa8b0b5b6ff02dca0a7d71bd10cbeb26ccbdc1d Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 Nov 2015 11:07:01 -0800 Subject: [PATCH 43/99] fix unix build --- libraries/networking/src/MessagesClient.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/MessagesClient.cpp b/libraries/networking/src/MessagesClient.cpp index 95b61cf8d7..ef8ecb534c 100644 --- a/libraries/networking/src/MessagesClient.cpp +++ b/libraries/networking/src/MessagesClient.cpp @@ -55,7 +55,8 @@ void MessagesClient::decodeMessagesPacket(QSharedPointer packetLis if (bytesSenderID.length() == NUM_BYTES_RFC4122_UUID) { senderID = QUuid::fromRfc4122(bytesSenderID); } else { - senderID = QUuid::QUuid(); // packet was missing UUID use default instead + QUuid emptyUUID; + senderID = emptyUUID; // packet was missing UUID use default instead } } From 7441e20f5842ab2b7d1fe8ff175a05feafd41d5d Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 Nov 2015 11:20:36 -0800 Subject: [PATCH 44/99] cleanup --- .../src/messages/MessagesMixer.cpp | 39 ++++++------------- .../src/messages/MessagesMixer.h | 1 - 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/assignment-client/src/messages/MessagesMixer.cpp b/assignment-client/src/messages/MessagesMixer.cpp index 684c5f262c..8f2b8a5475 100644 --- a/assignment-client/src/messages/MessagesMixer.cpp +++ b/assignment-client/src/messages/MessagesMixer.cpp @@ -12,18 +12,15 @@ #include #include #include - #include #include #include #include - #include "MessagesMixer.h" const QString MESSAGES_MIXER_LOGGING_NAME = "messages-mixer"; -MessagesMixer::MessagesMixer(NLPacket& packet) : - ThreadedAssignment(packet) +MessagesMixer::MessagesMixer(NLPacket& packet) : ThreadedAssignment(packet) { connect(DependencyManager::get().data(), &NodeList::nodeKilled, this, &MessagesMixer::nodeKilled); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); @@ -32,9 +29,6 @@ MessagesMixer::MessagesMixer(NLPacket& packet) : packetReceiver.registerMessageListener(PacketType::MessagesUnsubscribe, this, "handleMessagesUnsubscribe"); } -MessagesMixer::~MessagesMixer() { -} - void MessagesMixer::nodeKilled(SharedNodePointer killedNode) { for (auto& channel : _channelSubscribers) { channel.remove(killedNode->getUUID()); @@ -42,8 +36,6 @@ void MessagesMixer::nodeKilled(SharedNodePointer killedNode) { } void MessagesMixer::handleMessages(QSharedPointer packetList, SharedNodePointer senderNode) { - Q_ASSERT(packetList->getType() == PacketType::MessagesData); - QString channel, message; QUuid senderID; MessagesClient::decodeMessagesPacket(packetList, channel, message, senderID); @@ -62,43 +54,34 @@ void MessagesMixer::handleMessages(QSharedPointer packetList, Shar } void MessagesMixer::handleMessagesSubscribe(QSharedPointer packetList, SharedNodePointer senderNode) { - Q_ASSERT(packetList->getType() == PacketType::MessagesSubscribe); QString channel = QString::fromUtf8(packetList->getMessage()); - qDebug() << "Node [" << senderNode->getUUID() << "] subscribed to channel:" << channel; _channelSubscribers[channel] << senderNode->getUUID(); } void MessagesMixer::handleMessagesUnsubscribe(QSharedPointer packetList, SharedNodePointer senderNode) { - Q_ASSERT(packetList->getType() == PacketType::MessagesUnsubscribe); QString channel = QString::fromUtf8(packetList->getMessage()); - qDebug() << "Node [" << senderNode->getUUID() << "] unsubscribed from channel:" << channel; - if (_channelSubscribers.contains(channel)) { _channelSubscribers[channel].remove(senderNode->getUUID()); } } -// FIXME - make these stats relevant void MessagesMixer::sendStatsPacket() { - QJsonObject statsObject; - QJsonObject messagesObject; - auto nodeList = DependencyManager::get(); + QJsonObject statsObject, messagesMixerObject; // add stats for each listerner - nodeList->eachNode([&](const SharedNodePointer& node) { - QJsonObject messagesStats; - messagesStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidStringWithoutCurlyBraces(node->getUUID()); - messagesStats["outbound_kbps"] = node->getOutboundBandwidth(); - messagesStats["inbound_kbps"] = node->getInboundBandwidth(); - messagesObject[uuidStringWithoutCurlyBraces(node->getUUID())] = messagesStats; + DependencyManager::get()->eachNode([&](const SharedNodePointer& node) { + QJsonObject clientStats; + clientStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidStringWithoutCurlyBraces(node->getUUID()); + clientStats["outbound_kbps"] = node->getOutboundBandwidth(); + clientStats["inbound_kbps"] = node->getInboundBandwidth(); + messagesMixerObject[uuidStringWithoutCurlyBraces(node->getUUID())] = clientStats; }); - statsObject["messages"] = messagesObject; + statsObject["messages"] = messagesMixerObject; ThreadedAssignment::addPacketStatsAndSendStatsPacket(statsObject); } void MessagesMixer::run() { ThreadedAssignment::commonInit(MESSAGES_MIXER_LOGGING_NAME, NodeType::MessagesMixer); - auto nodeList = DependencyManager::get(); - nodeList->addNodeTypeToInterestSet(NodeType::Agent); -} + DependencyManager::get()->addNodeTypeToInterestSet(NodeType::Agent); +} \ No newline at end of file diff --git a/assignment-client/src/messages/MessagesMixer.h b/assignment-client/src/messages/MessagesMixer.h index 65419a8ca6..cf5fc79e17 100644 --- a/assignment-client/src/messages/MessagesMixer.h +++ b/assignment-client/src/messages/MessagesMixer.h @@ -22,7 +22,6 @@ class MessagesMixer : public ThreadedAssignment { Q_OBJECT public: MessagesMixer(NLPacket& packet); - ~MessagesMixer(); public slots: void run(); From 3b4011999234682f39c8ca4ef8324b799c3f91a6 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 20 Nov 2015 11:35:24 -0800 Subject: [PATCH 45/99] use Agent session UUID to request script --- assignment-client/src/Agent.cpp | 118 +++++++++++++++++------------ assignment-client/src/Agent.h | 5 ++ domain-server/src/DomainServer.cpp | 52 +++++++------ 3 files changed, 103 insertions(+), 72 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index f19f4ff86b..5ec4b91fa6 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -116,6 +117,11 @@ void Agent::handleAudioPacket(QSharedPointer packet) { const QString AGENT_LOGGING_NAME = "agent"; void Agent::run() { + + // make sure we request our script once the agent connects to the domain + auto nodeList = DependencyManager::get(); + connect(&nodeList->getDomainHandler(), &DomainHandler::connectedToDomain, this, &Agent::requestScript); + ThreadedAssignment::commonInit(AGENT_LOGGING_NAME, NodeType::Agent); // Setup MessagesClient @@ -125,72 +131,86 @@ void Agent::run() { messagesClient->moveToThread(messagesThread); connect(messagesThread, &QThread::started, messagesClient.data(), &MessagesClient::init); messagesThread->start(); + + nodeList->addSetOfNodeTypesToNodeInterestSet({ + NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::EntityServer, NodeType::MessagesMixer + }); +} - +void Agent::requestScript() { auto nodeList = DependencyManager::get(); - nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() - << NodeType::AudioMixer - << NodeType::AvatarMixer - << NodeType::EntityServer - << NodeType::MessagesMixer - ); - + disconnect(&nodeList->getDomainHandler(), &DomainHandler::connectedToDomain, this, &Agent::requestScript); + // figure out the URL for the script for this agent assignment QUrl scriptURL; if (_payload.isEmpty()) { - scriptURL = QUrl(QString("http://%1:%2/assignment/%3") - .arg(DependencyManager::get()->getDomainHandler().getIP().toString()) - .arg(DOMAIN_SERVER_HTTP_PORT) - .arg(uuidStringWithoutCurlyBraces(_uuid))); + scriptURL = QUrl(QString("http://%1:%2/assignment/%3/") + .arg(nodeList->getDomainHandler().getIP().toString()) + .arg(DOMAIN_SERVER_HTTP_PORT) + .arg(uuidStringWithoutCurlyBraces(nodeList->getSessionUUID()))); } else { scriptURL = QUrl(_payload); } - + + // setup a network access manager and QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = QNetworkRequest(scriptURL); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(networkRequest); - + QNetworkDiskCache* cache = new QNetworkDiskCache(); QString cachePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); cache->setCacheDirectory(!cachePath.isEmpty() ? cachePath : "agentCache"); networkAccessManager.setCache(cache); - + + QNetworkRequest networkRequest = QNetworkRequest(scriptURL); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + qDebug() << "Downloading script at" << scriptURL.toString(); + QNetworkReply* reply = networkAccessManager.get(networkRequest); + connect(reply, &QNetworkReply::finished, this, &Agent::scriptRequestFinished); +} - QEventLoop loop; - QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); +void Agent::scriptRequestFinished() { + auto reply = qobject_cast(sender()); + + if (reply->error() != QNetworkReply::NoError) { + _scriptContents = reply->readAll(); + qDebug() << "Downloaded script:" << _scriptContents; + + // we could just call executeScript directly - we use a QueuedConnect to allow scriptRequestFinished + // to return before calling executeScript + QMetaObject::invokeMethod(this, "executeScript", Qt::QueuedConnection); + } else { + qDebug() << "Failed to download script at" << reply->url().toString() << " - bailing on assignment."; + qDebug() << "QNetworkReply error was" << reply->errorString(); + setFinished(true); + } + + reply->deleteLater(); +} - loop.exec(); - - QString scriptContents(reply->readAll()); - delete reply; - - qDebug() << "Downloaded script:" << scriptContents; - - _scriptEngine = std::unique_ptr(new ScriptEngine(scriptContents, _payload)); +void Agent::executeScript() { + _scriptEngine = std::unique_ptr(new ScriptEngine(_scriptContents, _payload)); _scriptEngine->setParent(this); // be the parent of the script engine so it gets moved when we do - + // setup an Avatar for the script to use auto scriptedAvatar = DependencyManager::get(); connect(_scriptEngine.get(), SIGNAL(update(float)), scriptedAvatar.data(), SLOT(update(float)), Qt::ConnectionType::QueuedConnection); scriptedAvatar->setForceFaceTrackerConnected(true); - + // call model URL setters with empty URLs so our avatar, if user, will have the default models scriptedAvatar->setFaceModelURL(QUrl()); scriptedAvatar->setSkeletonModelURL(QUrl()); // give this AvatarData object to the script engine _scriptEngine->registerGlobalObject("Avatar", scriptedAvatar.data()); - - + + using namespace recording; static const FrameType AVATAR_FRAME_TYPE = Frame::registerFrameType(AvatarData::FRAME_NAME); - // FIXME how to deal with driving multiple avatars locally? + // FIXME how to deal with driving multiple avatars locally? Frame::registerFrameHandler(AVATAR_FRAME_TYPE, [this, scriptedAvatar](Frame::ConstPointer frame) { AvatarData::fromFrame(frame->data, *scriptedAvatar); }); - - + + using namespace recording; static const FrameType AUDIO_FRAME_TYPE = Frame::registerFrameType(AudioConstants::AUDIO_FRAME_NAME); Frame::registerFrameHandler(AUDIO_FRAME_TYPE, [this, &scriptedAvatar](Frame::ConstPointer frame) { @@ -201,32 +221,30 @@ void Agent::run() { audioTransform.setRotation(scriptedAvatar->getOrientation()); AbstractAudioInterface::emitAudioPacket(audio.data(), audio.size(), audioSequenceNumber, audioTransform, PacketType::MicrophoneAudioNoEcho); }); - - - + auto avatarHashMap = DependencyManager::set(); _scriptEngine->registerGlobalObject("AvatarList", avatarHashMap.data()); - + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListener(PacketType::BulkAvatarData, avatarHashMap.data(), "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, avatarHashMap.data(), "processKillAvatar"); packetReceiver.registerListener(PacketType::AvatarIdentity, avatarHashMap.data(), "processAvatarIdentityPacket"); packetReceiver.registerListener(PacketType::AvatarBillboard, avatarHashMap.data(), "processAvatarBillboardPacket"); - + // register ourselves to the script engine _scriptEngine->registerGlobalObject("Agent", this); - - // FIXME -we shouldn't be calling this directly, it's normally called by run(), not sure why + + // FIXME -we shouldn't be calling this directly, it's normally called by run(), not sure why // viewers would need this called. //_scriptEngine->init(); // must be done before we set up the viewers - + _scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get().data()); - + QScriptValue webSocketServerConstructorValue = _scriptEngine->newFunction(WebSocketServerClass::constructor); _scriptEngine->globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue); - + auto entityScriptingInterface = DependencyManager::get(); - + _scriptEngine->registerGlobalObject("EntityViewer", &_entityViewer); // we need to make sure that init has been called for our EntityScriptingInterface @@ -237,15 +255,15 @@ void Agent::run() { _entityViewer.init(); entityScriptingInterface->setEntityTree(_entityViewer.getTree()); - + // wire up our additional agent related processing to the update signal QObject::connect(_scriptEngine.get(), &ScriptEngine::update, this, &Agent::processAgentAvatarAndAudio); - + _scriptEngine->run(); - + Frame::clearFrameHandler(AUDIO_FRAME_TYPE); Frame::clearFrameHandler(AVATAR_FRAME_TYPE); - + setFinished(true); } diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index d643b65267..205d4d4a80 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -55,6 +55,10 @@ public slots: void playAvatarSound(Sound* avatarSound) { setAvatarSound(avatarSound); } private slots: + void requestScript(); + void scriptRequestFinished(); + void executeScript(); + void handleAudioPacket(QSharedPointer packet); void handleOctreePacket(QSharedPointer packet, SharedNodePointer senderNode); void handleJurisdictionPacket(QSharedPointer packet, SharedNodePointer senderNode); @@ -73,6 +77,7 @@ private: void sendAvatarIdentityPacket(); void sendAvatarBillboardPacket(); + QString _scriptContents; bool _isListeningToAudioStream = false; Sound* _avatarSound = nullptr; int _numAvatarSoundSentBytes = 0; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index b4243ef8a0..ddc0003eaa 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1097,29 +1097,37 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url if (connection->requestOperation() == QNetworkAccessManager::GetOperation && assignmentRegex.indexIn(url.path()) != -1) { - QUuid matchingUUID = QUuid(assignmentRegex.cap(1)); - - SharedAssignmentPointer matchingAssignment = _allAssignments.value(matchingUUID); - if (!matchingAssignment) { - // check if we have a pending assignment that matches this temp UUID, and it is a scripted assignment - QUuid assignmentUUID = _gatekeeper.assignmentUUIDForPendingAssignment(matchingUUID); - if (!assignmentUUID.isNull()) { - matchingAssignment = _allAssignments.value(assignmentUUID); - - if (matchingAssignment && matchingAssignment->getType() == Assignment::AgentType) { - // we have a matching assignment and it is for the right type, have the HTTP manager handle it - // via correct URL for the script so the client can download - - QUrl scriptURL = url; - scriptURL.setPath(URI_ASSIGNMENT + "/scripts/" - + uuidStringWithoutCurlyBraces(assignmentUUID)); - - // have the HTTPManager serve the appropriate script file - return _httpManager.handleHTTPRequest(connection, scriptURL, true); - } - } + QUuid nodeUUID = QUuid(assignmentRegex.cap(1)); + + auto matchingNode = nodeList->nodeWithUUID(nodeUUID); + + // don't handle if we don't have a matching node + if (!matchingNode) { + return false; } - + + auto nodeData = dynamic_cast(matchingNode->getLinkedData()); + + // don't handle if we don't have node data for this node + if (!nodeData) { + return false; + } + + SharedAssignmentPointer matchingAssignment = _allAssignments.value(nodeData->getAssignmentUUID()); + + // check if we have an assignment that matches this temp UUID, and it is a scripted assignment + if (matchingAssignment && matchingAssignment->getType() == Assignment::AgentType) { + // we have a matching assignment and it is for the right type, have the HTTP manager handle it + // via correct URL for the script so the client can download + + QUrl scriptURL = url; + scriptURL.setPath(URI_ASSIGNMENT + "/scripts/" + + uuidStringWithoutCurlyBraces(matchingAssignment->getUUID())); + + // have the HTTPManager serve the appropriate script file + return _httpManager.handleHTTPRequest(connection, scriptURL, true); + } + // request not handled return false; } From f1badc017b3c79b11d70fcc583fe78dd2f2911dd Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 Nov 2015 11:43:50 -0800 Subject: [PATCH 46/99] CR feedback --- libraries/networking/src/AssetClient.cpp | 39 +++++++++--------------- libraries/networking/src/AssetClient.h | 1 - 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 4dc08962c5..8ac019ff56 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -369,18 +369,19 @@ void AssetClient::handleNodeKilled(SharedNodePointer node) { void AssetScriptingInterface::uploadData(QString data, QString extension, QScriptValue callback) { QByteArray dataByteArray = data.toUtf8(); auto upload = DependencyManager::get()->createUpload(dataByteArray, extension); - if (upload) { - QObject::connect(upload, &AssetUpload::finished, this, [callback, extension](AssetUpload* upload, const QString& hash) mutable { - if (callback.isFunction()) { - QString url = "atp://" + hash + "." + extension; - QScriptValueList args { url }; - callback.call(QScriptValue(), args); - } - }); - - // start the upload now - upload->start(); + if (!upload) { + qCWarning(asset_client) << "Error uploading file to asset server"; + return; } + + QObject::connect(upload, &AssetUpload::finished, this, [callback, extension](AssetUpload* upload, const QString& hash) mutable { + if (callback.isFunction()) { + QString url = "atp://" + hash + "." + extension; + QScriptValueList args { url }; + callback.call(QScriptValue(), args); + } + }); + upload->start(); } void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) { @@ -407,10 +408,7 @@ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callb return; } - { - QWriteLocker locker(&_lock); - _pendingRequests << assetRequest; - } + _pendingRequests << assetRequest; connect(assetRequest, &AssetRequest::finished, [this, callback](AssetRequest* request) mutable { Q_ASSERT(request->getState() == AssetRequest::Finished); @@ -424,15 +422,8 @@ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callb } request->deleteLater(); - - { - QWriteLocker locker(&_lock); - _pendingRequests.remove(request); - } + _pendingRequests.remove(request); }); assetRequest->start(); -} - - - +} \ No newline at end of file diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index a194db225b..f1bb210614 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -77,7 +77,6 @@ public: Q_INVOKABLE void uploadData(QString data, QString extension, QScriptValue callback); Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete); protected: - mutable QReadWriteLock _lock; QSet _pendingRequests; }; From aa836aa4b55d9771ecc5ad0623baf8cfc18c04c7 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 20 Nov 2015 11:44:04 -0800 Subject: [PATCH 47/99] add a script request timeout --- assignment-client/src/Agent.cpp | 19 ++++++++++++++++--- assignment-client/src/Agent.h | 1 + 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 5ec4b91fa6..cb3f4d9b05 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -163,6 +163,12 @@ void Agent::requestScript() { QNetworkRequest networkRequest = QNetworkRequest(scriptURL); networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + // setup a timeout for script request + static const int SCRIPT_TIMEOUT_MS = 10000; + _scriptRequestTimeout = new QTimer(this); + connect(_scriptRequestTimeout, &QTimer::timeout, this, &Agent::scriptRequestFinished); + _scriptRequestTimeout->start(SCRIPT_TIMEOUT_MS); + qDebug() << "Downloading script at" << scriptURL.toString(); QNetworkReply* reply = networkAccessManager.get(networkRequest); connect(reply, &QNetworkReply::finished, this, &Agent::scriptRequestFinished); @@ -170,8 +176,10 @@ void Agent::requestScript() { void Agent::scriptRequestFinished() { auto reply = qobject_cast(sender()); + + _scriptRequestTimeout->stop(); - if (reply->error() != QNetworkReply::NoError) { + if (reply && reply->error() == QNetworkReply::NoError) { _scriptContents = reply->readAll(); qDebug() << "Downloaded script:" << _scriptContents; @@ -179,8 +187,13 @@ void Agent::scriptRequestFinished() { // to return before calling executeScript QMetaObject::invokeMethod(this, "executeScript", Qt::QueuedConnection); } else { - qDebug() << "Failed to download script at" << reply->url().toString() << " - bailing on assignment."; - qDebug() << "QNetworkReply error was" << reply->errorString(); + if (reply) { + qDebug() << "Failed to download script at" << reply->url().toString() << " - bailing on assignment."; + qDebug() << "QNetworkReply error was" << reply->errorString(); + } else { + qDebug() << "Failed to download script - request timed out. Bailing on assignment."; + } + setFinished(true); } diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 205d4d4a80..6819976633 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -78,6 +78,7 @@ private: void sendAvatarBillboardPacket(); QString _scriptContents; + QTimer* _scriptRequestTimeout { nullptr }; bool _isListeningToAudioStream = false; Sound* _avatarSound = nullptr; int _numAvatarSoundSentBytes = 0; From cf204ad5b07abc3a4db42febb9b861fa24bd68ef Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 20 Nov 2015 11:44:58 -0800 Subject: [PATCH 48/99] cleanup executeScript comment in Agent --- assignment-client/src/Agent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index cb3f4d9b05..2fdba9e256 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -183,7 +183,7 @@ void Agent::scriptRequestFinished() { _scriptContents = reply->readAll(); qDebug() << "Downloaded script:" << _scriptContents; - // we could just call executeScript directly - we use a QueuedConnect to allow scriptRequestFinished + // we could just call executeScript directly - we use a QueuedConnection to allow scriptRequestFinished // to return before calling executeScript QMetaObject::invokeMethod(this, "executeScript", Qt::QueuedConnection); } else { From cc2a7cbda891c823af9123987bf657c5f793b9ec Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 20 Nov 2015 11:51:08 -0800 Subject: [PATCH 49/99] Force synchronous loading of clips in JS --- .../src/RecordingScriptingInterface.cpp | 20 ++++++++++++------- .../src/RecordingScriptingInterface.h | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index d82d471d79..83e44ef173 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -44,19 +44,25 @@ float RecordingScriptingInterface::playerLength() const { return _player->length(); } -void RecordingScriptingInterface::loadRecording(const QString& url) { +bool RecordingScriptingInterface::loadRecording(const QString& url) { using namespace recording; - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "loadRecording", Qt::BlockingQueuedConnection, - Q_ARG(QString, url)); - return; + auto loader = ClipCache::instance().getClipLoader(url); + QEventLoop loop; + QObject::connect(loader.data(), &Resource::loaded, &loop, &QEventLoop::quit); + QObject::connect(loader.data(), &Resource::failed, &loop, &QEventLoop::quit); + loop.exec(); + + if (!loader->isLoaded()) { + qWarning() << "Clip failed to load from " << url; + return false; } - // FIXME make blocking and force off main thread? - _player->queueClip(ClipCache::instance().getClipLoader(url)->getClip()); + _player->queueClip(loader->getClip()); + return true; } + void RecordingScriptingInterface::startPlaying() { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "startPlaying", Qt::BlockingQueuedConnection); diff --git a/libraries/script-engine/src/RecordingScriptingInterface.h b/libraries/script-engine/src/RecordingScriptingInterface.h index 3834089177..b39485d75c 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.h +++ b/libraries/script-engine/src/RecordingScriptingInterface.h @@ -25,7 +25,7 @@ public: RecordingScriptingInterface(); public slots: - void loadRecording(const QString& url); + bool loadRecording(const QString& url); void startPlaying(); void pausePlayer(); From 6105191a3f514d3949674ec3e944be63ab0b93bf Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Fri, 20 Nov 2015 14:45:25 -0600 Subject: [PATCH 50/99] OS X Bundling --- cmake/macros/SetupHifiProject.cmake | 7 +++++-- stack-manager/CMakeLists.txt | 4 +--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cmake/macros/SetupHifiProject.cmake b/cmake/macros/SetupHifiProject.cmake index 166a3fd4b9..79ca0122c9 100644 --- a/cmake/macros/SetupHifiProject.cmake +++ b/cmake/macros/SetupHifiProject.cmake @@ -22,8 +22,11 @@ macro(SETUP_HIFI_PROJECT) endif () endforeach() - # add the executable, include additional optional sources - add_executable(${TARGET_NAME} ${TARGET_SRCS} ${AUTOMTC_SRC} ${AUTOSCRIBE_SHADER_LIB_SRC}) + if (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE) + add_executable(${TARGET_NAME} MACOSX_BUNDLE ${TARGET_SRCS} ${AUTOMTC_SRC} ${AUTOSCRIBE_SHADER_LIB_SRC}) + else () + add_executable(${TARGET_NAME} ${TARGET_SRCS} ${AUTOMTC_SRC} ${AUTOSCRIBE_SHADER_LIB_SRC}) + endif() set(${TARGET_NAME}_DEPENDENCY_QT_MODULES ${ARGN}) list(APPEND ${TARGET_NAME}_DEPENDENCY_QT_MODULES Core) diff --git a/stack-manager/CMakeLists.txt b/stack-manager/CMakeLists.txt index a4071295e1..e11bd0fa50 100644 --- a/stack-manager/CMakeLists.txt +++ b/stack-manager/CMakeLists.txt @@ -1,4 +1,5 @@ set(TARGET_NAME "stack-manager") +set(BUILD_BUNDLE YES) setup_hifi_project(Widgets Gui Svg Core Network WebKitWidgets) if (WIN32) @@ -31,8 +32,6 @@ include_directories( target_quazip() -target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) - if (APPLE) set(CMAKE_OSX_DEPLOYMENT_TARGET 10.8) set(MACOSX_BUNDLE_BUNDLE_NAME "Stack Manager") @@ -40,7 +39,6 @@ if (APPLE) set(MACOSX_BUNDLE_ICON_FILE icon.icns) set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/assets/icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set(SM_SRCS ${SM_SRCS} "${CMAKE_CURRENT_SOURCE_DIR}/assets/icon.icns") -# add_executable(${TARGET_NAME} MACOSX_BUNDLE ${SM_SRCS}) else () if (WIN32) # add_executable(${TARGET_NAME} WIN32 ${SM_SRCS} windows_icon.rc) From e17d0b2af3f34454ffb058b8d13525f72dee8ade Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Fri, 20 Nov 2015 15:10:56 -0600 Subject: [PATCH 51/99] Bundling and consolidating deployment package --- assignment-client/CMakeLists.txt | 3 +-- .../CopyDllsBesideWindowsExecutable.cmake | 23 +++++++++++++------ cmake/modules/FindQuaZip.cmake | 3 --- domain-server/CMakeLists.txt | 2 +- interface/CMakeLists.txt | 2 +- stack-manager/CMakeLists.txt | 2 ++ 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 830164eb60..58f200b0fe 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -10,5 +10,4 @@ link_hifi_libraries( ) include_application_version() - -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/cmake/macros/CopyDllsBesideWindowsExecutable.cmake b/cmake/macros/CopyDllsBesideWindowsExecutable.cmake index 69fd20a57b..9330515a62 100644 --- a/cmake/macros/CopyDllsBesideWindowsExecutable.cmake +++ b/cmake/macros/CopyDllsBesideWindowsExecutable.cmake @@ -9,7 +9,7 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # -macro(COPY_DLLS_BESIDE_WINDOWS_EXECUTABLE) +macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) if (WIN32) configure_file( @@ -18,11 +18,7 @@ macro(COPY_DLLS_BESIDE_WINDOWS_EXECUTABLE) @ONLY ) - if (APPLE) - set(PLUGIN_PATH "interface.app/Contents/MacOS/plugins") - else() - set(PLUGIN_PATH "plugins") - endif() + set(PLUGIN_PATH "plugins") # add a post-build command to copy DLLs beside the executable add_custom_command( @@ -46,5 +42,18 @@ macro(COPY_DLLS_BESIDE_WINDOWS_EXECUTABLE) POST_BUILD COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} $<$,$,$>:--release> $" ) + elseif (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE) + find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) + + if (NOT MACDEPLOYQT_COMMAND) + message(FATAL_ERROR "Could not find macdeployqt at ${QT_DIR}/bin. macdeployqt is required.") + endif () + + # add a post-build command to call macdeployqt to copy Qt plugins + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND ${MACDEPLOYQT_COMMAND} ${CMAKE_CURRENT_BINARY_DIR}/\${CONFIGURATION}/${TARGET_NAME}.app -verbose 0 + ) endif () -endmacro() \ No newline at end of file +endmacro() diff --git a/cmake/modules/FindQuaZip.cmake b/cmake/modules/FindQuaZip.cmake index 5fbc8e1ac4..110f239c68 100644 --- a/cmake/modules/FindQuaZip.cmake +++ b/cmake/modules/FindQuaZip.cmake @@ -17,13 +17,10 @@ hifi_library_search_hints("quazip") if (WIN32) find_path(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES include/quazip HINTS ${QUAZIP_SEARCH_DIRS}) -# find_library(QUAZIP_LIBRARIES NAMES quazip PATH_SUFFIXES lib HINTS ${QUAZIP_SEARCH_DIRS}) elseif (APPLE) find_path(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES include/quazip HINTS ${QUAZIP_SEARCH_DIRS}) -# find_library(QUAZIP_LIBRARIES NAMES quazip PATH_SUFFIXES lib HINTS ${QUAZIP_SEARCH_DIRS}) else () find_path(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES quazip HINTS ${QUAZIP_SEARCH_DIRS}) -# find_library(QUAZIP_LIBRARIES NAMES quazip-qt5 PATH_SUFFIXES lib HINTS ${QUAZIP_SEARCH_DIRS}) endif () include(FindPackageHandleStandardArgs) diff --git a/domain-server/CMakeLists.txt b/domain-server/CMakeLists.txt index ce683df698..1f9280a899 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -37,4 +37,4 @@ if (UNIX) endif (UNIX) include_application_version() -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 98a9dad909..3357b57858 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -201,4 +201,4 @@ else (APPLE) endif() endif (APPLE) -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/stack-manager/CMakeLists.txt b/stack-manager/CMakeLists.txt index e11bd0fa50..45ad9686c0 100644 --- a/stack-manager/CMakeLists.txt +++ b/stack-manager/CMakeLists.txt @@ -46,3 +46,5 @@ else () # add_executable(${TARGET_NAME} ${SM_SRCS}) endif () endif () + +package_libraries_for_deployment() \ No newline at end of file From 9ad865c7f0700d51a26b1c32cd55de1b7f44f4ca Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Fri, 20 Nov 2015 15:19:45 -0600 Subject: [PATCH 52/99] checkpoint --- ice-server/CMakeLists.txt | 3 +-- tests/animation/CMakeLists.txt | 2 +- tests/audio/CMakeLists.txt | 2 +- tests/controllers/CMakeLists.txt | 2 +- tests/entities/CMakeLists.txt | 2 +- tests/gpu-test/CMakeLists.txt | 2 +- tests/jitter/CMakeLists.txt | 2 +- tests/networking/CMakeLists.txt | 2 +- tests/octree/CMakeLists.txt | 2 +- tests/physics/CMakeLists.txt | 2 +- tests/recording/CMakeLists.txt | 4 ++-- tests/render-utils/CMakeLists.txt | 2 +- tests/shaders/CMakeLists.txt | 2 +- tests/shared/CMakeLists.txt | 2 +- tests/ui/CMakeLists.txt | 2 +- tools/mtc/CMakeLists.txt | 2 +- tools/udt-test/CMakeLists.txt | 3 +-- 17 files changed, 18 insertions(+), 20 deletions(-) diff --git a/ice-server/CMakeLists.txt b/ice-server/CMakeLists.txt index b056d79efd..cfec3c966c 100644 --- a/ice-server/CMakeLists.txt +++ b/ice-server/CMakeLists.txt @@ -5,5 +5,4 @@ setup_hifi_project(Network) # link the shared hifi libraries link_hifi_libraries(embedded-webserver networking shared) - -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/tests/animation/CMakeLists.txt b/tests/animation/CMakeLists.txt index a66e391f69..2a07f6f429 100644 --- a/tests/animation/CMakeLists.txt +++ b/tests/animation/CMakeLists.txt @@ -3,7 +3,7 @@ macro (setup_testcase_dependencies) # link in the shared libraries link_hifi_libraries(shared animation gpu fbx model networking) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro () setup_hifi_testcase() diff --git a/tests/audio/CMakeLists.txt b/tests/audio/CMakeLists.txt index f7dd1d23b9..239bc41fd0 100644 --- a/tests/audio/CMakeLists.txt +++ b/tests/audio/CMakeLists.txt @@ -3,7 +3,7 @@ macro (SETUP_TESTCASE_DEPENDENCIES) # link in the shared libraries link_hifi_libraries(shared audio networking) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro () setup_hifi_testcase() diff --git a/tests/controllers/CMakeLists.txt b/tests/controllers/CMakeLists.txt index d1c8464dd5..cf1152da02 100644 --- a/tests/controllers/CMakeLists.txt +++ b/tests/controllers/CMakeLists.txt @@ -16,4 +16,4 @@ if (WIN32) target_link_libraries(${TARGET_NAME} ${OPENVR_LIBRARIES}) endif() -copy_dlls_beside_windows_executable() \ No newline at end of file +package_libraries_for_deployment() \ No newline at end of file diff --git a/tests/entities/CMakeLists.txt b/tests/entities/CMakeLists.txt index b2c7969386..81b8b86368 100644 --- a/tests/entities/CMakeLists.txt +++ b/tests/entities/CMakeLists.txt @@ -9,4 +9,4 @@ set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries link_hifi_libraries(entities avatars shared octree gpu model fbx networking animation environment) -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/tests/gpu-test/CMakeLists.txt b/tests/gpu-test/CMakeLists.txt index 2c9ee23c47..3d83c310cf 100644 --- a/tests/gpu-test/CMakeLists.txt +++ b/tests/gpu-test/CMakeLists.txt @@ -4,4 +4,4 @@ AUTOSCRIBE_SHADER_LIB(gpu model render-utils) setup_hifi_project(Quick Gui OpenGL Script Widgets) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries(networking gl gpu procedural shared fbx model model-networking animation script-engine render-utils ) -copy_dlls_beside_windows_executable() \ No newline at end of file +package_libraries_for_deployment() \ No newline at end of file diff --git a/tests/jitter/CMakeLists.txt b/tests/jitter/CMakeLists.txt index 98be42530c..c10961d687 100644 --- a/tests/jitter/CMakeLists.txt +++ b/tests/jitter/CMakeLists.txt @@ -4,7 +4,7 @@ macro (setup_testcase_dependencies) # link in the shared libraries link_hifi_libraries(shared networking) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro() setup_hifi_testcase() diff --git a/tests/networking/CMakeLists.txt b/tests/networking/CMakeLists.txt index efa744e4c9..03578ba01d 100644 --- a/tests/networking/CMakeLists.txt +++ b/tests/networking/CMakeLists.txt @@ -4,7 +4,7 @@ macro (setup_testcase_dependencies) # link in the shared libraries link_hifi_libraries(shared networking) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro () setup_hifi_testcase() diff --git a/tests/octree/CMakeLists.txt b/tests/octree/CMakeLists.txt index e2a756105a..9f9315ba2b 100644 --- a/tests/octree/CMakeLists.txt +++ b/tests/octree/CMakeLists.txt @@ -4,7 +4,7 @@ macro (setup_testcase_dependencies) # link in the shared libraries link_hifi_libraries(shared octree gpu model fbx networking environment entities avatars audio animation script-engine physics) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro () setup_hifi_testcase(Script Network) diff --git a/tests/physics/CMakeLists.txt b/tests/physics/CMakeLists.txt index f789a7b2ba..cc3df5ea8e 100644 --- a/tests/physics/CMakeLists.txt +++ b/tests/physics/CMakeLists.txt @@ -3,7 +3,7 @@ macro (SETUP_TESTCASE_DEPENDENCIES) target_bullet() link_hifi_libraries(shared physics) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro () setup_hifi_testcase(Script) diff --git a/tests/recording/CMakeLists.txt b/tests/recording/CMakeLists.txt index a523947f52..4e881fcbd9 100644 --- a/tests/recording/CMakeLists.txt +++ b/tests/recording/CMakeLists.txt @@ -3,7 +3,7 @@ set(TARGET_NAME recording-test) setup_hifi_project(Test) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries(shared recording) -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() # FIXME convert to unit tests # Declare dependencies @@ -11,6 +11,6 @@ copy_dlls_beside_windows_executable() # # link in the shared libraries # link_hifi_libraries(shared recording) # -# copy_dlls_beside_windows_executable() +# package_libraries_for_deployment() #endmacro () #setup_hifi_testcase() diff --git a/tests/render-utils/CMakeLists.txt b/tests/render-utils/CMakeLists.txt index 865db4dad5..25221a1e86 100644 --- a/tests/render-utils/CMakeLists.txt +++ b/tests/render-utils/CMakeLists.txt @@ -8,4 +8,4 @@ set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries link_hifi_libraries(render-utils gl gpu shared) -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/tests/shaders/CMakeLists.txt b/tests/shaders/CMakeLists.txt index fd58a5911f..3ba29c5626 100644 --- a/tests/shaders/CMakeLists.txt +++ b/tests/shaders/CMakeLists.txt @@ -17,4 +17,4 @@ include_directories("${PROJECT_BINARY_DIR}/../../libraries/render-utils/") include_directories("${PROJECT_BINARY_DIR}/../../libraries/entities-renderer/") include_directories("${PROJECT_BINARY_DIR}/../../libraries/model/") -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/tests/shared/CMakeLists.txt b/tests/shared/CMakeLists.txt index 7bddb4b2ed..740014ea75 100644 --- a/tests/shared/CMakeLists.txt +++ b/tests/shared/CMakeLists.txt @@ -5,7 +5,7 @@ macro (setup_testcase_dependencies) # link in the shared libraries link_hifi_libraries(shared) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro () setup_hifi_testcase() diff --git a/tests/ui/CMakeLists.txt b/tests/ui/CMakeLists.txt index 82fc23e680..8fda001e14 100644 --- a/tests/ui/CMakeLists.txt +++ b/tests/ui/CMakeLists.txt @@ -13,4 +13,4 @@ endif() # link in the shared libraries link_hifi_libraries(shared networking gl gpu ui) -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/tools/mtc/CMakeLists.txt b/tools/mtc/CMakeLists.txt index 16c812673d..c7134a869f 100644 --- a/tools/mtc/CMakeLists.txt +++ b/tools/mtc/CMakeLists.txt @@ -1,4 +1,4 @@ set(TARGET_NAME mtc) setup_hifi_project() -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/tools/udt-test/CMakeLists.txt b/tools/udt-test/CMakeLists.txt index 648ef6f00c..f21e3e9aea 100644 --- a/tools/udt-test/CMakeLists.txt +++ b/tools/udt-test/CMakeLists.txt @@ -2,5 +2,4 @@ set(TARGET_NAME udt-test) setup_hifi_project() link_hifi_libraries(networking shared) - -copy_dlls_beside_windows_executable() \ No newline at end of file +package_libraries_for_deployment() \ No newline at end of file From c72c6a0a4773c377524fecfb587b18b3c2cbcbe4 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 21 Nov 2015 10:44:21 +1300 Subject: [PATCH 53/99] Make multiline strings from JavaScript's print() readably in log file Addresses quoting of newline characters as "\n" which was introduced in Qt 5.5. --- libraries/script-engine/src/ScriptEngine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 5326090723..f313cd7d9e 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -62,7 +62,7 @@ static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ } message += context->argument(i).toString(); } - qCDebug(scriptengine) << "script:print()<<" << message; + qCDebug(scriptengine).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline message = message.replace("\\", "\\\\") .replace("\n", "\\n") From 75dfef067c790fd5c250bbaef74dc11d69713207 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 Nov 2015 14:40:27 -0800 Subject: [PATCH 54/99] test script --- examples/acScripts/acSessionID.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 examples/acScripts/acSessionID.js diff --git a/examples/acScripts/acSessionID.js b/examples/acScripts/acSessionID.js new file mode 100644 index 0000000000..d32185daa1 --- /dev/null +++ b/examples/acScripts/acSessionID.js @@ -0,0 +1 @@ +print("sessionID:" + Agent.sessionUUID); \ No newline at end of file From a43b0e0ed8551a5227a80dbb86ee51929d5e9a80 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 20 Nov 2015 15:03:31 -0800 Subject: [PATCH 55/99] Remove obsolete script (superseed by animatedAvatarAgent.js), per https://app.asana.com/0/26225263936266/55964805077915/f (This PR does _not_ remove proceduralAnimationAPI.js that it includes, as the api is included in other scripts. Nor does this PR remove other scripts.) --- examples/acScripts/bot_procedural.js | 673 --------------------------- 1 file changed, 673 deletions(-) delete mode 100644 examples/acScripts/bot_procedural.js diff --git a/examples/acScripts/bot_procedural.js b/examples/acScripts/bot_procedural.js deleted file mode 100644 index 18d42a0e60..0000000000 --- a/examples/acScripts/bot_procedural.js +++ /dev/null @@ -1,673 +0,0 @@ -// -// bot_procedural.js -// hifi -// -// Created by Ben Arnold on 7/29/2013 -// -// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. -// -// This is an example script that demonstrates an NPC avatar. -// -// - -//For procedural walk animation -HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; -Script.include(HIFI_PUBLIC_BUCKET + "scripts/acScripts/proceduralAnimationAPI.js"); - -var procAnimAPI = new ProcAnimAPI(); - -function getRandomFloat(min, max) { - return Math.random() * (max - min) + min; -} - -function getRandomInt (min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -function printVector(string, vector) { - print(string + " " + vector.x + ", " + vector.y + ", " + vector.z); -} - -var CHANCE_OF_MOVING = 0.005; -var CHANCE_OF_SOUND = 0.005; -var CHANCE_OF_HEAD_TURNING = 0.01; -var CHANCE_OF_BIG_MOVE = 1.0; - -var isMoving = false; -var isTurningHead = false; -var isPlayingAudio = false; - -var X_MIN = 0.50; -var X_MAX = 15.60; -var Z_MIN = 0.50; -var Z_MAX = 15.10; -var Y_FEET = 0.0; -var AVATAR_PELVIS_HEIGHT = 0.84; -var Y_PELVIS = Y_FEET + AVATAR_PELVIS_HEIGHT; -var MAX_PELVIS_DELTA = 2.5; - -var MOVE_RANGE_SMALL = 3.0; -var MOVE_RANGE_BIG = 10.0; -var TURN_RANGE = 70.0; -var STOP_TOLERANCE = 0.05; -var MOVE_RATE = 0.05; -var TURN_RATE = 0.2; -var HEAD_TURN_RATE = 0.05; -var PITCH_RANGE = 15.0; -var YAW_RANGE = 35.0; - -var firstPosition = { x: getRandomFloat(X_MIN, X_MAX), y: Y_PELVIS, z: getRandomFloat(Z_MIN, Z_MAX) }; -var targetPosition = { x: 0, y: 0, z: 0 }; -var targetOrientation = { x: 0, y: 0, z: 0, w: 0 }; -var currentOrientation = { x: 0, y: 0, z: 0, w: 0 }; -var targetHeadPitch = 0.0; -var targetHeadYaw = 0.0; - -var basePelvisHeight = 0.0; -var pelvisOscillatorPosition = 0.0; -var pelvisOscillatorVelocity = 0.0; - -function clamp(val, min, max){ - return Math.max(min, Math.min(max, val)) -} - -//Array of all valid bot numbers -var validBotNumbers = []; - -// right now we only use bot 63, since many other bots have messed up skeletons and LOD issues -var botNumber = 63;//getRandomInt(0, 99); - -var newFaceFilePrefix = "ron"; - -var newBodyFilePrefix = "bot" + botNumber; - -// set the face model fst using the bot number -// there is no need to change the body model - we're using the default -Avatar.faceModelURL = HIFI_PUBLIC_BUCKET + "meshes/" + newFaceFilePrefix + ".fst"; -Avatar.skeletonModelURL = HIFI_PUBLIC_BUCKET + "meshes/" + newBodyFilePrefix + "_a.fst"; -Avatar.billboardURL = HIFI_PUBLIC_BUCKET + "meshes/billboards/bot" + botNumber + ".png"; - -Agent.isAvatar = true; -Agent.isListeningToAudioStream = true; - -// change the avatar's position to the random one -Avatar.position = firstPosition; -basePelvisHeight = firstPosition.y; -printVector("New dancer, position = ", Avatar.position); - -function loadSounds() { - var sound_filenames = ["AB1.raw", "Anchorman2.raw", "B1.raw", "B1.raw", "Bale1.raw", "Bandcamp.raw", - "Big1.raw", "Big2.raw", "Brian1.raw", "Buster1.raw", "CES1.raw", "CES2.raw", "CES3.raw", "CES4.raw", - "Carrie1.raw", "Carrie3.raw", "Charlotte1.raw", "EN1.raw", "EN2.raw", "EN3.raw", "Eugene1.raw", "Francesco1.raw", - "Italian1.raw", "Japanese1.raw", "Leigh1.raw", "Lucille1.raw", "Lucille2.raw", "MeanGirls.raw", "Murray2.raw", - "Nigel1.raw", "PennyLane.raw", "Pitt1.raw", "Ricardo.raw", "SN.raw", "Sake1.raw", "Samantha1.raw", "Samantha2.raw", - "Spicoli1.raw", "Supernatural.raw", "Swearengen1.raw", "TheDude.raw", "Tony.raw", "Triumph1.raw", "Uma1.raw", - "Walken1.raw", "Walken2.raw", "Z1.raw", "Z2.raw" - ]; - - var footstep_filenames = ["FootstepW2Left-12db.wav", "FootstepW2Right-12db.wav", "FootstepW3Left-12db.wav", "FootstepW3Right-12db.wav", - "FootstepW5Left-12db.wav", "FootstepW5Right-12db.wav"]; - - var SOUND_BASE_URL = HIFI_PUBLIC_BUCKET + "sounds/Cocktail+Party+Snippets/Raws/"; - - var FOOTSTEP_BASE_URL = HIFI_PUBLIC_BUCKET + "sounds/Footsteps/"; - - for (var i = 0; i < sound_filenames.length; i++) { - sounds.push(SoundCache.getSound(SOUND_BASE_URL + sound_filenames[i])); - } - - for (var i = 0; i < footstep_filenames.length; i++) { - footstepSounds.push(SoundCache.getSound(FOOTSTEP_BASE_URL + footstep_filenames[i])); - } -} - -var sounds = []; -var footstepSounds = []; -loadSounds(); - - -function playRandomSound() { - if (!Agent.isPlayingAvatarSound) { - var whichSound = Math.floor((Math.random() * sounds.length)); - Agent.playAvatarSound(sounds[whichSound]); - } -} - -function playRandomFootstepSound() { - var whichSound = Math.floor((Math.random() * footstepSounds.length)); - Audio.playSound(footstepSounds[whichSound], { - position: Avatar.position, - volume: 1.0 - }); -} - -// ************************************ Facial Animation ********************************** -var allBlendShapes = []; -var targetBlendCoefficient = []; -var currentBlendCoefficient = []; - -//Blendshape constructor -function addBlendshapeToPose(pose, shapeIndex, val) { - var index = pose.blendShapes.length; - pose.blendShapes[index] = {shapeIndex: shapeIndex, val: val }; -} -//The mood of the avatar, determines face. 0 = happy, 1 = angry, 2 = sad. - -//Randomly pick avatar mood. 80% happy, 10% mad 10% sad -var randMood = Math.floor(Math.random() * 11); -var avatarMood; -if (randMood == 0) { - avatarMood = 1; -} else if (randMood == 2) { - avatarMood = 2; -} else { - avatarMood = 0; -} - -var currentExpression = -1; -//Face pose constructor -var happyPoses = []; - -happyPoses[0] = {blendShapes: []}; -addBlendshapeToPose(happyPoses[0], 28, 0.7); //MouthSmile_L -addBlendshapeToPose(happyPoses[0], 29, 0.7); //MouthSmile_R - -happyPoses[1] = {blendShapes: []}; -addBlendshapeToPose(happyPoses[1], 28, 1.0); //MouthSmile_L -addBlendshapeToPose(happyPoses[1], 29, 1.0); //MouthSmile_R -addBlendshapeToPose(happyPoses[1], 21, 0.2); //JawOpen - -happyPoses[2] = {blendShapes: []}; -addBlendshapeToPose(happyPoses[2], 28, 1.0); //MouthSmile_L -addBlendshapeToPose(happyPoses[2], 29, 1.0); //MouthSmile_R -addBlendshapeToPose(happyPoses[2], 21, 0.5); //JawOpen -addBlendshapeToPose(happyPoses[2], 46, 1.0); //CheekSquint_L -addBlendshapeToPose(happyPoses[2], 47, 1.0); //CheekSquint_R -addBlendshapeToPose(happyPoses[2], 17, 1.0); //BrowsU_L -addBlendshapeToPose(happyPoses[2], 18, 1.0); //BrowsU_R - -var angryPoses = []; - -angryPoses[0] = {blendShapes: []}; -addBlendshapeToPose(angryPoses[0], 26, 0.6); //MouthFrown_L -addBlendshapeToPose(angryPoses[0], 27, 0.6); //MouthFrown_R -addBlendshapeToPose(angryPoses[0], 14, 0.6); //BrowsD_L -addBlendshapeToPose(angryPoses[0], 15, 0.6); //BrowsD_R - -angryPoses[1] = {blendShapes: []}; -addBlendshapeToPose(angryPoses[1], 26, 0.9); //MouthFrown_L -addBlendshapeToPose(angryPoses[1], 27, 0.9); //MouthFrown_R -addBlendshapeToPose(angryPoses[1], 14, 0.9); //BrowsD_L -addBlendshapeToPose(angryPoses[1], 15, 0.9); //BrowsD_R - -angryPoses[2] = {blendShapes: []}; -addBlendshapeToPose(angryPoses[2], 26, 1.0); //MouthFrown_L -addBlendshapeToPose(angryPoses[2], 27, 1.0); //MouthFrown_R -addBlendshapeToPose(angryPoses[2], 14, 1.0); //BrowsD_L -addBlendshapeToPose(angryPoses[2], 15, 1.0); //BrowsD_R -addBlendshapeToPose(angryPoses[2], 21, 0.5); //JawOpen -addBlendshapeToPose(angryPoses[2], 46, 1.0); //CheekSquint_L -addBlendshapeToPose(angryPoses[2], 47, 1.0); //CheekSquint_R - -var sadPoses = []; - -sadPoses[0] = {blendShapes: []}; -addBlendshapeToPose(sadPoses[0], 26, 0.6); //MouthFrown_L -addBlendshapeToPose(sadPoses[0], 27, 0.6); //MouthFrown_R -addBlendshapeToPose(sadPoses[0], 16, 0.2); //BrowsU_C -addBlendshapeToPose(sadPoses[0], 2, 0.6); //EyeSquint_L -addBlendshapeToPose(sadPoses[0], 3, 0.6); //EyeSquint_R - -sadPoses[1] = {blendShapes: []}; -addBlendshapeToPose(sadPoses[1], 26, 0.9); //MouthFrown_L -addBlendshapeToPose(sadPoses[1], 27, 0.9); //MouthFrown_R -addBlendshapeToPose(sadPoses[1], 16, 0.6); //BrowsU_C -addBlendshapeToPose(sadPoses[1], 2, 0.9); //EyeSquint_L -addBlendshapeToPose(sadPoses[1], 3, 0.9); //EyeSquint_R - -sadPoses[2] = {blendShapes: []}; -addBlendshapeToPose(sadPoses[2], 26, 1.0); //MouthFrown_L -addBlendshapeToPose(sadPoses[2], 27, 1.0); //MouthFrown_R -addBlendshapeToPose(sadPoses[2], 16, 0.1); //BrowsU_C -addBlendshapeToPose(sadPoses[2], 2, 1.0); //EyeSquint_L -addBlendshapeToPose(sadPoses[2], 3, 1.0); //EyeSquint_R -addBlendshapeToPose(sadPoses[2], 21, 0.3); //JawOpen - -var facePoses = []; -facePoses[0] = happyPoses; -facePoses[1] = angryPoses; -facePoses[2] = sadPoses; - - -function addBlendShape(s) { - allBlendShapes[allBlendShapes.length] = s; -} - -//It is imperative that the following blendshapes are all present and are in the correct order -addBlendShape("EyeBlink_L"); //0 -addBlendShape("EyeBlink_R"); //1 -addBlendShape("EyeSquint_L"); //2 -addBlendShape("EyeSquint_R"); //3 -addBlendShape("EyeDown_L"); //4 -addBlendShape("EyeDown_R"); //5 -addBlendShape("EyeIn_L"); //6 -addBlendShape("EyeIn_R"); //7 -addBlendShape("EyeOpen_L"); //8 -addBlendShape("EyeOpen_R"); //9 -addBlendShape("EyeOut_L"); //10 -addBlendShape("EyeOut_R"); //11 -addBlendShape("EyeUp_L"); //12 -addBlendShape("EyeUp_R"); //13 -addBlendShape("BrowsD_L"); //14 -addBlendShape("BrowsD_R"); //15 -addBlendShape("BrowsU_C"); //16 -addBlendShape("BrowsU_L"); //17 -addBlendShape("BrowsU_R"); //18 -addBlendShape("JawFwd"); //19 -addBlendShape("JawLeft"); //20 -addBlendShape("JawOpen"); //21 -addBlendShape("JawChew"); //22 -addBlendShape("JawRight"); //23 -addBlendShape("MouthLeft"); //24 -addBlendShape("MouthRight"); //25 -addBlendShape("MouthFrown_L"); //26 -addBlendShape("MouthFrown_R"); //27 -addBlendShape("MouthSmile_L"); //28 -addBlendShape("MouthSmile_R"); //29 -addBlendShape("MouthDimple_L"); //30 -addBlendShape("MouthDimple_R"); //31 -addBlendShape("LipsStretch_L"); //32 -addBlendShape("LipsStretch_R"); //33 -addBlendShape("LipsUpperClose"); //34 -addBlendShape("LipsLowerClose"); //35 -addBlendShape("LipsUpperUp"); //36 -addBlendShape("LipsLowerDown"); //37 -addBlendShape("LipsUpperOpen"); //38 -addBlendShape("LipsLowerOpen"); //39 -addBlendShape("LipsFunnel"); //40 -addBlendShape("LipsPucker"); //41 -addBlendShape("ChinLowerRaise"); //42 -addBlendShape("ChinUpperRaise"); //43 -addBlendShape("Sneer"); //44 -addBlendShape("Puff"); //45 -addBlendShape("CheekSquint_L"); //46 -addBlendShape("CheekSquint_R"); //47 - -for (var i = 0; i < allBlendShapes.length; i++) { - targetBlendCoefficient[i] = 0; - currentBlendCoefficient[i] = 0; -} - -function setRandomExpression() { - - //Clear all expression data for current expression - if (currentExpression != -1) { - var expression = facePoses[avatarMood][currentExpression]; - for (var i = 0; i < expression.blendShapes.length; i++) { - targetBlendCoefficient[expression.blendShapes[i].shapeIndex] = 0.0; - } - } - //Get a new current expression - currentExpression = Math.floor(Math.random() * facePoses[avatarMood].length); - var expression = facePoses[avatarMood][currentExpression]; - for (var i = 0; i < expression.blendShapes.length; i++) { - targetBlendCoefficient[expression.blendShapes[i].shapeIndex] = expression.blendShapes[i].val; - } -} - -var expressionChangeSpeed = 0.1; -function updateBlendShapes(deltaTime) { - - for (var i = 0; i < allBlendShapes.length; i++) { - currentBlendCoefficient[i] += (targetBlendCoefficient[i] - currentBlendCoefficient[i]) * expressionChangeSpeed; - Avatar.setBlendshape(allBlendShapes[i], currentBlendCoefficient[i]); - } -} - -var BLINK_SPEED = 0.15; -var CHANCE_TO_BLINK = 0.0025; -var MAX_BLINK = 0.85; -var blink = 0.0; -var isBlinking = false; -function updateBlinking(deltaTime) { - if (isBlinking == false) { - if (Math.random() < CHANCE_TO_BLINK) { - isBlinking = true; - } else { - blink -= BLINK_SPEED; - if (blink < 0.0) blink = 0.0; - } - } else { - blink += BLINK_SPEED; - if (blink > MAX_BLINK) { - blink = MAX_BLINK; - isBlinking = false; - } - } - - currentBlendCoefficient[0] = blink; - currentBlendCoefficient[1] = blink; - targetBlendCoefficient[0] = blink; - targetBlendCoefficient[1] = blink; -} - -// ************************************************************************************* - -//Procedural walk animation using two keyframes -//We use a separate array for front and back joints -//Pitch, yaw, and roll for the joints -var rightAngles = []; -var leftAngles = []; -//for non mirrored joints such as the spine -var middleAngles = []; - -//Actual joint mappings -var SHOULDER_JOINT_NUMBER = 15; -var ELBOW_JOINT_NUMBER = 16; -var JOINT_R_HIP = 1; -var JOINT_R_KNEE = 2; -var JOINT_L_HIP = 6; -var JOINT_L_KNEE = 7; -var JOINT_R_ARM = 15; -var JOINT_R_FOREARM = 16; -var JOINT_L_ARM = 39; -var JOINT_L_FOREARM = 40; -var JOINT_SPINE = 11; -var JOINT_R_FOOT = 3; -var JOINT_L_FOOT = 8; -var JOINT_R_TOE = 4; -var JOINT_L_TOE = 9; - -// ******************************* Animation Is Defined Below ************************************* - -var NUM_FRAMES = 2; -for (var i = 0; i < NUM_FRAMES; i++) { - rightAngles[i] = []; - leftAngles[i] = []; - middleAngles[i] = []; -} -//Joint order for actual joint mappings, should be interleaved R,L,R,L,...S,S,S for R = right, L = left, S = single -var JOINT_ORDER = []; -//*** right / left joints *** -var HIP = 0; -JOINT_ORDER.push(JOINT_R_HIP); -JOINT_ORDER.push(JOINT_L_HIP); -var KNEE = 1; -JOINT_ORDER.push(JOINT_R_KNEE); -JOINT_ORDER.push(JOINT_L_KNEE); -var ARM = 2; -JOINT_ORDER.push(JOINT_R_ARM); -JOINT_ORDER.push(JOINT_L_ARM); -var FOREARM = 3; -JOINT_ORDER.push(JOINT_R_FOREARM); -JOINT_ORDER.push(JOINT_L_FOREARM); -var FOOT = 4; -JOINT_ORDER.push(JOINT_R_FOOT); -JOINT_ORDER.push(JOINT_L_FOOT); -var TOE = 5; -JOINT_ORDER.push(JOINT_R_TOE); -JOINT_ORDER.push(JOINT_L_TOE); -//*** middle joints *** -var SPINE = 0; -JOINT_ORDER.push(JOINT_SPINE); - -//We have to store the angles so we can invert yaw and roll when making the animation -//symmetrical - -//Front refers to leg, not arm. -//Legs Extending -rightAngles[0][HIP] = [30.0, 0.0, 8.0]; -rightAngles[0][KNEE] = [-15.0, 0.0, 0.0]; -rightAngles[0][ARM] = [85.0, -25.0, 0.0]; -rightAngles[0][FOREARM] = [0.0, 0.0, -15.0]; -rightAngles[0][FOOT] = [0.0, 0.0, 0.0]; -rightAngles[0][TOE] = [0.0, 0.0, 0.0]; - -leftAngles[0][HIP] = [-15, 0.0, 8.0]; -leftAngles[0][KNEE] = [-26, 0.0, 0.0]; -leftAngles[0][ARM] = [85.0, 20.0, 0.0]; -leftAngles[0][FOREARM] = [10.0, 0.0, -25.0]; -leftAngles[0][FOOT] = [-13.0, 0.0, 0.0]; -leftAngles[0][TOE] = [34.0, 0.0, 0.0]; - -middleAngles[0][SPINE] = [0.0, -15.0, 5.0]; - -//Legs Passing -rightAngles[1][HIP] = [6.0, 0.0, 8.0]; -rightAngles[1][KNEE] = [-12.0, 0.0, 0.0]; -rightAngles[1][ARM] = [85.0, 0.0, 0.0]; -rightAngles[1][FOREARM] = [0.0, 0.0, -15.0]; -rightAngles[1][FOOT] = [6.0, -8.0, 0.0]; -rightAngles[1][TOE] = [0.0, 0.0, 0.0]; - -leftAngles[1][HIP] = [10.0, 0.0, 8.0]; -leftAngles[1][KNEE] = [-60.0, 0.0, 0.0]; -leftAngles[1][ARM] = [85.0, 0.0, 0.0]; -leftAngles[1][FOREARM] = [0.0, 0.0, -15.0]; -leftAngles[1][FOOT] = [0.0, 0.0, 0.0]; -leftAngles[1][TOE] = [0.0, 0.0, 0.0]; - -middleAngles[1][SPINE] = [0.0, 0.0, 0.0]; - -//Actual keyframes for the animation -var walkKeyFrames = procAnimAPI.generateKeyframes(rightAngles, leftAngles, middleAngles, NUM_FRAMES); - -// ******************************* Animation Is Defined Above ************************************* - -// ********************************** Standing Key Frame ****************************************** -//We don't have to do any mirroring or anything, since this is just a single pose. -var rightQuats = []; -var leftQuats = []; -var middleQuats = []; - -rightQuats[HIP] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 7.0); -rightQuats[KNEE] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0); -rightQuats[ARM] = Quat.fromPitchYawRollDegrees(85.0, 0.0, 0.0); -rightQuats[FOREARM] = Quat.fromPitchYawRollDegrees(0.0, 0.0, -10.0); -rightQuats[FOOT] = Quat.fromPitchYawRollDegrees(0.0, -8.0, 0.0); -rightQuats[TOE] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0); - -leftQuats[HIP] = Quat.fromPitchYawRollDegrees(0, 0.0, -7.0); -leftQuats[KNEE] = Quat.fromPitchYawRollDegrees(0, 0.0, 0.0); -leftQuats[ARM] = Quat.fromPitchYawRollDegrees(85.0, 0.0, 0.0); -leftQuats[FOREARM] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 10.0); -leftQuats[FOOT] = Quat.fromPitchYawRollDegrees(0.0, 8.0, 0.0); -leftQuats[TOE] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0); - -middleQuats[SPINE] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0); - -var standingKeyFrame = new procAnimAPI.KeyFrame(rightQuats, leftQuats, middleQuats); - -// ************************************************************************************************ - - -var currentFrame = 0; - -var walkTime = 0.0; - -var walkWheelRadius = 0.5; -var walkWheelRate = 2.0 * 3.141592 * walkWheelRadius / 8.0; - -var avatarAcceleration = 0.75; -var avatarVelocity = 0.0; -var avatarMaxVelocity = 1.4; - -function handleAnimation(deltaTime) { - - updateBlinking(deltaTime); - updateBlendShapes(deltaTime); - - if (Math.random() < 0.01) { - setRandomExpression(); - } - - if (avatarVelocity == 0.0) { - walkTime = 0.0; - currentFrame = 0; - } else { - walkTime += avatarVelocity * deltaTime; - if (walkTime > walkWheelRate) { - walkTime = 0.0; - currentFrame++; - if (currentFrame % 2 == 1) { - playRandomFootstepSound(); - } - if (currentFrame > 3) { - currentFrame = 0; - } - } - } - - var frame = walkKeyFrames[currentFrame]; - - var walkInterp = walkTime / walkWheelRate; - var animInterp = avatarVelocity / (avatarMaxVelocity / 1.3); - if (animInterp > 1.0) animInterp = 1.0; - - for (var i = 0; i < JOINT_ORDER.length; i++) { - var walkJoint = procAnimAPI.deCasteljau(frame.rotations[i], frame.nextFrame.rotations[i], frame.controlPoints[i][0], frame.controlPoints[i][1], walkInterp); - var standJoint = standingKeyFrame.rotations[i]; - var finalJoint = Quat.mix(standJoint, walkJoint, animInterp); - Avatar.setJointData(JOINT_ORDER[i], finalJoint); - } -} - -function jumpWithLoudness(deltaTime) { - // potentially change pelvis height depending on trailing average loudness - - pelvisOscillatorVelocity += deltaTime * Agent.lastReceivedAudioLoudness * 700.0 ; - - pelvisOscillatorVelocity -= pelvisOscillatorPosition * 0.75; - pelvisOscillatorVelocity *= 0.97; - pelvisOscillatorPosition += deltaTime * pelvisOscillatorVelocity; - Avatar.headPitch = pelvisOscillatorPosition * 60.0; - - var pelvisPosition = Avatar.position; - pelvisPosition.y = (Y_PELVIS - 0.35) + pelvisOscillatorPosition; - - if (pelvisPosition.y < Y_PELVIS) { - pelvisPosition.y = Y_PELVIS; - } else if (pelvisPosition.y > Y_PELVIS + 1.0) { - pelvisPosition.y = Y_PELVIS + 1.0; - } - - Avatar.position = pelvisPosition; -} - -var forcedMove = false; - -var wasMovingLastFrame = false; - -function handleHeadTurn() { - if (!isTurningHead && (Math.random() < CHANCE_OF_HEAD_TURNING)) { - targetHeadPitch = getRandomFloat(-PITCH_RANGE, PITCH_RANGE); - targetHeadYaw = getRandomFloat(-YAW_RANGE, YAW_RANGE); - isTurningHead = true; - } else { - Avatar.headPitch = Avatar.headPitch + (targetHeadPitch - Avatar.headPitch) * HEAD_TURN_RATE; - Avatar.headYaw = Avatar.headYaw + (targetHeadYaw - Avatar.headYaw) * HEAD_TURN_RATE; - if (Math.abs(Avatar.headPitch - targetHeadPitch) < STOP_TOLERANCE && - Math.abs(Avatar.headYaw - targetHeadYaw) < STOP_TOLERANCE) { - isTurningHead = false; - } - } -} - -function stopWalking() { - avatarVelocity = 0.0; - isMoving = false; -} - -var MAX_ATTEMPTS = 40; -function handleWalking(deltaTime) { - - if (forcedMove || (!isMoving && Math.random() < CHANCE_OF_MOVING)) { - // Set new target location - - var moveRange; - if (Math.random() < CHANCE_OF_BIG_MOVE) { - moveRange = MOVE_RANGE_BIG; - } else { - moveRange = MOVE_RANGE_SMALL; - } - - //Keep trying new orientations if the desired target location is out of bounds - var attempts = 0; - do { - targetOrientation = Quat.multiply(Avatar.orientation, Quat.angleAxis(getRandomFloat(-TURN_RANGE, TURN_RANGE), { x:0, y:1, z:0 })); - var front = Quat.getFront(targetOrientation); - - targetPosition = Vec3.sum(Avatar.position, Vec3.multiply(front, getRandomFloat(0.0, moveRange))); - } - while ((targetPosition.x < X_MIN || targetPosition.x > X_MAX || targetPosition.z < Z_MIN || targetPosition.z > Z_MAX) - && attempts < MAX_ATTEMPTS); - - targetPosition.x = clamp(targetPosition.x, X_MIN, X_MAX); - targetPosition.z = clamp(targetPosition.z, Z_MIN, Z_MAX); - targetPosition.y = Y_PELVIS; - - wasMovingLastFrame = true; - isMoving = true; - forcedMove = false; - } else if (isMoving) { - - var targetVector = Vec3.subtract(targetPosition, Avatar.position); - var distance = Vec3.length(targetVector); - if (distance <= avatarVelocity * deltaTime) { - Avatar.position = targetPosition; - stopWalking(); - } else { - var direction = Vec3.normalize(targetVector); - //Figure out if we should be slowing down - var t = avatarVelocity / avatarAcceleration; - var d = (avatarVelocity / 2.0) * t; - if (distance < d) { - avatarVelocity -= avatarAcceleration * deltaTime; - if (avatarVelocity <= 0) { - stopWalking(); - } - } else { - avatarVelocity += avatarAcceleration * deltaTime; - if (avatarVelocity > avatarMaxVelocity) avatarVelocity = avatarMaxVelocity; - } - Avatar.position = Vec3.sum(Avatar.position, Vec3.multiply(direction, avatarVelocity * deltaTime)); - Avatar.orientation = Quat.mix(Avatar.orientation, targetOrientation, TURN_RATE); - - wasMovingLastFrame = true; - - } - } -} - -function handleTalking() { - if (Math.random() < CHANCE_OF_SOUND) { - playRandomSound(); - } -} - -function changePelvisHeight(newHeight) { - var newPosition = Avatar.position; - newPosition.y = newHeight; - Avatar.position = newPosition; -} - -function updateBehavior(deltaTime) { - - if (AvatarList.containsAvatarWithDisplayName("mrdj")) { - if (wasMovingLastFrame) { - isMoving = false; - } - - // we have a DJ, shouldn't we be dancing? - jumpWithLoudness(deltaTime); - } else { - - // no DJ, let's just chill on the dancefloor - randomly walking and talking - handleHeadTurn(); - handleAnimation(deltaTime); - handleWalking(deltaTime); - handleTalking(); - } -} - -Script.update.connect(updateBehavior); \ No newline at end of file From e97e864de77ae0e3dea74b30b5d122852dcdca81 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 20 Nov 2015 15:05:17 -0800 Subject: [PATCH 56/99] Getting the animation playback to work --- libraries/avatars/src/AvatarData.cpp | 23 +++++++++++++++-------- libraries/avatars/src/AvatarData.h | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 44a781bc24..3b8623243e 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -801,6 +801,14 @@ void AvatarData::changeReferential(Referential* ref) { _referential = ref; } +void AvatarData::setRawJointData(QVector data) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setRawJointData", Q_ARG(QVector, data)); + return; + } + _jointData = data; +} + void AvatarData::setJointData(int index, const glm::quat& rotation, const glm::vec3& translation) { if (index == -1) { return; @@ -1538,16 +1546,15 @@ void AvatarData::fromFrame(const QByteArray& frameData, AvatarData& result) { QVector jointArray; QJsonArray jointArrayJson = root[JSON_AVATAR_JOINT_ARRAY].toArray(); jointArray.reserve(jointArrayJson.size()); + int i = 0; for (const auto& jointJson : jointArrayJson) { - jointArray.push_back(jointDataFromJsonValue(jointJson)); + auto joint = jointDataFromJsonValue(jointJson); + jointArray.push_back(joint); + result.setJointData(i, joint.rotation, joint.translation); + result._jointData[i].rotationSet = true; // Have to do that yep + i++; } - - QVector jointRotations; - jointRotations.reserve(jointArray.size()); - for (const auto& joint : jointArray) { - jointRotations.push_back(joint.rotation); - } - result.setJointRotations(jointRotations); + result.setRawJointData(jointArray); } #if 0 diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 846c314e4b..847a369185 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -247,7 +247,7 @@ public: Q_INVOKABLE char getHandState() const { return _handState; } const QVector& getRawJointData() const { return _jointData; } - void setRawJointData(QVector data) { _jointData = data; } + Q_INVOKABLE void setRawJointData(QVector data); Q_INVOKABLE virtual void setJointData(int index, const glm::quat& rotation, const glm::vec3& translation); Q_INVOKABLE virtual void setJointRotation(int index, const glm::quat& rotation); From 0dc551409dcac284d1ace3d7b2ad8f185861edf9 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 20 Nov 2015 15:22:01 -0800 Subject: [PATCH 57/99] Fixing a comment --- libraries/avatars/src/AvatarData.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 3b8623243e..0574f712bc 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1551,7 +1551,7 @@ void AvatarData::fromFrame(const QByteArray& frameData, AvatarData& result) { auto joint = jointDataFromJsonValue(jointJson); jointArray.push_back(joint); result.setJointData(i, joint.rotation, joint.translation); - result._jointData[i].rotationSet = true; // Have to do that yep + result._jointData[i].rotationSet = true; // Have to do that to broadcast the avatar new pose i++; } result.setRawJointData(jointArray); From aba65bd9b8484d5bc93cfa98e04922cc370c2dd7 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 20 Nov 2015 15:45:08 -0800 Subject: [PATCH 58/99] rename linearDamping to damping --- examples/FlockOfbirds.js | 2 +- examples/libraries/easyStarExample.js | 2 +- examples/toybox/basketball/createRack.js | 4 ++-- examples/toybox/basketball/createSingleBasketball.js | 2 +- examples/toybox/blockers/createTestBlocks.js | 6 +++--- examples/toybox/bubblewand/wand.js | 4 ++-- examples/toybox/ping_pong_gun/pingPongGun.js | 2 +- unpublishedScripts/basketballsResetter.js | 2 +- unpublishedScripts/hiddenEntityReset.js | 12 ++++++------ unpublishedScripts/masterReset.js | 12 ++++++------ 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/FlockOfbirds.js b/examples/FlockOfbirds.js index dab086eff4..5d67c25c17 100644 --- a/examples/FlockOfbirds.js +++ b/examples/FlockOfbirds.js @@ -271,7 +271,7 @@ function loadBirds(howMany) { dimensions: { x: BIRD_SIZE, y: BIRD_SIZE, z: BIRD_SIZE }, gravity: { x: 0, y: BIRD_GRAVITY, z: 0 }, velocity: { x: 0, y: -0.1, z: 0 }, - linearDamping: LINEAR_DAMPING, + damping: LINEAR_DAMPING, collisionsWillMove: true, lifetime: STARTING_LIFETIME, color: colors[whichBird] diff --git a/examples/libraries/easyStarExample.js b/examples/libraries/easyStarExample.js index 20a50d2f7d..76c791a81f 100644 --- a/examples/libraries/easyStarExample.js +++ b/examples/libraries/easyStarExample.js @@ -76,7 +76,7 @@ var playerSphere = Entities.addEntity({ z: 0 }, collisionsWillMove: true, - linearDamping: 0.2 + damping: 0.2 }); Script.setInterval(function(){ diff --git a/examples/toybox/basketball/createRack.js b/examples/toybox/basketball/createRack.js index cda1a115d4..aee492f684 100644 --- a/examples/toybox/basketball/createRack.js +++ b/examples/toybox/basketball/createRack.js @@ -41,7 +41,7 @@ var rack = Entities.addEntity({ y: -9.8, z: 0 }, - linearDamping: 1, + damping: 1, dimensions: { x: 0.4, y: 1.37, @@ -83,7 +83,7 @@ function createBalls() { z: DIAMETER }, restitution: 1.0, - linearDamping: 0.00001, + damping: 0.00001, gravity: { x: 0, y: -9.8, diff --git a/examples/toybox/basketball/createSingleBasketball.js b/examples/toybox/basketball/createSingleBasketball.js index 162b572bd1..a1e0140553 100644 --- a/examples/toybox/basketball/createSingleBasketball.js +++ b/examples/toybox/basketball/createSingleBasketball.js @@ -46,7 +46,7 @@ function makeBasketball() { collisionSoundURL: collisionSoundURL, modelURL: basketballURL, restitution: 1.0, - linearDamping: 0.00001, + damping: 0.00001, shapeType: "sphere" }); originalPosition = position; diff --git a/examples/toybox/blockers/createTestBlocks.js b/examples/toybox/blockers/createTestBlocks.js index d92d8d2b32..10bdb14d76 100644 --- a/examples/toybox/blockers/createTestBlocks.js +++ b/examples/toybox/blockers/createTestBlocks.js @@ -88,7 +88,7 @@ var topBlock = Entities.addEntity({ dimensions: blockDimensions, position: topBlock_position, rotation: topBlock_rotation, - linearDamping: LINEAR_DAMPING, + damping: LINEAR_DAMPING, gravity: BLOCK_GRAVITY, collisionsWillMove: true, velocity: { @@ -106,7 +106,7 @@ var sideBlock1 = Entities.addEntity({ dimensions: blockDimensions, position: sideBlock1_position, rotation: sideBlock1_rotation, - linearDamping: LINEAR_DAMPING, + damping: LINEAR_DAMPING, gravity: BLOCK_GRAVITY, collisionsWillMove: true }); @@ -120,7 +120,7 @@ var sideBlock2 = Entities.addEntity({ position: sideBlock2_position, rotation: sideBlock2_rotation, collsionsWillMove: true, - linearDamping: LINEAR_DAMPING, + damping: LINEAR_DAMPING, gravity: BLOCK_GRAVITY, collisionsWillMove: true }); diff --git a/examples/toybox/bubblewand/wand.js b/examples/toybox/bubblewand/wand.js index f429ef6d55..a6e4eae34d 100644 --- a/examples/toybox/bubblewand/wand.js +++ b/examples/toybox/bubblewand/wand.js @@ -28,7 +28,7 @@ var BUBBLE_LIFETIME_MAX = 8; var BUBBLE_SIZE_MIN = 0.02; var BUBBLE_SIZE_MAX = 0.1; - var BUBBLE_LINEAR_DAMPING = 0.4; + var BUBBLE_LINEAR_DAMPING = 0.2; var BUBBLE_GRAVITY_MIN = 0.1; var BUBBLE_GRAVITY_MAX = 0.3; var GROWTH_FACTOR = 0.005; @@ -163,7 +163,7 @@ dimensions: BUBBLE_INITIAL_DIMENSIONS, collisionsWillMove: false, ignoreForCollisions: true, - linearDamping: BUBBLE_LINEAR_DAMPING, + damping: BUBBLE_LINEAR_DAMPING, shapeType: "sphere" }); diff --git a/examples/toybox/ping_pong_gun/pingPongGun.js b/examples/toybox/ping_pong_gun/pingPongGun.js index 48b82f0a36..bd8b5f710d 100644 --- a/examples/toybox/ping_pong_gun/pingPongGun.js +++ b/examples/toybox/ping_pong_gun/pingPongGun.js @@ -123,7 +123,7 @@ type:'Sphere', color: BALL_COLOR, dimensions: BALL_DIMENSIONS, - linearDamping: BALL_LINEAR_DAMPING, + damping: BALL_LINEAR_DAMPING, gravity: BALL_GRAVITY, restitution: BALL_RESTITUTION, collisionsWillMove: true, diff --git a/unpublishedScripts/basketballsResetter.js b/unpublishedScripts/basketballsResetter.js index 791ac1c0cf..6b8bc75f0d 100644 --- a/unpublishedScripts/basketballsResetter.js +++ b/unpublishedScripts/basketballsResetter.js @@ -75,7 +75,7 @@ var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; z: DIAMETER }, restitution: 1.0, - linearDamping: 0.00001, + damping: 0.00001, gravity: { x: 0, y: -9.8, diff --git a/unpublishedScripts/hiddenEntityReset.js b/unpublishedScripts/hiddenEntityReset.js index a8b4383b04..cf9eaaa451 100644 --- a/unpublishedScripts/hiddenEntityReset.js +++ b/unpublishedScripts/hiddenEntityReset.js @@ -253,7 +253,7 @@ y: -9.8, z: 0 }, - linearDamping: 1, + damping: 1, dimensions: { x: 0.4, y: 1.37, @@ -301,7 +301,7 @@ z: DIAMETER }, restitution: 1.0, - linearDamping: 0.00001, + damping: 0.00001, gravity: { x: 0, y: -9.8, @@ -871,7 +871,7 @@ y: -100, z: 0 }, - linearDamping: 1, + damping: 1, angularDamping: 10, mass: 10, userData: JSON.stringify({ @@ -990,7 +990,7 @@ z: 0 }, restitution: 10, - linearDamping: 0.0, + damping: 0.0, velocity: { x: 0, y: -0.01, @@ -1113,7 +1113,7 @@ y: 0, z: 0 }, - linearDamping: 0.4, + damping: 0.4, userData: JSON.stringify({ resetMe: { resetMe: true @@ -1156,7 +1156,7 @@ y: 0, z: 0 }, - linearDamping: 0.2, + damping: 0.2, userData: JSON.stringify({ resetMe: { resetMe: true diff --git a/unpublishedScripts/masterReset.js b/unpublishedScripts/masterReset.js index 1b4c83a1d5..60b4e7a72f 100644 --- a/unpublishedScripts/masterReset.js +++ b/unpublishedScripts/masterReset.js @@ -233,7 +233,7 @@ MasterReset = function() { y: -9.8, z: 0 }, - linearDamping: 1, + damping: 1, dimensions: { x: 0.4, y: 1.37, @@ -282,7 +282,7 @@ MasterReset = function() { z: DIAMETER }, restitution: 1.0, - linearDamping: 0.00001, + damping: 0.00001, gravity: { x: 0, y: -9.8, @@ -853,7 +853,7 @@ MasterReset = function() { y: -100, z: 0 }, - linearDamping: 1, + damping: 1, angularDamping: 10, mass: 10, userData: JSON.stringify({ @@ -973,7 +973,7 @@ MasterReset = function() { z: 0 }, restitution: 10, - linearDamping: 0.0, + damping: 0.01, velocity: { x: 0, y: -0.01, @@ -1096,7 +1096,7 @@ MasterReset = function() { y: 0, z: 0 }, - linearDamping: 0.4, + damping: 0.4, userData: JSON.stringify({ resetMe: { resetMe: true @@ -1139,7 +1139,7 @@ MasterReset = function() { y: 0, z: 0 }, - linearDamping: 0.2, + damping: 0.2, userData: JSON.stringify({ resetMe: { resetMe: true From 46bb45cee937ef9b910b06883da61e5e773ea767 Mon Sep 17 00:00:00 2001 From: EdgarPironti Date: Fri, 20 Nov 2015 11:47:49 -0800 Subject: [PATCH 59/99] Agents/Master upgrading --- examples/acScripts/playbackAgents.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/examples/acScripts/playbackAgents.js b/examples/acScripts/playbackAgents.js index c50c727277..a84701354e 100644 --- a/examples/acScripts/playbackAgents.js +++ b/examples/acScripts/playbackAgents.js @@ -52,24 +52,19 @@ function getAction(channel, message, senderID) { if (command.id_key == id || command.id_key == -1) { if (command.action_key === 6) { - clip_url = command.clip_url_key; - // If the id is -1 (broadcast) and the action is 6, in the url should be the performance file - // with all the clips recorded in a session (not just the single clip url). - // It has to be computed here in order to retrieve the url for the single agent. - // Checking the id we can assign the correct url to the correct agent. + clip_url = command.clip_url_key; if (command.id_key == -1) { Assets.downloadData(clip_url, function (data) { var myJSONObject = JSON.parse(data); - var hash = myJSONObject.results[id].hashATP; + var hash = myJSONObject.avatarClips[id]; }); - Assets.downloadData(hash, function (data) { - clip_url = JSON.parse(data); - }); + clip_url = hash; } } + action = command.action_key; print("That command was for me!"); From f56d2fba7f651af6b06b22dec4a6260f6d3c499a Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 Nov 2015 16:00:43 -0800 Subject: [PATCH 60/99] allow playbackAgents to auto-register --- assignment-client/src/Agent.cpp | 6 +++- assignment-client/src/Agent.h | 4 +++ examples/acScripts/playbackAgents.js | 46 ++++++++++++++++++++++------ examples/acScripts/playbackMaster.js | 34 +++++++++++++++++--- 4 files changed, 74 insertions(+), 16 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 2fdba9e256..5cdceb06ae 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -223,7 +223,6 @@ void Agent::executeScript() { AvatarData::fromFrame(frame->data, *scriptedAvatar); }); - using namespace recording; static const FrameType AUDIO_FRAME_TYPE = Frame::registerFrameType(AudioConstants::AUDIO_FRAME_NAME); Frame::registerFrameHandler(AUDIO_FRAME_TYPE, [this, &scriptedAvatar](Frame::ConstPointer frame) { @@ -280,6 +279,11 @@ void Agent::executeScript() { setFinished(true); } +QUuid Agent::getSessionUUID() const { + return DependencyManager::get()->getSessionUUID(); +} + + void Agent::setIsAvatar(bool isAvatar) { _isAvatar = isAvatar; diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 6819976633..274e5f3b55 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -35,6 +36,8 @@ class Agent : public ThreadedAssignment { Q_PROPERTY(bool isPlayingAvatarSound READ isPlayingAvatarSound) Q_PROPERTY(bool isListeningToAudioStream READ isListeningToAudioStream WRITE setIsListeningToAudioStream) Q_PROPERTY(float lastReceivedAudioLoudness READ getLastReceivedAudioLoudness) + Q_PROPERTY(QUuid sessionUUID READ getSessionUUID) + public: Agent(NLPacket& packet); @@ -47,6 +50,7 @@ public: void setIsListeningToAudioStream(bool isListeningToAudioStream) { _isListeningToAudioStream = isListeningToAudioStream; } float getLastReceivedAudioLoudness() const { return _lastReceivedAudioLoudness; } + QUuid getSessionUUID() const; virtual void aboutToFinish(); diff --git a/examples/acScripts/playbackAgents.js b/examples/acScripts/playbackAgents.js index c50c727277..66c3696fe3 100644 --- a/examples/acScripts/playbackAgents.js +++ b/examples/acScripts/playbackAgents.js @@ -10,7 +10,7 @@ // // Set the following variables to the values needed -var channel = "PlaybackChannel1"; +var commandChannel = "com.highfidelity.PlaybackChannel1"; var clip_url = null; var playFromCurrentLocation = true; var useDisplayName = true; @@ -18,7 +18,9 @@ var useAttachments = true; var useAvatarModel = true; // ID of the agent. Two agents can't have the same ID. -var id = 0; +var announceIDChannel = "com.highfidelity.playbackAgent.announceID"; +var UNKNOWN_AGENT_ID = -2; +var id = UNKNOWN_AGENT_ID; // unknown until aknowledged // Set position/orientation/scale here if playFromCurrentLocation is true Avatar.position = { x:0, y: 0, z: 0 }; @@ -45,7 +47,6 @@ Recording.setPlayerUseHeadModel(false); Recording.setPlayerUseSkeletonModel(useAvatarModel); function getAction(channel, message, senderID) { - if(subscribed) { var command = JSON.parse(message); print("I'm the agent " + id + " and I received this: ID: " + command.id_key + " Action: " + command.action_key + " URL: " + command.clip_url_key); @@ -139,17 +140,42 @@ function getAction(channel, message, senderID) { } -function update(deltaTime) { +function update(deltaTime) { totalTime += deltaTime; - if (totalTime > WAIT_FOR_AUDIO_MIXER && !subscribed) { - Messages.subscribe(channel); - subscribed = true; - print("I'm the agent and I am ready to receive!") + if (totalTime > WAIT_FOR_AUDIO_MIXER) { + if (!subscribed) { + Messages.subscribe(commandChannel); // command channel + Messages.subscribe(announceIDChannel); // id announce channel + subscribed = true; + print("I'm the agent and I am ready to receive!"); + } + if (subscribed && id == UNKNOWN_AGENT_ID) { + print("sending ready, id:" + id); + Messages.sendMessage(announceIDChannel, "ready"); + } } + } -Script.update.connect(update); -Messages.messageReceived.connect(getAction); +Messages.messageReceived.connect(function (channel, message, senderID) { + if (channel == announceIDChannel && message != "ready") { + // If I don't yet know if my ID has been recieved, then check to see if the master has acknowledged me + if (id == UNKNOWN_AGENT_ID) { + var parts = message.split("."); + var agentID = parts[0]; + var agentIndex = parts[1]; + if (agentID == Agent.sessionUUID) { + id = agentIndex; + Messages.unsubscribe(announceIDChannel); // id announce channel + } + } + } + if (channel == commandChannel) { + getAction(channel, message, senderID); + } +}); + +Script.update.connect(update); diff --git a/examples/acScripts/playbackMaster.js b/examples/acScripts/playbackMaster.js index e3448c0256..4703f0e4fd 100644 --- a/examples/acScripts/playbackMaster.js +++ b/examples/acScripts/playbackMaster.js @@ -14,11 +14,16 @@ HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; var ac_number = 1; // This is the default number of ACs. Their ID need to be unique and between 0 (included) and ac_number (excluded) var names = new Array(); // It is possible to specify the name of the ACs in this array. ACs names ordered by IDs (Default name is "ACx", x = ID + 1)) -var channel = "PlaybackChannel1"; +var channel = "com.highfidelity.PlaybackChannel1"; var subscribed = false; var clip_url = null; var input_text = null; +var knownAgents = new Array; // We will add our known agents here when we discover them + +// available playbackAgents will announce their sessionID here. +var announceIDChannel = "com.highfidelity.playbackAgent.announceID"; + // Script. DO NOT MODIFY BEYOND THIS LINE. Script.include("../libraries/toolBars.js"); @@ -51,8 +56,9 @@ setupPlayback(); function setupPlayback() { ac_number = Window.prompt("Insert number of agents: ","1"); - if (ac_number === "" || ac_number === null) + if (ac_number === "" || ac_number === null) { ac_number = 1; + } Messages.subscribe(channel); subscribed = true; setupToolBars(); @@ -134,7 +140,6 @@ function setupToolBars() { } function sendCommand(id, action) { - if (action === SHOW) { toolBars[id].selectTool(onOffIcon[id], false); toolBars[id].setAlpha(ALPHA_ON, playIcon[id]); @@ -151,8 +156,9 @@ function sendCommand(id, action) { return; } - if (id == (toolBars.length - 1)) + if (id == (toolBars.length - 1)) { id = -1; // Master command becomes broadcast. + } var message = { id_key: id, @@ -249,12 +255,30 @@ function scriptEnding() { Overlays.deleteOverlay(nameOverlays[i]); } - if(subscribed) + if (subscribed) { Messages.unsubscribe(channel); + } + Messages.unsubscribe(announceIDChannel); } Controller.mousePressEvent.connect(mousePressEvent); Script.update.connect(update); Script.scriptEnding.connect(scriptEnding); + + +Messages.subscribe(announceIDChannel); +Messages.messageReceived.connect(function (channel, message, senderID) { + if (channel == announceIDChannel && message == "ready") { + // check to see if we know about this agent + if (knownAgents.indexOf(senderID) < 0) { + var indexOfNewAgent = knownAgents.length; + knownAgents[indexOfNewAgent] = senderID; + var acknowledgeMessage = senderID + "." + indexOfNewAgent; + Messages.sendMessage(announceIDChannel, acknowledgeMessage); + } + + } +}); + moveUI(); \ No newline at end of file From 985fcf7c4b0619e6414d466bb41d045fc466b0a4 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 Nov 2015 16:02:01 -0800 Subject: [PATCH 61/99] remove unneeded example --- examples/acScripts/acSessionID.js | 1 - 1 file changed, 1 deletion(-) delete mode 100644 examples/acScripts/acSessionID.js diff --git a/examples/acScripts/acSessionID.js b/examples/acScripts/acSessionID.js deleted file mode 100644 index d32185daa1..0000000000 --- a/examples/acScripts/acSessionID.js +++ /dev/null @@ -1 +0,0 @@ -print("sessionID:" + Agent.sessionUUID); \ No newline at end of file From 1b18d3656ffeee15c7cc5db0f91bdcbc7977b702 Mon Sep 17 00:00:00 2001 From: EdgarPironti Date: Thu, 19 Nov 2015 19:36:17 -0800 Subject: [PATCH 62/99] Save clip to asset --- .../src/RecordingScriptingInterface.cpp | 56 +++++++++++++++++++ .../src/RecordingScriptingInterface.h | 9 +++ 2 files changed, 65 insertions(+) diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index 83e44ef173..fbabdc73af 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -18,10 +18,15 @@ #include #include +#include +#include + #include "ScriptEngineLogging.h" using namespace recording; +static const QString HFR_EXTENSION = ".hfr"; +const QString URL_SCHEME_ATP = "atp"; RecordingScriptingInterface::RecordingScriptingInterface() { _player = DependencyManager::get(); @@ -171,6 +176,57 @@ void RecordingScriptingInterface::saveRecording(const QString& filename) { recording::Clip::toFile(filename, _lastClip); } +bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUrl) { + + if (!getClipAtpUrl.isFunction()) + return false; + + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "saveRecordingToAsset", Qt::BlockingQueuedConnection, + Q_ARG(QScriptValue, getClipAtpUrl)); + return false; + } + + if (!_lastClip) { + qWarning() << "There is no recording to save"; + return false; + } + + QString filename = "./temp.hfr"; + recording::Clip::toFile(filename, _lastClip); + + QUrl url{ filename }; + + if (auto upload = DependencyManager::get()->createUpload(url.toLocalFile())) { // Here we should use the other implementation of createUpload, but we need the QByteArray of the _lastClip + + QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable { + auto filename = QFileInfo(upload->getFilename()).fileName(); + QString clip_atp_url = ""; + + if (upload->getError() == AssetUpload::NoError) { + + clip_atp_url = QString("%1:%2.%3").arg(URL_SCHEME_ATP, hash, upload->getExtension()); + upload->deleteLater(); + } else { + AssetUploadDialogFactory::getInstance().handleUploadFinished(upload, hash); + } + + QScriptValueList args; + args << clip_atp_url; + getClipAtpUrl.call(QScriptValue(), args); + }); + + // start the upload now + upload->start(); + return true; + + } + + return false; + +} + + void RecordingScriptingInterface::loadLastRecording() { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "loadLastRecording", Qt::BlockingQueuedConnection); diff --git a/libraries/script-engine/src/RecordingScriptingInterface.h b/libraries/script-engine/src/RecordingScriptingInterface.h index b39485d75c..704e10efa1 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.h +++ b/libraries/script-engine/src/RecordingScriptingInterface.h @@ -18,6 +18,11 @@ #include #include +#include +#include +#include +#include "../../../interface/src/ui/AssetUploadDialogFactory.h" + class RecordingScriptingInterface : public QObject, public Dependency { Q_OBJECT @@ -60,6 +65,7 @@ public slots: float recorderElapsed() const; void saveRecording(const QString& filename); + bool saveRecordingToAsset(QScriptValue getClipAtpUrl); void loadLastRecording(); protected: @@ -76,6 +82,9 @@ protected: Flag _useAttachments { false }; Flag _useSkeletonModel { false }; recording::ClipPointer _lastClip; + +private: + void clipUploadFinished(AssetUpload* upload, const QString& hash); }; #endif // hifi_RecordingScriptingInterface_h From 1db6d0e3c192b669e03a46f938b3ca5ee43c0f66 Mon Sep 17 00:00:00 2001 From: EdgarPironti Date: Thu, 19 Nov 2015 22:51:36 -0800 Subject: [PATCH 63/99] Upload QByteArray --- .../src/RecordingScriptingInterface.cpp | 21 +++++++------------ .../src/RecordingScriptingInterface.h | 3 --- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index fbabdc73af..128182d2a8 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -177,8 +177,7 @@ void RecordingScriptingInterface::saveRecording(const QString& filename) { } bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUrl) { - - if (!getClipAtpUrl.isFunction()) + if (!getClipAtpUrl.isFunction()) return false; if (QThread::currentThread() != thread()) { @@ -192,13 +191,14 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr return false; } - QString filename = "./temp.hfr"; - recording::Clip::toFile(filename, _lastClip); + //QString filename = "./temp.hfr"; + //recording::Clip::toFile(filename, _lastClip); - QUrl url{ filename }; - - if (auto upload = DependencyManager::get()->createUpload(url.toLocalFile())) { // Here we should use the other implementation of createUpload, but we need the QByteArray of the _lastClip + //QUrl url{ filename }; + //if (auto upload = DependencyManager::get()->createUpload(url.toLocalFile())) { + + if (auto upload = DependencyManager::get()->createUpload(recording::Clip::toBuffer(_lastClip), HFR_EXTENSION)) { QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable { auto filename = QFileInfo(upload->getFilename()).fileName(); QString clip_atp_url = ""; @@ -215,18 +215,13 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr args << clip_atp_url; getClipAtpUrl.call(QScriptValue(), args); }); - - // start the upload now upload->start(); return true; - - } + } return false; - } - void RecordingScriptingInterface::loadLastRecording() { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "loadLastRecording", Qt::BlockingQueuedConnection); diff --git a/libraries/script-engine/src/RecordingScriptingInterface.h b/libraries/script-engine/src/RecordingScriptingInterface.h index 704e10efa1..21aa9b35d4 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.h +++ b/libraries/script-engine/src/RecordingScriptingInterface.h @@ -82,9 +82,6 @@ protected: Flag _useAttachments { false }; Flag _useSkeletonModel { false }; recording::ClipPointer _lastClip; - -private: - void clipUploadFinished(AssetUpload* upload, const QString& hash); }; #endif // hifi_RecordingScriptingInterface_h From 35365f9157f24f13a2b79ce61858d9c18b148507 Mon Sep 17 00:00:00 2001 From: EdgarPironti Date: Fri, 20 Nov 2015 13:32:55 -0800 Subject: [PATCH 64/99] Fixes --- libraries/script-engine/src/RecordingScriptingInterface.cpp | 5 ++--- libraries/script-engine/src/RecordingScriptingInterface.h | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index 128182d2a8..34114fdf2f 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -25,8 +25,7 @@ using namespace recording; -static const QString HFR_EXTENSION = ".hfr"; -const QString URL_SCHEME_ATP = "atp"; +static const QString HFR_EXTENSION = "hfr"; RecordingScriptingInterface::RecordingScriptingInterface() { _player = DependencyManager::get(); @@ -208,7 +207,7 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr clip_atp_url = QString("%1:%2.%3").arg(URL_SCHEME_ATP, hash, upload->getExtension()); upload->deleteLater(); } else { - AssetUploadDialogFactory::getInstance().handleUploadFinished(upload, hash); + qDebug() << "Error during the Asset upload."; } QScriptValueList args; diff --git a/libraries/script-engine/src/RecordingScriptingInterface.h b/libraries/script-engine/src/RecordingScriptingInterface.h index 21aa9b35d4..c1287a42d7 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.h +++ b/libraries/script-engine/src/RecordingScriptingInterface.h @@ -21,7 +21,6 @@ #include #include #include -#include "../../../interface/src/ui/AssetUploadDialogFactory.h" class RecordingScriptingInterface : public QObject, public Dependency { Q_OBJECT From e70f6855aa355c09f3a559dc66124bb5635cc008 Mon Sep 17 00:00:00 2001 From: EdgarPironti Date: Fri, 20 Nov 2015 13:36:57 -0800 Subject: [PATCH 65/99] Fixes --- .../script-engine/src/RecordingScriptingInterface.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index 34114fdf2f..36ef88354e 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -190,14 +190,7 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr return false; } - //QString filename = "./temp.hfr"; - //recording::Clip::toFile(filename, _lastClip); - - //QUrl url{ filename }; - - //if (auto upload = DependencyManager::get()->createUpload(url.toLocalFile())) { - - if (auto upload = DependencyManager::get()->createUpload(recording::Clip::toBuffer(_lastClip), HFR_EXTENSION)) { + if (auto upload = DependencyManager::get()->createUpload(recording::Clip::toBuffer(_lastClip), HFR_EXTENSION)) { QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable { auto filename = QFileInfo(upload->getFilename()).fileName(); QString clip_atp_url = ""; From df2153186d6c6005137f0854cd7dad0bddd72c07 Mon Sep 17 00:00:00 2001 From: EdgarPironti Date: Fri, 20 Nov 2015 16:04:28 -0800 Subject: [PATCH 66/99] Fixes --- .../src/RecordingScriptingInterface.cpp | 19 ++++++++++++++----- .../src/RecordingScriptingInterface.h | 4 +--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index 36ef88354e..1a88d6e260 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -18,6 +18,10 @@ #include #include + +#include +#include +#include #include #include @@ -176,13 +180,18 @@ void RecordingScriptingInterface::saveRecording(const QString& filename) { } bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUrl) { - if (!getClipAtpUrl.isFunction()) + if (!getClipAtpUrl.isFunction()) { + qCWarning(scriptengine) << "The argument is not a function."; return false; + } + if (QThread::currentThread() != thread()) { + bool result{ false }; QMetaObject::invokeMethod(this, "saveRecordingToAsset", Qt::BlockingQueuedConnection, - Q_ARG(QScriptValue, getClipAtpUrl)); - return false; + Q_ARG(QScriptValue, getClipAtpUrl), + Q_RETURN_ARG(bool, result)); + return result; } if (!_lastClip) { @@ -192,7 +201,6 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr if (auto upload = DependencyManager::get()->createUpload(recording::Clip::toBuffer(_lastClip), HFR_EXTENSION)) { QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable { - auto filename = QFileInfo(upload->getFilename()).fileName(); QString clip_atp_url = ""; if (upload->getError() == AssetUpload::NoError) { @@ -200,7 +208,7 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr clip_atp_url = QString("%1:%2.%3").arg(URL_SCHEME_ATP, hash, upload->getExtension()); upload->deleteLater(); } else { - qDebug() << "Error during the Asset upload."; + qCWarning(scriptengine) << "Error during the Asset upload."; } QScriptValueList args; @@ -211,6 +219,7 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr return true; } + qCWarning(scriptengine) << "Saving on asset failed."; return false; } diff --git a/libraries/script-engine/src/RecordingScriptingInterface.h b/libraries/script-engine/src/RecordingScriptingInterface.h index c1287a42d7..9661ee9d80 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.h +++ b/libraries/script-engine/src/RecordingScriptingInterface.h @@ -18,9 +18,7 @@ #include #include -#include -#include -#include +class QScriptValue; class RecordingScriptingInterface : public QObject, public Dependency { Q_OBJECT From 6e9e53bd0cb8f9485b2957dc9aa7f9c95cd655c5 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 20 Nov 2015 18:17:53 -0800 Subject: [PATCH 67/99] Clean up a bit of the playbackAgent script --- examples/acScripts/playbackAgents.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/examples/acScripts/playbackAgents.js b/examples/acScripts/playbackAgents.js index 918d8a1e1d..16dd469a89 100644 --- a/examples/acScripts/playbackAgents.js +++ b/examples/acScripts/playbackAgents.js @@ -53,20 +53,9 @@ function getAction(channel, message, senderID) { if (command.id_key == id || command.id_key == -1) { if (command.action_key === 6) { - clip_url = command.clip_url_key; - - if (command.id_key == -1) { - Assets.downloadData(clip_url, function (data) { - var myJSONObject = JSON.parse(data); - var hash = myJSONObject.avatarClips[id]; - }); - - clip_url = hash; - } } - - + action = command.action_key; print("That command was for me!"); print("My clip is: " + clip_url); @@ -173,4 +162,3 @@ Messages.messageReceived.connect(function (channel, message, senderID) { }); Script.update.connect(update); - From ebbed7c525c598e2e57783e3197e5a370cb9e5ee Mon Sep 17 00:00:00 2001 From: EdgarPironti Date: Fri, 20 Nov 2015 18:18:04 -0800 Subject: [PATCH 68/99] Save clip to asset - removing last fix --- .../script-engine/src/RecordingScriptingInterface.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index 1a88d6e260..6f82e22b8d 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -185,13 +185,10 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr return false; } - if (QThread::currentThread() != thread()) { - bool result{ false }; QMetaObject::invokeMethod(this, "saveRecordingToAsset", Qt::BlockingQueuedConnection, - Q_ARG(QScriptValue, getClipAtpUrl), - Q_RETURN_ARG(bool, result)); - return result; + Q_ARG(QScriptValue, getClipAtpUrl)); + return false; } if (!_lastClip) { From a306e8d5b1fe3b1d5bd9f5a17cada6398a5d20ce Mon Sep 17 00:00:00 2001 From: AlessandroSigna Date: Thu, 19 Nov 2015 19:36:19 -0800 Subject: [PATCH 69/99] master can upload performance file on asset --- .../entityScripts/recordingEntityScript.js | 28 +++--- examples/entityScripts/recordingMaster.js | 86 +++++++++++++++++-- 2 files changed, 97 insertions(+), 17 deletions(-) diff --git a/examples/entityScripts/recordingEntityScript.js b/examples/entityScripts/recordingEntityScript.js index e423de9afd..dfbaf0a172 100644 --- a/examples/entityScripts/recordingEntityScript.js +++ b/examples/entityScripts/recordingEntityScript.js @@ -16,9 +16,11 @@ var _this; var isAvatarRecording = false; - var channel = "groupRecordingChannel"; - var startMessage = "RECONDING STARTED"; - var stopMessage = "RECONDING ENDED"; + var MASTER_TO_CLIENTS_CHANNEL = "startStopChannel"; + var CLIENTS_TO_MASTER_CHANNEL = "resultsChannel"; + var START_MESSAGE = "recordingStarted"; + var STOP_MESSAGE = "recordingEnded"; + var PARTICIPATING_MESSAGE = "participatingToRecording"; function recordingEntity() { _this = this; @@ -26,11 +28,13 @@ } function receivingMessage(channel, message, senderID) { - print("message received on channel:" + channel + ", message:" + message + ", senderID:" + senderID); - if(message === startMessage) { - _this.startRecording(); - } else if(message === stopMessage) { - _this.stopRecording(); + if(channel === MASTER_TO_CLIENTS_CHANNEL){ + print("CLIENT received message:" + message); + if(message === START_MESSAGE) { + _this.startRecording(); + } else if(message === STOP_MESSAGE) { + _this.stopRecording(); + } } }; @@ -50,19 +54,20 @@ enterEntity: function (entityID) { print("entering in the recording area"); - Messages.subscribe(channel); + Messages.subscribe(MASTER_TO_CLIENTS_CHANNEL); }, leaveEntity: function (entityID) { print("leaving the recording area"); _this.stopRecording(); - Messages.unsubscribe(channel); + Messages.unsubscribe(MASTER_TO_CLIENTS_CHANNEL); }, startRecording: function (entityID) { if (!isAvatarRecording) { print("RECORDING STARTED"); + Messages.sendMessage(CLIENTS_TO_MASTER_CHANNEL, PARTICIPATING_MESSAGE); //tell to master that I'm participating Recording.startRecording(); isAvatarRecording = true; } @@ -77,13 +82,14 @@ if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.saveRecording(recordingFile); } + Messages.sendMessage(CLIENTS_TO_MASTER_CHANNEL, recordingFile); //send back to the master the url } }, unload: function (entityID) { print("RECORDING ENTITY UNLOAD"); _this.stopRecording(); - Messages.unsubscribe(channel); + Messages.unsubscribe(MASTER_TO_CLIENTS_CHANNEL); Messages.messageReceived.disconnect(receivingMessage); } } diff --git a/examples/entityScripts/recordingMaster.js b/examples/entityScripts/recordingMaster.js index 27f84f44c2..c757f94300 100644 --- a/examples/entityScripts/recordingMaster.js +++ b/examples/entityScripts/recordingMaster.js @@ -22,11 +22,24 @@ var TOOL_ICON_URL = HIFI_PUBLIC_BUCKET + "images/tools/"; var ALPHA_ON = 1.0; var ALPHA_OFF = 0.7; var COLOR_TOOL_BAR = { red: 0, green: 0, blue: 0 }; +var MASTER_TO_CLIENTS_CHANNEL = "startStopChannel"; +var CLIENTS_TO_MASTER_CHANNEL = "resultsChannel"; +var START_MESSAGE = "recordingStarted"; +var STOP_MESSAGE = "recordingEnded"; +var PARTICIPATING_MESSAGE = "participatingToRecording"; +var TIMEOUT = 20; var toolBar = null; var recordIcon; var isRecording = false; -var channel = "groupRecordingChannel"; +var results = []; +var performanceJSON = null; +var responsesExpected = 0; +var waitingForPerformanceFile = true; +var totalWaitingTime = 0; +var extension = "txt"; + +Messages.subscribe(CLIENTS_TO_MASTER_CHANNEL); setupToolBar(); function setupToolBar() { @@ -55,22 +68,83 @@ function mousePressEvent(event) { if (recordIcon === toolBar.clicked(clickedOverlay, false)) { if (!isRecording) { print("I'm the master. I want to start recording"); - var message = "RECONDING STARTED"; - Messages.sendMessage(channel, message); + Messages.sendMessage(MASTER_TO_CLIENTS_CHANNEL, START_MESSAGE); isRecording = true; } else { print("I want to stop recording"); - var message = "RECONDING ENDED"; - Messages.sendMessage(channel, message); + waitingForPerformanceFile = true; + Script.update.connect(update); + Messages.sendMessage(MASTER_TO_CLIENTS_CHANNEL, STOP_MESSAGE); isRecording = false; } } } +function masterReceivingMessage(channel, message, senderID) { + if(channel === CLIENTS_TO_MASTER_CHANNEL) { + print("MASTER received message:" + message ); + if(message === PARTICIPATING_MESSAGE){ + //increment the counter of all the participants + responsesExpected++; + } else if(waitingForPerformanceFile) { + //I get a atp url from one participant + results[results.length] = message; + var textJSON = '{ "results" : ['; + for (var index = 0; index < results.length; index++) { + var newRecord = index === (results.length - 1) ? '{ "hashATP":"' + results[index] + '" }' : '{ "hashATP":"' + results[index] + '" },'; + textJSON += newRecord; + } + textJSON += ']}'; + performanceJSON = JSON.parse(textJSON); + } + + } + } + +function update(deltaTime) { + if(waitingForPerformanceFile) { + totalWaitingTime += deltaTime; + if(totalWaitingTime > TIMEOUT || results.length === responsesExpected) { + //I can upload the performance file on the asset + print("UPLOADING PERFORMANCE FILE"); + print(JSON.stringify(performanceJSON)); + if(performanceJSON !== null) { + //upload + Assets.uploadData(JSON.stringify(performanceJSON), extension, uploadFinished); + } + //clean things after upload performance file to asset + waitingForPerformanceFile = false; + responsesExpected = 0; + totalWaitingTime = 0; + Script.update.disconnect(update); + results = []; + performanceJSON = null; + } + } +} + +function uploadFinished(url){ + print("data uploaded to:" + url); + uploadedFile = url; + Assets.downloadData(url, function (data) { + print("data downloaded from:" + url + " the data is: "); + printPerformanceJSON(JSON.parse(data)); + }); +} + +function printPerformanceJSON(obj) { + var results = obj.results; + results.forEach(function(param) { + var hash = param.hashATP; + print("url obtained: " + hash); + }); +} + function cleanup() { toolBar.cleanup(); - Messages.unsubscribe(channel); + Messages.unsubscribe(CLIENTS_TO_MASTER_CHANNEL); } Script.scriptEnding.connect(cleanup); Controller.mousePressEvent.connect(mousePressEvent); +Messages.messageReceived.connect(masterReceivingMessage); \ No newline at end of file From 7069f7d6ab00e5e5783f573ad872086a9ca56c47 Mon Sep 17 00:00:00 2001 From: AlessandroSigna Date: Fri, 20 Nov 2015 15:22:18 -0800 Subject: [PATCH 70/99] added clip load to asset + clean code --- .../entityScripts/recordingEntityScript.js | 21 +++++--- examples/entityScripts/recordingMaster.js | 50 ++++++++----------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/examples/entityScripts/recordingEntityScript.js b/examples/entityScripts/recordingEntityScript.js index dfbaf0a172..8f5e9a03b9 100644 --- a/examples/entityScripts/recordingEntityScript.js +++ b/examples/entityScripts/recordingEntityScript.js @@ -25,19 +25,24 @@ function recordingEntity() { _this = this; return; - } + }; function receivingMessage(channel, message, senderID) { - if(channel === MASTER_TO_CLIENTS_CHANNEL){ + if (channel === MASTER_TO_CLIENTS_CHANNEL) { print("CLIENT received message:" + message); - if(message === START_MESSAGE) { + if (message === START_MESSAGE) { _this.startRecording(); - } else if(message === STOP_MESSAGE) { + } else if (message === STOP_MESSAGE) { _this.stopRecording(); } } }; + function getClipUrl(url) { + Messages.sendMessage(CLIENTS_TO_MASTER_CHANNEL, url); //send back the url to the master + print("clip uploaded and url sent to master"); + }; + recordingEntity.prototype = { preload: function (entityID) { @@ -55,7 +60,6 @@ enterEntity: function (entityID) { print("entering in the recording area"); Messages.subscribe(MASTER_TO_CLIENTS_CHANNEL); - }, leaveEntity: function (entityID) { @@ -78,11 +82,12 @@ print("RECORDING ENDED"); Recording.stopRecording(); isAvatarRecording = false; - recordingFile = Window.save("Save recording to file", "./groupRecording", "Recordings (*.hfr)"); + Recording.saveRecordingToAsset(getClipUrl); //save the clip to the asset and link a callback to get its url + + var recordingFile = Window.save("Save recording to file", "./groupRecording", "Recordings (*.hfr)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { - Recording.saveRecording(recordingFile); + Recording.saveRecording(recordingFile); //save the clip locally } - Messages.sendMessage(CLIENTS_TO_MASTER_CHANNEL, recordingFile); //send back to the master the url } }, diff --git a/examples/entityScripts/recordingMaster.js b/examples/entityScripts/recordingMaster.js index c757f94300..cc3fa79026 100644 --- a/examples/entityScripts/recordingMaster.js +++ b/examples/entityScripts/recordingMaster.js @@ -32,13 +32,14 @@ var TIMEOUT = 20; var toolBar = null; var recordIcon; var isRecording = false; -var results = []; -var performanceJSON = null; +var avatarClips = []; +var performanceJSON = { "avatarClips" : [] }; var responsesExpected = 0; var waitingForPerformanceFile = true; var totalWaitingTime = 0; var extension = "txt"; + Messages.subscribe(CLIENTS_TO_MASTER_CHANNEL); setupToolBar(); @@ -81,35 +82,26 @@ function mousePressEvent(event) { } function masterReceivingMessage(channel, message, senderID) { - if(channel === CLIENTS_TO_MASTER_CHANNEL) { + if (channel === CLIENTS_TO_MASTER_CHANNEL) { print("MASTER received message:" + message ); - if(message === PARTICIPATING_MESSAGE){ + if (message === PARTICIPATING_MESSAGE) { //increment the counter of all the participants responsesExpected++; - } else if(waitingForPerformanceFile) { - //I get a atp url from one participant - results[results.length] = message; - var textJSON = '{ "results" : ['; - for (var index = 0; index < results.length; index++) { - var newRecord = index === (results.length - 1) ? '{ "hashATP":"' + results[index] + '" }' : '{ "hashATP":"' + results[index] + '" },'; - textJSON += newRecord; - } - textJSON += ']}'; - performanceJSON = JSON.parse(textJSON); + } else if (waitingForPerformanceFile) { + //I get an atp url from one participant + performanceJSON.avatarClips[performanceJSON.avatarClips.length] = message; } } } function update(deltaTime) { - if(waitingForPerformanceFile) { + if (waitingForPerformanceFile) { totalWaitingTime += deltaTime; - if(totalWaitingTime > TIMEOUT || results.length === responsesExpected) { - //I can upload the performance file on the asset + if (totalWaitingTime > TIMEOUT || performanceJSON.avatarClips.length === responsesExpected) { print("UPLOADING PERFORMANCE FILE"); - print(JSON.stringify(performanceJSON)); - if(performanceJSON !== null) { - //upload + if (performanceJSON.avatarClips.length !== 0) { + //I can upload the performance file on the asset Assets.uploadData(JSON.stringify(performanceJSON), extension, uploadFinished); } //clean things after upload performance file to asset @@ -117,26 +109,28 @@ function update(deltaTime) { responsesExpected = 0; totalWaitingTime = 0; Script.update.disconnect(update); - results = []; - performanceJSON = null; + avatarClips = []; + performanceJSON = { "avatarClips" : [] }; } } } function uploadFinished(url){ - print("data uploaded to:" + url); + print("some info:"); + print("performance file uploaded to: " + url); uploadedFile = url; Assets.downloadData(url, function (data) { - print("data downloaded from:" + url + " the data is: "); printPerformanceJSON(JSON.parse(data)); }); + //need to print somehow the url here //this way the master can copy the url + //Window.prompt(url); } function printPerformanceJSON(obj) { - var results = obj.results; - results.forEach(function(param) { - var hash = param.hashATP; - print("url obtained: " + hash); + print("downloaded performance file from asset and examinating its content..."); + var avatarClips = obj.avatarClips; + avatarClips.forEach(function(param) { + print("clip url obtained: " + param); }); } From a1053ee5a09af175178c2267ed904d462dacf259 Mon Sep 17 00:00:00 2001 From: AlessandroSigna Date: Fri, 20 Nov 2015 16:21:42 -0800 Subject: [PATCH 71/99] pop up a prompt with url --- examples/entityScripts/recordingEntityScript.js | 2 +- examples/entityScripts/recordingMaster.js | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/entityScripts/recordingEntityScript.js b/examples/entityScripts/recordingEntityScript.js index 8f5e9a03b9..132b064997 100644 --- a/examples/entityScripts/recordingEntityScript.js +++ b/examples/entityScripts/recordingEntityScript.js @@ -82,12 +82,12 @@ print("RECORDING ENDED"); Recording.stopRecording(); isAvatarRecording = false; - Recording.saveRecordingToAsset(getClipUrl); //save the clip to the asset and link a callback to get its url var recordingFile = Window.save("Save recording to file", "./groupRecording", "Recordings (*.hfr)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.saveRecording(recordingFile); //save the clip locally } + Recording.saveRecordingToAsset(getClipUrl); //save the clip to the asset and link a callback to get its url } }, diff --git a/examples/entityScripts/recordingMaster.js b/examples/entityScripts/recordingMaster.js index cc3fa79026..70ae93075f 100644 --- a/examples/entityScripts/recordingMaster.js +++ b/examples/entityScripts/recordingMaster.js @@ -32,7 +32,6 @@ var TIMEOUT = 20; var toolBar = null; var recordIcon; var isRecording = false; -var avatarClips = []; var performanceJSON = { "avatarClips" : [] }; var responsesExpected = 0; var waitingForPerformanceFile = true; @@ -109,7 +108,6 @@ function update(deltaTime) { responsesExpected = 0; totalWaitingTime = 0; Script.update.disconnect(update); - avatarClips = []; performanceJSON = { "avatarClips" : [] }; } } @@ -122,8 +120,8 @@ function uploadFinished(url){ Assets.downloadData(url, function (data) { printPerformanceJSON(JSON.parse(data)); }); - //need to print somehow the url here //this way the master can copy the url - //Window.prompt(url); + //need to print somehow the url here this way the master can copy the url + Window.prompt("Performance file url: ", url); } function printPerformanceJSON(obj) { From 05d311cff31893bfb3a8e17b1fcde24ae3ef643a Mon Sep 17 00:00:00 2001 From: AlessandroSigna Date: Fri, 20 Nov 2015 18:57:51 -0800 Subject: [PATCH 72/99] remove Windows.prompt cause of crash --- examples/entityScripts/recordingMaster.js | 27 ++++++++++------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/examples/entityScripts/recordingMaster.js b/examples/entityScripts/recordingMaster.js index 70ae93075f..732521cff7 100644 --- a/examples/entityScripts/recordingMaster.js +++ b/examples/entityScripts/recordingMaster.js @@ -81,18 +81,17 @@ function mousePressEvent(event) { } function masterReceivingMessage(channel, message, senderID) { - if (channel === CLIENTS_TO_MASTER_CHANNEL) { - print("MASTER received message:" + message ); - if (message === PARTICIPATING_MESSAGE) { - //increment the counter of all the participants - responsesExpected++; - } else if (waitingForPerformanceFile) { - //I get an atp url from one participant - performanceJSON.avatarClips[performanceJSON.avatarClips.length] = message; - } - + if (channel === CLIENTS_TO_MASTER_CHANNEL) { + print("MASTER received message:" + message ); + if (message === PARTICIPATING_MESSAGE) { + //increment the counter of all the participants + responsesExpected++; + } else if (waitingForPerformanceFile) { + //I get an atp url from one participant + performanceJSON.avatarClips[performanceJSON.avatarClips.length] = message; } } +} function update(deltaTime) { if (waitingForPerformanceFile) { @@ -114,17 +113,15 @@ function update(deltaTime) { } function uploadFinished(url){ - print("some info:"); - print("performance file uploaded to: " + url); - uploadedFile = url; + //need to print somehow the url here this way the master can copy the url + print("PERFORMANCE FILE URL: " + url); Assets.downloadData(url, function (data) { printPerformanceJSON(JSON.parse(data)); }); - //need to print somehow the url here this way the master can copy the url - Window.prompt("Performance file url: ", url); } function printPerformanceJSON(obj) { + print("some info:"); print("downloaded performance file from asset and examinating its content..."); var avatarClips = obj.avatarClips; avatarClips.forEach(function(param) { From 6f19093cb9959881b2002393ba6f66aec6c1d498 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 23 Nov 2015 10:03:35 -0800 Subject: [PATCH 73/99] if an entity with an action has a motion-type of static, flag it so it gets updated in bullet --- libraries/entities/src/EntityItem.h | 1 + libraries/physics/src/ObjectAction.cpp | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 5ceccef4b1..727a806b62 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -398,6 +398,7 @@ public: void getAllTerseUpdateProperties(EntityItemProperties& properties) const; void flagForOwnership() { _dirtyFlags |= Simulation::DIRTY_SIMULATOR_OWNERSHIP; } + void flagForMotionStateChange() { _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; } bool addAction(EntitySimulation* simulation, EntityActionPointer action); bool updateAction(EntitySimulation* simulation, const QUuid& actionID, const QVariantMap& arguments); diff --git a/libraries/physics/src/ObjectAction.cpp b/libraries/physics/src/ObjectAction.cpp index 3188283f68..8e0cdfd266 100644 --- a/libraries/physics/src/ObjectAction.cpp +++ b/libraries/physics/src/ObjectAction.cpp @@ -244,6 +244,15 @@ void ObjectAction::activateBody() { if (rigidBody) { rigidBody->activate(); } + auto ownerEntity = _ownerEntity.lock(); + if (!ownerEntity) { + return; + } + void* physicsInfo = ownerEntity->getPhysicsInfo(); + ObjectMotionState* motionState = static_cast(physicsInfo); + if (motionState && motionState->getMotionType() == MOTION_TYPE_STATIC) { + ownerEntity->flagForMotionStateChange(); + } } bool ObjectAction::lifetimeIsOver() { From 08a64f6a6bf58e8b8b1695505221b3c38fb829f4 Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Mon, 23 Nov 2015 10:08:23 -0800 Subject: [PATCH 74/99] updated whiteboard to use new controller approach' --- .../whiteboard/whiteboardEntityScript.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/examples/painting/whiteboard/whiteboardEntityScript.js b/examples/painting/whiteboard/whiteboardEntityScript.js index d2b2c386ef..c0200b173a 100644 --- a/examples/painting/whiteboard/whiteboardEntityScript.js +++ b/examples/painting/whiteboard/whiteboardEntityScript.js @@ -20,7 +20,7 @@ var _this; var RIGHT_HAND = 1; var LEFT_HAND = 0; - var MIN_POINT_DISTANCE = 0.01 ; + var MIN_POINT_DISTANCE = 0.01; var MAX_POINT_DISTANCE = 0.5; var MAX_POINTS_PER_LINE = 40; var MAX_DISTANCE = 5; @@ -29,6 +29,11 @@ var MIN_STROKE_WIDTH = 0.0005; var MAX_STROKE_WIDTH = 0.03; + var TRIGGER_CONTROLS = [ + Controller.Standard.LT, + Controller.Standard.RT, + ]; + Whiteboard = function() { _this = this; }; @@ -51,11 +56,9 @@ if (this.hand === RIGHT_HAND) { this.getHandPosition = MyAvatar.getRightPalmPosition; this.getHandRotation = MyAvatar.getRightPalmRotation; - this.triggerAction = Controller.findAction("RIGHT_HAND_CLICK"); } else if (this.hand === LEFT_HAND) { this.getHandPosition = MyAvatar.getLeftPalmPosition; this.getHandRotation = MyAvatar.getLeftPalmRotation; - this.triggerAction = Controller.findAction("LEFT_HAND_CLICK"); } Overlays.editOverlay(this.laserPointer, { visible: true @@ -76,7 +79,7 @@ if (this.intersection.intersects) { var distance = Vec3.distance(handPosition, this.intersection.intersection); if (distance < MAX_DISTANCE) { - this.triggerValue = Controller.getActionValue(this.triggerAction); + this.triggerValue = Controller.getValue(TRIGGER_CONTROLS[this.hand]); this.currentStrokeWidth = map(this.triggerValue, 0, 1, MIN_STROKE_WIDTH, MAX_STROKE_WIDTH); var displayPoint = this.intersection.intersection; displayPoint = Vec3.sum(displayPoint, Vec3.multiply(this.normal, 0.01)); @@ -87,7 +90,9 @@ y: this.currentStrokeWidth } }); + print("TRIGGER VALUE " + this.triggerValue); if (this.triggerValue > PAINT_TRIGGER_THRESHOLD) { + print("PAINT") this.paint(this.intersection.intersection, this.intersection.surfaceNormal); } else { this.painting = false; @@ -184,7 +189,7 @@ }, stopFarTrigger: function() { - if(this.hand !== this.whichHand) { + if (this.hand !== this.whichHand) { return; } this.stopPainting(); @@ -209,7 +214,7 @@ entities.forEach(function(entity) { var props = Entities.getEntityProperties(entity, ["name, userData"]); var name = props.name; - if(!props.userData) { + if (!props.userData) { return; } var whiteboardID = JSON.parse(props.userData).whiteboard; @@ -249,4 +254,4 @@ // entity scripts always need to return a newly constructed object of our type return new Whiteboard(); -}); +}); \ No newline at end of file From 5612174894b602f818528023dfea37376e1ff658 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 23 Nov 2015 11:02:41 -0800 Subject: [PATCH 75/99] call activateBody when a hold action is deserialized --- interface/src/avatar/AvatarActionHold.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 8e13fa8385..ca96cc9166 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -326,5 +326,6 @@ void AvatarActionHold::deserialize(QByteArray serializedArguments) { #endif _active = true; + activateBody(); }); } From c3821d7202b4acc409e56c6702f2278304612170 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 23 Nov 2015 11:24:56 -0800 Subject: [PATCH 76/99] avoid deadlock --- interface/src/avatar/AvatarActionHold.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index ca96cc9166..39490ddbc5 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -326,6 +326,7 @@ void AvatarActionHold::deserialize(QByteArray serializedArguments) { #endif _active = true; - activateBody(); }); + + activateBody(); } From 301f0ba145c0cce374d16456b67061b94a48503c Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 23 Nov 2015 11:48:02 -0800 Subject: [PATCH 77/99] Trying to instenciate the AssetCLient in the Agent --- assignment-client/src/Agent.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 5cdceb06ae..d52ce6d16c 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -53,10 +53,20 @@ Agent::Agent(NLPacket& packet) : DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + // Setup AssetClient + auto assetClient = DependencyManager::get(); + QThread* assetThread = new QThread; + assetThread->setObjectName("Asset Thread"); + assetClient->moveToThread(assetThread); + connect(assetThread, &QThread::started, assetClient.data(), &AssetClient::init); + assetThread->start(); + + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListenerForTypes( From 7e8b692499b3d8e075cdb904dfe5d8a40ac4d0c5 Mon Sep 17 00:00:00 2001 From: Eric Levin Date: Mon, 23 Nov 2015 12:04:13 -0800 Subject: [PATCH 78/99] Update whiteboardEntityScript.js Removed logging --- examples/painting/whiteboard/whiteboardEntityScript.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/painting/whiteboard/whiteboardEntityScript.js b/examples/painting/whiteboard/whiteboardEntityScript.js index c0200b173a..61d7291e11 100644 --- a/examples/painting/whiteboard/whiteboardEntityScript.js +++ b/examples/painting/whiteboard/whiteboardEntityScript.js @@ -90,9 +90,7 @@ y: this.currentStrokeWidth } }); - print("TRIGGER VALUE " + this.triggerValue); if (this.triggerValue > PAINT_TRIGGER_THRESHOLD) { - print("PAINT") this.paint(this.intersection.intersection, this.intersection.surfaceNormal); } else { this.painting = false; @@ -254,4 +252,4 @@ // entity scripts always need to return a newly constructed object of our type return new Whiteboard(); -}); \ No newline at end of file +}); From d4937071e189121dab2dd0d46b0d33eeb1878771 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 23 Nov 2015 12:17:15 -0800 Subject: [PATCH 79/99] rather than activating rigid body and forcing non-static on deserialize, just force non-static --- interface/src/avatar/AvatarActionHold.cpp | 2 +- libraries/physics/src/ObjectAction.cpp | 3 +++ libraries/physics/src/ObjectAction.h | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 39490ddbc5..dd96b3a3d9 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -328,5 +328,5 @@ void AvatarActionHold::deserialize(QByteArray serializedArguments) { _active = true; }); - activateBody(); + forceBodyNonStatic(); } diff --git a/libraries/physics/src/ObjectAction.cpp b/libraries/physics/src/ObjectAction.cpp index 8e0cdfd266..17b565ba21 100644 --- a/libraries/physics/src/ObjectAction.cpp +++ b/libraries/physics/src/ObjectAction.cpp @@ -244,6 +244,9 @@ void ObjectAction::activateBody() { if (rigidBody) { rigidBody->activate(); } +} + +void ObjectAction::forceBodyNonStatic() { auto ownerEntity = _ownerEntity.lock(); if (!ownerEntity) { return; diff --git a/libraries/physics/src/ObjectAction.h b/libraries/physics/src/ObjectAction.h index afb6745e9c..e44036eadc 100644 --- a/libraries/physics/src/ObjectAction.h +++ b/libraries/physics/src/ObjectAction.h @@ -63,6 +63,7 @@ protected: virtual glm::vec3 getAngularVelocity(); virtual void setAngularVelocity(glm::vec3 angularVelocity); virtual void activateBody(); + virtual void forceBodyNonStatic(); EntityItemWeakPointer _ownerEntity; QString _tag; From 8e12e32dd26bcf9dcfbfe170396690005ede8dc0 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Mon, 23 Nov 2015 14:21:06 -0600 Subject: [PATCH 80/99] Checkpoint - Windows specifics --- cmake/externals/quazip/CMakeLists.txt | 27 +++++++++++++++++++++------ cmake/externals/zlib/CMakeLists.txt | 14 +++++++------- stack-manager/CMakeLists.txt | 5 ++--- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/cmake/externals/quazip/CMakeLists.txt b/cmake/externals/quazip/CMakeLists.txt index e6a8a0fa2a..68506c14cd 100644 --- a/cmake/externals/quazip/CMakeLists.txt +++ b/cmake/externals/quazip/CMakeLists.txt @@ -1,4 +1,9 @@ set(EXTERNAL_NAME quazip) +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) + +# Choose correct version of zlib for QuaZip build +include(SelectLibraryConfigurations) +select_library_configurations(ZLIB) include(ExternalProject) ExternalProject_Add( @@ -6,12 +11,14 @@ ExternalProject_Add( URL https://s3-us-west-1.amazonaws.com/hifi-production/dependencies/quazip-0.6.2.zip URL_MD5 514851970f1a14d815bdc3ad6267af4d BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build - CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_PREFIX_PATH=$ENV{QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=/lib + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_PREFIX_PATH=$ENV{QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=/lib -DZLIB_INCLUDE_DIR=${ZLIB_INCLUDE_DIR} -DZLIB_LIBRARY=${ZLIB_LIBRARY_DEBUG} LOG_DOWNLOAD 1 LOG_CONFIGURE 1 LOG_BUILD 1 ) +add_dependencies(quazip zlib) + # Hide this external target (for ide users) set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals" @@ -19,10 +26,18 @@ set_target_properties(${EXTERNAL_NAME} PROPERTIES BUILD_WITH_INSTALL_RPATH True) ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) - -string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) -set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE PATH "List of QuaZip include directories") +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIR ${INSTALL_DIR}/include CACHE PATH "List of QuaZip include directories") +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${${EXTERNAL_NAME_UPPER}_INCLUDE_DIR} CACHE PATH "List of QuaZip include directories") +set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${INSTALL_DIR}/lib CACHE FILEPATH "Location of ZLib DLL") if (APPLE) - set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${INSTALL_DIR}/lib/libquazip.1.0.0.dylib CACHE PATH "List of QuaZip libraries") -endif () + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip.1.0.0.dylib CACHE FILEPATH "Location of QuaZip release library") +elseif (WIN32) + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/quazip.lib CACHE FILEPATH "Location of QuaZip release library") +endif() + +select_library_configurations(${EXTERNAL_NAME_UPPER}) + +# Force selected libraries into the cache +set(${EXTERNAL_NAME_UPPER}_LIBRARY ${${EXTERNAL_NAME_UPPER}_LIBRARY} CACHE FILEPATH "Location of QuaZip libraries") +set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES} CACHE FILEPATH "Location of QuaZip libraries") \ No newline at end of file diff --git a/cmake/externals/zlib/CMakeLists.txt b/cmake/externals/zlib/CMakeLists.txt index 0d279cc469..22a2703c46 100644 --- a/cmake/externals/zlib/CMakeLists.txt +++ b/cmake/externals/zlib/CMakeLists.txt @@ -4,13 +4,13 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) include(ExternalProject) ExternalProject_Add( -${EXTERNAL_NAME} -URL http://zlib.net/zlib128.zip -CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= -BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build -LOG_DOWNLOAD 1 -LOG_CONFIGURE 1 -LOG_BUILD 1 + ${EXTERNAL_NAME} + URL http://zlib.net/zlib128.zip + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= + BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build + LOG_DOWNLOAD 1 + LOG_CONFIGURE 1 + LOG_BUILD 1 ) # Hide this external target (for ide users) diff --git a/stack-manager/CMakeLists.txt b/stack-manager/CMakeLists.txt index 45ad9686c0..142921c515 100644 --- a/stack-manager/CMakeLists.txt +++ b/stack-manager/CMakeLists.txt @@ -3,8 +3,9 @@ set(BUILD_BUNDLE YES) setup_hifi_project(Widgets Gui Svg Core Network WebKitWidgets) if (WIN32) - find_package(ZLIB REQUIRED) + target_zlib() endif () +target_quazip() if (DEFINED ENV{JOB_ID}) set(PR_BUILD "false") @@ -30,8 +31,6 @@ include_directories( ${ZLIB_INCLUDE_DIRS} ) -target_quazip() - if (APPLE) set(CMAKE_OSX_DEPLOYMENT_TARGET 10.8) set(MACOSX_BUNDLE_BUNDLE_NAME "Stack Manager") From 25654824a24a7e582def551dee7bd3b3a71a8b8c Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 23 Nov 2015 12:32:27 -0800 Subject: [PATCH 81/99] Copy pasting the code that Stephen did and also adding the AssetServer to the list of dependent nodes for the agent --- assignment-client/src/Agent.cpp | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index d52ce6d16c..de9d7e2453 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -51,21 +52,19 @@ Agent::Agent(NLPacket& packet) : { DependencyManager::get()->setPacketSender(&_entityEditSender); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); + auto assetClient = DependencyManager::set(); - // Setup AssetClient - auto assetClient = DependencyManager::get(); QThread* assetThread = new QThread; assetThread->setObjectName("Asset Thread"); assetClient->moveToThread(assetThread); connect(assetThread, &QThread::started, assetClient.data(), &AssetClient::init); assetThread->start(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); @@ -143,7 +142,7 @@ void Agent::run() { messagesThread->start(); nodeList->addSetOfNodeTypesToNodeInterestSet({ - NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::EntityServer, NodeType::MessagesMixer + NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::EntityServer, NodeType::MessagesMixer, NodeType::AssetServer }); } @@ -458,4 +457,10 @@ void Agent::aboutToFinish() { // our entity tree is going to go away so tell that to the EntityScriptingInterface DependencyManager::get()->setEntityTree(NULL); + + // cleanup the AssetClient thread + QThread* assetThread = DependencyManager::get()->thread(); + DependencyManager::destroy(); + assetThread->quit(); + assetThread->wait(); } From f5d66bbf2eb5fb541cfe46ca895102ce1e392b1b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 24 Nov 2015 09:42:12 +1300 Subject: [PATCH 82/99] Fix avatar rotation rate depending on FPS --- interface/src/Application.cpp | 2 +- interface/src/avatar/Avatar.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ff3cba182c..e85ed9f51a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2841,7 +2841,7 @@ void Application::update(float deltaTime) { const float EXPECTED_FRAME_RATE = 60.0f; float timeFactor = EXPECTED_FRAME_RATE * deltaTime; myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH) / timeFactor); - myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW) / timeFactor); + myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); } } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 44b5d91015..dd317fcacd 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -173,7 +173,7 @@ protected: QVector _attachmentModels; QVector _attachmentsToRemove; QVector _unusedAttachments; - float _bodyYawDelta; + float _bodyYawDelta; // degrees/sec // These position histories and derivatives are in the world-frame. // The derivatives are the MEASURED results of all external and internal forces From d73e3a5490ca8f10cd80a1e40549e4b1221f224e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 24 Nov 2015 09:48:37 +1300 Subject: [PATCH 83/99] Fix avatar pitch rate depending on FPS --- interface/src/Application.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e85ed9f51a..9acd2b82c6 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2834,13 +2834,7 @@ void Application::update(float deltaTime) { myAvatar->setDriveKeys(TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); myAvatar->setDriveKeys(TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); if (deltaTime > FLT_EPSILON) { - // For rotations what we really want are meausures of "angles per second" (in order to prevent - // fps-dependent spin rates) so we need to scale the units of the controller contribution. - // (TODO?: maybe we should similarly scale ALL action state info, or change the expected behavior - // controllers to provide a delta_per_second value rather than a raw delta.) - const float EXPECTED_FRAME_RATE = 60.0f; - float timeFactor = EXPECTED_FRAME_RATE * deltaTime; - myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH) / timeFactor); + myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); } From 8b573cebfbfffdd584948176a5ef065461f6cc95 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Mon, 23 Nov 2015 15:16:48 -0600 Subject: [PATCH 84/99] Successfull windows build given any build type checkpoint --- cmake/externals/quazip/CMakeLists.txt | 9 +++------ cmake/externals/zlib/CMakeLists.txt | 1 + cmake/macros/TargetQuazip.cmake | 3 +++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cmake/externals/quazip/CMakeLists.txt b/cmake/externals/quazip/CMakeLists.txt index 68506c14cd..794dffb951 100644 --- a/cmake/externals/quazip/CMakeLists.txt +++ b/cmake/externals/quazip/CMakeLists.txt @@ -1,17 +1,13 @@ set(EXTERNAL_NAME quazip) string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) -# Choose correct version of zlib for QuaZip build -include(SelectLibraryConfigurations) -select_library_configurations(ZLIB) - include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} URL https://s3-us-west-1.amazonaws.com/hifi-production/dependencies/quazip-0.6.2.zip URL_MD5 514851970f1a14d815bdc3ad6267af4d BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build - CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_PREFIX_PATH=$ENV{QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=/lib -DZLIB_INCLUDE_DIR=${ZLIB_INCLUDE_DIR} -DZLIB_LIBRARY=${ZLIB_LIBRARY_DEBUG} + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_PREFIX_PATH=$ENV{QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=/lib -DZLIB_ROOT=${ZLIB_ROOT} LOG_DOWNLOAD 1 LOG_CONFIGURE 1 LOG_BUILD 1 @@ -28,7 +24,7 @@ set_target_properties(${EXTERNAL_NAME} PROPERTIES ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIR ${INSTALL_DIR}/include CACHE PATH "List of QuaZip include directories") set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${${EXTERNAL_NAME_UPPER}_INCLUDE_DIR} CACHE PATH "List of QuaZip include directories") -set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${INSTALL_DIR}/lib CACHE FILEPATH "Location of ZLib DLL") +set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${INSTALL_DIR}/lib CACHE FILEPATH "Location of QuaZip DLL") if (APPLE) set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip.1.0.0.dylib CACHE FILEPATH "Location of QuaZip release library") @@ -36,6 +32,7 @@ elseif (WIN32) set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/quazip.lib CACHE FILEPATH "Location of QuaZip release library") endif() +include(SelectLibraryConfigurations) select_library_configurations(${EXTERNAL_NAME_UPPER}) # Force selected libraries into the cache diff --git a/cmake/externals/zlib/CMakeLists.txt b/cmake/externals/zlib/CMakeLists.txt index 22a2703c46..06b6b564ba 100644 --- a/cmake/externals/zlib/CMakeLists.txt +++ b/cmake/externals/zlib/CMakeLists.txt @@ -17,6 +17,7 @@ ExternalProject_Add( set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) +set(${EXTERNAL_NAME_UPPER}_ROOT ${INSTALL_DIR} CACHE PATH "Path for Zlib install root") set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIR ${INSTALL_DIR}/include CACHE PATH "List of zlib include directories") set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${${EXTERNAL_NAME_UPPER}_INCLUDE_DIR} CACHE PATH "List of zlib include directories") set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${INSTALL_DIR}/bin CACHE FILEPATH "Location of ZLib DLL") diff --git a/cmake/macros/TargetQuazip.cmake b/cmake/macros/TargetQuazip.cmake index 1a06ab612a..e4b9ab086a 100644 --- a/cmake/macros/TargetQuazip.cmake +++ b/cmake/macros/TargetQuazip.cmake @@ -10,4 +10,7 @@ macro(TARGET_QUAZIP) find_package(QUAZIP REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${QUAZIP_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) + if (WIN32) + add_paths_to_fixup_libs(${QUAZIP_DLL_PATH}) + endif () endmacro() From b83e466767ae72d6658dd078c6299a8e77602920 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Mon, 23 Nov 2015 16:31:29 -0600 Subject: [PATCH 85/99] Removing comments --- stack-manager/CMakeLists.txt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/stack-manager/CMakeLists.txt b/stack-manager/CMakeLists.txt index 142921c515..e70fefc6e0 100644 --- a/stack-manager/CMakeLists.txt +++ b/stack-manager/CMakeLists.txt @@ -7,6 +7,11 @@ if (WIN32) endif () target_quazip() +set_target_properties( + ${TARGET_NAME} PROPERTIES + EXCLUDE_FROM_ALL TRUE +) + if (DEFINED ENV{JOB_ID}) set(PR_BUILD "false") set(BUILD_SEQ $ENV{JOB_ID}) @@ -38,12 +43,6 @@ if (APPLE) set(MACOSX_BUNDLE_ICON_FILE icon.icns) set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/assets/icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set(SM_SRCS ${SM_SRCS} "${CMAKE_CURRENT_SOURCE_DIR}/assets/icon.icns") -else () - if (WIN32) -# add_executable(${TARGET_NAME} WIN32 ${SM_SRCS} windows_icon.rc) - else () -# add_executable(${TARGET_NAME} ${SM_SRCS}) - endif () endif () package_libraries_for_deployment() \ No newline at end of file From 8f1ce6e1fb0260bf179c20923ef6d91d366ee383 Mon Sep 17 00:00:00 2001 From: AlessandroSigna Date: Mon, 23 Nov 2015 14:42:16 -0800 Subject: [PATCH 86/99] overlay added - waiting for official logos missing null assignment --- .../entityScripts/recordingEntityScript.js | 20 +++++++++++++++++++ examples/entityScripts/recordingMaster.js | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/examples/entityScripts/recordingEntityScript.js b/examples/entityScripts/recordingEntityScript.js index 132b064997..e1e161a79d 100644 --- a/examples/entityScripts/recordingEntityScript.js +++ b/examples/entityScripts/recordingEntityScript.js @@ -21,6 +21,10 @@ var START_MESSAGE = "recordingStarted"; var STOP_MESSAGE = "recordingEnded"; var PARTICIPATING_MESSAGE = "participatingToRecording"; + var ICON_WIDTH = 60; + var ICON_HEIGHT = 60; + var overlay = null; + function recordingEntity() { _this = this; @@ -60,12 +64,22 @@ enterEntity: function (entityID) { print("entering in the recording area"); Messages.subscribe(MASTER_TO_CLIENTS_CHANNEL); + overlay = Overlays.addOverlay("image", { + imageURL: "http://wcdn2.dataknet.com/static/resources/icons/set49/1c828b8c.png", //waiting for the official logo + width: ICON_HEIGHT, + height: ICON_WIDTH, + x: 600, + y: 0, + visible: true + }); }, leaveEntity: function (entityID) { print("leaving the recording area"); _this.stopRecording(); Messages.unsubscribe(MASTER_TO_CLIENTS_CHANNEL); + Overlays.deleteOverlay(overlay); + overlay = null; }, startRecording: function (entityID) { @@ -74,6 +88,7 @@ Messages.sendMessage(CLIENTS_TO_MASTER_CHANNEL, PARTICIPATING_MESSAGE); //tell to master that I'm participating Recording.startRecording(); isAvatarRecording = true; + Overlays.editOverlay(overlay, {imageURL: "http://www.polyrythmic.org/picts/REC.png"}); //waiting for the official logo } }, @@ -88,6 +103,7 @@ Recording.saveRecording(recordingFile); //save the clip locally } Recording.saveRecordingToAsset(getClipUrl); //save the clip to the asset and link a callback to get its url + Overlays.editOverlay(overlay, {imageURL: "http://wcdn2.dataknet.com/static/resources/icons/set49/1c828b8c.png"}); //waiting for the official logo } }, @@ -96,6 +112,10 @@ _this.stopRecording(); Messages.unsubscribe(MASTER_TO_CLIENTS_CHANNEL); Messages.messageReceived.disconnect(receivingMessage); + if(overlay !== null){ + Overlays.deleteOverlay(overlay); + overlay = null; + } } } diff --git a/examples/entityScripts/recordingMaster.js b/examples/entityScripts/recordingMaster.js index 732521cff7..1eb10d9bf2 100644 --- a/examples/entityScripts/recordingMaster.js +++ b/examples/entityScripts/recordingMaster.js @@ -97,10 +97,12 @@ function update(deltaTime) { if (waitingForPerformanceFile) { totalWaitingTime += deltaTime; if (totalWaitingTime > TIMEOUT || performanceJSON.avatarClips.length === responsesExpected) { - print("UPLOADING PERFORMANCE FILE"); if (performanceJSON.avatarClips.length !== 0) { + print("UPLOADING PERFORMANCE FILE"); //I can upload the performance file on the asset Assets.uploadData(JSON.stringify(performanceJSON), extension, uploadFinished); + } else { + print("PERFORMANCE FILE EMPTY"); } //clean things after upload performance file to asset waitingForPerformanceFile = false; From 905e11faa34f42ddcb3c94f59ee4761273a1ea50 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 23 Nov 2015 16:54:17 -0600 Subject: [PATCH 87/99] use MIN_AVATAR_SCALE to ensure avatar fades complete --- interface/src/avatar/AvatarManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 769b1d56a2..8e5166d7b7 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -154,7 +154,7 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { QVector::iterator fadingIterator = _avatarFades.begin(); const float SHRINK_RATE = 0.9f; - const float MIN_FADE_SCALE = 0.001f; + const float MIN_FADE_SCALE = MIN_AVATAR_SCALE; render::ScenePointer scene = qApp->getMain3DScene(); render::PendingChanges pendingChanges; @@ -162,7 +162,7 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { auto avatar = std::static_pointer_cast(*fadingIterator); avatar->startUpdate(); avatar->setTargetScale(avatar->getScale() * SHRINK_RATE, true); - if (avatar->getTargetScale() < MIN_FADE_SCALE) { + if (avatar->getTargetScale() <= MIN_FADE_SCALE) { avatar->removeFromScene(*fadingIterator, scene, pendingChanges); fadingIterator = _avatarFades.erase(fadingIterator); } else { From fee50a56e5e1882611fcebd81a5d52ed07400e3c Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Mon, 23 Nov 2015 18:02:20 -0600 Subject: [PATCH 88/99] Ubuntu fix - case sensitivity --- cmake/macros/TargetQuazip.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/macros/TargetQuazip.cmake b/cmake/macros/TargetQuazip.cmake index e4b9ab086a..3a4d1466b9 100644 --- a/cmake/macros/TargetQuazip.cmake +++ b/cmake/macros/TargetQuazip.cmake @@ -7,7 +7,7 @@ # macro(TARGET_QUAZIP) add_dependency_external_projects(quazip) - find_package(QUAZIP REQUIRED) + find_package(quazip REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${QUAZIP_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) if (WIN32) From 66803ce3566906c84efd19fcb641cf8959f84169 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Mon, 23 Nov 2015 18:04:36 -0600 Subject: [PATCH 89/99] =?UTF-8?q?Ubuntu=20fix=20-=20case=20sensitivity?= =?UTF-8?q?=C2=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmake/macros/TargetQuazip.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/macros/TargetQuazip.cmake b/cmake/macros/TargetQuazip.cmake index 3a4d1466b9..0cb6efb989 100644 --- a/cmake/macros/TargetQuazip.cmake +++ b/cmake/macros/TargetQuazip.cmake @@ -7,7 +7,7 @@ # macro(TARGET_QUAZIP) add_dependency_external_projects(quazip) - find_package(quazip REQUIRED) + find_package(Quazip REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${QUAZIP_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) if (WIN32) From d58b11f0a2ba325bec0a84092f24fcef7a8825a6 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Mon, 23 Nov 2015 18:13:41 -0600 Subject: [PATCH 90/99] =?UTF-8?q?Ubuntu=20fix=20-=20case=20sensitivity?= =?UTF-8?q?=C2=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmake/macros/TargetQuazip.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/macros/TargetQuazip.cmake b/cmake/macros/TargetQuazip.cmake index 0cb6efb989..0536a1a9d8 100644 --- a/cmake/macros/TargetQuazip.cmake +++ b/cmake/macros/TargetQuazip.cmake @@ -7,7 +7,7 @@ # macro(TARGET_QUAZIP) add_dependency_external_projects(quazip) - find_package(Quazip REQUIRED) + find_package(QuaZip REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${QUAZIP_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) if (WIN32) From ce46c3064754f940cf3ba6a10609e956a57f55b6 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 23 Nov 2015 13:09:02 -0800 Subject: [PATCH 91/99] change the ScriptEngine::waitTillDoneRunning() to wait for the script thread to complete --- .../src/EntityTreeRenderer.cpp | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 28 ++++++++++--------- libraries/script-engine/src/ScriptEngine.h | 20 ++++++------- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 8c81ceaeb8..8775040feb 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -122,7 +122,7 @@ void EntityTreeRenderer::init() { } void EntityTreeRenderer::shutdown() { - _entitiesScriptEngine->disconnect(); // disconnect all slots/signals from the script engine + _entitiesScriptEngine->disconnectNonEssentialSignals(); // disconnect all slots/signals from the script engine, except essential _shuttingDown = true; } diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index f313cd7d9e..0fdc5169b6 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -124,14 +124,9 @@ static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNameString, bool wantSignals) : _scriptContents(scriptContents), - _isFinished(false), - _isRunning(false), - _isInitialized(false), _timerFunctionMap(), _wantSignals(wantSignals), _fileNameString(fileNameString), - _isUserLoaded(false), - _isReloading(false), _arrayBufferClass(new ArrayBufferClass(this)) { _allScriptsMutex.lock(); @@ -140,6 +135,8 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam } ScriptEngine::~ScriptEngine() { + qDebug() << "Script Engine shutting down (destructor) for script:" << getFilename(); + // If we're not already in the middle of stopping all scripts, then we should remove ourselves // from the list of running scripts. We don't do this if we're in the process of stopping all scripts // because that method removes scripts from its list as it iterates them @@ -150,7 +147,13 @@ ScriptEngine::~ScriptEngine() { } } +void ScriptEngine::disconnectNonEssentialSignals() { + disconnect(); + connect(this, &ScriptEngine::doneRunning, thread(), &QThread::quit); +} + void ScriptEngine::runInThread() { + _isThreaded = true; QThread* workerThread = new QThread(); // thread is not owned, so we need to manage the delete QString scriptEngineName = QString("Script Thread:") + getFilename(); workerThread->setObjectName(scriptEngineName); @@ -176,12 +179,13 @@ void ScriptEngine::runInThread() { QSet ScriptEngine::_allKnownScriptEngines; QMutex ScriptEngine::_allScriptsMutex; bool ScriptEngine::_stoppingAllScripts = false; -bool ScriptEngine::_doneRunningThisScript = false; void ScriptEngine::stopAllScripts(QObject* application) { _allScriptsMutex.lock(); _stoppingAllScripts = true; + qDebug() << "Stopping all scripts.... currently known scripts:" << _allKnownScriptEngines.size(); + QMutableSetIterator i(_allKnownScriptEngines); while (i.hasNext()) { ScriptEngine* scriptEngine = i.next(); @@ -219,7 +223,9 @@ void ScriptEngine::stopAllScripts(QObject* application) { // We need to wait for the engine to be done running before we proceed, because we don't // want any of the scripts final "scriptEnding()" or pending "update()" methods from accessing // any application state after we leave this stopAllScripts() method + qDebug() << "waiting on script:" << scriptName; scriptEngine->waitTillDoneRunning(); + qDebug() << "done waiting on script:" << scriptName; // If the script is stopped, we can remove it from our set i.remove(); @@ -227,21 +233,19 @@ void ScriptEngine::stopAllScripts(QObject* application) { } _stoppingAllScripts = false; _allScriptsMutex.unlock(); + qDebug() << "DONE Stopping all scripts...."; } void ScriptEngine::waitTillDoneRunning() { // If the script never started running or finished running before we got here, we don't need to wait for it - if (_isRunning) { - - _doneRunningThisScript = false; // NOTE: this is static, we serialize our waiting for scripts to finish + if (_isRunning && _isThreaded) { // NOTE: waitTillDoneRunning() will be called on the main Application thread, inside of stopAllScripts() // we want the application thread to continue to process events, because the scripts will likely need to // marshall messages across to the main thread. For example if they access Settings or Meny in any of their // shutdown code. - while (!_doneRunningThisScript) { - + while (thread()->isRunning()) { // process events for the main application thread, allowing invokeMethod calls to pass between threads QCoreApplication::processEvents(); } @@ -752,8 +756,6 @@ void ScriptEngine::run() { emit runningStateChanged(); emit doneRunning(); } - - _doneRunningThisScript = true; } // NOTE: This is private because it must be called on the same thread that created the timers, which is why diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 1412ba7aaf..0cf9eb2c10 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -128,6 +128,7 @@ public: bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget bool isRunning() const { return _isRunning; } // used by ScriptWidget + void disconnectNonEssentialSignals(); static void stopAllScripts(QObject* application); // used by Application on shutdown //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -165,15 +166,16 @@ signals: protected: QString _scriptContents; QString _parentURL; - bool _isFinished; - bool _isRunning; - int _evaluatesPending = 0; - bool _isInitialized; + bool _isFinished { false }; + bool _isRunning { false }; + int _evaluatesPending { 0 }; + bool _isInitialized { false }; QHash _timerFunctionMap; QSet _includedURLs; - bool _wantSignals = true; + bool _wantSignals { true }; QHash _entityScripts; -private: + bool _isThreaded { false }; + void init(); QString getFilename() const; void waitTillDoneRunning(); @@ -191,8 +193,8 @@ private: Quat _quatLibrary; Vec3 _vec3Library; ScriptUUID _uuidLibrary; - bool _isUserLoaded; - bool _isReloading; + bool _isUserLoaded { false }; + bool _isReloading { false }; ArrayBufferClass* _arrayBufferClass; @@ -205,8 +207,6 @@ private: static QSet _allKnownScriptEngines; static QMutex _allScriptsMutex; static bool _stoppingAllScripts; - static bool _doneRunningThisScript; - }; #endif // hifi_ScriptEngine_h From f759fa2cfa24b10a9c5a0eeee2aed24bb50bf572 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Mon, 23 Nov 2015 18:30:40 -0600 Subject: [PATCH 92/99] Ubuntu fixes --- cmake/externals/quazip/CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmake/externals/quazip/CMakeLists.txt b/cmake/externals/quazip/CMakeLists.txt index 794dffb951..7c30721143 100644 --- a/cmake/externals/quazip/CMakeLists.txt +++ b/cmake/externals/quazip/CMakeLists.txt @@ -1,5 +1,6 @@ set(EXTERNAL_NAME quazip) string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) +cmake_policy(SET CMP0046 OLD) include(ExternalProject) ExternalProject_Add( @@ -30,7 +31,9 @@ if (APPLE) set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip.1.0.0.dylib CACHE FILEPATH "Location of QuaZip release library") elseif (WIN32) set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/quazip.lib CACHE FILEPATH "Location of QuaZip release library") -endif() +else () + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip.so CACHE FILEPATH "Location of QuaZip release library") +endif () include(SelectLibraryConfigurations) select_library_configurations(${EXTERNAL_NAME_UPPER}) From 44c95c13fb32da97d61c01eb524186da5b40cfa2 Mon Sep 17 00:00:00 2001 From: AlessandroSigna Date: Mon, 23 Nov 2015 16:57:24 -0800 Subject: [PATCH 93/99] further UI fixes --- examples/entityScripts/recordingEntityScript.js | 9 ++++++--- examples/entityScripts/recordingMaster.js | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/entityScripts/recordingEntityScript.js b/examples/entityScripts/recordingEntityScript.js index e1e161a79d..8916072e39 100644 --- a/examples/entityScripts/recordingEntityScript.js +++ b/examples/entityScripts/recordingEntityScript.js @@ -21,6 +21,9 @@ var START_MESSAGE = "recordingStarted"; var STOP_MESSAGE = "recordingEnded"; var PARTICIPATING_MESSAGE = "participatingToRecording"; + var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; + var RECORDING_ICON_URL = HIFI_PUBLIC_BUCKET + "images/tools/play.svg"; + var NOT_RECORDING_ICON_URL = HIFI_PUBLIC_BUCKET + "images/tools/ac-on-off.svg"; var ICON_WIDTH = 60; var ICON_HEIGHT = 60; var overlay = null; @@ -65,7 +68,7 @@ print("entering in the recording area"); Messages.subscribe(MASTER_TO_CLIENTS_CHANNEL); overlay = Overlays.addOverlay("image", { - imageURL: "http://wcdn2.dataknet.com/static/resources/icons/set49/1c828b8c.png", //waiting for the official logo + imageURL: NOT_RECORDING_ICON_URL, //waiting for the official logo width: ICON_HEIGHT, height: ICON_WIDTH, x: 600, @@ -88,7 +91,7 @@ Messages.sendMessage(CLIENTS_TO_MASTER_CHANNEL, PARTICIPATING_MESSAGE); //tell to master that I'm participating Recording.startRecording(); isAvatarRecording = true; - Overlays.editOverlay(overlay, {imageURL: "http://www.polyrythmic.org/picts/REC.png"}); //waiting for the official logo + Overlays.editOverlay(overlay, {imageURL: RECORDING_ICON_URL}); //waiting for the official logo } }, @@ -103,7 +106,7 @@ Recording.saveRecording(recordingFile); //save the clip locally } Recording.saveRecordingToAsset(getClipUrl); //save the clip to the asset and link a callback to get its url - Overlays.editOverlay(overlay, {imageURL: "http://wcdn2.dataknet.com/static/resources/icons/set49/1c828b8c.png"}); //waiting for the official logo + Overlays.editOverlay(overlay, {imageURL: NOT_RECORDING_ICON_URL}); //waiting for the official logo } }, diff --git a/examples/entityScripts/recordingMaster.js b/examples/entityScripts/recordingMaster.js index 1eb10d9bf2..51149991c2 100644 --- a/examples/entityScripts/recordingMaster.js +++ b/examples/entityScripts/recordingMaster.js @@ -62,6 +62,7 @@ function setupToolBar() { visible: true, }, true, isRecording); } +toolBar.selectTool(recordIcon, !isRecording); function mousePressEvent(event) { clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); @@ -77,6 +78,7 @@ function mousePressEvent(event) { Messages.sendMessage(MASTER_TO_CLIENTS_CHANNEL, STOP_MESSAGE); isRecording = false; } + toolBar.selectTool(recordIcon, !isRecording); } } From 489cde7269c8abbc16f2280d90120e65637e17d2 Mon Sep 17 00:00:00 2001 From: AlessandroSigna Date: Mon, 23 Nov 2015 17:51:58 -0800 Subject: [PATCH 94/99] removed the local save of the recording --- .../entityScripts/recordingEntityScript.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/examples/entityScripts/recordingEntityScript.js b/examples/entityScripts/recordingEntityScript.js index 8916072e39..a566bf96fc 100644 --- a/examples/entityScripts/recordingEntityScript.js +++ b/examples/entityScripts/recordingEntityScript.js @@ -21,9 +21,8 @@ var START_MESSAGE = "recordingStarted"; var STOP_MESSAGE = "recordingEnded"; var PARTICIPATING_MESSAGE = "participatingToRecording"; - var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; - var RECORDING_ICON_URL = HIFI_PUBLIC_BUCKET + "images/tools/play.svg"; - var NOT_RECORDING_ICON_URL = HIFI_PUBLIC_BUCKET + "images/tools/ac-on-off.svg"; + var RECORDING_ICON_URL = "http://cdn.highfidelity.com/alan/production/icons/ICO_rec-active.svg"; + var NOT_RECORDING_ICON_URL = "http://cdn.highfidelity.com/alan/production/icons/ICO_rec-inactive.svg"; var ICON_WIDTH = 60; var ICON_HEIGHT = 60; var overlay = null; @@ -68,10 +67,10 @@ print("entering in the recording area"); Messages.subscribe(MASTER_TO_CLIENTS_CHANNEL); overlay = Overlays.addOverlay("image", { - imageURL: NOT_RECORDING_ICON_URL, //waiting for the official logo + imageURL: NOT_RECORDING_ICON_URL, width: ICON_HEIGHT, height: ICON_WIDTH, - x: 600, + x: 210, y: 0, visible: true }); @@ -91,7 +90,7 @@ Messages.sendMessage(CLIENTS_TO_MASTER_CHANNEL, PARTICIPATING_MESSAGE); //tell to master that I'm participating Recording.startRecording(); isAvatarRecording = true; - Overlays.editOverlay(overlay, {imageURL: RECORDING_ICON_URL}); //waiting for the official logo + Overlays.editOverlay(overlay, {imageURL: RECORDING_ICON_URL}); } }, @@ -100,13 +99,8 @@ print("RECORDING ENDED"); Recording.stopRecording(); isAvatarRecording = false; - - var recordingFile = Window.save("Save recording to file", "./groupRecording", "Recordings (*.hfr)"); - if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { - Recording.saveRecording(recordingFile); //save the clip locally - } Recording.saveRecordingToAsset(getClipUrl); //save the clip to the asset and link a callback to get its url - Overlays.editOverlay(overlay, {imageURL: NOT_RECORDING_ICON_URL}); //waiting for the official logo + Overlays.editOverlay(overlay, {imageURL: NOT_RECORDING_ICON_URL}); } }, From 70cf5c672608a45d6d2192751a3a7ddcc6a85b42 Mon Sep 17 00:00:00 2001 From: AlessandroSigna Date: Mon, 23 Nov 2015 17:59:31 -0800 Subject: [PATCH 95/99] fixed icon position --- examples/entityScripts/recordingEntityScript.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/entityScripts/recordingEntityScript.js b/examples/entityScripts/recordingEntityScript.js index a566bf96fc..0694ff431e 100644 --- a/examples/entityScripts/recordingEntityScript.js +++ b/examples/entityScripts/recordingEntityScript.js @@ -70,7 +70,7 @@ imageURL: NOT_RECORDING_ICON_URL, width: ICON_HEIGHT, height: ICON_WIDTH, - x: 210, + x: 275, y: 0, visible: true }); From 55649717c50aebd65cc7b310cd332dbb7c705255 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Mon, 23 Nov 2015 20:10:32 -0600 Subject: [PATCH 96/99] Using http for quazip external --- cmake/externals/quazip/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/externals/quazip/CMakeLists.txt b/cmake/externals/quazip/CMakeLists.txt index 7c30721143..db0e218835 100644 --- a/cmake/externals/quazip/CMakeLists.txt +++ b/cmake/externals/quazip/CMakeLists.txt @@ -5,7 +5,7 @@ cmake_policy(SET CMP0046 OLD) include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} - URL https://s3-us-west-1.amazonaws.com/hifi-production/dependencies/quazip-0.6.2.zip + URL http://s3-us-west-1.amazonaws.com/hifi-production/dependencies/quazip-0.6.2.zip URL_MD5 514851970f1a14d815bdc3ad6267af4d BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_PREFIX_PATH=$ENV{QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=/lib -DZLIB_ROOT=${ZLIB_ROOT} @@ -40,4 +40,4 @@ select_library_configurations(${EXTERNAL_NAME_UPPER}) # Force selected libraries into the cache set(${EXTERNAL_NAME_UPPER}_LIBRARY ${${EXTERNAL_NAME_UPPER}_LIBRARY} CACHE FILEPATH "Location of QuaZip libraries") -set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES} CACHE FILEPATH "Location of QuaZip libraries") \ No newline at end of file +set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES} CACHE FILEPATH "Location of QuaZip libraries") From 388157d4329d9273f45f00f925ae134e882e40d6 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Mon, 23 Nov 2015 20:16:18 -0600 Subject: [PATCH 97/99] Typo --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e5807146ff..9e95974383 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,7 +203,7 @@ if (NOT ANDROID) add_subdirectory(interface) set_target_properties(interface PROPERTIES FOLDER "Apps") add_subdirectory(stack-manager) - set_target_properties(stack-manager PROPERTIES FOLER "Apps") + set_target_properties(stack-manager PROPERTIES FOLDER "Apps") add_subdirectory(tests) add_subdirectory(plugins) add_subdirectory(tools) From 5f88d958ab592f21122eaa388363700bb0052fe4 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 23 Nov 2015 18:26:15 -0800 Subject: [PATCH 98/99] CR feedback --- libraries/script-engine/src/ScriptEngine.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 0fdc5169b6..995a92bf83 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -135,7 +135,7 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam } ScriptEngine::~ScriptEngine() { - qDebug() << "Script Engine shutting down (destructor) for script:" << getFilename(); + qCDebug(scriptengine) << "Script Engine shutting down (destructor) for script:" << getFilename(); // If we're not already in the middle of stopping all scripts, then we should remove ourselves // from the list of running scripts. We don't do this if we're in the process of stopping all scripts @@ -158,6 +158,10 @@ void ScriptEngine::runInThread() { QString scriptEngineName = QString("Script Thread:") + getFilename(); workerThread->setObjectName(scriptEngineName); + // NOTE: If you connect any essential signals for proper shutdown or cleanup of + // the script engine, make sure to add code to "reconnect" them to the + // disconnectNonEssentialSignals() method + // when the worker thread is started, call our engine's run.. connect(workerThread, &QThread::started, this, &ScriptEngine::run); @@ -184,7 +188,7 @@ void ScriptEngine::stopAllScripts(QObject* application) { _allScriptsMutex.lock(); _stoppingAllScripts = true; - qDebug() << "Stopping all scripts.... currently known scripts:" << _allKnownScriptEngines.size(); + qCDebug(scriptengine) << "Stopping all scripts.... currently known scripts:" << _allKnownScriptEngines.size(); QMutableSetIterator i(_allKnownScriptEngines); while (i.hasNext()) { @@ -223,9 +227,9 @@ void ScriptEngine::stopAllScripts(QObject* application) { // We need to wait for the engine to be done running before we proceed, because we don't // want any of the scripts final "scriptEnding()" or pending "update()" methods from accessing // any application state after we leave this stopAllScripts() method - qDebug() << "waiting on script:" << scriptName; + qCDebug(scriptengine) << "waiting on script:" << scriptName; scriptEngine->waitTillDoneRunning(); - qDebug() << "done waiting on script:" << scriptName; + qCDebug(scriptengine) << "done waiting on script:" << scriptName; // If the script is stopped, we can remove it from our set i.remove(); @@ -233,7 +237,7 @@ void ScriptEngine::stopAllScripts(QObject* application) { } _stoppingAllScripts = false; _allScriptsMutex.unlock(); - qDebug() << "DONE Stopping all scripts...."; + qCDebug(scriptengine) << "DONE Stopping all scripts...."; } @@ -1170,7 +1174,7 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { QString filePath = QUrl(details.scriptText).toLocalFile(); auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch(); if (lastModified > details.lastModified) { - qDebug() << "Reloading modified script " << details.scriptText; + qCDebug(scriptengine) << "Reloading modified script " << details.scriptText; QFile file(filePath); file.open(QIODevice::ReadOnly); From a1668cdc737c3a56a06453ac59059272a63d3f94 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 23 Nov 2015 22:28:59 -0800 Subject: [PATCH 99/99] Let the AssertClient JS interface use global vars --- libraries/networking/src/AssetClient.cpp | 14 ++++++++++---- libraries/networking/src/AssetClient.h | 3 +++ libraries/script-engine/src/ScriptEngine.h | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 8ac019ff56..83d91b32d3 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include "AssetRequest.h" @@ -374,16 +375,21 @@ void AssetScriptingInterface::uploadData(QString data, QString extension, QScrip return; } - QObject::connect(upload, &AssetUpload::finished, this, [callback, extension](AssetUpload* upload, const QString& hash) mutable { + QObject::connect(upload, &AssetUpload::finished, this, [this, callback, extension](AssetUpload* upload, const QString& hash) mutable { if (callback.isFunction()) { QString url = "atp://" + hash + "." + extension; QScriptValueList args { url }; - callback.call(QScriptValue(), args); + callback.call(_engine->currentContext()->thisObject(), args); } }); upload->start(); } +AssetScriptingInterface::AssetScriptingInterface(QScriptEngine* engine) : + _engine(engine) +{ +} + void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) { const QString ATP_SCHEME { "atp://" }; @@ -410,14 +416,14 @@ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callb _pendingRequests << assetRequest; - connect(assetRequest, &AssetRequest::finished, [this, callback](AssetRequest* request) mutable { + connect(assetRequest, &AssetRequest::finished, this, [this, callback](AssetRequest* request) mutable { Q_ASSERT(request->getState() == AssetRequest::Finished); if (request->getError() == AssetRequest::Error::NoError) { if (callback.isFunction()) { QString data = QString::fromUtf8(request->getData()); QScriptValueList args { data }; - callback.call(QScriptValue(), args); + callback.call(_engine->currentContext()->thisObject(), args); } } diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index f1bb210614..0616317eec 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -74,10 +74,13 @@ private: class AssetScriptingInterface : public QObject { Q_OBJECT public: + AssetScriptingInterface(QScriptEngine* engine); + Q_INVOKABLE void uploadData(QString data, QString extension, QScriptValue callback); Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete); protected: QSet _pendingRequests; + QScriptEngine* _engine; }; diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 1412ba7aaf..75322f369a 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -196,7 +196,7 @@ private: ArrayBufferClass* _arrayBufferClass; - AssetScriptingInterface _assetScriptingInterface; + AssetScriptingInterface _assetScriptingInterface{ this }; QHash _registeredHandlers; void forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHanderArgs);