diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 300976f81c..ddbe164884 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -42,8 +42,7 @@ Agent::Agent(NLPacket& packet) : 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); + _scriptEngine->setParent(this); DependencyManager::get()->setPacketSender(&_entityEditSender); @@ -157,8 +156,10 @@ void Agent::run() { qDebug() << "Downloaded script:" << scriptContents; + _scriptEngine = new ScriptEngine(scriptContents, _payload); + // 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 +167,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 +179,189 @@ 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) { + qDebug() << "processAgentAvatarAndAudio()"; + 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/interface/src/Application.cpp b/interface/src/Application.cpp index 164f04f2eb..89ce392ba0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -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."); 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/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..1ef3769970 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,27 @@ ScriptEngine::~ScriptEngine() { } } +void ScriptEngine::runInThread() { + QThread* workerThread = new QThread(this); + 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); + + // when the thread is terminated, add both scriptEngine and thread to the deleteLater queue + connect(this, &ScriptEngine::doneRunning, this, &ScriptEngine::deleteLater); + connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater); + + // tell the thread to stop when the script engine is done + connect(this, &ScriptEngine::destroyed, workerThread, &QThread::quit); + + moveToThread(workerThread); + + // Starts an event loop, and emits workerThread->started() + workerThread->start(); +} + QSet ScriptEngine::_allKnownScriptEngines; QMutex ScriptEngine::_allScriptsMutex; bool ScriptEngine::_stoppingAllScripts = false; @@ -180,8 +196,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 +223,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 +234,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 +247,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 +321,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 +447,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 +517,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 +550,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 +601,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 +627,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 +856,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