From f85cb2c888f4516bc1ef745cd1048cd7b8665a51 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 11 Sep 2015 19:12:09 -0700 Subject: [PATCH 1/9] first cut at cleaning up ScriptEngine class --- assignment-client/src/Agent.cpp | 7 +- assignment-client/src/Agent.h | 2 +- interface/src/Application.cpp | 68 +++--- .../src/EntityTreeRenderer.cpp | 3 + libraries/script-engine/src/ScriptEngine.cpp | 208 ++++++++++++------ libraries/script-engine/src/ScriptEngine.h | 160 ++++++++------ 6 files changed, 282 insertions(+), 166 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 300976f81c..4da7ff5c78 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -169,9 +169,8 @@ void Agent::run() { _scriptEngine.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"); @@ -185,6 +184,8 @@ void Agent::run() { _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.registerGlobalObject("SoundCache", DependencyManager::get().data()); diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 4c207e59aa..abd99e8c7e 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -37,7 +37,7 @@ class Agent : public ThreadedAssignment { public: Agent(NLPacket& packet); - void setIsAvatar(bool isAvatar) { QMetaObject::invokeMethod(&_scriptEngine, "setIsAvatar", Q_ARG(bool, isAvatar)); } + void setIsAvatar(bool isAvatar) { _scriptEngine.setIsAvatar(isAvatar); } bool isAvatar() const { return _scriptEngine.isAvatar(); } bool isPlayingAvatarSound() const { return _scriptEngine.isPlayingAvatarSound(); } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e4f902ef1d..4abfed1a03 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4011,8 +4011,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); @@ -4036,9 +4036,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); @@ -4068,9 +4068,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()); @@ -4079,31 +4079,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() { @@ -4249,17 +4224,34 @@ 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... + + qDebug() << "############################# HELLO WE ARE HERE!!!! ##################################"; // 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(); + + //FIXME - intentionally attempting to test thread safe call to registerGlobalObject() + qDebug() << "about to attempt to call registerGlobalObject on wrong thread!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; + scriptEngine->registerGlobalObject("LODManager2", DependencyManager::get().data()); + + qDebug() << "about to attempt to call registerFunction on wrong thread!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; + scriptEngine->registerFunction("WebWindow2", WebWindowClass::constructor, 1); + + qDebug() << "about to attempt to call registerGetterSetter on wrong thread!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; + scriptEngine->registerGetterSetter("foo_location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter); + + } else { // connect to the appropriate signals of this script engine connect(scriptEngine, &ScriptEngine::scriptLoaded, this, &Application::handleScriptEngineLoaded); connect(scriptEngine, &ScriptEngine::errorLoadingScript, this, &Application::handleScriptLoadError); // get the script engine object to load the script at the designated script URL + qDebug() << "calling scriptEngine->loadURL(" << scriptUrl << ", " << reload << ");"; scriptEngine->loadURL(scriptUrl, reload); } @@ -4276,6 +4268,7 @@ void Application::reloadScript(const QString& scriptName, bool isUserLoaded) { } void Application::handleScriptEngineLoaded(const QString& scriptFilename) { + qDebug() << "handleScriptEngineLoaded().... scriptFilename:" << scriptFilename; ScriptEngine* scriptEngine = qobject_cast(sender()); _scriptEnginesHash.insertMulti(scriptFilename, scriptEngine); @@ -4284,6 +4277,17 @@ void Application::handleScriptEngineLoaded(const QString& scriptFilename) { // register our application services and set it off on its own thread registerScriptEngineWithApplicationServices(scriptEngine); + scriptEngine->runInThread(); + + //FIXME - intentionally attempting to test thread safe call to registerGlobalObject() + qDebug() << "about to attempt to call registerGlobalObject on wrong thread!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; + scriptEngine->registerGlobalObject("LODManager2", DependencyManager::get().data()); + + qDebug() << "about to attempt to call registerFunction on wrong thread!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; + scriptEngine->registerFunction("WebWindow2", WebWindowClass::constructor, 1); + + qDebug() << "about to attempt to call registerGetterSetter on wrong thread!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; + scriptEngine->registerGetterSetter("foo_location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter); } void Application::handleScriptLoadError(const QString& scriptFilename) { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 93a9996740..61ff8bbea8 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -116,6 +116,9 @@ void EntityTreeRenderer::init() { _scriptingServices->getControllerScriptingInterface(), false); _scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine); + // FIXME - this is dubious need to rework + _entitiesScriptEngine->runInThread(); + _sandboxScriptEngine = new ScriptEngine(NO_SCRIPT, "Entities Sandbox", NULL, false); } diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index e401398d24..d9adb1f6d2 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -44,6 +44,8 @@ #include "MIDIEvent.h" +Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) +static int functionSignatureMetaID = qRegisterMetaType(); static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ QString message = ""; @@ -97,7 +99,6 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam _controllerScriptingInterface(controllerScriptingInterface), _wantSignals(wantSignals), _avatarData(NULL), - _scriptName(), _fileNameString(fileNameString), _quatLibrary(), _vec3Library(), @@ -122,6 +123,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 +202,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) { @@ -244,14 +264,6 @@ void ScriptEngine::setAvatarData(AvatarData* avatarData, const QString& objectNa 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; @@ -261,7 +273,10 @@ bool ScriptEngine::setScriptContents(const QString& scriptContents, const QStrin return true; } +// FIXME - remove the file/url scheme code here and let the script cache handle things void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { + qDebug() << "ScriptEngine::loadURL(" << scriptURL << ", " << reload << ");"; + if (_isRunning) { return; } @@ -298,7 +313,10 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { } else { bool isPending; auto scriptCache = DependencyManager::get(); + qDebug() << "calling scriptCache->getScript(" << url << ", " << reload << ");"; scriptCache->getScript(url, this, isPending, reload); + qDebug() << " scriptCache->getScript(" << url << ", " << reload << ") ... isPending:" << isPending; + } } } @@ -383,57 +401,97 @@ 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()) { + qDebug() << "*** WARNING *** ScriptEngine::registerGlobalObject() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; + QMetaObject::invokeMethod(this, "registerGlobalObject", + Q_ARG(const QString&, name), + Q_ARG(QObject*, object)); + return; + } + //qDebug() << "ScriptEngine::registerGlobalObject() called on thread [" << QThread::currentThread() << "] name:" << name; + 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()) { + qDebug() << "*** WARNING *** ScriptEngine::registerFunction() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; + QMetaObject::invokeMethod(this, "registerFunction", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, functionSignature), + Q_ARG(int, numArguments)); + return; + } + //qDebug() << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] name:" << name; + + 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()) { + qDebug() << "*** WARNING *** ScriptEngine::registerFunction() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] parent:" << parent << "name:" << name; + QMetaObject::invokeMethod(this, "registerFunction", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, functionSignature), + Q_ARG(int, numArguments)); + return; + } + //qDebug() << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] parent:" << parent << "name:" << name; + + 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()) { + qDebug() << "*** WARNING *** ScriptEngine::registerGetterSetter() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + " name:" << name << "parent:" << parent; + 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; + } + //qDebug() << "ScriptEngine::registerGetterSetter() called on thread [" << QThread::currentThread() << "] name:" << name << "parent:" << parent; + 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()) { + qDebug() << "*** WARNING *** ScriptEngine::removeEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << " eventName:" << eventName; + QMetaObject::invokeMethod(this, "removeEventHandler", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, eventName), + Q_ARG(QScriptValue, handler)); + return; + } + //qDebug() << "ScriptEngine::removeEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; + if (!_registeredHandlers.contains(entityID)) { return; } @@ -449,6 +507,17 @@ 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()) { + qDebug() << "*** WARNING *** ScriptEngine::addEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << " eventName:" << eventName; + QMetaObject::invokeMethod(this, "addEventHandler", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, eventName), + Q_ARG(QScriptValue, handler)); + return; + } + //qDebug() << "ScriptEngine::addEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; + 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 +572,23 @@ 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; + qDebug() << "*** WARNING *** ScriptEngine::evaluate() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "program:" << program << " fileName:" << fileName << "lineNumber:" << lineNumber; + 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()) { @@ -967,6 +1025,26 @@ 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); + } + } } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index b9156c718e..ed8d6ad727 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -50,79 +50,75 @@ public: ~ScriptEngine(); - ArrayBufferClass* getArrayBufferClass() { return _arrayBufferClass; } + /// Launch the script running 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(); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // 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 + + /// registers a global object by name + Q_INVOKABLE void registerGlobalObject(const QString& name, QObject* object); + + /// registers a global getter/setter + Q_INVOKABLE void registerGetterSetter(const QString& name, QScriptEngine::FunctionSignature getter, + QScriptEngine::FunctionSignature setter, const QString& parent = QString("")); - /// 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("")); + /// register a global function + Q_INVOKABLE void registerFunction(const QString& name, QScriptEngine::FunctionSignature fun, int numArguments = -1); - const QString& getScriptName() const { return _scriptName; } - - 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, + /// 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); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // 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); @@ -158,16 +154,17 @@ protected: bool _wantSignals = true; private: + QString getFilename() const; + void waitTillDoneRunning(); + bool evaluatePending() const { return _evaluatesPending > 0; } + void timerFired(); void stopAllTimers(); - void sendAvatarIdentityPacket(); - void sendAvatarBillboardPacket(); 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; @@ -186,6 +183,39 @@ private: static QMutex _allScriptsMutex; static bool _stoppingAllScripts; static bool _doneRunningThisScript; + +private: + //FIXME- EntityTreeRender shouldn't be using these methods directly -- these methods need to be depricated from the public interfaces + friend class EntityTreeRenderer; + + //FIXME - used in EntityTreeRenderer when evaluating entity scripts, which needs to be moved into ScriptEngine + // also used in Agent, but that would be fixed if Agent was using proper apis for loading content + void setParentURL(const QString& parentURL) { _parentURL = parentURL; } + +private: + //FIXME- Agent shouldn't be using these methods directly -- these methods need to be depricated from the public interfaces + friend class Agent; + + /// FIXME - DEPRICATED - remove callers and fix to use standard API + /// 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("")); + + /// FIXME - these should only be used internally + void init(); + void run(); + + // FIXME - all of these needto be removed and the code that depends on it in Agent.cpp should be moved into Agent.cpp + void setIsAgent(bool isAgent) { _isAgent = isAgent; } + void setIsAvatar(bool isAvatar); + bool isAvatar() const { return _isAvatar; } + void setAvatarData(AvatarData* avatarData, 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 sendAvatarIdentityPacket(); + void sendAvatarBillboardPacket(); }; #endif // hifi_ScriptEngine_h From 8395fb6eff2e21e0c53fed9069c52fb315b65a2a Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 11 Sep 2015 21:24:39 -0700 Subject: [PATCH 2/9] normalize how ScriptCache::getScript() works relative to ResourceManager --- libraries/networking/src/ResourceManager.cpp | 26 +++++++++----- libraries/networking/src/ResourceManager.h | 1 + libraries/script-engine/src/ScriptCache.cpp | 6 ++-- libraries/script-engine/src/ScriptCache.h | 4 +-- libraries/script-engine/src/ScriptEngine.cpp | 38 ++------------------ 5 files changed, 28 insertions(+), 47 deletions(-) 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..38837ec4b9 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -26,7 +26,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(); @@ -50,7 +51,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); diff --git a/libraries/script-engine/src/ScriptCache.h b/libraries/script-engine/src/ScriptCache.h index 25a36c04d8..89db5596ba 100644 --- a/libraries/script-engine/src/ScriptCache.h +++ b/libraries/script-engine/src/ScriptCache.h @@ -26,8 +26,8 @@ class ScriptCache : public QObject, public Dependency { SINGLETON_DEPENDENCY public: - QString getScript(const QUrl& url, ScriptUser* scriptUser, bool& isPending, bool redownload = false); - void deleteScript(const QUrl& url); + QString getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUser, bool& isPending, bool redownload = false); + void deleteScript(const QUrl& unnormalizedURL); void addScriptToBadScriptList(const QUrl& url) { _badScripts.insert(url); } bool isInBadScriptList(const QUrl& url) { return _badScripts.contains(url); } diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index d9adb1f6d2..5c4623f7cf 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -275,8 +275,6 @@ bool ScriptEngine::setScriptContents(const QString& scriptContents, const QStrin // FIXME - remove the file/url scheme code here and let the script cache handle things void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { - qDebug() << "ScriptEngine::loadURL(" << scriptURL << ", " << reload << ");"; - if (_isRunning) { return; } @@ -286,39 +284,9 @@ 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(); - qDebug() << "calling scriptCache->getScript(" << url << ", " << reload << ");"; - scriptCache->getScript(url, this, isPending, reload); - qDebug() << " scriptCache->getScript(" << url << ", " << reload << ") ... isPending:" << isPending; - - } - } + bool isPending; + auto scriptCache = DependencyManager::get(); + scriptCache->getScript(url, this, isPending, reload); } void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) { From 54c56a92f1f5f1d63b78f3d3856aac5c2511bc64 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 11 Sep 2015 21:56:00 -0700 Subject: [PATCH 3/9] move agent specific code out of ScriptEngine --- assignment-client/src/Agent.cpp | 159 ++++++++++++++++++- assignment-client/src/Agent.h | 37 +++-- libraries/script-engine/src/ScriptEngine.cpp | 158 ------------------ libraries/script-engine/src/ScriptEngine.h | 23 +-- 4 files changed, 185 insertions(+), 192 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 4da7ff5c78..3cbc9cbdd7 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -43,7 +43,6 @@ Agent::Agent(NLPacket& packet) : { // be the parent of the script engine so it gets moved when we do _scriptEngine.setParent(this); - _scriptEngine.setIsAgent(true); DependencyManager::get()->setPacketSender(&_entityEditSender); @@ -166,7 +165,7 @@ 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.registerGlobalObject("AvatarList", avatarHashMap.data()); @@ -200,9 +199,165 @@ void Agent::run() { _entityViewer.init(); entityScriptingInterface->setEntityTree(_entityViewer.getTree()); + // wire up our additional agent related processing to the update signal + QObject::connect(&_scriptEngine, &ScriptEngine::update, this, &Agent::processAgentAvatarAndAudio); + _scriptEngine.setScriptContents(scriptContents); _scriptEngine.run(); setFinished(true); + + // kill the avatar identity timer + delete _avatarIdentityTimer; + +} + +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() { diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index abd99e8c7e..8d16079325 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -37,28 +37,28 @@ class Agent : public ThreadedAssignment { public: Agent(NLPacket& packet); - void setIsAvatar(bool isAvatar) { _scriptEngine.setIsAvatar(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; @@ -68,6 +68,23 @@ private: MixedAudioStream _receivedAudioStream; float _lastReceivedAudioLoudness; + + void setAvatarData(AvatarData* avatarData, const QString& objectName); + void setAvatarSound(Sound* avatarSound) { _avatarSound = avatarSound; } + + void sendAvatarIdentityPacket(); + void sendAvatarBillboardPacket(); + + // FIXME - Agent only data + 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/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 5c4623f7cf..3b01915e6d 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -89,16 +89,8 @@ 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), _controllerScriptingInterface(controllerScriptingInterface), _wantSignals(wantSignals), - _avatarData(NULL), _fileNameString(fileNameString), _quatLibrary(), _vec3Library(), @@ -229,40 +221,6 @@ 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); -} bool ScriptEngine::setScriptContents(const QString& scriptContents, const QString& fileNameString) { if (_isRunning) { @@ -571,18 +529,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? @@ -634,107 +580,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; @@ -761,9 +606,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(); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index ed8d6ad727..f783c1259b 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -142,17 +142,10 @@ 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; - + private: QString getFilename() const; void waitTillDoneRunning(); @@ -164,7 +157,6 @@ private: void stopTimer(QTimer* timer); AbstractControllerScriptingInterface* _controllerScriptingInterface; - AvatarData* _avatarData; QString _fileNameString; Quat _quatLibrary; Vec3 _vec3Library; @@ -174,7 +166,6 @@ private: ArrayBufferClass* _arrayBufferClass; - QHash _outgoingScriptAudioSequenceNumbers; QHash _registeredHandlers; void generalHandler(const EntityItemID& entityID, const QString& eventName, std::function argGenerator); @@ -204,18 +195,6 @@ private: void init(); void run(); - // FIXME - all of these needto be removed and the code that depends on it in Agent.cpp should be moved into Agent.cpp - void setIsAgent(bool isAgent) { _isAgent = isAgent; } - void setIsAvatar(bool isAvatar); - bool isAvatar() const { return _isAvatar; } - void setAvatarData(AvatarData* avatarData, 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 sendAvatarIdentityPacket(); - void sendAvatarBillboardPacket(); }; #endif // hifi_ScriptEngine_h From 56118e420491e46aaff05d27b66c347090324a72 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 11 Sep 2015 22:12:42 -0700 Subject: [PATCH 4/9] migrate Agent to use standard APIs of ScriptEngine --- assignment-client/src/Agent.cpp | 38 ++++++++++---------- assignment-client/src/Agent.h | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 10 ------ libraries/script-engine/src/ScriptEngine.h | 18 +++------- 4 files changed, 25 insertions(+), 43 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 3cbc9cbdd7..ddbe164884 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -42,7 +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->setParent(this); DependencyManager::get()->setPacketSender(&_entityEditSender); @@ -156,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 @@ -168,7 +170,7 @@ void Agent::run() { setAvatarData(&scriptedAvatar, "Avatar"); auto avatarHashMap = DependencyManager::set(); - _scriptEngine.registerGlobalObject("AvatarList", avatarHashMap.data()); + _scriptEngine->registerGlobalObject("AvatarList", avatarHashMap.data()); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListener(PacketType::BulkAvatarData, avatarHashMap.data(), "processAvatarDataPacket"); @@ -177,38 +179,36 @@ void Agent::run() { packetReceiver.registerListener(PacketType::AvatarBillboard, avatarHashMap.data(), "processAvatarBillboardPacket"); // register ourselves to the script engine - _scriptEngine.registerGlobalObject("Agent", this); - - if (!_payload.isEmpty()) { - _scriptEngine.setParentURL(_payload); - } + _scriptEngine->registerGlobalObject("Agent", this); // 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()); // wire up our additional agent related processing to the update signal - QObject::connect(&_scriptEngine, &ScriptEngine::update, this, &Agent::processAgentAvatarAndAudio); + QObject::connect(_scriptEngine, &ScriptEngine::update, this, &Agent::processAgentAvatarAndAudio); - _scriptEngine.setScriptContents(scriptContents); - _scriptEngine.run(); + _scriptEngine->run(); setFinished(true); // kill the avatar identity timer delete _avatarIdentityTimer; + // delete the script engine + delete _scriptEngine; + } void Agent::setIsAvatar(bool isAvatar) { @@ -238,7 +238,7 @@ void Agent::setIsAvatar(bool isAvatar) { void Agent::setAvatarData(AvatarData* avatarData, const QString& objectName) { _avatarData = avatarData; - _scriptEngine.registerGlobalObject(objectName, avatarData); + _scriptEngine->registerGlobalObject(objectName, avatarData); } void Agent::sendAvatarIdentityPacket() { @@ -256,7 +256,7 @@ void Agent::sendAvatarBillboardPacket() { void Agent::processAgentAvatarAndAudio(float deltaTime) { qDebug() << "processAgentAvatarAndAudio()"; - if (!_scriptEngine.isFinished() && _isAvatar && _avatarData) { + if (!_scriptEngine->isFinished() && _isAvatar && _avatarData) { const int SCRIPT_AUDIO_BUFFER_SAMPLES = floor(((SCRIPT_DATA_CALLBACK_USECS * AudioConstants::SAMPLE_RATE) / (1000 * 1000)) + 0.5); @@ -361,7 +361,7 @@ void Agent::processAgentAvatarAndAudio(float deltaTime) { } 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 8d16079325..52e1dfec6c 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -61,7 +61,7 @@ private slots: void processAgentAvatarAndAudio(float deltaTime); private: - ScriptEngine _scriptEngine; + ScriptEngine* _scriptEngine; EntityEditPacketSender _entityEditSender; EntityTreeHeadlessViewer _entityViewer; QTimer* _pingTimer; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 3b01915e6d..c69f017f90 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -221,16 +221,6 @@ QString ScriptEngine::getFilename() const { } - -bool ScriptEngine::setScriptContents(const QString& scriptContents, const QString& fileNameString) { - if (_isRunning) { - return false; - } - _scriptContents = scriptContents; - _fileNameString = fileNameString; - return true; -} - // FIXME - remove the file/url scheme code here and let the script cache handle things void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { if (_isRunning) { diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index f783c1259b..19c96822c2 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -50,11 +50,14 @@ public: ~ScriptEngine(); - /// Launch the script running in a dedicated thread. This will have the side effect of evalulating + /// 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(); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 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 @@ -147,6 +150,7 @@ protected: bool _wantSignals = true; private: + void init(); QString getFilename() const; void waitTillDoneRunning(); bool evaluatePending() const { return _evaluatesPending > 0; } @@ -183,18 +187,6 @@ private: // also used in Agent, but that would be fixed if Agent was using proper apis for loading content void setParentURL(const QString& parentURL) { _parentURL = parentURL; } -private: - //FIXME- Agent shouldn't be using these methods directly -- these methods need to be depricated from the public interfaces - friend class Agent; - - /// FIXME - DEPRICATED - remove callers and fix to use standard API - /// 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("")); - - /// FIXME - these should only be used internally - void init(); - void run(); - }; #endif // hifi_ScriptEngine_h From 18fbf896f11af63eee0174ec4d5a3157ef320077 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 14 Sep 2015 15:13:43 -0700 Subject: [PATCH 5/9] first cut at moving entity scripts into ScriptEngine --- .../src/EntityTreeRenderer.cpp | 329 ++---------------- .../src/EntityTreeRenderer.h | 14 +- libraries/script-engine/src/ScriptCache.cpp | 47 +++ libraries/script-engine/src/ScriptCache.h | 14 +- libraries/script-engine/src/ScriptEngine.cpp | 179 ++++++++++ libraries/script-engine/src/ScriptEngine.h | 16 + 6 files changed, 283 insertions(+), 316 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 61ff8bbea8..22d857c763 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -90,10 +90,12 @@ EntityTreeRenderer::~EntityTreeRenderer() { void EntityTreeRenderer::clear() { leaveAllEntities(); + /* foreach (const EntityItemID& entityID, _entityScripts.keys()) { checkAndCallUnload(entityID); } _entityScripts.clear(); + */ auto scene = _viewState->getMain3DScene(); render::PendingChanges pendingChanges; @@ -136,175 +138,8 @@ 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 = static_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(Octree* newTree) { OctreeRenderer::setTree(newTree); @@ -324,11 +159,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); } } @@ -356,19 +187,14 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { _tree->unlock(); // 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"); } } @@ -376,11 +202,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; @@ -395,11 +217,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(); @@ -821,27 +639,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. @@ -864,18 +661,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); } @@ -896,24 +686,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 @@ -935,17 +715,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... @@ -953,30 +725,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; @@ -987,14 +748,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 } } @@ -1003,13 +757,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; @@ -1017,9 +765,9 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { if (_tree && !_shuttingDown) { - checkAndCallUnload(entityID); + //checkAndCallUnload(entityID); } - _entityScripts.remove(entityID); + //_entityScripts.remove(entityID); // here's where we remove the entity payload from the scene if (_entitiesInScene.contains(entityID)) { @@ -1032,7 +780,7 @@ void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { } void EntityTreeRenderer::addingEntity(const EntityItemID& entityID) { - checkAndCallPreload(entityID); + //checkAndCallPreload(entityID); auto entity = static_cast(_tree)->findEntityByID(entityID); if (entity) { addEntityToScene(entity); @@ -1059,22 +807,14 @@ void EntityTreeRenderer::entitySciptChanging(const EntityItemID& entityID, const 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); - } + EntityItemPointer entity = getTree()->findEntityByEntityItemID(entityID); + _entitiesScriptEngine->loadEntityScript(entityID, entity->getScript(), reload); } } 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); - } + _entitiesScriptEngine->unloadEntityScript(entityID); } } @@ -1152,24 +892,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 1bfbdaf810..b9a3eb5cdd 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; } @@ -157,10 +149,8 @@ private: 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; + //QHash _entityScripts; void playEntityCollisionSound(const QUuid& myNodeID, EntityTree* entityTree, const EntityItemID& id, const Collision& collision); diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index 38837ec4b9..1b4997c5cd 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -81,4 +81,51 @@ void ScriptCache::scriptDownloaded() { req->deleteLater(); } +void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload) { + 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(); + 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 { + auto request = ResourceManager::createResourceRequest(this, url); + request->setCacheEnabled(!forceDownload); + connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptContentAvailable); + request->send(); + } + } +} + +void ScriptCache::scriptContentAvailable() { + 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 89db5596ba..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: + 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 c69f017f90..f277a40470 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 @@ -848,3 +849,181 @@ void ScriptEngine::generalHandler(const EntityItemID& entityID, const QString& e } } } + +// 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()) { + qDebug() << "*** WARNING *** ScriptEngine::loadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << + "entityScript:" << entityScript << + "forceRedownload:" << forceRedownload; + + QMetaObject::invokeMethod(this, "loadEntityScript", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, entityScript), + Q_ARG(bool, forceRedownload)); + return; + } + qDebug() << "ScriptEngine::loadEntityScript() called on correct thread [" << thread() << "] " + "entityID:" << entityID << + "entityScript:" << entityScript << + "forceRedownload:" << forceRedownload; + + // If we've been called our known entityScripts should not know about us.. + assert(!_entityScripts.contains(entityID)); + + auto scriptCache = DependencyManager::get(); + + scriptCache->getScriptContents(entityScript, [=](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { + + // 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"); + + }, forceRedownload); +} + +void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { + if (QThread::currentThread() != thread()) { + qDebug() << "*** WARNING *** ScriptEngine::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID; + + QMetaObject::invokeMethod(this, "unloadEntityScript", + Q_ARG(const EntityItemID&, entityID)); + return; + } + qDebug() << "ScriptEngine::unloadEntityScript() called on correct thread [" << thread() << "] " + "entityID:" << entityID; + + if (_entityScripts.contains(entityID)) { + callEntityScriptMethod(entityID, "unload"); + _entityScripts.remove(entityID); + } +} + +void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName) { + if (QThread::currentThread() != thread()) { + qDebug() << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << + "methodName:" << methodName; + + QMetaObject::invokeMethod(this, "callEntityScriptMethod", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, methodName)); + return; + } + qDebug() << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " + "entityID:" << entityID << + "methodName:" << methodName; + + 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()) { + qDebug() << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << + "methodName:" << methodName << + "event: mouseEvent"; + + QMetaObject::invokeMethod(this, "callEntityScriptMethod", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, methodName), + Q_ARG(const MouseEvent&, event)); + return; + } + qDebug() << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " + "entityID:" << entityID << + "methodName:" << methodName << + "event: mouseEvent"; + + 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()) { + qDebug() << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " + "entityID:" << entityID << + "methodName:" << methodName << + "otherID:" << otherID << + "collision: collision"; + + 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; + } + qDebug() << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " + "entityID:" << entityID << + "methodName:" << methodName << + "otherID:" << otherID << + "collision: collision"; + + 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 19c96822c2..ab1833636c 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: @@ -100,6 +106,13 @@ public: Q_INVOKABLE void print(const QString& message); Q_INVOKABLE QUrl resolvePath(const QString& path) const; + // 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 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(); @@ -149,6 +162,9 @@ protected: QSet _includedURLs; bool _wantSignals = true; + QHash _entityScripts; + + private: void init(); QString getFilename() const; From a3c0288eae5b28f0080ff38a0fc2707f01c2f230 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 14 Sep 2015 20:26:04 -0700 Subject: [PATCH 6/9] more work on proper threading of ScriptEngine --- interface/src/Application.cpp | 26 +-- .../src/EntityTreeRenderer.cpp | 22 +- .../src/EntityTreeRenderer.h | 8 - libraries/script-engine/src/ScriptCache.cpp | 17 +- libraries/script-engine/src/ScriptEngine.cpp | 195 ++++++++++++------ libraries/script-engine/src/ScriptEngine.h | 1 + 6 files changed, 152 insertions(+), 117 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4abfed1a03..a4b52cc67e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4226,25 +4226,12 @@ ScriptEngine* Application::loadScript(const QString& scriptFilename, bool isUser if (scriptFilename.isNull()) { // This appears to be the script engine used by the script widget's evaluation window before the file has been saved... - qDebug() << "############################# HELLO WE ARE HERE!!!! ##################################"; // 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(); - - //FIXME - intentionally attempting to test thread safe call to registerGlobalObject() - qDebug() << "about to attempt to call registerGlobalObject on wrong thread!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; - scriptEngine->registerGlobalObject("LODManager2", DependencyManager::get().data()); - - qDebug() << "about to attempt to call registerFunction on wrong thread!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; - scriptEngine->registerFunction("WebWindow2", WebWindowClass::constructor, 1); - - qDebug() << "about to attempt to call registerGetterSetter on wrong thread!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; - scriptEngine->registerGetterSetter("foo_location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter); - - } else { // connect to the appropriate signals of this script engine connect(scriptEngine, &ScriptEngine::scriptLoaded, this, &Application::handleScriptEngineLoaded); @@ -4267,8 +4254,8 @@ 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) { - qDebug() << "handleScriptEngineLoaded().... scriptFilename:" << scriptFilename; ScriptEngine* scriptEngine = qobject_cast(sender()); _scriptEnginesHash.insertMulti(scriptFilename, scriptEngine); @@ -4278,18 +4265,9 @@ void Application::handleScriptEngineLoaded(const QString& scriptFilename) { // register our application services and set it off on its own thread registerScriptEngineWithApplicationServices(scriptEngine); scriptEngine->runInThread(); - - //FIXME - intentionally attempting to test thread safe call to registerGlobalObject() - qDebug() << "about to attempt to call registerGlobalObject on wrong thread!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; - scriptEngine->registerGlobalObject("LODManager2", DependencyManager::get().data()); - - qDebug() << "about to attempt to call registerFunction on wrong thread!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; - scriptEngine->registerFunction("WebWindow2", WebWindowClass::constructor, 1); - - qDebug() << "about to attempt to call registerGetterSetter on wrong thread!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; - scriptEngine->registerGetterSetter("foo_location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter); } +// 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 22d857c763..d34aabf6ca 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,15 +76,6 @@ 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() { @@ -115,13 +105,9 @@ void EntityTreeRenderer::init() { if (_wantScripts) { _entitiesScriptEngine = new ScriptEngine(NO_SCRIPT, "Entities", - _scriptingServices->getControllerScriptingInterface(), false); + _scriptingServices->getControllerScriptingInterface()); _scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine); - - // FIXME - this is dubious need to rework _entitiesScriptEngine->runInThread(); - - _sandboxScriptEngine = new ScriptEngine(NO_SCRIPT, "Entities Sandbox", NULL, false); } // make sure our "last avatar position" is something other than our current position, so that on our @@ -780,7 +766,7 @@ void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { } void EntityTreeRenderer::addingEntity(const EntityItemID& entityID) { - //checkAndCallPreload(entityID); + checkAndCallPreload(entityID); auto entity = static_cast(_tree)->findEntityByID(entityID); if (entity) { addEntityToScene(entity); @@ -808,7 +794,9 @@ void EntityTreeRenderer::entitySciptChanging(const EntityItemID& entityID, const void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload) { if (_tree && !_shuttingDown) { EntityItemPointer entity = getTree()->findEntityByEntityItemID(entityID); - _entitiesScriptEngine->loadEntityScript(entityID, entity->getScript(), reload); + if (!entity->getScript().isEmpty()) { + _entitiesScriptEngine->loadEntityScript(entityID, entity->getScript(), reload); + } } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index b9a3eb5cdd..d740bb2729 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -143,14 +143,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); - - //QHash _entityScripts; void playEntityCollisionSound(const QUuid& myNodeID, EntityTree* entityTree, const EntityItemID& id, const Collision& collision); diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index 1b4997c5cd..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 @@ -42,7 +43,7 @@ QString ScriptCache::getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUs 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(); @@ -82,6 +83,9 @@ void ScriptCache::scriptDownloaded() { } 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); @@ -93,6 +97,9 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable 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); @@ -101,7 +108,10 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable if (alreadyWaiting) { qCDebug(scriptengine) << "Already downloading script at:" << url.toString(); } else { - auto request = ResourceManager::createResourceRequest(this, url); + #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(); @@ -110,6 +120,9 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable } 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); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index f277a40470..e5ce867df3 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -222,7 +222,7 @@ QString ScriptEngine::getFilename() const { } -// FIXME - remove the file/url scheme code here and let the script cache handle things +// FIXME - switch this to the new model of ScriptCache callbacks void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { if (_isRunning) { return; @@ -238,6 +238,7 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { 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) { @@ -245,6 +246,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) { @@ -320,13 +322,17 @@ void ScriptEngine::init() { 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; } - //qDebug() << "ScriptEngine::registerGlobalObject() called on thread [" << QThread::currentThread() << "] name:" << name; + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::registerGlobalObject() called on thread [" << QThread::currentThread() << "] name:" << name; + #endif if (object) { QScriptValue value = newQObject(object); @@ -336,14 +342,18 @@ void ScriptEngine::registerGlobalObject(const QString& name, QObject* object) { 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; } - //qDebug() << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] name:" << name; + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] name:" << name; + #endif QScriptValue scriptFun = newFunction(functionSignature, numArguments); globalObject().setProperty(name, scriptFun); @@ -351,14 +361,18 @@ void ScriptEngine::registerFunction(const QString& name, QScriptEngine::Function 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; } - //qDebug() << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] parent:" << parent << "name:" << name; + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] parent:" << parent << "name:" << name; + #endif QScriptValue object = globalObject().property(parent); if (object.isValid()) { @@ -370,8 +384,10 @@ void ScriptEngine::registerFunction(const QString& parent, const QString& name, void ScriptEngine::registerGetterSetter(const QString& name, QScriptEngine::FunctionSignature getter, 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), @@ -379,7 +395,9 @@ void ScriptEngine::registerGetterSetter(const QString& name, QScriptEngine::Func Q_ARG(const QString&, parent)); return; } - //qDebug() << "ScriptEngine::registerGetterSetter() called on thread [" << QThread::currentThread() << "] name:" << name << "parent:" << parent; + #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); @@ -399,15 +417,19 @@ void ScriptEngine::registerGetterSetter(const QString& name, QScriptEngine::Func // 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; } - //qDebug() << "ScriptEngine::removeEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; + #ifdef THREAD_DEBUGGING + qDebug() << "ScriptEngine::removeEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; + #endif if (!_registeredHandlers.contains(entityID)) { return; @@ -425,15 +447,20 @@ 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; } - //qDebug() << "ScriptEngine::addEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; + #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. @@ -496,8 +523,10 @@ QScriptValue ScriptEngine::evaluate(const QString& program, const QString& fileN 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), @@ -854,10 +883,10 @@ void ScriptEngine::generalHandler(const EntityItemID& entityID, const QString& e // 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; + "entityID:" << entityID << "entityScript:" << entityScript <<"forceRedownload:" << forceRedownload; + #endif QMetaObject::invokeMethod(this, "loadEntityScript", Q_ARG(const EntityItemID&, entityID), @@ -865,71 +894,104 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& Q_ARG(bool, forceRedownload)); return; } + #ifdef THREAD_DEBUGGING qDebug() << "ScriptEngine::loadEntityScript() called on correct thread [" << thread() << "] " - "entityID:" << entityID << - "entityScript:" << entityScript << - "forceRedownload:" << forceRedownload; + "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(); - scriptCache->getScriptContents(entityScript, [=](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { - - // 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() << ":" + // 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 - } + qCDebug(scriptengine) << " SCRIPT:" << scriptOrURL; + scriptCache->addScriptToBadScriptList(scriptOrURL); + return; // done processing script + } - if (isURL) { - setParentURL(scriptOrURL); - } + if (isURL) { + setParentURL(scriptOrURL); + } - QScriptEngine sandbox; - QScriptValue testConstructor = sandbox.evaluate(contents); + 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 - } + 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 entityScriptConstructor = evaluate(contents); - QScriptValue entityScriptObject = entityScriptConstructor.construct(); - EntityScriptDetails newDetails = { scriptOrURL, entityScriptObject }; - _entityScripts[entityID] = newDetails; + QScriptValue entityScriptObject = entityScriptConstructor.construct(); + EntityScriptDetails newDetails = { scriptOrURL, entityScriptObject }; + _entityScripts[entityID] = newDetails; + if (isURL) { + setParentURL(""); + } - if (isURL) { - setParentURL(""); - } - - // if we got this far, then call the preload method - callEntityScriptMethod(entityID, "preload"); - - }, forceRedownload); + // 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"); @@ -939,18 +1001,20 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { 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; + "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; + "entityID:" << entityID << "methodName:" << methodName; + #endif if (_entityScripts.contains(entityID)) { EntityScriptDetails details = _entityScripts[entityID]; @@ -960,15 +1024,16 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS 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"; + "entityID:" << entityID << "methodName:" << methodName << "event: mouseEvent"; + #endif QMetaObject::invokeMethod(this, "callEntityScriptMethod", Q_ARG(const EntityItemID&, entityID), @@ -976,10 +1041,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS Q_ARG(const MouseEvent&, event)); return; } + #ifdef THREAD_DEBUGGING qDebug() << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " - "entityID:" << entityID << - "methodName:" << methodName << - "event: mouseEvent"; + "entityID:" << entityID << "methodName:" << methodName << "event: mouseEvent"; + #endif if (_entityScripts.contains(entityID)) { EntityScriptDetails details = _entityScripts[entityID]; @@ -996,11 +1061,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS 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"; + "entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision"; + #endif QMetaObject::invokeMethod(this, "callEntityScriptMethod", Q_ARG(const EntityItemID&, entityID), @@ -1009,11 +1073,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS 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"; + "entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision"; + #endif if (_entityScripts.contains(entityID)) { EntityScriptDetails details = _entityScripts[entityID]; diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index ab1833636c..3b42fcc373 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -188,6 +188,7 @@ private: 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; From 94b273a029033da34526f9a9e3d829fbb0f8e258 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 14 Sep 2015 20:59:10 -0700 Subject: [PATCH 7/9] more work on script engine --- assignment-client/src/Agent.h | 1 - interface/src/Application.cpp | 1 - .../src/EntityTreeRenderer.cpp | 18 +++--------------- .../entities-renderer/src/EntityTreeRenderer.h | 1 - libraries/script-engine/src/ScriptEngine.cpp | 18 ++++++++++++++++++ libraries/script-engine/src/ScriptEngine.h | 12 +++--------- 6 files changed, 24 insertions(+), 27 deletions(-) diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 52e1dfec6c..33a8eb58c2 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -75,7 +75,6 @@ private: void sendAvatarIdentityPacket(); void sendAvatarBillboardPacket(); - // FIXME - Agent only data AvatarData* _avatarData = nullptr; bool _isListeningToAudioStream = false; Sound* _avatarSound = nullptr; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a4b52cc67e..0d9a0278ec 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4238,7 +4238,6 @@ ScriptEngine* Application::loadScript(const QString& scriptFilename, bool isUser connect(scriptEngine, &ScriptEngine::errorLoadingScript, this, &Application::handleScriptLoadError); // get the script engine object to load the script at the designated script URL - qDebug() << "calling scriptEngine->loadURL(" << scriptUrl << ", " << reload << ");"; scriptEngine->loadURL(scriptUrl, reload); } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index d34aabf6ca..3736f164c2 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -80,12 +80,7 @@ EntityTreeRenderer::~EntityTreeRenderer() { void EntityTreeRenderer::clear() { leaveAllEntities(); - /* - foreach (const EntityItemID& entityID, _entityScripts.keys()) { - checkAndCallUnload(entityID); - } - _entityScripts.clear(); - */ + _entitiesScriptEngine->unloadAllEntityScripts(); auto scene = _viewState->getMain3DScene(); render::PendingChanges pendingChanges; @@ -751,9 +746,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)) { @@ -786,7 +780,7 @@ 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); } } @@ -800,12 +794,6 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const } } -void EntityTreeRenderer::checkAndCallUnload(const EntityItemID& entityID) { - if (_tree && !_shuttingDown) { - _entitiesScriptEngine->unloadEntityScript(entityID); - } -} - void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityTree* entityTree, const EntityItemID& id, const Collision& collision) { EntityItemPointer entity = entityTree->findEntityByEntityItemID(id); if (!entity) { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index d740bb2729..2b1784c8b5 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -125,7 +125,6 @@ private: void applyZonePropertiesToScene(std::shared_ptr zone); void renderElementProxy(EntityTreeElement* 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); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index e5ce867df3..62b9410ede 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -999,6 +999,24 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& 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 diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 3b42fcc373..3f509cf994 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -109,6 +109,7 @@ public: // 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); @@ -173,6 +174,8 @@ private: void timerFired(); void stopAllTimers(); + void setParentURL(const QString& parentURL) { _parentURL = parentURL; } + QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot); void stopTimer(QTimer* timer); @@ -190,20 +193,11 @@ private: 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; -private: - //FIXME- EntityTreeRender shouldn't be using these methods directly -- these methods need to be depricated from the public interfaces - friend class EntityTreeRenderer; - - //FIXME - used in EntityTreeRenderer when evaluating entity scripts, which needs to be moved into ScriptEngine - // also used in Agent, but that would be fixed if Agent was using proper apis for loading content - void setParentURL(const QString& parentURL) { _parentURL = parentURL; } - }; #endif // hifi_ScriptEngine_h From e92d195030b2188ecc194e78e18ad80dc47637b7 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 14 Sep 2015 21:18:36 -0700 Subject: [PATCH 8/9] tweak --- libraries/script-engine/src/ScriptEngine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index e635f9f470..1ef3769970 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -90,9 +90,9 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam _isFinished(false), _isRunning(false), _isInitialized(false), - _controllerScriptingInterface(controllerScriptingInterface), _timerFunctionMap(), _wantSignals(wantSignals), + _controllerScriptingInterface(controllerScriptingInterface), _fileNameString(fileNameString), _quatLibrary(), _vec3Library(), From 70d2288407816cea1cf1862af7e211b6f00dd0cf Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 15 Sep 2015 08:04:26 -0700 Subject: [PATCH 9/9] CR feedback --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 4c74bf3793..2fd760bbd3 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -778,7 +778,7 @@ void EntityTreeRenderer::entitySciptChanging(const EntityItemID& entityID, const void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload) { if (_tree && !_shuttingDown) { EntityItemPointer entity = getTree()->findEntityByEntityItemID(entityID); - if (!entity->getScript().isEmpty()) { + if (entity && !entity->getScript().isEmpty()) { _entitiesScriptEngine->loadEntityScript(entityID, entity->getScript(), reload); } }