} } else { globalObject().setProperty(name, setterFunction, QScriptValue::PropertySetter); globalObject().setProperty(name, getterFunction, QScriptValue::PropertyGetter); }} // Unregister the handlers for this eventName and entityID.void ScriptEngine::removeEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qDebug() << "*** WARNING *** ScriptEngine::removeEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << " eventName:" << eventName; #endif QMetaObject::invokeMethod(this, "removeEventHandler", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, eventName), Q_ARG(QScriptValue, handler)); return; } #ifdef THREAD_DEBUGGING qDebug() << "ScriptEngine::removeEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; #endif if (!_registeredHandlers.contains(entityID)) { return; } RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; QScriptValueList& handlersForEvent = handlersOnEntity[eventName]; // QScriptValue does not have operator==(), so we can't use QList::removeOne and friends. So iterate. for (int i = 0; i < handlersForEvent.count(); ++i) { if (handlersForEvent[i].equals(handler)) { handlersForEvent.removeAt(i); return; // Design choice: since comparison is relatively expensive, just remove the first matching handler. } }}// Register the handler.void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qDebug() << "*** WARNING *** ScriptEngine::addEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << " eventName:" << eventName; #endif QMetaObject::invokeMethod(this, "addEventHandler", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, eventName), Q_ARG(QScriptValue, handler)); return; } #ifdef THREAD_DEBUGGING qDebug() << "ScriptEngine::addEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; #endif if (_registeredHandlers.count() == 0) { // First time any per-entity handler has been added in this script... // Connect up ALL the handlers to the global entities object's signals. // (We could go signal by signal, or even handler by handler, but I don't think the efficiency is worth the complexity.) auto entities = DependencyManager::get<EntityScriptingInterface>(); connect(entities.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { _registeredHandlers.remove(entityID); }); // Two common cases of event handler, differing only in argument signature. using SingleEntityHandler = std::function<void(const EntityItemID&)>; auto makeSingleEntityHandler = [this](QString eventName) -> SingleEntityHandler { return [this, eventName](const EntityItemID& entityItemID) { forwardHandlerCall(entityItemID, eventName, { entityItemID.toScriptValue(this) }); }; }; using MouseHandler = std::function<void(const EntityItemID&, const MouseEvent&)>; auto makeMouseHandler = [this](QString eventName) -> MouseHandler { return [this, eventName](const EntityItemID& entityItemID, const MouseEvent& event) { forwardHandlerCall(entityItemID, eventName, { entityItemID.toScriptValue(this), event.toScriptValue(this) }); }; }; using CollisionHandler = std::function<void(const EntityItemID&, const EntityItemID&, const Collision&)>; auto makeCollisionHandler = [this](QString eventName) -> CollisionHandler { return [this, eventName](const EntityItemID& idA, const EntityItemID& idB, const Collision& collision) { forwardHandlerCall(idA, eventName, { idA.toScriptValue(this), idB.toScriptValue(this), collisionToScriptValue(this, collision) }); }; }; connect(entities.data(), &EntityScriptingInterface::enterEntity, this, makeSingleEntityHandler("enterEntity")); connect(entities.data(), &EntityScriptingInterface::leaveEntity, this, makeSingleEntityHandler("leaveEntity")); connect(entities.data(), &EntityScriptingInterface::mousePressOnEntity, this, makeMouseHandler("mousePressOnEntity")); connect(entities.data(), &EntityScriptingInterface::mouseMoveOnEntity, this, makeMouseHandler("mouseMoveOnEntity")); connect(entities.data(), &EntityScriptingInterface::mouseReleaseOnEntity, this, makeMouseHandler("mouseReleaseOnEntity")); connect(entities.data(), &EntityScriptingInterface::clickDownOnEntity, this, makeMouseHandler("clickDownOnEntity")); connect(entities.data(), &EntityScriptingInterface::holdingClickOnEntity, this, makeMouseHandler("holdingClickOnEntity")); connect(entities.data(), &EntityScriptingInterface::clickReleaseOnEntity, this, makeMouseHandler("clickReleaseOnEntity")); connect(entities.data(), &EntityScriptingInterface::hoverEnterEntity, this, makeMouseHandler("hoverEnterEntity")); connect(entities.data(), &EntityScriptingInterface::hoverOverEntity, this, makeMouseHandler("hoverOverEntity")); connect(entities.data(), &EntityScriptingInterface::hoverLeaveEntity, this, makeMouseHandler("hoverLeaveEntity")); connect(entities.data(), &EntityScriptingInterface::collisionWithEntity, this, makeCollisionHandler("collisionWithEntity")); } if (!_registeredHandlers.contains(entityID)) { _registeredHandlers[entityID] = RegisteredEventHandlers(); } QScriptValueList& handlersForEvent = _registeredHandlers[entityID][eventName]; handlersForEvent << handler; // Note that the same handler can be added many times. See removeEntityEventHandler().} QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { if (_stoppingAllScripts) { return QScriptValue(); // bail early } if (QThread::currentThread() != thread()) { QScriptValue result; #ifdef THREAD_DEBUGGING qDebug() << "*** WARNING *** ScriptEngine::evaluate() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "sourceCode:" << sourceCode << " fileName:" << fileName << "lineNumber:" << lineNumber; #endif QMetaObject::invokeMethod(this, "evaluate", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QScriptValue, result), Q_ARG(const QString&, sourceCode), Q_ARG(const QString&, fileName), Q_ARG(int, lineNumber)); return result; } // Check syntax const QScriptProgram program(sourceCode, fileName, lineNumber); if (!hasCorrectSyntax(program)) { return QScriptValue(); } ++_evaluatesPending; const auto result = QScriptEngine::evaluate(program); --_evaluatesPending; const auto hadUncaughtException = hadUncaughtExceptions(*this, program.fileName()); if (_wantSignals) { emit evaluationFinished(result, hadUncaughtException); } return result;} 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? if (!_isInitialized) { init(); } _isRunning = true; _isFinished = false; if (_wantSignals) { emit runningStateChanged(); } QScriptValue result = evaluate(_scriptContents, _fileNameString); QElapsedTimer startTime; startTime.start(); int thisFrame = 0; auto nodeList = DependencyManager::get<NodeList>(); auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>(); qint64 lastUpdate = usecTimestampNow(); while (!_isFinished) { int usecToSleep = (thisFrame++ * SCRIPT_DATA_CALLBACK_USECS) - startTime.nsecsElapsed() / 1000; // nsec to usec if (usecToSleep > 0) { usleep(usecToSleep); } if (_isFinished) { break; } QCoreApplication::processEvents(); if (_isFinished) { break; } if (!_isFinished && entityScriptingInterface->getEntityPacketSender()->serversExist()) { // release the queue of edit entity messages. entityScriptingInterface->getEntityPacketSender()->releaseQueuedMessages(); // since we're in non-threaded mode, call process so that the packets are sent if (!entityScriptingInterface->getEntityPacketSender()->isThreaded()) { entityScriptingInterface->getEntityPacketSender()->process(); } } qint64 now = usecTimestampNow(); float deltaTime = (float) (now - lastUpdate) / (float) USECS_PER_SECOND; if (!_isFinished) { if (_wantSignals) { emit update(deltaTime); } } lastUpdate = now; // Debug and clear exceptions hadUncaughtExceptions(*this, _fileNameString); } stopAllTimers(); // make sure all our timers are stopped if the script is ending if (_wantSignals) { emit scriptEnding(); } if (entityScriptingInterface->getEntityPacketSender()->serversExist()) { // release the queue of edit entity messages. entityScriptingInterface->getEntityPacketSender()->releaseQueuedMessages(); // since we're in non-threaded mode, call process so that the packets are sent if (!entityScriptingInterface->getEntityPacketSender()->isThreaded()) { // wait here till the edit packet sender is completely done sending while (entityScriptingInterface->getEntityPacketSender()->hasPacketsToSend()) { entityScriptingInterface->getEntityPacketSender()->process(); QCoreApplication::processEvents(); } } else { // FIXME - do we need to have a similar "wait here" loop for non-threaded packet senders? } } if (_wantSignals) { emit finished(_fileNameString); } _isRunning = false; if (_wantSignals) { emit runningStateChanged(); emit doneRunning(); } _doneRunningThisScript = true;} // NOTE: This is private because it must be called on the same thread that created the timers, which is why// we want to only call it in our own run "shutdown" processing.void ScriptEngine::stopAllTimers() { QMutableHashIterator<QTimer*, QScriptValue> i(_timerFunctionMap); while (i.hasNext()) { i.next(); QTimer* timer = i.key(); stopTimer(timer); }} void ScriptEngine::stop() { if (!_isFinished) { _isFinished = true; if (_wantSignals) { emit runningStateChanged(); } }} // Other threads can invoke this through invokeMethod, which causes the callback to be asynchronously executed in this script's thread.void ScriptEngine::callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler) { if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING qDebug() << "*** WARNING *** ScriptEngine::callAnimationStateHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name;#endif QMetaObject::invokeMethod(this, "callAnimationStateHandler", Q_ARG(QScriptValue, callback), Q_ARG(AnimVariantMap, parameters), Q_ARG(QStringList, names), Q_ARG(bool, useNames), Q_ARG(AnimVariantResultHandler, resultHandler)); return; } QScriptValue javascriptParameters = parameters.animVariantMapToScriptValue(this, names, useNames); QScriptValueList callingArguments; callingArguments << javascriptParameters; QScriptValue result = callback.call(QScriptValue(), callingArguments); resultHandler(result);} void ScriptEngine::timerFired() { QTimer* callingTimer = reinterpret_cast<QTimer*>(sender()); QScriptValue timerFunction = _timerFunctionMap.value(callingTimer); if (!callingTimer->isActive()) { // this timer is done, we can kill it _timerFunctionMap.remove(callingTimer); delete callingTimer; } // call the associated JS function, if it exists if (timerFunction.isValid()) { timerFunction.call(); }} QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { // create the timer, add it to the map, and start it QTimer* newTimer = new QTimer(this); newTimer->setSingleShot(isSingleShot); connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); // make sure the timer stops when the script does connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); _timerFunctionMap.insert(newTimer, function); newTimer->start(intervalMS); return newTimer;} QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) { if (_stoppingAllScripts) { qCDebug(scriptengine) << "Script.setInterval() while shutting down is ignored... parent script:" << getFilename(); return NULL; // bail early } return setupTimerWithInterval(function, intervalMS, false);} QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) { if (_stoppingAllScripts) { qCDebug(scriptengine) << "Script.setTimeout() while shutting down is ignored... parent script:" << getFilename(); return NULL; // bail early } return setupTimerWithInterval(function, timeoutMS, true);} void ScriptEngine::stopTimer(QTimer *timer) { if (_timerFunctionMap.contains(timer)) { timer->stop(); _timerFunctionMap.remove(timer); delete timer; }} QUrl ScriptEngine::resolvePath(const QString& include) const { QUrl url(include); // first lets check to see if it's already a full URL if (!url.scheme().isEmpty()) { return url; } // we apparently weren't a fully qualified url, so, let's assume we're relative // to the original URL of our script QUrl parentURL; if (_parentURL.isEmpty()) { parentURL = QUrl(_fileNameString); } else { parentURL = QUrl(_parentURL); } // if the parent URL's scheme is empty, then this is probably a local file... if (parentURL.scheme().isEmpty()) { parentURL = QUrl::fromLocalFile(_fileNameString); } // at this point we should have a legitimate fully qualified URL for our parent url = parentURL.resolved(url); return url;} void ScriptEngine::print(const QString& message) { if (_wantSignals) { emit printedMessage(message); }} // If a callback is specified, the included files will be loaded asynchronously and the callback will be called// when all of the files have finished loading.// If no callback is specified, the included files will be loaded synchronously and will block execution until// all of the files have finished loading.void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { if (_stoppingAllScripts) { qCDebug(scriptengine) << "Script.include() while shutting down is ignored..." << "includeFiles:" << includeFiles << "parent script:" << getFilename(); return; // bail early } QList<QUrl> urls; for (QString file : includeFiles) { QUrl thisURL { resolvePath(file) }; if (!_includedURLs.contains(thisURL)) { urls.append(thisURL); _includedURLs << thisURL; } else { qCDebug(scriptengine) << "Script.include() ignoring previously included url:" << thisURL; } } BatchLoader* loader = new BatchLoader(urls); auto evaluateScripts = [=](const QMap<QUrl, QString>& data) { for (QUrl url : urls) { QString contents = data[url]; if (contents.isNull()) { qCDebug(scriptengine) << "Error loading file: " << url << "line:" << __LINE__; } else { QScriptValue result = evaluate(contents, url.toString()); } } if (callback.isFunction()) { QScriptValue(callback).call(); } loader->deleteLater(); }; connect(loader, &BatchLoader::finished, this, evaluateScripts); // If we are destroyed before the loader completes, make sure to clean it up connect(this, &QObject::destroyed, loader, &QObject::deleteLater); loader->start(); if (!callback.isFunction() && !loader->isFinished()) { QEventLoop loop; QObject::connect(loader, &BatchLoader::finished, &loop, &QEventLoop::quit); loop.exec(); }} void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { if (_stoppingAllScripts) { qCDebug(scriptengine) << "Script.include() while shutting down is ignored... " << "includeFile:" << includeFile << "parent script:" << getFilename(); return; // bail early } QStringList urls; urls.append(includeFile); include(urls, callback);} // NOTE: The load() command is similar to the include() command except that it loads the script// as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which// the Application or other context will connect to in order to know to actually load the scriptvoid ScriptEngine::load(const QString& loadFile) { if (_stoppingAllScripts) { qCDebug(scriptengine) << "Script.load() while shutting down is ignored... " << "loadFile:" << loadFile << "parent script:" << getFilename(); return; // bail early } QUrl url = resolvePath(loadFile); if (_isReloading) { auto scriptCache = DependencyManager::get<ScriptCache>(); scriptCache->deleteScript(url.toString()); if (_wantSignals) { emit reloadScript(url.toString(), false); } } else { if (_wantSignals) { emit loadScript(url.toString(), false); } }} // Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those argsvoid ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHanderArgs) { if (QThread::currentThread() != thread()) { qDebug() << "*** ERROR *** ScriptEngine::forwardHandlerCall() 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()) { for (int i = 0; i < handlersForEvent.count(); ++i) { handlersForEvent[i].call(QScriptValue(), eventHanderArgs); } }} // since all of these operations can be asynch we will always do the actual work in the response handler// for the downloadvoid ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qDebug() << "*** WARNING *** ScriptEngine::loadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << "entityScript:" << entityScript <<"forceRedownload:" << forceRedownload; #endif QMetaObject::invokeMethod(this, "loadEntityScript",Lines 501–1000 of 1266. Previous lines Load more
} } else { globalObject().setProperty(name, setterFunction, QScriptValue::PropertySetter); globalObject().setProperty(name, getterFunction, QScriptValue::PropertyGetter); } } // Unregister the handlers for this eventName and entityID. void ScriptEngine::removeEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qDebug() << "*** WARNING *** ScriptEngine::removeEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << " eventName:" << eventName; #endif QMetaObject::invokeMethod(this, "removeEventHandler", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, eventName), Q_ARG(QScriptValue, handler)); return; } #ifdef THREAD_DEBUGGING qDebug() << "ScriptEngine::removeEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; #endif if (!_registeredHandlers.contains(entityID)) { return; } RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; QScriptValueList& handlersForEvent = handlersOnEntity[eventName]; // QScriptValue does not have operator==(), so we can't use QList::removeOne and friends. So iterate. for (int i = 0; i < handlersForEvent.count(); ++i) { if (handlersForEvent[i].equals(handler)) { handlersForEvent.removeAt(i); return; // Design choice: since comparison is relatively expensive, just remove the first matching handler. } } } // Register the handler. void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qDebug() << "*** WARNING *** ScriptEngine::addEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << " eventName:" << eventName; #endif QMetaObject::invokeMethod(this, "addEventHandler", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, eventName), Q_ARG(QScriptValue, handler)); return; } #ifdef THREAD_DEBUGGING qDebug() << "ScriptEngine::addEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; #endif if (_registeredHandlers.count() == 0) { // First time any per-entity handler has been added in this script... // Connect up ALL the handlers to the global entities object's signals. // (We could go signal by signal, or even handler by handler, but I don't think the efficiency is worth the complexity.) auto entities = DependencyManager::get<EntityScriptingInterface>(); connect(entities.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { _registeredHandlers.remove(entityID); }); // Two common cases of event handler, differing only in argument signature. using SingleEntityHandler = std::function<void(const EntityItemID&)>; auto makeSingleEntityHandler = [this](QString eventName) -> SingleEntityHandler { return [this, eventName](const EntityItemID& entityItemID) { forwardHandlerCall(entityItemID, eventName, { entityItemID.toScriptValue(this) }); }; }; using MouseHandler = std::function<void(const EntityItemID&, const MouseEvent&)>; auto makeMouseHandler = [this](QString eventName) -> MouseHandler { return [this, eventName](const EntityItemID& entityItemID, const MouseEvent& event) { forwardHandlerCall(entityItemID, eventName, { entityItemID.toScriptValue(this), event.toScriptValue(this) }); }; }; using CollisionHandler = std::function<void(const EntityItemID&, const EntityItemID&, const Collision&)>; auto makeCollisionHandler = [this](QString eventName) -> CollisionHandler { return [this, eventName](const EntityItemID& idA, const EntityItemID& idB, const Collision& collision) { forwardHandlerCall(idA, eventName, { idA.toScriptValue(this), idB.toScriptValue(this), collisionToScriptValue(this, collision) }); }; }; connect(entities.data(), &EntityScriptingInterface::enterEntity, this, makeSingleEntityHandler("enterEntity")); connect(entities.data(), &EntityScriptingInterface::leaveEntity, this, makeSingleEntityHandler("leaveEntity")); connect(entities.data(), &EntityScriptingInterface::mousePressOnEntity, this, makeMouseHandler("mousePressOnEntity")); connect(entities.data(), &EntityScriptingInterface::mouseMoveOnEntity, this, makeMouseHandler("mouseMoveOnEntity")); connect(entities.data(), &EntityScriptingInterface::mouseReleaseOnEntity, this, makeMouseHandler("mouseReleaseOnEntity")); connect(entities.data(), &EntityScriptingInterface::clickDownOnEntity, this, makeMouseHandler("clickDownOnEntity")); connect(entities.data(), &EntityScriptingInterface::holdingClickOnEntity, this, makeMouseHandler("holdingClickOnEntity")); connect(entities.data(), &EntityScriptingInterface::clickReleaseOnEntity, this, makeMouseHandler("clickReleaseOnEntity")); connect(entities.data(), &EntityScriptingInterface::hoverEnterEntity, this, makeMouseHandler("hoverEnterEntity")); connect(entities.data(), &EntityScriptingInterface::hoverOverEntity, this, makeMouseHandler("hoverOverEntity")); connect(entities.data(), &EntityScriptingInterface::hoverLeaveEntity, this, makeMouseHandler("hoverLeaveEntity")); connect(entities.data(), &EntityScriptingInterface::collisionWithEntity, this, makeCollisionHandler("collisionWithEntity")); } if (!_registeredHandlers.contains(entityID)) { _registeredHandlers[entityID] = RegisteredEventHandlers(); } QScriptValueList& handlersForEvent = _registeredHandlers[entityID][eventName]; handlersForEvent << handler; // Note that the same handler can be added many times. See removeEntityEventHandler(). } QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { if (_stoppingAllScripts) { return QScriptValue(); // bail early } if (QThread::currentThread() != thread()) { QScriptValue result; #ifdef THREAD_DEBUGGING qDebug() << "*** WARNING *** ScriptEngine::evaluate() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "sourceCode:" << sourceCode << " fileName:" << fileName << "lineNumber:" << lineNumber; #endif QMetaObject::invokeMethod(this, "evaluate", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QScriptValue, result), Q_ARG(const QString&, sourceCode), Q_ARG(const QString&, fileName), Q_ARG(int, lineNumber)); return result; } // Check syntax const QScriptProgram program(sourceCode, fileName, lineNumber); if (!hasCorrectSyntax(program)) { return QScriptValue(); } ++_evaluatesPending; const auto result = QScriptEngine::evaluate(program); --_evaluatesPending; const auto hadUncaughtException = hadUncaughtExceptions(*this, program.fileName()); if (_wantSignals) { emit evaluationFinished(result, hadUncaughtException); } return result; } 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? if (!_isInitialized) { init(); } _isRunning = true; _isFinished = false; if (_wantSignals) { emit runningStateChanged(); } QScriptValue result = evaluate(_scriptContents, _fileNameString); QElapsedTimer startTime; startTime.start(); int thisFrame = 0; auto nodeList = DependencyManager::get<NodeList>(); auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>(); qint64 lastUpdate = usecTimestampNow(); while (!_isFinished) { int usecToSleep = (thisFrame++ * SCRIPT_DATA_CALLBACK_USECS) - startTime.nsecsElapsed() / 1000; // nsec to usec if (usecToSleep > 0) { usleep(usecToSleep); } if (_isFinished) { break; } QCoreApplication::processEvents(); if (_isFinished) { break; } if (!_isFinished && entityScriptingInterface->getEntityPacketSender()->serversExist()) { // release the queue of edit entity messages. entityScriptingInterface->getEntityPacketSender()->releaseQueuedMessages(); // since we're in non-threaded mode, call process so that the packets are sent if (!entityScriptingInterface->getEntityPacketSender()->isThreaded()) { entityScriptingInterface->getEntityPacketSender()->process(); } } qint64 now = usecTimestampNow(); float deltaTime = (float) (now - lastUpdate) / (float) USECS_PER_SECOND; if (!_isFinished) { if (_wantSignals) { emit update(deltaTime); } } lastUpdate = now; // Debug and clear exceptions hadUncaughtExceptions(*this, _fileNameString); } stopAllTimers(); // make sure all our timers are stopped if the script is ending if (_wantSignals) { emit scriptEnding(); } if (entityScriptingInterface->getEntityPacketSender()->serversExist()) { // release the queue of edit entity messages. entityScriptingInterface->getEntityPacketSender()->releaseQueuedMessages(); // since we're in non-threaded mode, call process so that the packets are sent if (!entityScriptingInterface->getEntityPacketSender()->isThreaded()) { // wait here till the edit packet sender is completely done sending while (entityScriptingInterface->getEntityPacketSender()->hasPacketsToSend()) { entityScriptingInterface->getEntityPacketSender()->process(); QCoreApplication::processEvents(); } } else { // FIXME - do we need to have a similar "wait here" loop for non-threaded packet senders? } } if (_wantSignals) { emit finished(_fileNameString); } _isRunning = false; if (_wantSignals) { emit runningStateChanged(); emit doneRunning(); } _doneRunningThisScript = true; } // NOTE: This is private because it must be called on the same thread that created the timers, which is why // we want to only call it in our own run "shutdown" processing. void ScriptEngine::stopAllTimers() { QMutableHashIterator<QTimer*, QScriptValue> i(_timerFunctionMap); while (i.hasNext()) { i.next(); QTimer* timer = i.key(); stopTimer(timer); } } void ScriptEngine::stop() { if (!_isFinished) { _isFinished = true; if (_wantSignals) { emit runningStateChanged(); } } } // Other threads can invoke this through invokeMethod, which causes the callback to be asynchronously executed in this script's thread. void ScriptEngine::callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qDebug() << "*** WARNING *** ScriptEngine::callAnimationStateHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; #endif QMetaObject::invokeMethod(this, "callAnimationStateHandler", Q_ARG(QScriptValue, callback), Q_ARG(AnimVariantMap, parameters), Q_ARG(QStringList, names), Q_ARG(bool, useNames), Q_ARG(AnimVariantResultHandler, resultHandler)); return; } QScriptValue javascriptParameters = parameters.animVariantMapToScriptValue(this, names, useNames); QScriptValueList callingArguments; callingArguments << javascriptParameters; QScriptValue result = callback.call(QScriptValue(), callingArguments); resultHandler(result); } void ScriptEngine::timerFired() { QTimer* callingTimer = reinterpret_cast<QTimer*>(sender()); QScriptValue timerFunction = _timerFunctionMap.value(callingTimer); if (!callingTimer->isActive()) { // this timer is done, we can kill it _timerFunctionMap.remove(callingTimer); delete callingTimer; } // call the associated JS function, if it exists if (timerFunction.isValid()) { timerFunction.call(); } } QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { // create the timer, add it to the map, and start it QTimer* newTimer = new QTimer(this); newTimer->setSingleShot(isSingleShot); connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); // make sure the timer stops when the script does connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); _timerFunctionMap.insert(newTimer, function); newTimer->start(intervalMS); return newTimer; } QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) { if (_stoppingAllScripts) { qCDebug(scriptengine) << "Script.setInterval() while shutting down is ignored... parent script:" << getFilename(); return NULL; // bail early } return setupTimerWithInterval(function, intervalMS, false); } QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) { if (_stoppingAllScripts) { qCDebug(scriptengine) << "Script.setTimeout() while shutting down is ignored... parent script:" << getFilename(); return NULL; // bail early } return setupTimerWithInterval(function, timeoutMS, true); } void ScriptEngine::stopTimer(QTimer *timer) { if (_timerFunctionMap.contains(timer)) { timer->stop(); _timerFunctionMap.remove(timer); delete timer; } } QUrl ScriptEngine::resolvePath(const QString& include) const { QUrl url(include); // first lets check to see if it's already a full URL if (!url.scheme().isEmpty()) { return url; } // we apparently weren't a fully qualified url, so, let's assume we're relative // to the original URL of our script QUrl parentURL; if (_parentURL.isEmpty()) { parentURL = QUrl(_fileNameString); } else { parentURL = QUrl(_parentURL); } // if the parent URL's scheme is empty, then this is probably a local file... if (parentURL.scheme().isEmpty()) { parentURL = QUrl::fromLocalFile(_fileNameString); } // at this point we should have a legitimate fully qualified URL for our parent url = parentURL.resolved(url); return url; } void ScriptEngine::print(const QString& message) { if (_wantSignals) { emit printedMessage(message); } } // If a callback is specified, the included files will be loaded asynchronously and the callback will be called // when all of the files have finished loading. // If no callback is specified, the included files will be loaded synchronously and will block execution until // all of the files have finished loading. void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { if (_stoppingAllScripts) { qCDebug(scriptengine) << "Script.include() while shutting down is ignored..." << "includeFiles:" << includeFiles << "parent script:" << getFilename(); return; // bail early } QList<QUrl> urls; for (QString file : includeFiles) { QUrl thisURL { resolvePath(file) }; if (!_includedURLs.contains(thisURL)) { urls.append(thisURL); _includedURLs << thisURL; } else { qCDebug(scriptengine) << "Script.include() ignoring previously included url:" << thisURL; } } BatchLoader* loader = new BatchLoader(urls); auto evaluateScripts = [=](const QMap<QUrl, QString>& data) { for (QUrl url : urls) { QString contents = data[url]; if (contents.isNull()) { qCDebug(scriptengine) << "Error loading file: " << url << "line:" << __LINE__; } else { QScriptValue result = evaluate(contents, url.toString()); } } if (callback.isFunction()) { QScriptValue(callback).call(); } loader->deleteLater(); }; connect(loader, &BatchLoader::finished, this, evaluateScripts); // If we are destroyed before the loader completes, make sure to clean it up connect(this, &QObject::destroyed, loader, &QObject::deleteLater); loader->start(); if (!callback.isFunction() && !loader->isFinished()) { QEventLoop loop; QObject::connect(loader, &BatchLoader::finished, &loop, &QEventLoop::quit); loop.exec(); } } void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { if (_stoppingAllScripts) { qCDebug(scriptengine) << "Script.include() while shutting down is ignored... " << "includeFile:" << includeFile << "parent script:" << getFilename(); return; // bail early } QStringList urls; urls.append(includeFile); include(urls, callback); } // NOTE: The load() command is similar to the include() command except that it loads the script // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { if (_stoppingAllScripts) { qCDebug(scriptengine) << "Script.load() while shutting down is ignored... " << "loadFile:" << loadFile << "parent script:" << getFilename(); return; // bail early } QUrl url = resolvePath(loadFile); if (_isReloading) { auto scriptCache = DependencyManager::get<ScriptCache>(); scriptCache->deleteScript(url.toString()); if (_wantSignals) { emit reloadScript(url.toString(), false); } } else { if (_wantSignals) { emit loadScript(url.toString(), false); } } } // Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those args void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHanderArgs) { if (QThread::currentThread() != thread()) { qDebug() << "*** ERROR *** ScriptEngine::forwardHandlerCall() 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()) { for (int i = 0; i < handlersForEvent.count(); ++i) { handlersForEvent[i].call(QScriptValue(), eventHanderArgs); } } } // since all of these operations can be asynch we will always do the actual work in the response handler // for the download void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qDebug() << "*** WARNING *** ScriptEngine::loadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << "entityScript:" << entityScript <<"forceRedownload:" << forceRedownload; #endif QMetaObject::invokeMethod(this, "loadEntityScript",