diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 300976f81c..fda226b934 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -41,10 +41,6 @@ Agent::Agent(NLPacket& packet) : DEFAULT_WINDOW_STARVE_THRESHOLD, DEFAULT_WINDOW_SECONDS_FOR_DESIRED_CALC_ON_TOO_MANY_STARVES, DEFAULT_WINDOW_SECONDS_FOR_DESIRED_REDUCTION, false)) { - // be the parent of the script engine so it gets moved when we do - _scriptEngine.setParent(this); - _scriptEngine.setIsAgent(true); - DependencyManager::get()->setPacketSender(&_entityEditSender); DependencyManager::set(); @@ -157,8 +153,11 @@ void Agent::run() { qDebug() << "Downloaded script:" << scriptContents; + _scriptEngine = 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 - ScriptableAvatar scriptedAvatar(&_scriptEngine); + ScriptableAvatar scriptedAvatar(_scriptEngine); scriptedAvatar.setForceFaceTrackerConnected(true); // call model URL setters with empty URLs so our avatar, if user, will have the default models @@ -166,12 +165,11 @@ void Agent::run() { scriptedAvatar.setSkeletonModelURL(QUrl()); // give this AvatarData object to the script engine - _scriptEngine.setAvatarData(&scriptedAvatar, "Avatar"); + setAvatarData(&scriptedAvatar, "Avatar"); auto avatarHashMap = DependencyManager::set(); - - _scriptEngine.setAvatarHashMap(avatarHashMap.data(), "AvatarList"); - + _scriptEngine->registerGlobalObject("AvatarList", avatarHashMap.data()); + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListener(PacketType::BulkAvatarData, avatarHashMap.data(), "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, avatarHashMap.data(), "processKillAvatar"); @@ -179,33 +177,188 @@ void Agent::run() { packetReceiver.registerListener(PacketType::AvatarBillboard, avatarHashMap.data(), "processAvatarBillboardPacket"); // register ourselves to the script engine - _scriptEngine.registerGlobalObject("Agent", this); + _scriptEngine->registerGlobalObject("Agent", this); - if (!_payload.isEmpty()) { - _scriptEngine.setParentURL(_payload); - } + // 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.init(); // must be done before we set up the viewers + _scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get().data()); - _scriptEngine.registerGlobalObject("SoundCache", DependencyManager::get().data()); - - QScriptValue webSocketServerConstructorValue = _scriptEngine.newFunction(WebSocketServerClass::constructor); - _scriptEngine.globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue); + QScriptValue webSocketServerConstructorValue = _scriptEngine->newFunction(WebSocketServerClass::constructor); + _scriptEngine->globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue); auto entityScriptingInterface = DependencyManager::get(); - _scriptEngine.registerGlobalObject("EntityViewer", &_entityViewer); + _scriptEngine->registerGlobalObject("EntityViewer", &_entityViewer); _entityViewer.setJurisdictionListener(entityScriptingInterface->getJurisdictionListener()); _entityViewer.init(); entityScriptingInterface->setEntityTree(_entityViewer.getTree()); - _scriptEngine.setScriptContents(scriptContents); - _scriptEngine.run(); + // wire up our additional agent related processing to the update signal + QObject::connect(_scriptEngine, &ScriptEngine::update, this, &Agent::processAgentAvatarAndAudio); + + _scriptEngine->run(); setFinished(true); + + // kill the avatar identity timer + delete _avatarIdentityTimer; + + // delete the script engine + delete _scriptEngine; + +} + +void Agent::setIsAvatar(bool isAvatar) { + _isAvatar = isAvatar; + + if (_isAvatar && !_avatarIdentityTimer) { + // set up the avatar timers + _avatarIdentityTimer = new QTimer(this); + _avatarBillboardTimer = new QTimer(this); + + // connect our slot + connect(_avatarIdentityTimer, &QTimer::timeout, this, &Agent::sendAvatarIdentityPacket); + connect(_avatarBillboardTimer, &QTimer::timeout, this, &Agent::sendAvatarBillboardPacket); + + // start the timers + _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); + _avatarBillboardTimer->start(AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS); + } + + if (!_isAvatar) { + delete _avatarIdentityTimer; + _avatarIdentityTimer = NULL; + delete _avatarBillboardTimer; + _avatarBillboardTimer = NULL; + } +} + +void Agent::setAvatarData(AvatarData* avatarData, const QString& objectName) { + _avatarData = avatarData; + _scriptEngine->registerGlobalObject(objectName, avatarData); +} + +void Agent::sendAvatarIdentityPacket() { + if (_isAvatar && _avatarData) { + _avatarData->sendIdentityPacket(); + } +} + +void Agent::sendAvatarBillboardPacket() { + if (_isAvatar && _avatarData) { + _avatarData->sendBillboardPacket(); + } +} + + +void Agent::processAgentAvatarAndAudio(float deltaTime) { + if (!_scriptEngine->isFinished() && _isAvatar && _avatarData) { + + const int SCRIPT_AUDIO_BUFFER_SAMPLES = floor(((SCRIPT_DATA_CALLBACK_USECS * AudioConstants::SAMPLE_RATE) + / (1000 * 1000)) + 0.5); + const int SCRIPT_AUDIO_BUFFER_BYTES = SCRIPT_AUDIO_BUFFER_SAMPLES * sizeof(int16_t); + + QByteArray avatarByteArray = _avatarData->toByteArray(true, randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); + _avatarData->doneEncoding(true); + auto avatarPacket = NLPacket::create(PacketType::AvatarData, avatarByteArray.size()); + + avatarPacket->write(avatarByteArray); + + auto nodeList = DependencyManager::get(); + + nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); + + if (_isListeningToAudioStream || _avatarSound) { + // if we have an avatar audio stream then send it out to our audio-mixer + bool silentFrame = true; + + int16_t numAvailableSamples = SCRIPT_AUDIO_BUFFER_SAMPLES; + const int16_t* nextSoundOutput = NULL; + + if (_avatarSound) { + + const QByteArray& soundByteArray = _avatarSound->getByteArray(); + nextSoundOutput = reinterpret_cast(soundByteArray.data() + + _numAvatarSoundSentBytes); + + int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > SCRIPT_AUDIO_BUFFER_BYTES + ? SCRIPT_AUDIO_BUFFER_BYTES + : soundByteArray.size() - _numAvatarSoundSentBytes; + numAvailableSamples = numAvailableBytes / sizeof(int16_t); + + + // check if the all of the _numAvatarAudioBufferSamples to be sent are silence + for (int i = 0; i < numAvailableSamples; ++i) { + if (nextSoundOutput[i] != 0) { + silentFrame = false; + break; + } + } + + _numAvatarSoundSentBytes += numAvailableBytes; + if (_numAvatarSoundSentBytes == soundByteArray.size()) { + // we're done with this sound object - so set our pointer back to NULL + // and our sent bytes back to zero + _avatarSound = NULL; + _numAvatarSoundSentBytes = 0; + } + } + + auto audioPacket = NLPacket::create(silentFrame + ? PacketType::SilentAudioFrame + : PacketType::MicrophoneAudioNoEcho); + + // seek past the sequence number, will be packed when destination node is known + audioPacket->seek(sizeof(quint16)); + + if (silentFrame) { + if (!_isListeningToAudioStream) { + // if we have a silent frame and we're not listening then just send nothing and break out of here + return; + } + + // write the number of silent samples so the audio-mixer can uphold timing + audioPacket->writePrimitive(SCRIPT_AUDIO_BUFFER_SAMPLES); + + // use the orientation and position of this avatar for the source of this audio + audioPacket->writePrimitive(_avatarData->getPosition()); + glm::quat headOrientation = _avatarData->getHeadOrientation(); + audioPacket->writePrimitive(headOrientation); + + }else if (nextSoundOutput) { + // assume scripted avatar audio is mono and set channel flag to zero + audioPacket->writePrimitive((quint8)0); + + // use the orientation and position of this avatar for the source of this audio + audioPacket->writePrimitive(_avatarData->getPosition()); + glm::quat headOrientation = _avatarData->getHeadOrientation(); + audioPacket->writePrimitive(headOrientation); + + // write the raw audio data + audioPacket->write(reinterpret_cast(nextSoundOutput), numAvailableSamples * sizeof(int16_t)); + } + + // write audio packet to AudioMixer nodes + auto nodeList = DependencyManager::get(); + nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node){ + // only send to nodes of type AudioMixer + if (node->getType() == NodeType::AudioMixer) { + // pack sequence number + quint16 sequence = _outgoingScriptAudioSequenceNumbers[node->getUUID()]++; + audioPacket->seek(0); + audioPacket->writePrimitive(sequence); + + // send audio packet + nodeList->sendUnreliablePacket(*audioPacket, *node); + } + }); + } + } } void Agent::aboutToFinish() { - _scriptEngine.stop(); + _scriptEngine->stop(); _pingTimer->stop(); delete _pingTimer; diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 4c207e59aa..33a8eb58c2 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -37,37 +37,53 @@ class Agent : public ThreadedAssignment { public: Agent(NLPacket& packet); - void setIsAvatar(bool isAvatar) { QMetaObject::invokeMethod(&_scriptEngine, "setIsAvatar", Q_ARG(bool, isAvatar)); } - bool isAvatar() const { return _scriptEngine.isAvatar(); } - - bool isPlayingAvatarSound() const { return _scriptEngine.isPlayingAvatarSound(); } - - bool isListeningToAudioStream() const { return _scriptEngine.isListeningToAudioStream(); } - void setIsListeningToAudioStream(bool isListeningToAudioStream) - { _scriptEngine.setIsListeningToAudioStream(isListeningToAudioStream); } - + void setIsAvatar(bool isAvatar); + bool isAvatar() const { return _isAvatar; } + + bool isPlayingAvatarSound() const { return _avatarSound != NULL; } + + bool isListeningToAudioStream() const { return _isListeningToAudioStream; } + void setIsListeningToAudioStream(bool isListeningToAudioStream) { _isListeningToAudioStream = isListeningToAudioStream; } + float getLastReceivedAudioLoudness() const { return _lastReceivedAudioLoudness; } virtual void aboutToFinish(); public slots: void run(); - void playAvatarSound(Sound* avatarSound) { _scriptEngine.setAvatarSound(avatarSound); } + void playAvatarSound(Sound* avatarSound) { setAvatarSound(avatarSound); } private slots: void handleAudioPacket(QSharedPointer packet); void handleOctreePacket(QSharedPointer packet, SharedNodePointer senderNode); void handleJurisdictionPacket(QSharedPointer packet, SharedNodePointer senderNode); void sendPingRequests(); + void processAgentAvatarAndAudio(float deltaTime); private: - ScriptEngine _scriptEngine; + ScriptEngine* _scriptEngine; EntityEditPacketSender _entityEditSender; EntityTreeHeadlessViewer _entityViewer; QTimer* _pingTimer; MixedAudioStream _receivedAudioStream; float _lastReceivedAudioLoudness; + + void setAvatarData(AvatarData* avatarData, const QString& objectName); + void setAvatarSound(Sound* avatarSound) { _avatarSound = avatarSound; } + + void sendAvatarIdentityPacket(); + void sendAvatarBillboardPacket(); + + AvatarData* _avatarData = nullptr; + bool _isListeningToAudioStream = false; + Sound* _avatarSound = nullptr; + int _numAvatarSoundSentBytes = 0; + bool _isAvatar = false; + QTimer* _avatarIdentityTimer = nullptr; + QTimer* _avatarBillboardTimer = nullptr; + QHash _outgoingScriptAudioSequenceNumbers; + }; #endif // hifi_Agent_h diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index bde53e4a87..bdd5728246 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -142,7 +142,7 @@ void EntityServer::pruneDeletedEntities() { } } -void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectionObject) { +bool EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectionObject) { bool wantEditLogging = false; readOptionBool(QString("wantEditLogging"), settingsSectionObject, wantEditLogging); qDebug("wantEditLogging=%s", debug::valueOf(wantEditLogging)); @@ -150,4 +150,6 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio EntityTreePointer tree = std::static_pointer_cast(_tree); tree->setWantEditLogging(wantEditLogging); + + return true; } diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index d2be75d05e..114e0e1726 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -41,7 +41,7 @@ public: virtual int sendSpecialPackets(const SharedNodePointer& node, OctreeQueryNode* queryNode, int& packetsSent); virtual void entityCreated(const EntityItem& newEntity, const SharedNodePointer& senderNode); - virtual void readAdditionalConfiguration(const QJsonObject& settingsSectionObject); + virtual bool readAdditionalConfiguration(const QJsonObject& settingsSectionObject) override; public slots: void pruneDeletedEntities(); diff --git a/assignment-client/src/octree/OctreeQueryNode.cpp b/assignment-client/src/octree/OctreeQueryNode.cpp index c68fb4e1b5..f70ff62f91 100644 --- a/assignment-client/src/octree/OctreeQueryNode.cpp +++ b/assignment-client/src/octree/OctreeQueryNode.cpp @@ -242,7 +242,8 @@ bool OctreeQueryNode::updateCurrentViewFrustum() { if (0.0f != getCameraAspectRatio() && 0.0f != getCameraNearClip() && - 0.0f != getCameraFarClip()) { + 0.0f != getCameraFarClip() && + getCameraNearClip() != getCameraFarClip()) { newestViewFrustum.setProjection(glm::perspective( glm::radians(wideFOV), // hack getCameraAspectRatio(), diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 0368e8a408..ee0403d57b 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -887,7 +887,7 @@ bool OctreeServer::readOptionString(const QString& optionName, const QJsonObject return optionAvailable; } -void OctreeServer::readConfiguration() { +bool OctreeServer::readConfiguration() { // if the assignment had a payload, read and parse that if (getPayload().size() > 0) { parsePayload(); @@ -907,8 +907,11 @@ void OctreeServer::readConfiguration() { loop.exec(); if (domainHandler.getSettingsObject().isEmpty()) { - qDebug() << "No settings object from domain-server."; + qDebug() << "Failed to retreive settings object from domain-server. Bailing on assignment."; + setFinished(true); + return false; } + const QJsonObject& settingsObject = domainHandler.getSettingsObject(); QString settingsKey = getMyDomainSettingsKey(); QJsonObject settingsSectionObject = settingsObject[settingsKey].toObject(); @@ -1017,7 +1020,7 @@ void OctreeServer::readConfiguration() { packetsPerSecondTotalMax, _packetsTotalPerInterval); - readAdditionalConfiguration(settingsSectionObject); + return readAdditionalConfiguration(settingsSectionObject); } void OctreeServer::run() { @@ -1043,7 +1046,9 @@ void OctreeServer::run() { commonInit(getMyLoggingServerTargetName(), getMyNodeType()); // read the configuration from either the payload or the domain server configuration - readConfiguration(); + if (!readConfiguration()) { + return; // bailing on run, because readConfiguration failed + } beforeRun(); // after payload has been processed @@ -1333,19 +1338,22 @@ void OctreeServer::sendStatsPacket() { QJsonObject statsObject2; statsObject2["data"] = dataObject1; statsObject2["timing"] = timingArray1; - - // Stats Object 3 + QJsonObject dataArray2; - dataArray2["1. packetQueue"] = (double)_octreeInboundPacketProcessor->packetsToProcessCount(); - dataArray2["2. totalPackets"] = (double)_octreeInboundPacketProcessor->getTotalPacketsProcessed(); - dataArray2["3. totalElements"] = (double)_octreeInboundPacketProcessor->getTotalElementsProcessed(); - QJsonObject timingArray2; - timingArray2["1. avgTransitTimePerPacket"] = (double)_octreeInboundPacketProcessor->getAverageTransitTimePerPacket(); - timingArray2["2. avgProcessTimePerPacket"] = (double)_octreeInboundPacketProcessor->getAverageProcessTimePerPacket(); - timingArray2["3. avgLockWaitTimePerPacket"] = (double)_octreeInboundPacketProcessor->getAverageLockWaitTimePerPacket(); - timingArray2["4. avgProcessTimePerElement"] = (double)_octreeInboundPacketProcessor->getAverageProcessTimePerElement(); - timingArray2["5. avgLockWaitTimePerElement"] = (double)_octreeInboundPacketProcessor->getAverageLockWaitTimePerElement(); + + // Stats Object 3 + if (_octreeInboundPacketProcessor) { + dataArray2["1. packetQueue"] = (double)_octreeInboundPacketProcessor->packetsToProcessCount(); + dataArray2["2. totalPackets"] = (double)_octreeInboundPacketProcessor->getTotalPacketsProcessed(); + dataArray2["3. totalElements"] = (double)_octreeInboundPacketProcessor->getTotalElementsProcessed(); + + timingArray2["1. avgTransitTimePerPacket"] = (double)_octreeInboundPacketProcessor->getAverageTransitTimePerPacket(); + timingArray2["2. avgProcessTimePerPacket"] = (double)_octreeInboundPacketProcessor->getAverageProcessTimePerPacket(); + timingArray2["3. avgLockWaitTimePerPacket"] = (double)_octreeInboundPacketProcessor->getAverageLockWaitTimePerPacket(); + timingArray2["4. avgProcessTimePerElement"] = (double)_octreeInboundPacketProcessor->getAverageProcessTimePerElement(); + timingArray2["5. avgLockWaitTimePerElement"] = (double)_octreeInboundPacketProcessor->getAverageLockWaitTimePerElement(); + } QJsonObject statsObject3; statsObject3["data"] = dataArray2; diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 64903ee558..b8e4a5c261 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -135,8 +135,8 @@ protected: bool readOptionBool(const QString& optionName, const QJsonObject& settingsSectionObject, bool& result); bool readOptionInt(const QString& optionName, const QJsonObject& settingsSectionObject, int& result); bool readOptionString(const QString& optionName, const QJsonObject& settingsSectionObject, QString& result); - void readConfiguration(); - virtual void readAdditionalConfiguration(const QJsonObject& settingsSectionObject) { }; + bool readConfiguration(); + virtual bool readAdditionalConfiguration(const QJsonObject& settingsSectionObject) { return true; }; void parsePayload(); void initHTTPManager(int port); void resetSendingStats(); diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 7d4380d764..82fd847f25 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -4,7 +4,7 @@ // Created by Eric Levin on 9/2/15 // Copyright 2015 High Fidelity, Inc. // -// Grab's physically moveable entities with the hydra- works for either near or far objects. User can also grab a far away object and drag it towards them by pressing the "4" button on either the left or ride controller. +// Grabs physically moveable entities with hydra-like controllers; it works for either near or far objects. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -13,6 +13,8 @@ Script.include("../libraries/utils.js"); +var RADIUS_FACTOR = 4; + var RIGHT_HAND_CLICK = Controller.findAction("RIGHT_HAND_CLICK"); var rightTriggerAction = RIGHT_HAND_CLICK; @@ -45,7 +47,7 @@ var INTERSECT_COLOR = { blue: 10 }; -var GRAB_RADIUS = 1.5; +var GRAB_RADIUS = 0.3; var GRAB_COLOR = { red: 250, @@ -58,12 +60,11 @@ var DISTANCE_HOLD_THRESHOLD = 0.8; var right4Action = 18; var left4Action = 17; -var TRACTOR_BEAM_VELOCITY_THRESHOLD = 0.5; - var RIGHT = 1; var LEFT = 0; var rightController = new controller(RIGHT, rightTriggerAction, right4Action, "right"); var leftController = new controller(LEFT, leftTriggerAction, left4Action, "left"); +var startTime = Date.now(); //Need to wait before calling these methods for some reason... @@ -78,56 +79,67 @@ function controller(side, triggerAction, pullAction, hand) { this.getHandPosition = MyAvatar.getRightPalmPosition; this.getHandRotation = MyAvatar.getRightPalmRotation; } else { - this.getHandPosition = MyAvatar.getLeftPalmPosition; this.getHandRotation = MyAvatar.getLeftPalmRotation; } this.triggerAction = triggerAction; this.pullAction = pullAction; this.actionID = null; - this.tractorBeamActive = false; this.distanceHolding = false; this.closeGrabbing = false; this.triggerValue = 0; this.prevTriggerValue = 0; this.palm = 2 * side; this.tip = 2 * side + 1; - this.pointer = Entities.addEntity({ - type: "Line", - name: "pointer", - color: NO_INTERSECT_COLOR, - dimensions: { - x: 1000, - y: 1000, - z: 1000 - }, - visible: false, - lifetime: LIFETIME - }); - + this.pointer = null; } controller.prototype.updateLine = function() { - var handPosition = Controller.getSpatialControlPosition(this.palm); - var direction = Controller.getSpatialControlNormal(this.tip); + if (this.pointer != null) { + if (Entities.getEntityProperties(this.pointer).id != this.poitner) { + this.pointer == null; + } + } - Entities.editEntity(this.pointer, { - position: handPosition, - linePoints: [ - ZERO_VEC, - Vec3.multiply(direction, LINE_LENGTH) - ] - }); + if (this.pointer == null) { + this.lineCreationTime = Date.now(); + this.pointer = Entities.addEntity({ + type: "Line", + name: "pointer", + color: NO_INTERSECT_COLOR, + dimensions: { + x: 1000, + y: 1000, + z: 1000 + }, + visible: true, + lifetime: LIFETIME + }); + } + + + var handPosition = this.getHandPosition(); + var direction = Quat.getUp(this.getHandRotation()); //only check if we havent already grabbed an object if (this.distanceHolding) { + Entities.editEntity(this.pointer, { + position: handPosition, + linePoints: [ ZERO_VEC, Vec3.subtract(Entities.getEntityProperties(this.grabbedEntity).position, handPosition) ], + lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME + }); + return; } - //move origin a bit away from hand so nothing gets in way - var origin = Vec3.sum(handPosition, direction); - if (this.checkForIntersections(origin, direction)) { + Entities.editEntity(this.pointer, { + position: handPosition, + linePoints: [ ZERO_VEC, Vec3.multiply(direction, LINE_LENGTH) ], + lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME + }); + + if (this.checkForIntersections(handPosition, direction)) { Entities.editEntity(this.pointer, { color: INTERSECT_COLOR, }); @@ -144,7 +156,7 @@ controller.prototype.checkPointer = function() { Script.setTimeout(function() { var props = Entities.getEntityProperties(self.pointer); Entities.editEntity(self.pointer, { - lifetime: props.age + EXTRA_TIME + LIFETIME + lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME }); self.checkPointer(); }, POINTER_CHECK_TIME); @@ -172,28 +184,54 @@ controller.prototype.checkForIntersections = function(origin, direction) { return false; } + controller.prototype.attemptMove = function() { - if (this.tractorBeamActive) { - return; - } if (this.grabbedEntity || this.distanceHolding) { var handPosition = Controller.getSpatialControlPosition(this.palm); - var direction = Controller.getSpatialControlNormal(this.tip); + var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm)); - var newPosition = Vec3.sum(handPosition, Vec3.multiply(direction, this.distanceToEntity)) this.distanceHolding = true; if (this.actionID === null) { + this.currentObjectPosition = Entities.getEntityProperties(this.grabbedEntity).position; + this.currentObjectRotation = Entities.getEntityProperties(this.grabbedEntity).rotation; + + this.handPreviousPosition = handPosition; + this.handPreviousRotation = handRotation; + this.actionID = Entities.addAction("spring", this.grabbedEntity, { - targetPosition: newPosition, - linearTimeScale: .1 + targetPosition: this.currentObjectPosition, + linearTimeScale: .1, + targetRotation: this.currentObjectRotation, + angularTimeScale: .1 }); } else { + var radius = Math.max(Vec3.distance(this.currentObjectPosition, handPosition) * RADIUS_FACTOR, 1.0); + + var handMoved = Vec3.subtract(handPosition, this.handPreviousPosition); + this.handPreviousPosition = handPosition; + var superHandMoved = Vec3.multiply(handMoved, radius); + this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved); + + // ---------------- this tracks hand rotation + // var handChange = Quat.multiply(handRotation, Quat.inverse(this.handPreviousRotation)); + // this.handPreviousRotation = handRotation; + // this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); + // ---------------- + + // ---------------- this doubles hand rotation + var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation, 2.0), + Quat.inverse(this.handPreviousRotation)); + this.handPreviousRotation = handRotation; + this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); + // ---------------- + + Entities.updateAction(this.grabbedEntity, this.actionID, { - targetPosition: newPosition + targetPosition: this.currentObjectPosition, linearTimeScale: .1, + targetRotation: this.currentObjectRotation, angularTimeScale: .1 }); } } - } controller.prototype.showPointer = function() { @@ -218,19 +256,10 @@ controller.prototype.letGo = function() { this.grabbedEntity = null; this.actionID = null; this.distanceHolding = false; - this.tractorBeamActive = false; - this.checkForEntityArrival = false; this.closeGrabbing = false; } controller.prototype.update = function() { - if (this.tractorBeamActive && this.checkForEntityArrival) { - var entityVelocity = Entities.getEntityProperties(this.grabbedEntity).velocity - if (Vec3.length(entityVelocity) < TRACTOR_BEAM_VELOCITY_THRESHOLD) { - this.letGo(); - } - return; - } this.triggerValue = Controller.getActionValue(this.triggerAction); if (this.triggerValue > SHOW_LINE_THRESHOLD && this.prevTriggerValue < SHOW_LINE_THRESHOLD) { //First check if an object is within close range and then run the close grabbing logic @@ -253,7 +282,6 @@ controller.prototype.update = function() { this.attemptMove(); } - this.prevTriggerValue = this.triggerValue; } @@ -331,27 +359,6 @@ controller.prototype.deactivateEntity = function(entity) { setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data); } -controller.prototype.onActionEvent = function(action, state) { - if (this.pullAction === action && state === 1) { - if (this.actionID !== null) { - var self = this; - this.tractorBeamActive = true; - //We need to wait a bit before checking for entity arrival at target destination (meaning checking for velocity being close to some - //low threshold) because otherwise we'll think the entity has arrived before its even really gotten moving! - Script.setTimeout(function() { - self.checkForEntityArrival = true; - }, 500); - var handPosition = Controller.getSpatialControlPosition(this.palm); - var direction = Vec3.normalize(Controller.getSpatialControlNormal(this.tip)); - //move final destination along line a bit, so it doesnt hit avatar hand - Entities.updateAction(this.grabbedEntity, this.actionID, { - targetPosition: Vec3.sum(handPosition, Vec3.multiply(3, direction)) - }); - } - } - -} - controller.prototype.cleanup = function() { Entities.deleteEntity(this.pointer); if (this.grabbedEntity) { @@ -364,13 +371,6 @@ function update() { leftController.update(); } -function onActionEvent(action, state) { - rightController.onActionEvent(action, state); - leftController.onActionEvent(action, state); - -} - - function cleanup() { rightController.cleanup(); leftController.cleanup(); @@ -378,4 +378,3 @@ function cleanup() { Script.scriptEnding.connect(cleanup); Script.update.connect(update) -Controller.actionEvent.connect(onActionEvent); \ No newline at end of file diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 164f04f2eb..b6e0a871b3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2868,8 +2868,8 @@ void Application::update(float deltaTime) { UserInputMapper::PoseValue leftHand = userInputMapper->getPoseState(UserInputMapper::LEFT_HAND); UserInputMapper::PoseValue rightHand = userInputMapper->getPoseState(UserInputMapper::RIGHT_HAND); Hand* hand = DependencyManager::get()->getMyAvatar()->getHand(); - setPalmData(hand, leftHand, deltaTime, LEFT_HAND_INDEX); - setPalmData(hand, rightHand, deltaTime, RIGHT_HAND_INDEX); + setPalmData(hand, leftHand, deltaTime, LEFT_HAND_INDEX, userInputMapper->getActionState(UserInputMapper::LEFT_HAND_CLICK)); + setPalmData(hand, rightHand, deltaTime, RIGHT_HAND_INDEX, userInputMapper->getActionState(UserInputMapper::RIGHT_HAND_CLICK)); if (Menu::getInstance()->isOptionChecked(MenuOption::HandMouseInput)) { emulateMouse(hand, userInputMapper->getActionState(UserInputMapper::LEFT_HAND_CLICK), userInputMapper->getActionState(UserInputMapper::SHIFT), LEFT_HAND_INDEX); @@ -4026,8 +4026,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri AvatarManager::registerMetaTypes(scriptEngine); // hook our avatar and avatar hash map object into this script engine - scriptEngine->setAvatarData(_myAvatar, "MyAvatar"); // leave it as a MyAvatar class to expose thrust features - scriptEngine->setAvatarHashMap(DependencyManager::get().data(), "AvatarList"); + scriptEngine->registerGlobalObject("MyAvatar", _myAvatar); + scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Camera", &_myCamera); @@ -4051,9 +4051,9 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); - QScriptValue windowValue = scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, - LocationScriptingInterface::locationSetter, windowValue); + LocationScriptingInterface::locationSetter, "Window"); // register `location` on the global object. scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter); @@ -4083,9 +4083,9 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Paths", DependencyManager::get().data()); - QScriptValue hmdInterface = scriptEngine->registerGlobalObject("HMD", &HMDScriptingInterface::getInstance()); - scriptEngine->registerFunction(hmdInterface, "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0); - scriptEngine->registerFunction(hmdInterface, "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0); + scriptEngine->registerGlobalObject("HMD", &HMDScriptingInterface::getInstance()); + scriptEngine->registerFunction("HMD", "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0); + scriptEngine->registerFunction("HMD", "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0); scriptEngine->registerGlobalObject("Scene", DependencyManager::get().data()); @@ -4094,31 +4094,6 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri #ifdef HAVE_RTMIDI scriptEngine->registerGlobalObject("MIDI", &MIDIManager::getInstance()); #endif - - // TODO: Consider moving some of this functionality into the ScriptEngine class instead. It seems wrong that this - // work is being done in the Application class when really these dependencies are more related to the ScriptEngine's - // implementation - QThread* workerThread = new QThread(this); - QString scriptEngineName = QString("Script Thread:") + scriptEngine->getFilename(); - workerThread->setObjectName(scriptEngineName); - - // when the worker thread is started, call our engine's run.. - connect(workerThread, &QThread::started, scriptEngine, &ScriptEngine::run); - - // when the thread is terminated, add both scriptEngine and thread to the deleteLater queue - connect(scriptEngine, &ScriptEngine::doneRunning, scriptEngine, &ScriptEngine::deleteLater); - connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater); - - // tell the thread to stop when the script engine is done - connect(scriptEngine, &ScriptEngine::destroyed, workerThread, &QThread::quit); - - auto nodeList = DependencyManager::get(); - connect(nodeList.data(), &NodeList::nodeKilled, scriptEngine, &ScriptEngine::nodeKilled); - - scriptEngine->moveToThread(workerThread); - - // Starts an event loop, and emits workerThread->started() - workerThread->start(); } void Application::initializeAcceptedFiles() { @@ -4264,11 +4239,14 @@ ScriptEngine* Application::loadScript(const QString& scriptFilename, bool isUser scriptEngine->setUserLoaded(isUserLoaded); if (scriptFilename.isNull()) { + // This appears to be the script engine used by the script widget's evaluation window before the file has been saved... + // this had better be the script editor (we should de-couple so somebody who thinks they are loading a script // doesn't just get an empty script engine) // we can complete setup now since there isn't a script we have to load registerScriptEngineWithApplicationServices(scriptEngine); + scriptEngine->runInThread(); } else { // connect to the appropriate signals of this script engine connect(scriptEngine, &ScriptEngine::scriptLoaded, this, &Application::handleScriptEngineLoaded); @@ -4290,6 +4268,7 @@ void Application::reloadScript(const QString& scriptName, bool isUserLoaded) { loadScript(scriptName, isUserLoaded, false, false, true); } +// FIXME - change to new version of ScriptCache loading notification void Application::handleScriptEngineLoaded(const QString& scriptFilename) { ScriptEngine* scriptEngine = qobject_cast(sender()); @@ -4299,8 +4278,10 @@ void Application::handleScriptEngineLoaded(const QString& scriptFilename) { // register our application services and set it off on its own thread registerScriptEngineWithApplicationServices(scriptEngine); + scriptEngine->runInThread(); } +// FIXME - change to new version of ScriptCache loading notification void Application::handleScriptLoadError(const QString& scriptFilename) { qCDebug(interfaceapp) << "Application::loadScript(), script failed to load..."; QMessageBox::warning(getWindow(), "Error Loading Script", scriptFilename + " failed to load."); @@ -4970,7 +4951,7 @@ mat4 Application::getHMDSensorPose() const { return mat4(); } -void Application::setPalmData(Hand* hand, UserInputMapper::PoseValue pose, float deltaTime, int index) { +void Application::setPalmData(Hand* hand, UserInputMapper::PoseValue pose, float deltaTime, int index, float triggerValue) { PalmData* palm; bool foundHand = false; for (size_t j = 0; j < hand->getNumPalms(); j++) { @@ -5040,6 +5021,7 @@ void Application::setPalmData(Hand* hand, UserInputMapper::PoseValue pose, float palm->setTipVelocity(glm::vec3(0.0f)); } palm->setTipPosition(newTipPosition); + palm->setTrigger(triggerValue); } void Application::emulateMouse(Hand* hand, float click, float shift, int index) { diff --git a/interface/src/Application.h b/interface/src/Application.h index b997fae823..6213dae4fa 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -485,7 +485,7 @@ private: void update(float deltaTime); - void setPalmData(Hand* hand, UserInputMapper::PoseValue pose, float deltaTime, int index); + void setPalmData(Hand* hand, UserInputMapper::PoseValue pose, float deltaTime, int index, float triggerValue); void emulateMouse(Hand* hand, float click, float shift, int index); // Various helper functions called during update() diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 9496c60d26..da2e78c5a3 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1296,10 +1296,13 @@ void MyAvatar::initAnimGraph() { // ik-avatar.json // https://gist.github.com/hyperlogic/e58e0a24cc341ad5d060 // + // ik-avatar-hands.json + // https://gist.githubusercontent.com/hyperlogic/04a02c47eb56d8bfaebb + // // or run a local web-server // python -m SimpleHTTPServer& - // auto graphUrl = QUrl("http://localhost:8000/avatar.json"); - auto graphUrl = QUrl("https://gist.githubusercontent.com/hyperlogic/e58e0a24cc341ad5d060/raw/8f824da2908fd89ad1befadd1d8f5d7b3b6efa66/ik-avatar.json"); + //auto graphUrl = QUrl("http://localhost:8000/avatar.json"); + auto graphUrl = QUrl("https://gist.githubusercontent.com/hyperlogic/04a02c47eb56d8bfaebb/raw/883c7ce8e75ad3f72a0d513c317fe4b74a41c3b8/ik-avatar-hands.json"); _rig->initAnimGraph(graphUrl, _skeletonModel.getGeometry()->getFBXGeometry()); } diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 41ca193a7a..3894a0ade9 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -96,6 +96,17 @@ void SkeletonModel::initJointStates(QVector states) { emit skeletonLoaded(); } +static const PalmData* getPalmWithIndex(Hand* hand, int index) { + const PalmData* palm = nullptr; + for (size_t j = 0; j < hand->getNumPalms(); j++) { + if (hand->getPalms()[j].getSixenseID() == index) { + palm = &(hand->getPalms()[j]); + break; + } + } + return palm; +} + const float PALM_PRIORITY = DEFAULT_PRIORITY; // Called within Model::simulate call, below. void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { @@ -108,34 +119,59 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { MyAvatar* myAvatar = static_cast(_owningAvatar); const FBXGeometry& geometry = _geometry->getFBXGeometry(); - Rig::HeadParameters params; - params.modelRotation = getRotation(); - params.modelTranslation = getTranslation(); - params.enableLean = qApp->getAvatarUpdater()->isHMDMode() && !myAvatar->getStandingHMDSensorMode(); - params.leanSideways = head->getFinalLeanSideways(); - params.leanForward = head->getFinalLeanForward(); - params.torsoTwist = head->getTorsoTwist(); - params.localHeadOrientation = head->getFinalOrientationInLocalFrame(); - params.localHeadPitch = head->getFinalPitch(); - params.localHeadYaw = head->getFinalYaw(); - params.localHeadRoll = head->getFinalRoll(); - params.isInHMD = qApp->getAvatarUpdater()->isHMDMode(); + Rig::HeadParameters headParams; + headParams.modelRotation = getRotation(); + headParams.modelTranslation = getTranslation(); + headParams.enableLean = qApp->getAvatarUpdater()->isHMDMode() && !myAvatar->getStandingHMDSensorMode(); + headParams.leanSideways = head->getFinalLeanSideways(); + headParams.leanForward = head->getFinalLeanForward(); + headParams.torsoTwist = head->getTorsoTwist(); + headParams.localHeadOrientation = head->getFinalOrientationInLocalFrame(); + headParams.localHeadPitch = head->getFinalPitch(); + headParams.localHeadYaw = head->getFinalYaw(); + headParams.localHeadRoll = head->getFinalRoll(); + headParams.isInHMD = qApp->getAvatarUpdater()->isHMDMode(); // get HMD position from sensor space into world space, and back into model space glm::mat4 worldToModel = glm::inverse(createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition())); glm::vec3 yAxis(0.0f, 1.0f, 0.0f); glm::vec3 hmdPosition = glm::angleAxis((float)M_PI, yAxis) * transformPoint(worldToModel * myAvatar->getSensorToWorldMatrix(), myAvatar->getHMDSensorPosition()); - params.localHeadPosition = hmdPosition; + headParams.localHeadPosition = hmdPosition; - params.worldHeadOrientation = head->getFinalOrientationInWorldFrame(); - params.eyeLookAt = head->getLookAtPosition(); - params.eyeSaccade = head->getSaccade(); - params.leanJointIndex = geometry.leanJointIndex; - params.neckJointIndex = geometry.neckJointIndex; - params.leftEyeJointIndex = geometry.leftEyeJointIndex; - params.rightEyeJointIndex = geometry.rightEyeJointIndex; + headParams.worldHeadOrientation = head->getFinalOrientationInWorldFrame(); + headParams.eyeLookAt = head->getLookAtPosition(); + headParams.eyeSaccade = head->getSaccade(); + headParams.leanJointIndex = geometry.leanJointIndex; + headParams.neckJointIndex = geometry.neckJointIndex; + headParams.leftEyeJointIndex = geometry.leftEyeJointIndex; + headParams.rightEyeJointIndex = geometry.rightEyeJointIndex; + + _rig->updateFromHeadParameters(headParams, deltaTime); + + Rig::HandParameters handParams; + + const PalmData* leftPalm = getPalmWithIndex(myAvatar->getHand(), LEFT_HAND_INDEX); + if (leftPalm && leftPalm->isActive()) { + handParams.isLeftEnabled = true; + handParams.leftPosition = leftPalm->getRawPosition(); + handParams.leftOrientation = leftPalm->getRawRotation(); + handParams.leftTrigger = leftPalm->getTrigger(); + } else { + handParams.isLeftEnabled = false; + } + + const PalmData* rightPalm = getPalmWithIndex(myAvatar->getHand(), RIGHT_HAND_INDEX); + if (rightPalm && rightPalm->isActive()) { + handParams.isRightEnabled = true; + handParams.rightPosition = rightPalm->getRawPosition(); + handParams.rightOrientation = rightPalm->getRawRotation(); + handParams.rightTrigger = rightPalm->getTrigger(); + } else { + handParams.isRightEnabled = false; + } + + _rig->updateFromHandParameters(handParams, deltaTime); - _rig->updateFromHeadParameters(params); } else { // This is a little more work than we really want. // diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index aa58845f01..9d6eb12e44 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -172,7 +172,12 @@ static AnimNode::Pointer loadNode(const QJsonObject& jsonObj, const QUrl& jsonUr qCCritical(animation) << "AnimNodeLoader, bad object in \"children\", id =" << id << ", url =" << jsonUrl.toDisplayString(); return nullptr; } - node->addChild(loadNode(childValue.toObject(), jsonUrl)); + AnimNode::Pointer child = loadNode(childValue.toObject(), jsonUrl); + if (child) { + node->addChild(child); + } else { + return nullptr; + } } if ((animNodeTypeToProcessFunc(type))(node, dataObj, id, jsonUrl)) { @@ -232,13 +237,15 @@ static const char* boneSetStrings[AnimOverlay::NumBoneSets] = { "fullBody", "upperBody", "lowerBody", - "rightArm", "leftArm", + "rightArm", "aboveTheHead", "belowTheHead", "headOnly", "spineOnly", - "empty" + "empty", + "leftHand", + "rightHand" }; static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) { diff --git a/libraries/animation/src/AnimOverlay.cpp b/libraries/animation/src/AnimOverlay.cpp index 52026f7711..1a0a16ca8a 100644 --- a/libraries/animation/src/AnimOverlay.cpp +++ b/libraries/animation/src/AnimOverlay.cpp @@ -26,12 +26,14 @@ void AnimOverlay::buildBoneSet(BoneSet boneSet) { case FullBodyBoneSet: buildFullBodyBoneSet(); break; case UpperBodyBoneSet: buildUpperBodyBoneSet(); break; case LowerBodyBoneSet: buildLowerBodyBoneSet(); break; - case RightArmBoneSet: buildRightArmBoneSet(); break; case LeftArmBoneSet: buildLeftArmBoneSet(); break; + case RightArmBoneSet: buildRightArmBoneSet(); break; case AboveTheHeadBoneSet: buildAboveTheHeadBoneSet(); break; case BelowTheHeadBoneSet: buildBelowTheHeadBoneSet(); break; case HeadOnlyBoneSet: buildHeadOnlyBoneSet(); break; case SpineOnlyBoneSet: buildSpineOnlyBoneSet(); break; + case LeftHandBoneSet: buildLeftHandBoneSet(); break; + case RightHandBoneSet: buildRightHandBoneSet(); break; default: case EmptyBoneSet: buildEmptyBoneSet(); break; } @@ -110,15 +112,6 @@ void AnimOverlay::buildLowerBodyBoneSet() { _boneSetVec[hipsJoint] = 0.0f; } -void AnimOverlay::buildRightArmBoneSet() { - assert(_skeleton); - buildEmptyBoneSet(); - int rightShoulderJoint = _skeleton->nameToJointIndex("RightShoulder"); - for_each_child_joint(_skeleton, rightShoulderJoint, [&](int i) { - _boneSetVec[i] = 1.0f; - }); -} - void AnimOverlay::buildLeftArmBoneSet() { assert(_skeleton); buildEmptyBoneSet(); @@ -128,6 +121,15 @@ void AnimOverlay::buildLeftArmBoneSet() { }); } +void AnimOverlay::buildRightArmBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int rightShoulderJoint = _skeleton->nameToJointIndex("RightShoulder"); + for_each_child_joint(_skeleton, rightShoulderJoint, [&](int i) { + _boneSetVec[i] = 1.0f; + }); +} + void AnimOverlay::buildAboveTheHeadBoneSet() { assert(_skeleton); buildEmptyBoneSet(); @@ -168,13 +170,31 @@ void AnimOverlay::buildEmptyBoneSet() { } } +void AnimOverlay::buildLeftHandBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int headJoint = _skeleton->nameToJointIndex("LeftHand"); + for_each_child_joint(_skeleton, headJoint, [&](int i) { + _boneSetVec[i] = 1.0f; + }); +} + +void AnimOverlay::buildRightHandBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int headJoint = _skeleton->nameToJointIndex("RightHand"); + for_each_child_joint(_skeleton, headJoint, [&](int i) { + _boneSetVec[i] = 1.0f; + }); +} + // for AnimDebugDraw rendering const AnimPoseVec& AnimOverlay::getPosesInternal() const { return _poses; } void AnimOverlay::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { - _skeleton = skeleton; + AnimNode::setSkeletonInternal(skeleton); // we have to re-build the bone set when the skeleton changes. buildBoneSet(_boneSet); diff --git a/libraries/animation/src/AnimOverlay.h b/libraries/animation/src/AnimOverlay.h index de563cc403..2a87c54997 100644 --- a/libraries/animation/src/AnimOverlay.h +++ b/libraries/animation/src/AnimOverlay.h @@ -28,14 +28,16 @@ public: FullBodyBoneSet = 0, UpperBodyBoneSet, LowerBodyBoneSet, - RightArmBoneSet, LeftArmBoneSet, + RightArmBoneSet, AboveTheHeadBoneSet, BelowTheHeadBoneSet, HeadOnlyBoneSet, SpineOnlyBoneSet, EmptyBoneSet, - NumBoneSets, + LeftHandBoneSet, + RightHandBoneSet, + NumBoneSets }; AnimOverlay(const std::string& id, BoneSet boneSet, float alpha); @@ -64,13 +66,15 @@ public: void buildFullBodyBoneSet(); void buildUpperBodyBoneSet(); void buildLowerBodyBoneSet(); - void buildRightArmBoneSet(); void buildLeftArmBoneSet(); + void buildRightArmBoneSet(); void buildAboveTheHeadBoneSet(); void buildBelowTheHeadBoneSet(); void buildHeadOnlyBoneSet(); void buildSpineOnlyBoneSet(); void buildEmptyBoneSet(); + void buildLeftHandBoneSet(); + void buildRightHandBoneSet(); // no copies AnimOverlay(const AnimOverlay&) = delete; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 3729dc8ca7..076baf92c5 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -952,7 +952,7 @@ glm::quat Rig::getJointDefaultRotationInParentFrame(int jointIndex) { return _jointStates[jointIndex].getDefaultRotationInParentFrame(); } -void Rig::updateFromHeadParameters(const HeadParameters& params) { +void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) { if (params.enableLean) { updateLeanJoint(params.leanJointIndex, params.leanSideways, params.leanForward, params.torsoTwist); } @@ -1048,6 +1048,52 @@ void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm } } +void Rig::updateFromHandParameters(const HandParameters& params, float dt) { + + if (_enableAnimGraph && _animSkeleton) { + + // set leftHand grab vars + _animVars.set("isLeftHandIdle", false); + _animVars.set("isLeftHandPoint", false); + _animVars.set("isLeftHandGrab", false); + + // Split the trigger range into three zones. + bool rampOut = false; + if (params.leftTrigger > 0.6666f) { + _animVars.set("isLeftHandGrab", true); + } else if (params.leftTrigger > 0.3333f) { + _animVars.set("isLeftHandPoint", true); + } else { + _animVars.set("isLeftHandIdle", true); + rampOut = true; + } + const float OVERLAY_RAMP_OUT_SPEED = 6.0f; // ramp in and out over 1/6th of a sec + _leftHandOverlayAlpha = glm::clamp(_leftHandOverlayAlpha + (rampOut ? -1.0f : 1.0f) * OVERLAY_RAMP_OUT_SPEED * dt, 0.0f, 1.0f); + _animVars.set("leftHandOverlayAlpha", _leftHandOverlayAlpha); + _animVars.set("leftHandGrabBlend", params.leftTrigger); + + + // set leftHand grab vars + _animVars.set("isRightHandIdle", false); + _animVars.set("isRightHandPoint", false); + _animVars.set("isRightHandGrab", false); + + // Split the trigger range into three zones + rampOut = false; + if (params.rightTrigger > 0.6666f) { + _animVars.set("isRightHandGrab", true); + } else if (params.rightTrigger > 0.3333f) { + _animVars.set("isRightHandPoint", true); + } else { + _animVars.set("isRightHandIdle", true); + rampOut = true; + } + _rightHandOverlayAlpha = glm::clamp(_rightHandOverlayAlpha + (rampOut ? -1.0f : 1.0f) * OVERLAY_RAMP_OUT_SPEED * dt, 0.0f, 1.0f); + _animVars.set("rightHandOverlayAlpha", _rightHandOverlayAlpha); + _animVars.set("rightHandGrabBlend", params.rightTrigger); + } +} + void Rig::initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry) { if (!_enableAnimGraph) { return; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 18e888d2b0..9939f383b7 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -76,6 +76,17 @@ public: void dump() const; }; + struct HandParameters { + bool isLeftEnabled; + bool isRightEnabled; + glm::vec3 leftPosition = glm::vec3(); + glm::quat leftOrientation = glm::quat(); + glm::vec3 rightPosition = glm::vec3(); + glm::quat rightOrientation = glm::quat(); + float leftTrigger = 0.0f; + float rightTrigger = 0.0f; + }; + virtual ~Rig() {} RigPointer getRigPointer() { return shared_from_this(); } @@ -168,10 +179,12 @@ public: void setEnableAnimGraph(bool isEnabled) { _enableAnimGraph = isEnabled; } bool getEnableAnimGraph() const { return _enableAnimGraph; } - void updateFromHeadParameters(const HeadParameters& params); + void updateFromHeadParameters(const HeadParameters& params, float dt); void updateEyeJoints(int leftEyeIndex, int rightEyeIndex, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::quat& worldHeadOrientation, const glm::vec3& lookAtSpot, const glm::vec3& saccade = glm::vec3(0.0f)); + void updateFromHandParameters(const HandParameters& params, float dt); + virtual void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, float scale, float priority) = 0; @@ -215,6 +228,8 @@ public: Move }; RigRole _state = RigRole::Idle; + float _leftHandOverlayAlpha = 0.0f; + float _rightHandOverlayAlpha = 0.0f; }; #endif /* defined(__hifi__Rig__) */ diff --git a/libraries/animation/src/SwingTwistConstraint.cpp b/libraries/animation/src/SwingTwistConstraint.cpp index b788be5ce7..f9cea2abad 100644 --- a/libraries/animation/src/SwingTwistConstraint.cpp +++ b/libraries/animation/src/SwingTwistConstraint.cpp @@ -72,7 +72,7 @@ SwingTwistConstraint::SwingTwistConstraint() : _swingLimitFunction(), _minTwist(-PI), _maxTwist(PI), - _lastTwistBoundary(0) + _lastTwistBoundary(LAST_CLAMP_NO_BOUNDARY) { } @@ -249,3 +249,6 @@ bool SwingTwistConstraint::apply(glm::quat& rotation) const { return false; } +void SwingTwistConstraint::clearHistory() { + _lastTwistBoundary = LAST_CLAMP_NO_BOUNDARY; +} diff --git a/libraries/animation/src/SwingTwistConstraint.h b/libraries/animation/src/SwingTwistConstraint.h index 180127e1e1..155f72a518 100644 --- a/libraries/animation/src/SwingTwistConstraint.h +++ b/libraries/animation/src/SwingTwistConstraint.h @@ -73,6 +73,9 @@ public: /// \return reference to SwingLimitFunction instance for unit-testing const SwingLimitFunction& getSwingLimitFunction() const { return _swingLimitFunction; } + /// \brief exposed for unit testing + void clearHistory(); + protected: SwingLimitFunction _swingLimitFunction; float _minTwist; diff --git a/libraries/audio/src/AudioFilterBank.h b/libraries/audio/src/AudioFilterBank.h index 4ea5a3568a..64ea5396e1 100644 --- a/libraries/audio/src/AudioFilterBank.h +++ b/libraries/audio/src/AudioFilterBank.h @@ -125,12 +125,12 @@ public: return; } - const int scale = (2 << ((8 * sizeof(int16_t)) - 1)); + const int scale = (1 << ((8 * sizeof(int16_t)) - 1)); // de-interleave and convert int16_t to float32 (normalized to -1. ... 1.) for (uint32_t i = 0; i < frameCount; ++i) { for (uint32_t j = 0; j < _channelCount; ++j) { - _buffer[j][i] = ((float)(*in++)) / scale; + _buffer[j][i] = ((float)(*in++)) * (1.0f / scale); } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 824f562f6b..2fd760bbd3 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -49,7 +49,6 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf OctreeRenderer(), _wantScripts(wantScripts), _entitiesScriptEngine(NULL), - _sandboxScriptEngine(NULL), _lastMouseEventValid(false), _viewState(viewState), _scriptingServices(scriptingServices), @@ -77,23 +76,11 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf EntityTreeRenderer::~EntityTreeRenderer() { // NOTE: we don't need to delete _entitiesScriptEngine because it is registered with the application and has a // signal tied to call it's deleteLater on doneRunning - if (_sandboxScriptEngine) { - // TODO: consider reworking how _sandboxScriptEngine is managed. It's treated differently than _entitiesScriptEngine - // because we don't call registerScriptEngineWithApplicationServices() for it. This implementation is confusing and - // potentially error prone because it's not a full fledged ScriptEngine that has been fully connected to the - // application. We did this so that scripts that were ill-formed could be evaluated but not execute against the - // application services. But this means it's shutdown behavior is different from other ScriptEngines - delete _sandboxScriptEngine; - _sandboxScriptEngine = NULL; - } } void EntityTreeRenderer::clear() { leaveAllEntities(); - foreach (const EntityItemID& entityID, _entityScripts.keys()) { - checkAndCallUnload(entityID); - } - _entityScripts.clear(); + _entitiesScriptEngine->unloadAllEntityScripts(); auto scene = _viewState->getMain3DScene(); render::PendingChanges pendingChanges; @@ -113,10 +100,9 @@ void EntityTreeRenderer::init() { if (_wantScripts) { _entitiesScriptEngine = new ScriptEngine(NO_SCRIPT, "Entities", - _scriptingServices->getControllerScriptingInterface(), false); + _scriptingServices->getControllerScriptingInterface()); _scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine); - - _sandboxScriptEngine = new ScriptEngine(NO_SCRIPT, "Entities Sandbox", NULL, false); + _entitiesScriptEngine->runInThread(); } // make sure our "last avatar position" is something other than our current position, so that on our @@ -134,176 +120,6 @@ void EntityTreeRenderer::shutdown() { _shuttingDown = true; } -void EntityTreeRenderer::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) { - if (_waitingOnPreload.contains(url)) { - QList entityIDs = _waitingOnPreload.values(url); - _waitingOnPreload.remove(url); - foreach(EntityItemID entityID, entityIDs) { - checkAndCallPreload(entityID); - } - } -} - -void EntityTreeRenderer::errorInLoadingScript(const QUrl& url) { - if (_waitingOnPreload.contains(url)) { - _waitingOnPreload.remove(url); - } -} - -QScriptValue EntityTreeRenderer::loadEntityScript(const EntityItemID& entityItemID, bool isPreload, bool reload) { - EntityItemPointer entity = std::static_pointer_cast(_tree)->findEntityByEntityItemID(entityItemID); - return loadEntityScript(entity, isPreload, reload); -} - - -QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorText, bool& isURL, bool& isPending, QUrl& urlOut, - bool& reload) { - isPending = false; - QUrl url(scriptMaybeURLorText); - - // If the url is not valid, this must be script text... - // We document "direct injection" scripts as starting with "(function...", and that would never be a valid url. - // But QUrl thinks it is. - if (!url.isValid() || scriptMaybeURLorText.startsWith("(")) { - isURL = false; - return scriptMaybeURLorText; - } - isURL = true; - urlOut = url; - - QString scriptContents; // assume empty - - // if the scheme length is one or lower, maybe they typed in a file, let's try - const int WINDOWS_DRIVE_LETTER_SIZE = 1; - if (url.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) { - url = QUrl::fromLocalFile(scriptMaybeURLorText); - } - - // ok, let's see if it's valid... and if so, load it - if (url.isValid()) { - if (url.scheme() == "file") { - QString fileName = url.toLocalFile(); - QFile scriptFile(fileName); - if (scriptFile.open(QFile::ReadOnly | QFile::Text)) { - qCDebug(entitiesrenderer) << "Loading file:" << fileName; - QTextStream in(&scriptFile); - scriptContents = in.readAll(); - } else { - qCDebug(entitiesrenderer) << "ERROR Loading file:" << fileName; - } - } else { - auto scriptCache = DependencyManager::get(); - - if (!scriptCache->isInBadScriptList(url)) { - scriptContents = scriptCache->getScript(url, this, isPending, reload); - } - } - } - - return scriptContents; -} - - -QScriptValue EntityTreeRenderer::loadEntityScript(EntityItemPointer entity, bool isPreload, bool reload) { - if (_shuttingDown) { - return QScriptValue(); // since we're shutting down, we don't load any more scripts - } - - if (!entity) { - return QScriptValue(); // no entity... - } - - // NOTE: we keep local variables for the entityID and the script because - // below in loadScriptContents() it's possible for us to execute the - // application event loop, which may cause our entity to be deleted on - // us. We don't really need access the entity after this point, can - // can accomplish all we need to here with just the script "text" and the ID. - EntityItemID entityID = entity->getEntityItemID(); - QString entityScript = entity->getScript(); - - if (_entityScripts.contains(entityID)) { - EntityScriptDetails details = _entityScripts[entityID]; - - // check to make sure our script text hasn't changed on us since we last loaded it and we're not redownloading it - if (details.scriptText == entityScript && !reload) { - return details.scriptObject; // previously loaded - } - - // if we got here, then we previously loaded a script, but the entity's script value - // has changed and so we need to reload it. - _entityScripts.remove(entityID); - } - if (entityScript.isEmpty()) { - return QScriptValue(); // no script - } - - bool isURL = false; // loadScriptContents() will tell us if this is a URL or just text. - bool isPending = false; - QUrl url; - QString scriptContents = loadScriptContents(entityScript, isURL, isPending, url, reload); - - if (isPending && isPreload && isURL) { - _waitingOnPreload.insert(url, entityID); - } - - auto scriptCache = DependencyManager::get(); - - if (isURL && scriptCache->isInBadScriptList(url)) { - return QScriptValue(); // no script contents... - } - - if (scriptContents.isEmpty()) { - return QScriptValue(); // no script contents... - } - - QScriptSyntaxCheckResult syntaxCheck = QScriptEngine::checkSyntax(scriptContents); - if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { - qCDebug(entitiesrenderer) << "EntityTreeRenderer::loadEntityScript() entity:" << entityID; - qCDebug(entitiesrenderer) << " " << syntaxCheck.errorMessage() << ":" - << syntaxCheck.errorLineNumber() << syntaxCheck.errorColumnNumber(); - qCDebug(entitiesrenderer) << " SCRIPT:" << entityScript; - - scriptCache->addScriptToBadScriptList(url); - - return QScriptValue(); // invalid script - } - - if (isURL) { - _entitiesScriptEngine->setParentURL(entity->getScript()); - } - QScriptValue entityScriptConstructor = _sandboxScriptEngine->evaluate(scriptContents); - - if (!entityScriptConstructor.isFunction()) { - qCDebug(entitiesrenderer) << "EntityTreeRenderer::loadEntityScript() entity:" << entityID; - qCDebug(entitiesrenderer) << " NOT CONSTRUCTOR"; - qCDebug(entitiesrenderer) << " SCRIPT:" << entityScript; - - scriptCache->addScriptToBadScriptList(url); - - return QScriptValue(); // invalid script - } else { - entityScriptConstructor = _entitiesScriptEngine->evaluate(scriptContents); - } - - QScriptValue entityScriptObject = entityScriptConstructor.construct(); - EntityScriptDetails newDetails = { entityScript, entityScriptObject }; - _entityScripts[entityID] = newDetails; - - if (isURL) { - _entitiesScriptEngine->setParentURL(""); - } - - return entityScriptObject; // newly constructed -} - -QScriptValue EntityTreeRenderer::getPreviouslyLoadedEntityScript(const EntityItemID& entityID) { - if (_entityScripts.contains(entityID)) { - EntityScriptDetails details = _entityScripts[entityID]; - return details.scriptObject; // previously loaded - } - return QScriptValue(); // no script -} - void EntityTreeRenderer::setTree(OctreePointer newTree) { OctreeRenderer::setTree(newTree); std::static_pointer_cast(_tree)->setFBXService(this); @@ -322,11 +138,7 @@ void EntityTreeRenderer::update() { // and we want to simulate this message here as well as in mouse move if (_lastMouseEventValid && !_currentClickingOnEntityID.isInvalidID()) { emit holdingClickOnEntity(_currentClickingOnEntityID, _lastMouseEvent); - QScriptValueList currentClickingEntityArgs = createMouseEventArgs(_currentClickingOnEntityID, _lastMouseEvent); - QScriptValue currentClickingEntity = loadEntityScript(_currentClickingOnEntityID); - if (currentClickingEntity.property("holdingClickOnEntity").isValid()) { - currentClickingEntity.property("holdingClickOnEntity").call(currentClickingEntity, currentClickingEntityArgs); - } + _entitiesScriptEngine->callEntityScriptMethod(_currentClickingOnEntityID, "holdingClickOnEntity", _lastMouseEvent); } } @@ -355,19 +167,14 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { }); // Note: at this point we don't need to worry about the tree being locked, because we only deal with - // EntityItemIDs from here. The loadEntityScript() method is robust against attempting to load scripts + // EntityItemIDs from here. The callEntityScriptMethod() method is robust against attempting to call scripts // for entity IDs that no longer exist. // for all of our previous containing entities, if they are no longer containing then send them a leave event foreach(const EntityItemID& entityID, _currentEntitiesInside) { if (!entitiesContainingAvatar.contains(entityID)) { emit leaveEntity(entityID); - QScriptValueList entityArgs = createEntityArgs(entityID); - QScriptValue entityScript = loadEntityScript(entityID); - if (entityScript.property("leaveEntity").isValid()) { - entityScript.property("leaveEntity").call(entityScript, entityArgs); - } - + _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); } } @@ -375,11 +182,7 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { foreach(const EntityItemID& entityID, entitiesContainingAvatar) { if (!_currentEntitiesInside.contains(entityID)) { emit enterEntity(entityID); - QScriptValueList entityArgs = createEntityArgs(entityID); - QScriptValue entityScript = loadEntityScript(entityID); - if (entityScript.property("enterEntity").isValid()) { - entityScript.property("enterEntity").call(entityScript, entityArgs); - } + _entitiesScriptEngine->callEntityScriptMethod(entityID, "enterEntity"); } } _currentEntitiesInside = entitiesContainingAvatar; @@ -394,11 +197,7 @@ void EntityTreeRenderer::leaveAllEntities() { // for all of our previous containing entities, if they are no longer containing then send them a leave event foreach(const EntityItemID& entityID, _currentEntitiesInside) { emit leaveEntity(entityID); - QScriptValueList entityArgs = createEntityArgs(entityID); - QScriptValue entityScript = loadEntityScript(entityID); - if (entityScript.property("leaveEntity").isValid()) { - entityScript.property("leaveEntity").call(entityScript, entityArgs); - } + _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); } _currentEntitiesInside.clear(); @@ -811,27 +610,6 @@ void EntityTreeRenderer::connectSignalsToSlots(EntityScriptingInterface* entityS connect(DependencyManager::get().data(), &SceneScriptingInterface::shouldRenderEntitiesChanged, this, &EntityTreeRenderer::updateEntityRenderStatus, Qt::QueuedConnection); } -QScriptValueList EntityTreeRenderer::createMouseEventArgs(const EntityItemID& entityID, QMouseEvent* event, unsigned int deviceID) { - QScriptValueList args; - args << entityID.toScriptValue(_entitiesScriptEngine); - args << MouseEvent(*event, deviceID).toScriptValue(_entitiesScriptEngine); - return args; -} - -QScriptValueList EntityTreeRenderer::createMouseEventArgs(const EntityItemID& entityID, const MouseEvent& mouseEvent) { - QScriptValueList args; - args << entityID.toScriptValue(_entitiesScriptEngine); - args << mouseEvent.toScriptValue(_entitiesScriptEngine); - return args; -} - - -QScriptValueList EntityTreeRenderer::createEntityArgs(const EntityItemID& entityID) { - QScriptValueList args; - args << entityID.toScriptValue(_entitiesScriptEngine); - return args; -} - void EntityTreeRenderer::mousePressEvent(QMouseEvent* event, unsigned int deviceID) { // If we don't have a tree, or we're in the process of shutting down, then don't // process these events. @@ -854,18 +632,11 @@ void EntityTreeRenderer::mousePressEvent(QMouseEvent* event, unsigned int device } emit mousePressOnEntity(rayPickResult, event, deviceID); - - QScriptValueList entityScriptArgs = createMouseEventArgs(rayPickResult.entityID, event, deviceID); - QScriptValue entityScript = loadEntityScript(rayPickResult.entity); - if (entityScript.property("mousePressOnEntity").isValid()) { - entityScript.property("mousePressOnEntity").call(entityScript, entityScriptArgs); - } + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mousePressOnEntity", MouseEvent(*event, deviceID)); _currentClickingOnEntityID = rayPickResult.entityID; emit clickDownOnEntity(_currentClickingOnEntityID, MouseEvent(*event, deviceID)); - if (entityScript.property("clickDownOnEntity").isValid()) { - entityScript.property("clickDownOnEntity").call(entityScript, entityScriptArgs); - } + _entitiesScriptEngine->callEntityScriptMethod(_currentClickingOnEntityID, "clickDownOnEntity", MouseEvent(*event, deviceID)); } else { emit mousePressOffEntity(rayPickResult, event, deviceID); } @@ -886,24 +657,14 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event, unsigned int devi if (rayPickResult.intersects) { //qCDebug(entitiesrenderer) << "mouseReleaseEvent over entity:" << rayPickResult.entityID; emit mouseReleaseOnEntity(rayPickResult, event, deviceID); - - QScriptValueList entityScriptArgs = createMouseEventArgs(rayPickResult.entityID, event, deviceID); - QScriptValue entityScript = loadEntityScript(rayPickResult.entity); - if (entityScript.property("mouseReleaseOnEntity").isValid()) { - entityScript.property("mouseReleaseOnEntity").call(entityScript, entityScriptArgs); - } + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseReleaseOnEntity", MouseEvent(*event, deviceID)); } // Even if we're no longer intersecting with an entity, if we started clicking on it, and now // we're releasing the button, then this is considered a clickOn event if (!_currentClickingOnEntityID.isInvalidID()) { emit clickReleaseOnEntity(_currentClickingOnEntityID, MouseEvent(*event, deviceID)); - - QScriptValueList currentClickingEntityArgs = createMouseEventArgs(_currentClickingOnEntityID, event, deviceID); - QScriptValue currentClickingEntity = loadEntityScript(_currentClickingOnEntityID); - if (currentClickingEntity.property("clickReleaseOnEntity").isValid()) { - currentClickingEntity.property("clickReleaseOnEntity").call(currentClickingEntity, currentClickingEntityArgs); - } + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "clickReleaseOnEntity", MouseEvent(*event, deviceID)); } // makes it the unknown ID, we just released so we can't be clicking on anything @@ -925,17 +686,9 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI bool precisionPicking = false; // for mouse moves we do not do precision picking RayToEntityIntersectionResult rayPickResult = findRayIntersectionWorker(ray, Octree::TryLock, precisionPicking); if (rayPickResult.intersects) { - //qCDebug(entitiesrenderer) << "mouseReleaseEvent over entity:" << rayPickResult.entityID; - QScriptValueList entityScriptArgs = createMouseEventArgs(rayPickResult.entityID, event, deviceID); - // load the entity script if needed... - QScriptValue entityScript = loadEntityScript(rayPickResult.entity); - if (entityScript.property("mouseMoveEvent").isValid()) { - entityScript.property("mouseMoveEvent").call(entityScript, entityScriptArgs); - } - emit mouseMoveOnEntity(rayPickResult, event, deviceID); - if (entityScript.property("mouseMoveOnEntity").isValid()) { - entityScript.property("mouseMoveOnEntity").call(entityScript, entityScriptArgs); - } + + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseMoveEvent", MouseEvent(*event, deviceID)); + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseMoveOnEntity", MouseEvent(*event, deviceID)); // handle the hover logic... @@ -943,30 +696,19 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI // then we need to send the hover leave. if (!_currentHoverOverEntityID.isInvalidID() && rayPickResult.entityID != _currentHoverOverEntityID) { emit hoverLeaveEntity(_currentHoverOverEntityID, MouseEvent(*event, deviceID)); - - QScriptValueList currentHoverEntityArgs = createMouseEventArgs(_currentHoverOverEntityID, event, deviceID); - - QScriptValue currentHoverEntity = loadEntityScript(_currentHoverOverEntityID); - if (currentHoverEntity.property("hoverLeaveEntity").isValid()) { - currentHoverEntity.property("hoverLeaveEntity").call(currentHoverEntity, currentHoverEntityArgs); - } + _entitiesScriptEngine->callEntityScriptMethod(_currentHoverOverEntityID, "hoverLeaveEntity", MouseEvent(*event, deviceID)); } // If the new hover entity does not match the previous hover entity then we are entering the new one // this is true if the _currentHoverOverEntityID is known or unknown if (rayPickResult.entityID != _currentHoverOverEntityID) { - emit hoverEnterEntity(rayPickResult.entityID, MouseEvent(*event, deviceID)); - if (entityScript.property("hoverEnterEntity").isValid()) { - entityScript.property("hoverEnterEntity").call(entityScript, entityScriptArgs); - } + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "hoverEnterEntity", MouseEvent(*event, deviceID)); } // and finally, no matter what, if we're intersecting an entity then we're definitely hovering over it, and // we should send our hover over event emit hoverOverEntity(rayPickResult.entityID, MouseEvent(*event, deviceID)); - if (entityScript.property("hoverOverEntity").isValid()) { - entityScript.property("hoverOverEntity").call(entityScript, entityScriptArgs); - } + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "hoverOverEntity", MouseEvent(*event, deviceID)); // remember what we're hovering over _currentHoverOverEntityID = rayPickResult.entityID; @@ -977,14 +719,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI // send the hover leave for our previous entity if (!_currentHoverOverEntityID.isInvalidID()) { emit hoverLeaveEntity(_currentHoverOverEntityID, MouseEvent(*event, deviceID)); - - QScriptValueList currentHoverEntityArgs = createMouseEventArgs(_currentHoverOverEntityID, event, deviceID); - - QScriptValue currentHoverEntity = loadEntityScript(_currentHoverOverEntityID); - if (currentHoverEntity.property("hoverLeaveEntity").isValid()) { - currentHoverEntity.property("hoverLeaveEntity").call(currentHoverEntity, currentHoverEntityArgs); - } - + _entitiesScriptEngine->callEntityScriptMethod(_currentHoverOverEntityID, "hoverLeaveEntity", MouseEvent(*event, deviceID)); _currentHoverOverEntityID = UNKNOWN_ENTITY_ID; // makes it the unknown ID } } @@ -993,13 +728,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI // not yet released the hold then this is still considered a holdingClickOnEntity event if (!_currentClickingOnEntityID.isInvalidID()) { emit holdingClickOnEntity(_currentClickingOnEntityID, MouseEvent(*event, deviceID)); - - QScriptValueList currentClickingEntityArgs = createMouseEventArgs(_currentClickingOnEntityID, event, deviceID); - - QScriptValue currentClickingEntity = loadEntityScript(_currentClickingOnEntityID); - if (currentClickingEntity.property("holdingClickOnEntity").isValid()) { - currentClickingEntity.property("holdingClickOnEntity").call(currentClickingEntity, currentClickingEntityArgs); - } + _entitiesScriptEngine->callEntityScriptMethod(_currentClickingOnEntityID, "holdingClickOnEntity", MouseEvent(*event, deviceID)); } _lastMouseEvent = MouseEvent(*event, deviceID); _lastMouseEventValid = true; @@ -1007,9 +736,8 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { if (_tree && !_shuttingDown) { - checkAndCallUnload(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID); } - _entityScripts.remove(entityID); // here's where we remove the entity payload from the scene if (_entitiesInScene.contains(entityID)) { @@ -1042,28 +770,16 @@ void EntityTreeRenderer::addEntityToScene(EntityItemPointer entity) { void EntityTreeRenderer::entitySciptChanging(const EntityItemID& entityID, const bool reload) { if (_tree && !_shuttingDown) { - checkAndCallUnload(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID); checkAndCallPreload(entityID, reload); } } void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload) { if (_tree && !_shuttingDown) { - // load the entity script if needed... - QScriptValue entityScript = loadEntityScript(entityID, true, reload); // is preload! - if (entityScript.property("preload").isValid()) { - QScriptValueList entityArgs = createEntityArgs(entityID); - entityScript.property("preload").call(entityScript, entityArgs); - } - } -} - -void EntityTreeRenderer::checkAndCallUnload(const EntityItemID& entityID) { - if (_tree && !_shuttingDown) { - QScriptValue entityScript = getPreviouslyLoadedEntityScript(entityID); - if (entityScript.property("unload").isValid()) { - QScriptValueList entityArgs = createEntityArgs(entityID); - entityScript.property("unload").call(entityScript, entityArgs); + EntityItemPointer entity = getTree()->findEntityByEntityItemID(entityID); + if (entity && !entity->getScript().isEmpty()) { + _entitiesScriptEngine->loadEntityScript(entityID, entity->getScript(), reload); } } } @@ -1143,24 +859,9 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons // And now the entity scripts emit collisionWithEntity(idA, idB, collision); - QScriptValue entityScriptA = loadEntityScript(idA); - if (entityScriptA.property("collisionWithEntity").isValid()) { - QScriptValueList args; - args << idA.toScriptValue(_entitiesScriptEngine); - args << idB.toScriptValue(_entitiesScriptEngine); - args << collisionToScriptValue(_entitiesScriptEngine, collision); - entityScriptA.property("collisionWithEntity").call(entityScriptA, args); - } - + _entitiesScriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision); emit collisionWithEntity(idB, idA, collision); - QScriptValue entityScriptB = loadEntityScript(idB); - if (entityScriptB.property("collisionWithEntity").isValid()) { - QScriptValueList args; - args << idB.toScriptValue(_entitiesScriptEngine); - args << idA.toScriptValue(_entitiesScriptEngine); - args << collisionToScriptValue(_entitiesScriptEngine, collision); - entityScriptB.property("collisionWithEntity").call(entityScriptA, args); - } + _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, collision); } void EntityTreeRenderer::updateEntityRenderStatus(bool shouldRenderEntities) { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index f568618651..a2f343efd2 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -28,14 +28,9 @@ class Model; class ScriptEngine; class ZoneEntityItem; -class EntityScriptDetails { -public: - QString scriptText; - QScriptValue scriptObject; -}; // Generic client side Octree renderer class. -class EntityTreeRenderer : public OctreeRenderer, public EntityItemFBXService, public ScriptUser { +class EntityTreeRenderer : public OctreeRenderer, public EntityItemFBXService { Q_OBJECT public: EntityTreeRenderer(bool wantScripts, AbstractViewStateInterface* viewState, @@ -87,9 +82,6 @@ public: /// hovering over, and entering entities void connectSignalsToSlots(EntityScriptingInterface* entityScriptingInterface); - virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents); - virtual void errorInLoadingScript(const QUrl& url); - // For Scene.shouldRenderEntities QList& getEntitiesLastInScene() { return _entityIDsLastInScene; } @@ -137,7 +129,6 @@ private: void applyZonePropertiesToScene(std::shared_ptr zone); void renderElementProxy(EntityTreeElementPointer entityTreeElement, RenderArgs* args); void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false); - void checkAndCallUnload(const EntityItemID& entityID); QList _releasedModels; void renderProxies(EntityItemPointer entity, RenderArgs* args); @@ -155,16 +146,6 @@ private: bool _wantScripts; ScriptEngine* _entitiesScriptEngine; - ScriptEngine* _sandboxScriptEngine; - - QScriptValue loadEntityScript(EntityItemPointer entity, bool isPreload = false, bool reload = false); - QScriptValue loadEntityScript(const EntityItemID& entityItemID, bool isPreload = false, bool reload = false); - QScriptValue getPreviouslyLoadedEntityScript(const EntityItemID& entityItemID); - QString loadScriptContents(const QString& scriptMaybeURLorText, bool& isURL, bool& isPending, QUrl& url, bool& reload); - QScriptValueList createMouseEventArgs(const EntityItemID& entityID, QMouseEvent* event, unsigned int deviceID); - QScriptValueList createMouseEventArgs(const EntityItemID& entityID, const MouseEvent& mouseEvent); - - QHash _entityScripts; void playEntityCollisionSound(const QUuid& myNodeID, EntityTreePointer entityTree, const EntityItemID& id, const Collision& collision); diff --git a/libraries/networking/src/ResourceManager.cpp b/libraries/networking/src/ResourceManager.cpp index 8af33c5463..774664f2c8 100644 --- a/libraries/networking/src/ResourceManager.cpp +++ b/libraries/networking/src/ResourceManager.cpp @@ -17,21 +17,31 @@ #include -ResourceRequest* ResourceManager::createResourceRequest(QObject* parent, const QUrl& url) { +QUrl ResourceManager::normalizeURL(const QUrl& url) { auto scheme = url.scheme(); + if (!(scheme == URL_SCHEME_FILE || + scheme == URL_SCHEME_HTTP || scheme == URL_SCHEME_HTTPS || scheme == URL_SCHEME_FTP || + scheme == URL_SCHEME_ATP)) { + + // check the degenerative file case: on windows we can often have urls of the form c:/filename + // this checks for and works around that case. + QUrl urlWithFileScheme{ URL_SCHEME_FILE + ":///" + url.toString() }; + if (!urlWithFileScheme.toLocalFile().isEmpty()) { + return urlWithFileScheme; + } + } + return url; +} + +ResourceRequest* ResourceManager::createResourceRequest(QObject* parent, const QUrl& url) { + auto normalizedURL = normalizeURL(url); + auto scheme = normalizedURL.scheme(); if (scheme == URL_SCHEME_FILE) { return new FileResourceRequest(parent, url); } else if (scheme == URL_SCHEME_HTTP || scheme == URL_SCHEME_HTTPS || scheme == URL_SCHEME_FTP) { return new HTTPResourceRequest(parent, url); } else if (scheme == URL_SCHEME_ATP) { return new AssetResourceRequest(parent, url); - } else { - // check the degenerative file case: on windows we can often have urls of the form c:/filename - // this checks for and works around that case. - QUrl urlWithFileScheme { URL_SCHEME_FILE + ":///" + url.toString() }; - if (!urlWithFileScheme.toLocalFile().isEmpty()) { - return new FileResourceRequest(parent, urlWithFileScheme); - } } qDebug() << "Unknown scheme (" << scheme << ") for URL: " << url.url(); diff --git a/libraries/networking/src/ResourceManager.h b/libraries/networking/src/ResourceManager.h index 3748036c8e..40b67b1cd1 100644 --- a/libraries/networking/src/ResourceManager.h +++ b/libraries/networking/src/ResourceManager.h @@ -24,6 +24,7 @@ const QString URL_SCHEME_ATP = "atp"; class ResourceManager { public: + static QUrl normalizeURL(const QUrl& url); static ResourceRequest* createResourceRequest(QObject* parent, const QUrl& url); }; diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp index a5df256e29..aee2805f32 100644 --- a/libraries/networking/src/ThreadedAssignment.cpp +++ b/libraries/networking/src/ThreadedAssignment.cpp @@ -65,7 +65,7 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy auto nodeList = DependencyManager::get(); nodeList->setOwnerType(nodeType); - _domainServerTimer = new QTimer(); + _domainServerTimer = new QTimer(this); connect(_domainServerTimer, SIGNAL(timeout()), this, SLOT(checkInWithDomainServerOrExit())); _domainServerTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); diff --git a/libraries/networking/src/udt/CongestionControl.cpp b/libraries/networking/src/udt/CongestionControl.cpp index 2240f2c193..ea46d60acb 100644 --- a/libraries/networking/src/udt/CongestionControl.cpp +++ b/libraries/networking/src/udt/CongestionControl.cpp @@ -34,7 +34,6 @@ void CongestionControl::setPacketSendPeriod(double newSendPeriod) { } DefaultCC::DefaultCC() : - _lastRCTime(p_high_resolution_clock::now()), _slowStartLastAck(_sendCurrSeqNum), _lastDecreaseMaxSeq(SequenceNumber {SequenceNumber::MAX }) { diff --git a/libraries/networking/src/udt/CongestionControl.h b/libraries/networking/src/udt/CongestionControl.h index adbf1f0e85..fa1bf73ecf 100644 --- a/libraries/networking/src/udt/CongestionControl.h +++ b/libraries/networking/src/udt/CongestionControl.h @@ -108,7 +108,8 @@ public: private: void stopSlowStart(); // stops the slow start on loss or timeout - p_high_resolution_clock::time_point _lastRCTime; // last rate increase time + p_high_resolution_clock::time_point _lastRCTime = p_high_resolution_clock::now(); // last rate increase time + bool _slowStart { true }; // if in slow start phase SequenceNumber _slowStartLastAck; // last ACKed seq num bool _loss { false }; // if loss happened since last rate increase diff --git a/libraries/networking/src/udt/Connection.cpp b/libraries/networking/src/udt/Connection.cpp index 21dce2831c..2fb28f81ee 100644 --- a/libraries/networking/src/udt/Connection.cpp +++ b/libraries/networking/src/udt/Connection.cpp @@ -28,7 +28,6 @@ using namespace udt; using namespace std::chrono; Connection::Connection(Socket* parentSocket, HifiSockAddr destination, std::unique_ptr congestionControl) : - _connectionStart(p_high_resolution_clock::now()), _parentSocket(parentSocket), _destination(destination), _congestionControl(move(congestionControl)) @@ -278,11 +277,11 @@ void Connection::sendACK(bool wasCausedBySyncTimeout) { // pack in the receive speed and estimatedBandwidth ackPacket->writePrimitive(packetReceiveSpeed); ackPacket->writePrimitive(estimatedBandwidth); - - // record this as the last ACK send time - lastACKSendTime = p_high_resolution_clock::now(); } + // record this as the last ACK send time + lastACKSendTime = p_high_resolution_clock::now(); + // have the socket send off our packet _parentSocket->writeBasePacket(*ackPacket, _destination); @@ -534,7 +533,8 @@ void Connection::processACK(std::unique_ptr controlPacket) { // This will be the case if it has been longer than the sync interval OR // it looks like they haven't received our ACK2 for this ACK auto currentTime = p_high_resolution_clock::now(); - static p_high_resolution_clock::time_point lastACK2SendTime; + static p_high_resolution_clock::time_point lastACK2SendTime = + p_high_resolution_clock::now() - std::chrono::microseconds(_synInterval); microseconds sinceLastACK2 = duration_cast(currentTime - lastACK2SendTime); @@ -779,7 +779,7 @@ void Connection::resetReceiveState() { // clear the loss list and _lastNAKTime _lossList.clear(); - _lastNAKTime = p_high_resolution_clock::time_point(); + _lastNAKTime = p_high_resolution_clock::now(); // the _nakInterval need not be reset, that will happen on loss diff --git a/libraries/networking/src/udt/Connection.h b/libraries/networking/src/udt/Connection.h index 31ef664ce5..2b1dec1ae9 100644 --- a/libraries/networking/src/udt/Connection.h +++ b/libraries/networking/src/udt/Connection.h @@ -114,13 +114,14 @@ private: int _nakInterval { -1 }; // NAK timeout interval, in microseconds, set on loss int _minNAKInterval { 100000 }; // NAK timeout interval lower bound, default of 100ms - p_high_resolution_clock::time_point _lastNAKTime; + p_high_resolution_clock::time_point _lastNAKTime = p_high_resolution_clock::now(); bool _hasReceivedHandshake { false }; // flag for receipt of handshake from server bool _hasReceivedHandshakeACK { false }; // flag for receipt of handshake ACK from client - p_high_resolution_clock::time_point _connectionStart; // holds the time_point for creation of this connection + p_high_resolution_clock::time_point _connectionStart = p_high_resolution_clock::now(); // holds the time_point for creation of this connection p_high_resolution_clock::time_point _lastReceiveTime; // holds the last time we received anything from sender + bool _isReceivingData { false }; // flag used for expiry of receipt portion of connection LossList _lossList; // List of all missing packets diff --git a/libraries/networking/src/udt/PacketTimeWindow.cpp b/libraries/networking/src/udt/PacketTimeWindow.cpp index 51bba2c52c..0c95d21bc6 100644 --- a/libraries/networking/src/udt/PacketTimeWindow.cpp +++ b/libraries/networking/src/udt/PacketTimeWindow.cpp @@ -93,15 +93,16 @@ int32_t PacketTimeWindow::getEstimatedBandwidth() const { } void PacketTimeWindow::onPacketArrival() { + // take the current time auto now = p_high_resolution_clock::now(); - // record the interval between this packet and the last one - _packetIntervals[_currentPacketInterval++] = duration_cast(now - _lastPacketTime).count(); - - // reset the currentPacketInterval index when it wraps - if (_currentPacketInterval == _numPacketIntervals) { - _currentPacketInterval = 0; + if (_packetIntervals.size() > 0) { + // record the interval between this packet and the last one + _packetIntervals[_currentPacketInterval++] = duration_cast(now - _lastPacketTime).count(); + + // reset the currentPacketInterval index when it wraps + _currentPacketInterval %= _numPacketIntervals; } // remember this as the last packet arrival time @@ -120,7 +121,5 @@ void PacketTimeWindow::onProbePair2Arrival() { _probeIntervals[_currentProbeInterval++] = duration_cast(now - _firstProbeTime).count(); // reset the currentProbeInterval index when it wraps - if (_currentProbeInterval == _numProbeIntervals) { - _currentProbeInterval = 0; - } + _currentProbeInterval %= _numProbeIntervals; } diff --git a/libraries/networking/src/udt/PacketTimeWindow.h b/libraries/networking/src/udt/PacketTimeWindow.h index c2a90d0f6c..6f7ed9f70f 100644 --- a/libraries/networking/src/udt/PacketTimeWindow.h +++ b/libraries/networking/src/udt/PacketTimeWindow.h @@ -42,8 +42,8 @@ private: std::vector _packetIntervals; // vector of microsecond intervals between packet arrivals std::vector _probeIntervals; // vector of microsecond intervals between probe pair arrivals - p_high_resolution_clock::time_point _lastPacketTime; // the time_point when last packet arrived - p_high_resolution_clock::time_point _firstProbeTime; // the time_point when first probe in pair arrived + p_high_resolution_clock::time_point _lastPacketTime = p_high_resolution_clock::now(); // the time_point when last packet arrived + p_high_resolution_clock::time_point _firstProbeTime = p_high_resolution_clock::now(); // the time_point when first probe in pair arrived }; } diff --git a/libraries/networking/src/udt/SendQueue.cpp b/libraries/networking/src/udt/SendQueue.cpp index 2890d52c2b..a09ea6ca9a 100644 --- a/libraries/networking/src/udt/SendQueue.cpp +++ b/libraries/networking/src/udt/SendQueue.cpp @@ -279,17 +279,13 @@ void SendQueue::run() { if (!_hasReceivedHandshakeACK) { // we haven't received a handshake ACK from the client // if it has been at least 100ms since we last sent a handshake, send another now - - // hold the time of last send in a static - static auto lastSendHandshake = p_high_resolution_clock::time_point(); - + static const auto HANDSHAKE_RESEND_INTERVAL_MS = std::chrono::milliseconds(100); - // calculation the duration since the last handshake send - auto sinceLastHandshake = std::chrono::duration_cast(p_high_resolution_clock::now() - - lastSendHandshake); + // hold the time of last send in a static + static auto lastSendHandshake = p_high_resolution_clock::now() - HANDSHAKE_RESEND_INTERVAL_MS; - if (sinceLastHandshake >= HANDSHAKE_RESEND_INTERVAL_MS) { + if (p_high_resolution_clock::now() - lastSendHandshake >= HANDSHAKE_RESEND_INTERVAL_MS) { // it has been long enough since last handshake, send another static auto handshakePacket = ControlPacket::create(ControlPacket::Handshake, 0); @@ -299,9 +295,7 @@ void SendQueue::run() { } // we wait for the ACK or the re-send interval to expire - _handshakeACKCondition.wait_until(handshakeLock, - p_high_resolution_clock::now() - + HANDSHAKE_RESEND_INTERVAL_MS); + _handshakeACKCondition.wait_until(handshakeLock, p_high_resolution_clock::now() + HANDSHAKE_RESEND_INTERVAL_MS); // Once we're here we've either received the handshake ACK or it's going to be time to re-send a handshake. // Either way let's continue processing - no packets will be sent if no handshake ACK has been received. diff --git a/libraries/networking/src/udt/SendQueue.h b/libraries/networking/src/udt/SendQueue.h index 401ebd384d..2e7ec90c45 100644 --- a/libraries/networking/src/udt/SendQueue.h +++ b/libraries/networking/src/udt/SendQueue.h @@ -45,8 +45,6 @@ class SendQueue : public QObject { Q_OBJECT public: - using time_point = p_high_resolution_clock::time_point; - static std::unique_ptr create(Socket* socket, HifiSockAddr destination); void queuePacket(std::unique_ptr packet); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 4c9279c8fd..a4dc1b1de3 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -798,7 +798,7 @@ namespace render { return payload->model->renderPart(args, payload->meshIndex, payload->partIndex, payload->transparent); } } - + /* template <> const model::MaterialKey& shapeGetMaterialKey(const MeshPartPayload::Pointer& payload) { return payload->model->getPartMaterial(payload->meshIndex, payload->partIndex); }*/ @@ -829,6 +829,9 @@ bool Model::addToScene(std::shared_ptr scene, render::PendingChan auto renderData = MeshPartPayload::Pointer(renderItem); auto renderPayload = std::make_shared(renderData); pendingChanges.resetItem(item, renderPayload); + pendingChanges.updateItem(item, [&](MeshPartPayload& data) { + data.model->_needsUpdateClusterMatrices = true; + }); _renderItems.insert(item, renderPayload); somethingAdded = true; } @@ -838,6 +841,9 @@ bool Model::addToScene(std::shared_ptr scene, render::PendingChan auto renderData = MeshPartPayload::Pointer(renderItem); auto renderPayload = std::make_shared(renderData); pendingChanges.resetItem(item, renderPayload); + pendingChanges.updateItem(item, [&](MeshPartPayload& data) { + data.model->_needsUpdateClusterMatrices = true; + }); _renderItems.insert(item, renderPayload); somethingAdded = true; } @@ -860,6 +866,9 @@ bool Model::addToScene(std::shared_ptr scene, render::PendingChan auto renderPayload = std::make_shared(renderData); renderPayload->addStatusGetters(statusGetters); pendingChanges.resetItem(item, renderPayload); + pendingChanges.updateItem(item, [&](MeshPartPayload& data) { + data.model->_needsUpdateClusterMatrices = true; + }); _renderItems.insert(item, renderPayload); somethingAdded = true; } @@ -870,6 +879,9 @@ bool Model::addToScene(std::shared_ptr scene, render::PendingChan auto renderPayload = std::make_shared(renderData); renderPayload->addStatusGetters(statusGetters); pendingChanges.resetItem(item, renderPayload); + pendingChanges.updateItem(item, [&](MeshPartPayload& data) { + data.model->_needsUpdateClusterMatrices = true; + }); _renderItems.insert(item, renderPayload); somethingAdded = true; } @@ -1283,6 +1295,7 @@ void Model::simulate(float deltaTime, bool fullUpdate) { //virtual void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { + _needsUpdateClusterMatrices = true; _rig->updateAnimations(deltaTime, parentTransform); } void Model::simulateInternal(float deltaTime) { @@ -1293,11 +1306,15 @@ void Model::simulateInternal(float deltaTime) { updateRig(deltaTime, parentTransform); } void Model::updateClusterMatrices() { + if (!_needsUpdateClusterMatrices) { + return; + } + _needsUpdateClusterMatrices = false; const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 zeroScale(glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), - glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), - glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), - glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); auto cauterizeMatrix = _rig->getJointTransform(geometry.neckJointIndex) * zeroScale; glm::mat4 modelToWorld = glm::mat4_cast(_rotation); @@ -1803,13 +1820,15 @@ bool Model::initWhenReady(render::ScenePointer scene) { segregateMeshGroups(); render::PendingChanges pendingChanges; - foreach (auto renderItem, _transparentRenderItems) { auto item = scene->allocateID(); auto renderData = MeshPartPayload::Pointer(renderItem); auto renderPayload = std::make_shared(renderData); _renderItems.insert(item, renderPayload); pendingChanges.resetItem(item, renderPayload); + pendingChanges.updateItem(item, [&](MeshPartPayload& data) { + data.model->_needsUpdateClusterMatrices = true; + }); } foreach (auto renderItem, _opaqueRenderItems) { @@ -1818,6 +1837,9 @@ bool Model::initWhenReady(render::ScenePointer scene) { auto renderPayload = std::make_shared(renderData); _renderItems.insert(item, renderPayload); pendingChanges.resetItem(item, renderPayload); + pendingChanges.updateItem(item, [&](MeshPartPayload& data) { + data.model->_needsUpdateClusterMatrices = true; + }); } scene->enqueuePendingChanges(pendingChanges); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 348e5cf549..93b98da8b5 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -497,6 +497,7 @@ private: QMap _renderItems; bool _readyWhenAdded = false; bool _needsReload = true; + bool _needsUpdateClusterMatrices = true; protected: RigPointer _rig; diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index 9e04cd4ec3..e2c07c05d0 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -26,7 +27,8 @@ ScriptCache::ScriptCache(QObject* parent) { // nothing to do here... } -QString ScriptCache::getScript(const QUrl& url, ScriptUser* scriptUser, bool& isPending, bool reload) { +QString ScriptCache::getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUser, bool& isPending, bool reload) { + QUrl url = ResourceManager::normalizeURL(unnormalizedURL); QString scriptContents; if (_scriptCache.contains(url) && !reload) { qCDebug(scriptengine) << "Found script in cache:" << url.toString(); @@ -41,7 +43,7 @@ QString ScriptCache::getScript(const QUrl& url, ScriptUser* scriptUser, bool& is if (alreadyWaiting) { qCDebug(scriptengine) << "Already downloading script at:" << url.toString(); } else { - auto request = ResourceManager::createResourceRequest(this, url); + auto request = ResourceManager::createResourceRequest(nullptr, url); request->setCacheEnabled(!reload); connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptDownloaded); request->send(); @@ -50,7 +52,8 @@ QString ScriptCache::getScript(const QUrl& url, ScriptUser* scriptUser, bool& is return scriptContents; } -void ScriptCache::deleteScript(const QUrl& url) { +void ScriptCache::deleteScript(const QUrl& unnormalizedURL) { + QUrl url = ResourceManager::normalizeURL(unnormalizedURL); if (_scriptCache.contains(url)) { qCDebug(scriptengine) << "Delete script from cache:" << url.toString(); _scriptCache.remove(url); @@ -79,4 +82,63 @@ void ScriptCache::scriptDownloaded() { req->deleteLater(); } +void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload) { + #ifdef THREAD_DEBUGGING + qCDebug(scriptengine) << "ScriptCache::getScriptContents() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; + #endif + QUrl unnormalizedURL(scriptOrURL); + QUrl url = ResourceManager::normalizeURL(unnormalizedURL); + + // attempt to determine if this is a URL to a script, or if this is actually a script itself (which is valid in the entityScript use case) + if (url.scheme().isEmpty() && scriptOrURL.simplified().replace(" ", "").contains("(function(){")) { + contentAvailable(scriptOrURL, scriptOrURL, false, true); + return; + } + + if (_scriptCache.contains(url) && !forceDownload) { + qCDebug(scriptengine) << "Found script in cache:" << url.toString(); + #if 1 // def THREAD_DEBUGGING + qCDebug(scriptengine) << "ScriptCache::getScriptContents() about to call contentAvailable() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; + #endif + contentAvailable(url.toString(), _scriptCache[url], true, true); + } else { + bool alreadyWaiting = _contentCallbacks.contains(url); + _contentCallbacks.insert(url, contentAvailable); + + if (alreadyWaiting) { + qCDebug(scriptengine) << "Already downloading script at:" << url.toString(); + } else { + #ifdef THREAD_DEBUGGING + qCDebug(scriptengine) << "about to call: ResourceManager::createResourceRequest(this, url); on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; + #endif + auto request = ResourceManager::createResourceRequest(nullptr, url); + request->setCacheEnabled(!forceDownload); + connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptContentAvailable); + request->send(); + } + } +} + +void ScriptCache::scriptContentAvailable() { + #ifdef THREAD_DEBUGGING + qCDebug(scriptengine) << "ScriptCache::scriptContentAvailable() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; + #endif + ResourceRequest* req = qobject_cast(sender()); + QUrl url = req->getUrl(); + QList allCallbacks = _contentCallbacks.values(url); + _contentCallbacks.remove(url); + + bool success = req->getResult() == ResourceRequest::Success; + + if (success) { + _scriptCache[url] = req->getData(); + qCDebug(scriptengine) << "Done downloading script at:" << url.toString(); + } else { + qCWarning(scriptengine) << "Error loading script from URL " << url; + } + foreach(contentAvailableCallback thisCallback, allCallbacks) { + thisCallback(url.toString(), _scriptCache[url], true, success); + } + req->deleteLater(); +} diff --git a/libraries/script-engine/src/ScriptCache.h b/libraries/script-engine/src/ScriptCache.h index 25a36c04d8..7de14a09f7 100644 --- a/libraries/script-engine/src/ScriptCache.h +++ b/libraries/script-engine/src/ScriptCache.h @@ -20,23 +20,33 @@ public: virtual void errorInLoadingScript(const QUrl& url) = 0; }; +using contentAvailableCallback = std::function; + /// Interface for loading scripts class ScriptCache : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY public: - QString getScript(const QUrl& url, ScriptUser* scriptUser, bool& isPending, bool redownload = false); - void deleteScript(const QUrl& url); + void getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload = false); + + + QString getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUser, bool& isPending, bool redownload = false); + void deleteScript(const QUrl& unnormalizedURL); + + // FIXME - how do we remove a script from the bad script list in the case of a redownload? void addScriptToBadScriptList(const QUrl& url) { _badScripts.insert(url); } bool isInBadScriptList(const QUrl& url) { return _badScripts.contains(url); } private slots: - void scriptDownloaded(); - + void scriptDownloaded(); // old version + void scriptContentAvailable(); // new version + private: ScriptCache(QObject* parent = NULL); + QMultiMap _contentCallbacks; + QHash _scriptCache; QMultiMap _scriptUsers; QSet _badScripts; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index a9ada42543..a7136edd7b 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -44,6 +45,8 @@ #include "MIDIEvent.h" +Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) +static int functionSignatureMetaID = qRegisterMetaType(); static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ QString message = ""; @@ -87,17 +90,9 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam _isFinished(false), _isRunning(false), _isInitialized(false), - _isAvatar(false), - _avatarIdentityTimer(NULL), - _avatarBillboardTimer(NULL), _timerFunctionMap(), - _isListeningToAudioStream(false), - _avatarSound(NULL), - _numAvatarSoundSentBytes(0), _wantSignals(wantSignals), _controllerScriptingInterface(controllerScriptingInterface), - _avatarData(NULL), - _scriptName(), _fileNameString(fileNameString), _quatLibrary(), _vec3Library(), @@ -122,6 +117,29 @@ ScriptEngine::~ScriptEngine() { } } +void ScriptEngine::runInThread() { + QThread* workerThread = new QThread(); // thread is not owned, so we need to manage the delete + QString scriptEngineName = QString("Script Thread:") + getFilename(); + workerThread->setObjectName(scriptEngineName); + + // when the worker thread is started, call our engine's run.. + connect(workerThread, &QThread::started, this, &ScriptEngine::run); + + // tell the thread to stop when the script engine is done + connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit); + + // when the thread is finished, add thread to the deleteLater queue + connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater); + + // when the thread is finished, add scriptEngine to the deleteLater queue + connect(workerThread, &QThread::finished, this, &ScriptEngine::deleteLater); + + moveToThread(workerThread); + + // Starts an event loop, and emits workerThread->started() + workerThread->start(); +} + QSet ScriptEngine::_allKnownScriptEngines; QMutex ScriptEngine::_allScriptsMutex; bool ScriptEngine::_stoppingAllScripts = false; @@ -180,8 +198,6 @@ void ScriptEngine::stopAllScripts(QObject* application) { void ScriptEngine::waitTillDoneRunning() { - QString scriptName = getFilename(); - // If the script never started running or finished running before we got here, we don't need to wait for it if (_isRunning) { @@ -209,58 +225,7 @@ QString ScriptEngine::getFilename() const { } -void ScriptEngine::setIsAvatar(bool isAvatar) { - _isAvatar = isAvatar; - - if (_isAvatar && !_avatarIdentityTimer) { - // set up the avatar timers - _avatarIdentityTimer = new QTimer(this); - _avatarBillboardTimer = new QTimer(this); - - // connect our slot - connect(_avatarIdentityTimer, &QTimer::timeout, this, &ScriptEngine::sendAvatarIdentityPacket); - connect(_avatarBillboardTimer, &QTimer::timeout, this, &ScriptEngine::sendAvatarBillboardPacket); - - // start the timers - _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); - _avatarBillboardTimer->start(AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS); - } - - if (!_isAvatar) { - delete _avatarIdentityTimer; - _avatarIdentityTimer = NULL; - delete _avatarBillboardTimer; - _avatarBillboardTimer = NULL; - } -} - -void ScriptEngine::setAvatarData(AvatarData* avatarData, const QString& objectName) { - _avatarData = avatarData; - - // remove the old Avatar property, if it exists - globalObject().setProperty(objectName, QScriptValue()); - - // give the script engine the new Avatar script property - registerGlobalObject(objectName, _avatarData); -} - -void ScriptEngine::setAvatarHashMap(AvatarHashMap* avatarHashMap, const QString& objectName) { - // remove the old Avatar property, if it exists - globalObject().setProperty(objectName, QScriptValue()); - - // give the script engine the new avatar hash map - registerGlobalObject(objectName, avatarHashMap); -} - -bool ScriptEngine::setScriptContents(const QString& scriptContents, const QString& fileNameString) { - if (_isRunning) { - return false; - } - _scriptContents = scriptContents; - _fileNameString = fileNameString; - return true; -} - +// FIXME - switch this to the new model of ScriptCache callbacks void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { if (_isRunning) { return; @@ -271,38 +236,12 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { QUrl url(scriptURL); - // if the scheme length is one or lower, maybe they typed in a file, let's try - const int WINDOWS_DRIVE_LETTER_SIZE = 1; - if (url.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) { - url = QUrl::fromLocalFile(_fileNameString); - } - - // ok, let's see if it's valid... and if so, load it - if (url.isValid()) { - if (url.scheme() == "file") { - _fileNameString = url.toLocalFile(); - QFile scriptFile(_fileNameString); - if (scriptFile.open(QFile::ReadOnly | QFile::Text)) { - qCDebug(scriptengine) << "ScriptEngine loading file:" << _fileNameString; - QTextStream in(&scriptFile); - _scriptContents = in.readAll(); - if (_wantSignals) { - emit scriptLoaded(_fileNameString); - } - } else { - qCDebug(scriptengine) << "ERROR Loading file:" << _fileNameString << "line:" << __LINE__; - if (_wantSignals) { - emit errorLoadingScript(_fileNameString); - } - } - } else { - bool isPending; - auto scriptCache = DependencyManager::get(); - scriptCache->getScript(url, this, isPending, reload); - } - } + bool isPending; + auto scriptCache = DependencyManager::get(); + scriptCache->getScript(url, this, isPending, reload); } +// FIXME - switch this to the new model of ScriptCache callbacks void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) { _scriptContents = scriptContents; if (_wantSignals) { @@ -310,6 +249,7 @@ void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scrip } } +// FIXME - switch this to the new model of ScriptCache callbacks void ScriptEngine::errorInLoadingScript(const QUrl& url) { qCDebug(scriptengine) << "ERROR Loading file:" << url.toString() << "line:" << __LINE__; if (_wantSignals) { @@ -383,57 +323,117 @@ void ScriptEngine::init() { globalObject().setProperty("TREE_SCALE", newVariant(QVariant(TREE_SCALE))); } -QScriptValue ScriptEngine::registerGlobalObject(const QString& name, QObject* object) { +void ScriptEngine::registerGlobalObject(const QString& name, QObject* object) { + if (QThread::currentThread() != thread()) { + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::registerGlobalObject() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; + #endif + QMetaObject::invokeMethod(this, "registerGlobalObject", + Q_ARG(const QString&, name), + Q_ARG(QObject*, object)); + return; + } + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::registerGlobalObject() called on thread [" << QThread::currentThread() << "] name:" << name; + #endif + if (object) { QScriptValue value = newQObject(object); globalObject().setProperty(name, value); - return value; } - return QScriptValue::NullValue; } -void ScriptEngine::registerFunction(const QString& name, QScriptEngine::FunctionSignature fun, int numArguments) { - registerFunction(globalObject(), name, fun, numArguments); +void ScriptEngine::registerFunction(const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { + if (QThread::currentThread() != thread()) { + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::registerFunction() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; + #endif + QMetaObject::invokeMethod(this, "registerFunction", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, functionSignature), + Q_ARG(int, numArguments)); + return; + } + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] name:" << name; + #endif + + QScriptValue scriptFun = newFunction(functionSignature, numArguments); + globalObject().setProperty(name, scriptFun); } -void ScriptEngine::registerFunction(QScriptValue parent, const QString& name, QScriptEngine::FunctionSignature fun, int numArguments) { - QScriptValue scriptFun = newFunction(fun, numArguments); - parent.setProperty(name, scriptFun); +void ScriptEngine::registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { + if (QThread::currentThread() != thread()) { + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::registerFunction() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] parent:" << parent << "name:" << name; + #endif + QMetaObject::invokeMethod(this, "registerFunction", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, functionSignature), + Q_ARG(int, numArguments)); + return; + } + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] parent:" << parent << "name:" << name; + #endif + + QScriptValue object = globalObject().property(parent); + if (object.isValid()) { + QScriptValue scriptFun = newFunction(functionSignature, numArguments); + object.setProperty(name, scriptFun); + } } void ScriptEngine::registerGetterSetter(const QString& name, QScriptEngine::FunctionSignature getter, - QScriptEngine::FunctionSignature setter, QScriptValue object) { + QScriptEngine::FunctionSignature setter, const QString& parent) { + if (QThread::currentThread() != thread()) { + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::registerGetterSetter() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + " name:" << name << "parent:" << parent; + #endif + QMetaObject::invokeMethod(this, "registerGetterSetter", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, getter), + Q_ARG(QScriptEngine::FunctionSignature, setter), + Q_ARG(const QString&, parent)); + return; + } + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::registerGetterSetter() called on thread [" << QThread::currentThread() << "] name:" << name << "parent:" << parent; + #endif + QScriptValue setterFunction = newFunction(setter, 1); QScriptValue getterFunction = newFunction(getter); - if (!object.isNull()) { - object.setProperty(name, setterFunction, QScriptValue::PropertySetter); - object.setProperty(name, getterFunction, QScriptValue::PropertyGetter); + if (!parent.isNull()) { + QScriptValue object = globalObject().property(parent); + if (object.isValid()) { + object.setProperty(name, setterFunction, QScriptValue::PropertySetter); + object.setProperty(name, getterFunction, QScriptValue::PropertyGetter); + } } else { globalObject().setProperty(name, setterFunction, QScriptValue::PropertySetter); globalObject().setProperty(name, getterFunction, QScriptValue::PropertyGetter); } } -// Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those args -void ScriptEngine::generalHandler(const EntityItemID& entityID, const QString& eventName, std::function argGenerator) { - if (!_registeredHandlers.contains(entityID)) { - return; - } - const RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; - if (!handlersOnEntity.contains(eventName)) { - return; - } - QScriptValueList handlersForEvent = handlersOnEntity[eventName]; - if (!handlersForEvent.isEmpty()) { - QScriptValueList args = argGenerator(); - for (int i = 0; i < handlersForEvent.count(); ++i) { - handlersForEvent[i].call(QScriptValue(), args); - } - } -} // Unregister the handlers for this eventName and entityID. void ScriptEngine::removeEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler) { + if (QThread::currentThread() != thread()) { + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::removeEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << " eventName:" << eventName; + #endif + QMetaObject::invokeMethod(this, "removeEventHandler", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, eventName), + Q_ARG(QScriptValue, handler)); + return; + } + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::removeEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; + #endif + if (!_registeredHandlers.contains(entityID)) { return; } @@ -449,6 +449,22 @@ void ScriptEngine::removeEventHandler(const EntityItemID& entityID, const QStrin } // Register the handler. void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler) { + if (QThread::currentThread() != thread()) { + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::addEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << " eventName:" << eventName; + #endif + + QMetaObject::invokeMethod(this, "addEventHandler", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, eventName), + Q_ARG(QScriptValue, handler)); + return; + } + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::addEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; + #endif + if (_registeredHandlers.count() == 0) { // First time any per-entity handler has been added in this script... // Connect up ALL the handlers to the global entities object's signals. // (We could go signal by signal, or even handler by handler, but I don't think the efficiency is worth the complexity.) @@ -503,34 +519,25 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& } -void ScriptEngine::evaluate() { - if (_stoppingAllScripts) { - return; // bail early - } - - if (!_isInitialized) { - init(); - } - - QScriptValue result = evaluate(_scriptContents); - - // TODO: why do we check this twice? It seems like the call to clearExceptions() in the lower level evaluate call - // will cause this code to never actually run... - if (hasUncaughtException()) { - int line = uncaughtExceptionLineNumber(); - qCDebug(scriptengine) << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << result.toString(); - if (_wantSignals) { - emit errorMessage("Uncaught exception at (" + _fileNameString + ") line" + QString::number(line) + ":" + result.toString()); - } - clearExceptions(); - } -} - QScriptValue ScriptEngine::evaluate(const QString& program, const QString& fileName, int lineNumber) { if (_stoppingAllScripts) { return QScriptValue(); // bail early } + if (QThread::currentThread() != thread()) { + QScriptValue result; + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::evaluate() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "program:" << program << " fileName:" << fileName << "lineNumber:" << lineNumber; + #endif + QMetaObject::invokeMethod(this, "evaluate", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QScriptValue, result), + Q_ARG(const QString&, program), + Q_ARG(const QString&, fileName), + Q_ARG(int, lineNumber)); + return result; + } + _evaluatesPending++; QScriptValue result = QScriptEngine::evaluate(program, fileName, lineNumber); if (hasUncaughtException()) { @@ -545,18 +552,6 @@ QScriptValue ScriptEngine::evaluate(const QString& program, const QString& fileN return result; } -void ScriptEngine::sendAvatarIdentityPacket() { - if (_isAvatar && _avatarData) { - _avatarData->sendIdentityPacket(); - } -} - -void ScriptEngine::sendAvatarBillboardPacket() { - if (_isAvatar && _avatarData) { - _avatarData->sendBillboardPacket(); - } -} - void ScriptEngine::run() { // TODO: can we add a short circuit for _stoppingAllScripts here? What does it mean to not start running if // we're in the process of stopping? @@ -608,107 +603,6 @@ void ScriptEngine::run() { } } - if (!_isFinished && _isAvatar && _avatarData) { - - const int SCRIPT_AUDIO_BUFFER_SAMPLES = floor(((SCRIPT_DATA_CALLBACK_USECS * AudioConstants::SAMPLE_RATE) - / (1000 * 1000)) + 0.5); - const int SCRIPT_AUDIO_BUFFER_BYTES = SCRIPT_AUDIO_BUFFER_SAMPLES * sizeof(int16_t); - - QByteArray avatarByteArray = _avatarData->toByteArray(true, randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); - _avatarData->doneEncoding(true); - auto avatarPacket = NLPacket::create(PacketType::AvatarData, avatarByteArray.size()); - - avatarPacket->write(avatarByteArray); - - nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); - - if (_isListeningToAudioStream || _avatarSound) { - // if we have an avatar audio stream then send it out to our audio-mixer - bool silentFrame = true; - - int16_t numAvailableSamples = SCRIPT_AUDIO_BUFFER_SAMPLES; - const int16_t* nextSoundOutput = NULL; - - if (_avatarSound) { - - const QByteArray& soundByteArray = _avatarSound->getByteArray(); - nextSoundOutput = reinterpret_cast(soundByteArray.data() - + _numAvatarSoundSentBytes); - - int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > SCRIPT_AUDIO_BUFFER_BYTES - ? SCRIPT_AUDIO_BUFFER_BYTES - : soundByteArray.size() - _numAvatarSoundSentBytes; - numAvailableSamples = numAvailableBytes / sizeof(int16_t); - - - // check if the all of the _numAvatarAudioBufferSamples to be sent are silence - for (int i = 0; i < numAvailableSamples; ++i) { - if (nextSoundOutput[i] != 0) { - silentFrame = false; - break; - } - } - - _numAvatarSoundSentBytes += numAvailableBytes; - if (_numAvatarSoundSentBytes == soundByteArray.size()) { - // we're done with this sound object - so set our pointer back to NULL - // and our sent bytes back to zero - _avatarSound = NULL; - _numAvatarSoundSentBytes = 0; - } - } - - auto audioPacket = NLPacket::create(silentFrame - ? PacketType::SilentAudioFrame - : PacketType::MicrophoneAudioNoEcho); - - // seek past the sequence number, will be packed when destination node is known - audioPacket->seek(sizeof(quint16)); - - if (silentFrame) { - if (!_isListeningToAudioStream) { - // if we have a silent frame and we're not listening then just send nothing and break out of here - break; - } - - // write the number of silent samples so the audio-mixer can uphold timing - audioPacket->writePrimitive(SCRIPT_AUDIO_BUFFER_SAMPLES); - - // use the orientation and position of this avatar for the source of this audio - audioPacket->writePrimitive(_avatarData->getPosition()); - glm::quat headOrientation = _avatarData->getHeadOrientation(); - audioPacket->writePrimitive(headOrientation); - - } else if (nextSoundOutput) { - // assume scripted avatar audio is mono and set channel flag to zero - audioPacket->writePrimitive((quint8) 0); - - // use the orientation and position of this avatar for the source of this audio - audioPacket->writePrimitive(_avatarData->getPosition()); - glm::quat headOrientation = _avatarData->getHeadOrientation(); - audioPacket->writePrimitive(headOrientation); - - // write the raw audio data - audioPacket->write(reinterpret_cast(nextSoundOutput), numAvailableSamples * sizeof(int16_t)); - } - - // write audio packet to AudioMixer nodes - auto nodeList = DependencyManager::get(); - nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node){ - // only send to nodes of type AudioMixer - if (node->getType() == NodeType::AudioMixer) { - // pack sequence number - quint16 sequence = _outgoingScriptAudioSequenceNumbers[node->getUUID()]++; - audioPacket->seek(0); - audioPacket->writePrimitive(sequence); - - // send audio packet - nodeList->sendUnreliablePacket(*audioPacket, *node); - } - }); - } - } - qint64 now = usecTimestampNow(); float deltaTime = (float) (now - lastUpdate) / (float) USECS_PER_SECOND; @@ -735,9 +629,6 @@ void ScriptEngine::run() { emit scriptEnding(); } - // kill the avatar identity timer - delete _avatarIdentityTimer; - if (entityScriptingInterface->getEntityPacketSender()->serversExist()) { // release the queue of edit entity messages. entityScriptingInterface->getEntityPacketSender()->releaseQueuedMessages(); @@ -967,6 +858,256 @@ void ScriptEngine::load(const QString& loadFile) { } } -void ScriptEngine::nodeKilled(SharedNodePointer node) { - _outgoingScriptAudioSequenceNumbers.remove(node->getUUID()); +// Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those args +void ScriptEngine::generalHandler(const EntityItemID& entityID, const QString& eventName, std::function argGenerator) { + if (QThread::currentThread() != thread()) { + qDebug() << "*** ERROR *** ScriptEngine::generalHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; + assert(false); + return; + } + + if (!_registeredHandlers.contains(entityID)) { + return; + } + const RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; + if (!handlersOnEntity.contains(eventName)) { + return; + } + QScriptValueList handlersForEvent = handlersOnEntity[eventName]; + if (!handlersForEvent.isEmpty()) { + QScriptValueList args = argGenerator(); + for (int i = 0; i < handlersForEvent.count(); ++i) { + handlersForEvent[i].call(QScriptValue(), args); + } + } +} + +// since all of these operations can be asynch we will always do the actual work in the response handler +// for the download +void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { + if (QThread::currentThread() != thread()) { + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::loadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << "entityScript:" << entityScript <<"forceRedownload:" << forceRedownload; + #endif + + QMetaObject::invokeMethod(this, "loadEntityScript", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, entityScript), + Q_ARG(bool, forceRedownload)); + return; + } + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::loadEntityScript() called on correct thread [" << thread() << "] " + "entityID:" << entityID << "entityScript:" << entityScript << "forceRedownload:" << forceRedownload; + #endif + + // If we've been called our known entityScripts should not know about us.. + assert(!_entityScripts.contains(entityID)); + + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::loadEntityScript() calling scriptCache->getScriptContents() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; + #endif + DependencyManager::get()->getScriptContents(entityScript, [=](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; + #endif + + this->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success); + }, forceRedownload); +} + +// since all of these operations can be asynch we will always do the actual work in the response handler +// for the download +void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { + if (QThread::currentThread() != thread()) { + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::entityScriptContentAvailable() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << "scriptOrURL:" << scriptOrURL << "contents:" << contents << "isURL:" << isURL << "success:" << success; + #endif + + QMetaObject::invokeMethod(this, "entityScriptContentAvailable", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, scriptOrURL), + Q_ARG(const QString&, contents), + Q_ARG(bool, isURL), + Q_ARG(bool, success)); + return; + } + + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::entityScriptContentAvailable() thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; + #endif + + auto scriptCache = DependencyManager::get(); + + // first check the syntax of the script contents + QScriptSyntaxCheckResult syntaxCheck = QScriptEngine::checkSyntax(contents); + if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { + qCDebug(scriptengine) << "ScriptEngine::loadEntityScript() entity:" << entityID; + qCDebug(scriptengine) << " " << syntaxCheck.errorMessage() << ":" + << syntaxCheck.errorLineNumber() << syntaxCheck.errorColumnNumber(); + qCDebug(scriptengine) << " SCRIPT:" << scriptOrURL; + scriptCache->addScriptToBadScriptList(scriptOrURL); + return; // done processing script + } + + if (isURL) { + setParentURL(scriptOrURL); + } + + QScriptEngine sandbox; + QScriptValue testConstructor = sandbox.evaluate(contents); + + if (!testConstructor.isFunction()) { + qCDebug(scriptengine) << "ScriptEngine::loadEntityScript() entity:" << entityID; + qCDebug(scriptengine) << " NOT CONSTRUCTOR"; + qCDebug(scriptengine) << " SCRIPT:" << scriptOrURL; + scriptCache->addScriptToBadScriptList(scriptOrURL); + return; // done processing script + } + + QScriptValue entityScriptConstructor = evaluate(contents); + + QScriptValue entityScriptObject = entityScriptConstructor.construct(); + EntityScriptDetails newDetails = { scriptOrURL, entityScriptObject }; + _entityScripts[entityID] = newDetails; + if (isURL) { + setParentURL(""); + } + + // if we got this far, then call the preload method + callEntityScriptMethod(entityID, "preload"); +} + +void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { + if (QThread::currentThread() != thread()) { + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID; + #endif + + QMetaObject::invokeMethod(this, "unloadEntityScript", + Q_ARG(const EntityItemID&, entityID)); + return; + } + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::unloadEntityScript() called on correct thread [" << thread() << "] " + "entityID:" << entityID; + #endif + + if (_entityScripts.contains(entityID)) { + callEntityScriptMethod(entityID, "unload"); + _entityScripts.remove(entityID); + } +} + +void ScriptEngine::unloadAllEntityScripts() { + if (QThread::currentThread() != thread()) { +#ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::unloadAllEntityScripts() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; +#endif + + QMetaObject::invokeMethod(this, "unloadAllEntityScripts"); + return; + } +#ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::unloadAllEntityScripts() called on correct thread [" << thread() << "]"; +#endif + foreach(const EntityItemID& entityID, _entityScripts.keys()) { + callEntityScriptMethod(entityID, "unload"); + } + _entityScripts.clear(); +} + +void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName) { + if (QThread::currentThread() != thread()) { + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << "methodName:" << methodName; + #endif + + QMetaObject::invokeMethod(this, "callEntityScriptMethod", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, methodName)); + return; + } + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " + "entityID:" << entityID << "methodName:" << methodName; + #endif + + if (_entityScripts.contains(entityID)) { + EntityScriptDetails details = _entityScripts[entityID]; + QScriptValue entityScript = details.scriptObject; // previously loaded + if (entityScript.property(methodName).isFunction()) { + QScriptValueList args; + args << entityID.toScriptValue(this); + entityScript.property(methodName).call(entityScript, args); + } + + } +} + +void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const MouseEvent& event) { + if (QThread::currentThread() != thread()) { + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << "methodName:" << methodName << "event: mouseEvent"; + #endif + + QMetaObject::invokeMethod(this, "callEntityScriptMethod", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, methodName), + Q_ARG(const MouseEvent&, event)); + return; + } + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " + "entityID:" << entityID << "methodName:" << methodName << "event: mouseEvent"; + #endif + + if (_entityScripts.contains(entityID)) { + EntityScriptDetails details = _entityScripts[entityID]; + QScriptValue entityScript = details.scriptObject; // previously loaded + if (entityScript.property(methodName).isFunction()) { + QScriptValueList args; + args << entityID.toScriptValue(this); + args << event.toScriptValue(this); + entityScript.property(methodName).call(entityScript, args); + } + } +} + + +void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) { + if (QThread::currentThread() != thread()) { + #ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision"; + #endif + + QMetaObject::invokeMethod(this, "callEntityScriptMethod", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, methodName), + Q_ARG(const EntityItemID&, otherID), + Q_ARG(const Collision&, collision)); + return; + } + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " + "entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision"; + #endif + + if (_entityScripts.contains(entityID)) { + EntityScriptDetails details = _entityScripts[entityID]; + QScriptValue entityScript = details.scriptObject; // previously loaded + if (entityScript.property(methodName).isFunction()) { + QScriptValueList args; + args << entityID.toScriptValue(this); + args << otherID.toScriptValue(this); + args << collisionToScriptValue(this, collision); + entityScript.property(methodName).call(entityScript, args); + } + } } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 4608732571..83e65823a5 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -40,6 +40,12 @@ const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0f / 60.0f) * 1000 * 1 typedef QHash RegisteredEventHandlers; +class EntityScriptDetails { +public: + QString scriptText; + QScriptValue scriptObject; +}; + class ScriptEngine : public QScriptEngine, public ScriptUser { Q_OBJECT public: @@ -50,79 +56,86 @@ public: ~ScriptEngine(); - ArrayBufferClass* getArrayBufferClass() { return _arrayBufferClass; } + /// run the script in a dedicated thread. This will have the side effect of evalulating + /// the current script contents and calling run(). Callers will likely want to register the script with external + /// services before calling this. + void runInThread(); + + /// run the script in the callers thread, exit when stop() is called. + void run(); - /// sets the script contents, will return false if failed, will fail if script is already running - bool setScriptContents(const QString& scriptContents, const QString& fileNameString = QString("")); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // NOTE - these are NOT intended to be public interfaces available to scripts, the are only Q_INVOKABLE so we can + // properly ensure they are only called on the correct thread - const QString& getScriptName() const { return _scriptName; } + /// registers a global object by name + Q_INVOKABLE void registerGlobalObject(const QString& name, QObject* object); - QScriptValue registerGlobalObject(const QString& name, QObject* object); /// registers a global object by name - void registerGetterSetter(const QString& name, QScriptEngine::FunctionSignature getter, - QScriptEngine::FunctionSignature setter, QScriptValue object = QScriptValue::NullValue); - void registerFunction(const QString& name, QScriptEngine::FunctionSignature fun, int numArguments = -1); - void registerFunction(QScriptValue parent, const QString& name, QScriptEngine::FunctionSignature fun, + /// registers a global getter/setter + Q_INVOKABLE void registerGetterSetter(const QString& name, QScriptEngine::FunctionSignature getter, + QScriptEngine::FunctionSignature setter, const QString& parent = QString("")); + + /// register a global function + Q_INVOKABLE void registerFunction(const QString& name, QScriptEngine::FunctionSignature fun, int numArguments = -1); + + /// register a function as a method on a previously registered global object + Q_INVOKABLE void registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature fun, int numArguments = -1); - Q_INVOKABLE void setIsAvatar(bool isAvatar); - bool isAvatar() const { return _isAvatar; } + /// evaluate some code in the context of the ScriptEngine and return the result + Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName = QString(), int lineNumber = 1); // this is also used by the script tool widget - void setAvatarData(AvatarData* avatarData, const QString& objectName); - void setAvatarHashMap(AvatarHashMap* avatarHashMap, const QString& objectName); - - bool isListeningToAudioStream() const { return _isListeningToAudioStream; } - void setIsListeningToAudioStream(bool isListeningToAudioStream) { _isListeningToAudioStream = isListeningToAudioStream; } - - void setAvatarSound(Sound* avatarSound) { _avatarSound = avatarSound; } - bool isPlayingAvatarSound() const { return _avatarSound != NULL; } - - void init(); - void run(); /// runs continuously until Agent.stop() is called - void evaluate(); /// initializes the engine, and evaluates the script, but then returns control to caller - - void timerFired(); - - bool hasScript() const { return !_scriptContents.isEmpty(); } - - bool isFinished() const { return _isFinished; } - bool isRunning() const { return _isRunning; } - bool evaluatePending() const { return _evaluatesPending > 0; } - - void setUserLoaded(bool isUserLoaded) { _isUserLoaded = isUserLoaded; } - bool isUserLoaded() const { return _isUserLoaded; } - - void setIsAgent(bool isAgent) { _isAgent = isAgent; } - - void setParentURL(const QString& parentURL) { _parentURL = parentURL; } - - QString getFilename() const; - - static void stopAllScripts(QObject* application); - - void waitTillDoneRunning(); - - virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents); - virtual void errorInLoadingScript(const QUrl& url); + /// if the script engine is not already running, this will download the URL and start the process of seting it up + /// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed + /// to scripts. we may not need this to be invokable + void loadURL(const QUrl& scriptURL, bool reload); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // NOTE - these are intended to be public interfaces available to scripts Q_INVOKABLE void addEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler); Q_INVOKABLE void removeEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler); -public slots: - void loadURL(const QUrl& scriptURL, bool reload); - void stop(); + Q_INVOKABLE void load(const QString& loadfile); + Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void include(const QString& includeFile, QScriptValue callback = QScriptValue()); - QScriptValue evaluate(const QString& program, const QString& fileName = QString(), int lineNumber = 1); - QObject* setInterval(const QScriptValue& function, int intervalMS); - QObject* setTimeout(const QScriptValue& function, int timeoutMS); - void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } - void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast(timer)); } - void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue()); - void include(const QString& includeFile, QScriptValue callback = QScriptValue()); - void load(const QString& loadfile); - void print(const QString& message); - QUrl resolvePath(const QString& path) const; + Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS); + Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS); + Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } + Q_INVOKABLE void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast(timer)); } + Q_INVOKABLE void print(const QString& message); + Q_INVOKABLE QUrl resolvePath(const QString& path) const; - void nodeKilled(SharedNodePointer node); + // Entity Script Related methods + Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload = false); // will call the preload method once loaded + Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method + Q_INVOKABLE void unloadAllEntityScripts(); + Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName); + Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const MouseEvent& event); + Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts + Q_INVOKABLE void stop(); + + bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget + bool isRunning() const { return _isRunning; } // used by ScriptWidget + + static void stopAllScripts(QObject* application); // used by Application on shutdown + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // NOTE - These are the callback implementations for ScriptUser the get called by ScriptCache when the contents + // of a script are available. + virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents); + virtual void errorInLoadingScript(const QUrl& url); + + // These are currently used by Application to track if a script is user loaded or not. Consider finding a solution + // inside of Application so that the ScriptEngine class is not polluted by this notion + void setUserLoaded(bool isUserLoaded) { _isUserLoaded = isUserLoaded; } + bool isUserLoaded() const { return _isUserLoaded; } + + // NOTE - this is used by the TypedArray implemetation. we need to review this for thread safety + ArrayBufferClass* getArrayBufferClass() { return _arrayBufferClass; } signals: void scriptLoaded(const QString& scriptFilename); @@ -146,28 +159,24 @@ protected: bool _isRunning; int _evaluatesPending = 0; bool _isInitialized; - bool _isAvatar; - QTimer* _avatarIdentityTimer; - QTimer* _avatarBillboardTimer; QHash _timerFunctionMap; - bool _isListeningToAudioStream; - Sound* _avatarSound; - int _numAvatarSoundSentBytes; - bool _isAgent = false; QSet _includedURLs; bool _wantSignals = true; - + QHash _entityScripts; private: + void init(); + QString getFilename() const; + void waitTillDoneRunning(); + bool evaluatePending() const { return _evaluatesPending > 0; } + void timerFired(); void stopAllTimers(); - void sendAvatarIdentityPacket(); - void sendAvatarBillboardPacket(); + + void setParentURL(const QString& parentURL) { _parentURL = parentURL; } QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot); void stopTimer(QTimer* timer); AbstractControllerScriptingInterface* _controllerScriptingInterface; - AvatarData* _avatarData; - QString _scriptName; QString _fileNameString; Quat _quatLibrary; Vec3 _vec3Library; @@ -177,15 +186,15 @@ private: ArrayBufferClass* _arrayBufferClass; - QHash _outgoingScriptAudioSequenceNumbers; QHash _registeredHandlers; void generalHandler(const EntityItemID& entityID, const QString& eventName, std::function argGenerator); + Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success); -private: static QSet _allKnownScriptEngines; static QMutex _allScriptsMutex; static bool _stoppingAllScripts; static bool _doneRunningThisScript; + }; #endif // hifi_ScriptEngine_h diff --git a/tests/animation/src/AnimInverseKinematicsTests.cpp b/tests/animation/src/AnimInverseKinematicsTests.cpp index 3958db1242..bb15a1d257 100644 --- a/tests/animation/src/AnimInverseKinematicsTests.cpp +++ b/tests/animation/src/AnimInverseKinematicsTests.cpp @@ -35,7 +35,6 @@ void makeTestFBXJoints(std::vector& fbxJoints) { joint.freeLineage.clear(); joint.parentIndex = -1; joint.distanceToParent = 1.0f; - joint.boneRadius = 1.0f; joint.translation = origin; // T joint.preTransform = glm::mat4(); // Roff * Rp @@ -96,11 +95,11 @@ void makeTestFBXJoints(std::vector& fbxJoints) { void AnimInverseKinematicsTests::testSingleChain() { std::vector fbxJoints; - AnimPose offset; - makeTestFBXJoints(fbxJoints, offset); + makeTestFBXJoints(fbxJoints); // create a skeleton and doll - AnimSkeleton* skeleton = new AnimSkeleton(fbxJoints); + AnimPose offset; + AnimSkeleton* skeleton = new AnimSkeleton(fbxJoints, offset); AnimSkeleton::Pointer skeletonPtr(skeleton); AnimInverseKinematics ikDoll("doll"); ikDoll.setSkeleton(skeletonPtr); @@ -130,10 +129,13 @@ void AnimInverseKinematicsTests::testSingleChain() { // // A------>B------>C------>D // - int indexD = 3; glm::vec3 targetPosition(2.0f, 1.0f, 0.0f); glm::quat targetRotation = glm::angleAxis(PI / 2.0f, zAxis); - ikDoll.updateTarget(indexD, targetPosition, targetRotation); + AnimVariantMap varMap; + varMap.set("positionD", targetPosition); + varMap.set("rotationD", targetRotation); + ikDoll.setTargetVars("D", "positionD", "rotationD"); + AnimNode::Triggers triggers; // the IK solution should be: // @@ -143,7 +145,7 @@ void AnimInverseKinematicsTests::testSingleChain() { // A------>B------>C // float dt = 1.0f; - ikDoll.evaluate(dt); + ikDoll.evaluate(varMap, dt, triggers); // verify absolute results // NOTE: since we expect this solution to converge very quickly (one loop) @@ -204,17 +206,20 @@ void AnimInverseKinematicsTests::testSingleChain() { // | // A------>B t // - int indexD = 3; glm::vec3 targetPosition(3.0f, 0.0f, 0.0f); glm::quat targetRotation = identity; - ikDoll.updateTarget(indexD, targetPosition, targetRotation); + AnimVariantMap varMap; + varMap.set("positionD", targetPosition); + varMap.set("rotationD", targetRotation); + ikDoll.setTargetVars("D", "positionD", "rotationD"); + AnimNode::Triggers triggers; // the IK solution should be: // // A------>B------>C------>D // float dt = 1.0f; - ikDoll.evaluate(dt); + ikDoll.evaluate(varMap, dt, triggers); // verify absolute results // NOTE: the IK algorithm doesn't converge very fast for full-reach targets, diff --git a/tests/animation/src/RotationConstraintTests.cpp b/tests/animation/src/RotationConstraintTests.cpp index 4aac875f2a..7aacf26826 100644 --- a/tests/animation/src/RotationConstraintTests.cpp +++ b/tests/animation/src/RotationConstraintTests.cpp @@ -193,6 +193,7 @@ void RotationConstraintTests::testSwingTwistConstraint() { glm::quat inputRotation = swingRotation * twistRotation * referenceRotation; glm::quat outputRotation = inputRotation; + shoulder.clearHistory(); bool updated = shoulder.apply(outputRotation); QVERIFY(updated == false); QCOMPARE_WITH_ABS_ERROR(inputRotation, outputRotation, EPSILON); @@ -223,6 +224,7 @@ void RotationConstraintTests::testSwingTwistConstraint() { glm::quat inputRotation = swingRotation * twistRotation * referenceRotation; glm::quat outputRotation = inputRotation; + shoulder.clearHistory(); bool updated = shoulder.apply(outputRotation); QVERIFY(updated == true); @@ -257,6 +259,7 @@ void RotationConstraintTests::testSwingTwistConstraint() { glm::quat inputRotation = swingRotation * twistRotation * referenceRotation; glm::quat outputRotation = inputRotation; + shoulder.clearHistory(); bool updated = shoulder.apply(outputRotation); QVERIFY(updated == true); @@ -291,6 +294,7 @@ void RotationConstraintTests::testSwingTwistConstraint() { glm::quat inputRotation = swingRotation * twistRotation * referenceRotation; glm::quat outputRotation = inputRotation; + shoulder.clearHistory(); bool updated = shoulder.apply(outputRotation); QVERIFY(updated == true); diff --git a/tests/animation/src/data/avatar.json b/tests/animation/src/data/avatar.json index 08425201b0..72650893d7 100644 --- a/tests/animation/src/data/avatar.json +++ b/tests/animation/src/data/avatar.json @@ -52,190 +52,426 @@ "children": [] }, { - "id": "mainStateMachine", - "type": "stateMachine", + "id": "rightHandOverlay", + "type": "overlay", "data": { - "currentState": "idle", - "states": [ - { - "id": "idle", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isMovingForward", "state": "walkFwd" }, - { "var": "isMovingBackward", "state": "walkBwd" }, - { "var": "isMovingRight", "state": "strafeRight" }, - { "var": "isMovingLeft", "state": "strafeLeft" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" } - ] - }, - { - "id": "walkFwd", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotMoving", "state": "idle" }, - { "var": "isMovingBackward", "state": "walkBwd" }, - { "var": "isMovingRight", "state": "strafeRight" }, - { "var": "isMovingLeft", "state": "strafeLeft" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" } - ] - }, - { - "id": "walkBwd", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotMoving", "state": "idle" }, - { "var": "isMovingForward", "state": "walkFwd" }, - { "var": "isMovingRight", "state": "strafeRight" }, - { "var": "isMovingLeft", "state": "strafeLeft" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" } - ] - }, - { - "id": "strafeRight", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotMoving", "state": "idle" }, - { "var": "isMovingForward", "state": "walkFwd" }, - { "var": "isMovingBackward", "state": "walkBwd" }, - { "var": "isMovingLeft", "state": "strafeLeft" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" } - ] - }, - { - "id": "strafeLeft", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotMoving", "state": "idle" }, - { "var": "isMovingForward", "state": "walkFwd" }, - { "var": "isMovingBackward", "state": "walkBwd" }, - { "var": "isMovingRight", "state": "strafeRight" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" } - ] - }, - { - "id": "turnRight", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotTurning", "state": "idle" }, - { "var": "isMovingForward", "state": "walkFwd" }, - { "var": "isMovingBackward", "state": "walkBwd" }, - { "var": "isMovingRight", "state": "strafeRight" }, - { "var": "isMovingLeft", "state": "strafeLeft" }, - { "var": "isTurningLeft", "state": "turnLeft" } - ] - }, - { - "id": "turnLeft", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotTurning", "state": "idle" }, - { "var": "isMovingForward", "state": "walkFwd" }, - { "var": "isMovingBackward", "state": "walkBwd" }, - { "var": "isMovingRight", "state": "strafeRight" }, - { "var": "isMovingLeft", "state": "strafeLeft" }, - { "var": "isTurningRight", "state": "turnRight" } - ] - } - ] + "alpha": 1.0, + "boneSet": "rightHand", + "alphaVar": "rightHandOverlayAlpha" }, "children": [ { - "id": "idle", - "type": "clip", + "id": "rightHandStateMachine", + "type": "stateMachine", "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/idle.fbx", - "startFrame": 0.0, - "endFrame": 90.0, - "timeScale": 1.0, - "loopFlag": true + "currentState": "rightHandIdle", + "states": [ + { + "id": "rightHandIdle", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandPoint", "state": "rightHandPointIntro" }, + { "var": "isRightHandGrab", "state": "rightHandGrab" } + ] + }, + { + "id": "rightHandPointIntro", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandIdle", "state": "rightHandIdle" }, + { "var": "isRightHandPointIntroOnDone", "state": "rightHandPointHold" }, + { "var": "isRightHandGrab", "state": "rightHandGrab" } + ] + }, + { + "id": "rightHandPointHold", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandIdle", "state": "rightHandPointOutro" }, + { "var": "isRightHandGrab", "state": "rightHandGrab" } + ] + }, + { + "id": "rightHandPointOutro", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandPointOutroOnDone", "state": "rightHandIdle" }, + { "var": "isRightHandGrab", "state": "rightHandGrab" }, + { "var": "isRightHandPoint", "state": "rightHandPointHold" } + ] + }, + { + "id": "rightHandGrab", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandIdle", "state": "rightHandIdle" }, + { "var": "isRightHandPoint_DISABLED", "state": "rightHandPointHold" } + ] + } + ] }, - "children": [] + "children": [ + { + "id": "rightHandIdle", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/right_hand_point.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightHandPointHold", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/right_hand_point.fbx", + "startFrame": 12.0, + "endFrame": 12.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightHandPointIntro", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/right_hand_point.fbx", + "startFrame": 0.0, + "endFrame": 12.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "rightHandPointOutro", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/right_hand_point.fbx", + "startFrame": 0.0, + "endFrame": 65.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "rightHandGrab", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "rightHandGrabBlend" + }, + "children": [ + { + "id": "rightHandOpen", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/right_hand_point.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightHandClose", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/squeeze_hands/right_hand_anim.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } + ] }, { - "id": "walkFwd", - "type": "clip", + "id": "leftHandOverlay", + "type": "overlay", "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_fwd.fbx", - "startFrame": 0.0, - "endFrame": 35.0, - "timeScale": 1.0, - "loopFlag": true, - "timeScaleVar": "walkTimeScale" + "alpha": 1.0, + "boneSet": "leftHand", + "alphaVar" : "leftHandOverlay" }, - "children": [] - }, - { - "id": "walkBwd", - "type": "clip", - "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_bwd.fbx", - "startFrame": 0.0, - "endFrame": 37.0, - "timeScale": 1.0, - "loopFlag": true, - "timeScaleVar": "walkTimeScale" - }, - "children": [] - }, - { - "id": "turnLeft", - "type": "clip", - "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_left.fbx", - "startFrame": 0.0, - "endFrame": 28.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - }, - { - "id": "turnRight", - "type": "clip", - "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_right.fbx", - "startFrame": 0.0, - "endFrame": 30.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - }, - { - "id": "strafeLeft", - "type": "clip", - "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_left.fbx", - "startFrame": 0.0, - "endFrame": 31.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - }, - { - "id": "strafeRight", - "type": "clip", - "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_right.fbx", - "startFrame": 0.0, - "endFrame": 31.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] + "children": [ + { + "id": "leftHandStateMachine", + "type": "stateMachine", + "data": { + "currentState": "leftHandIdle", + "states": [ + { + "id": "leftHandIdle", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isLeftHandPoint", "state": "leftHandPoint" }, + { "var": "isLeftHandGrab", "state": "leftHandGrab" } + ] + }, + { + "id": "leftHandPoint", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isLeftHandIdle", "state": "leftHandIdle" }, + { "var": "isLeftHandGrab", "state": "leftHandGrab" } + ] + }, + { + "id": "leftHandGrab", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isLeftHandIdle", "state": "leftHandIdle" }, + { "var": "isLeftHandPoint", "state": "leftHandPoint" } + ] + } + ] + }, + "children": [ + { + "id": "leftHandIdle", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/squeeze_hands/left_hand_anim.fbx", + "startFrame": 30.0, + "endFrame": 30.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftHandPoint", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/squeeze_hands/left_hand_anim.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftHandGrab", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/squeeze_hands/left_hand_anim.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "mainStateMachine", + "type": "stateMachine", + "data": { + "currentState": "idle", + "states": [ + { + "id": "idle", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "walkFwd", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "walkBwd", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "strafeRight", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "strafeLeft", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "turnRight", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotTurning", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "turnLeft", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotTurning", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" } + ] + } + ] + }, + "children": [ + { + "id": "idle", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "walkFwd", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_fwd.fbx", + "startFrame": 0.0, + "endFrame": 35.0, + "timeScale": 1.0, + "loopFlag": true, + "timeScaleVar": "walkTimeScale" + }, + "children": [] + }, + { + "id": "walkBwd", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_bwd.fbx", + "startFrame": 0.0, + "endFrame": 37.0, + "timeScale": 1.0, + "loopFlag": true, + "timeScaleVar": "walkTimeScale" + }, + "children": [] + }, + { + "id": "turnLeft", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_left.fbx", + "startFrame": 0.0, + "endFrame": 28.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "turnRight", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_right.fbx", + "startFrame": 0.0, + "endFrame": 30.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeLeft", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_left.fbx", + "startFrame": 0.0, + "endFrame": 31.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeRight", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_right.fbx", + "startFrame": 0.0, + "endFrame": 31.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } + ] } ] }