mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-25 21:55:09 +02:00
933 lines
37 KiB
C++
933 lines
37 KiB
C++
//
|
|
// ScriptEngine.cpp
|
|
// libraries/script-engine/src
|
|
//
|
|
// Created by Brad Hefta-Gaub on 12/14/13.
|
|
// Copyright 2013 High Fidelity, Inc.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
//
|
|
|
|
#include <QtCore/QCoreApplication>
|
|
#include <QtCore/QEventLoop>
|
|
#include <QtCore/QTimer>
|
|
#include <QtCore/QThread>
|
|
#include <QtNetwork/QNetworkRequest>
|
|
#include <QtNetwork/QNetworkReply>
|
|
#include <QScriptEngine>
|
|
|
|
#include <AudioConstants.h>
|
|
#include <AudioEffectOptions.h>
|
|
#include <AvatarData.h>
|
|
#include <EntityScriptingInterface.h>
|
|
#include <NetworkAccessManager.h>
|
|
#include <NodeList.h>
|
|
#include <udt/PacketHeaders.h>
|
|
#include <UUID.h>
|
|
|
|
#include "AnimationObject.h"
|
|
#include "ArrayBufferViewClass.h"
|
|
#include "BatchLoader.h"
|
|
#include "DataViewClass.h"
|
|
#include "EventTypes.h"
|
|
#include "MenuItemProperties.h"
|
|
#include "ScriptAudioInjector.h"
|
|
#include "ScriptCache.h"
|
|
#include "ScriptEngineLogging.h"
|
|
#include "ScriptEngine.h"
|
|
#include "TypedArrays.h"
|
|
#include "XMLHttpRequestClass.h"
|
|
#include "WebSocketClass.h"
|
|
|
|
#include "SceneScriptingInterface.h"
|
|
|
|
#include "MIDIEvent.h"
|
|
|
|
|
|
static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){
|
|
QString message = "";
|
|
for (int i = 0; i < context->argumentCount(); i++) {
|
|
if (i > 0) {
|
|
message += " ";
|
|
}
|
|
message += context->argument(i).toString();
|
|
}
|
|
qCDebug(scriptengine) << "script:print()<<" << message;
|
|
|
|
message = message.replace("\\", "\\\\")
|
|
.replace("\n", "\\n")
|
|
.replace("\r", "\\r")
|
|
.replace("'", "\\'");
|
|
engine->evaluate("Script.print('" + message + "')");
|
|
|
|
return QScriptValue();
|
|
}
|
|
|
|
QScriptValue avatarDataToScriptValue(QScriptEngine* engine, AvatarData* const &in) {
|
|
return engine->newQObject(in);
|
|
}
|
|
|
|
void avatarDataFromScriptValue(const QScriptValue &object, AvatarData* &out) {
|
|
out = qobject_cast<AvatarData*>(object.toQObject());
|
|
}
|
|
|
|
QScriptValue inputControllerToScriptValue(QScriptEngine *engine, AbstractInputController* const &in) {
|
|
return engine->newQObject(in);
|
|
}
|
|
|
|
void inputControllerFromScriptValue(const QScriptValue &object, AbstractInputController* &out) {
|
|
out = qobject_cast<AbstractInputController*>(object.toQObject());
|
|
}
|
|
|
|
ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNameString,
|
|
AbstractControllerScriptingInterface* controllerScriptingInterface) :
|
|
|
|
_scriptContents(scriptContents),
|
|
_isFinished(false),
|
|
_isRunning(false),
|
|
_isInitialized(false),
|
|
_isAvatar(false),
|
|
_avatarIdentityTimer(NULL),
|
|
_avatarBillboardTimer(NULL),
|
|
_timerFunctionMap(),
|
|
_isListeningToAudioStream(false),
|
|
_avatarSound(NULL),
|
|
_numAvatarSoundSentBytes(0),
|
|
_controllerScriptingInterface(controllerScriptingInterface),
|
|
_avatarData(NULL),
|
|
_scriptName(),
|
|
_fileNameString(fileNameString),
|
|
_quatLibrary(),
|
|
_vec3Library(),
|
|
_uuidLibrary(),
|
|
_isUserLoaded(false),
|
|
_isReloading(false),
|
|
_arrayBufferClass(new ArrayBufferClass(this))
|
|
{
|
|
_allScriptsMutex.lock();
|
|
_allKnownScriptEngines.insert(this);
|
|
_allScriptsMutex.unlock();
|
|
}
|
|
|
|
ScriptEngine::~ScriptEngine() {
|
|
// If we're not already in the middle of stopping all scripts, then we should remove ourselves
|
|
// from the list of running scripts. We don't do this if we're in the process of stopping all scripts
|
|
// because that method removes scripts from its list as it iterates them
|
|
if (!_stoppingAllScripts) {
|
|
_allScriptsMutex.lock();
|
|
_allKnownScriptEngines.remove(this);
|
|
_allScriptsMutex.unlock();
|
|
}
|
|
}
|
|
|
|
QSet<ScriptEngine*> ScriptEngine::_allKnownScriptEngines;
|
|
QMutex ScriptEngine::_allScriptsMutex;
|
|
bool ScriptEngine::_stoppingAllScripts = false;
|
|
bool ScriptEngine::_doneRunningThisScript = false;
|
|
|
|
void ScriptEngine::stopAllScripts(QObject* application) {
|
|
_allScriptsMutex.lock();
|
|
_stoppingAllScripts = true;
|
|
|
|
QMutableSetIterator<ScriptEngine*> i(_allKnownScriptEngines);
|
|
while (i.hasNext()) {
|
|
ScriptEngine* scriptEngine = i.next();
|
|
|
|
QString scriptName = scriptEngine->getFilename();
|
|
|
|
// NOTE: typically all script engines are running. But there's at least one known exception to this, the
|
|
// "entities sandbox" which is only used to evaluate entities scripts to test their validity before using
|
|
// them. We don't need to stop scripts that aren't running.
|
|
if (scriptEngine->isRunning()) {
|
|
|
|
// If the script is running, but still evaluating then we need to wait for its evaluation step to
|
|
// complete. After that we can handle the stop process appropriately
|
|
if (scriptEngine->evaluatePending()) {
|
|
while (scriptEngine->evaluatePending()) {
|
|
|
|
// This event loop allows any started, but not yet finished evaluate() calls to complete
|
|
// we need to let these complete so that we can be guaranteed that the script engine isn't
|
|
// in a partially setup state, which can confuse our shutdown unwinding.
|
|
QEventLoop loop;
|
|
QObject::connect(scriptEngine, &ScriptEngine::evaluationFinished, &loop, &QEventLoop::quit);
|
|
loop.exec();
|
|
}
|
|
}
|
|
|
|
// We disconnect any script engine signals from the application because we don't want to do any
|
|
// extra stopScript/loadScript processing that the Application normally does when scripts start
|
|
// and stop. We can safely short circuit this because we know we're in the "quitting" process
|
|
scriptEngine->disconnect(application);
|
|
|
|
// Calling stop on the script engine will set it's internal _isFinished state to true, and result
|
|
// in the ScriptEngine gracefully ending it's run() method.
|
|
scriptEngine->stop();
|
|
|
|
// We need to wait for the engine to be done running before we proceed, because we don't
|
|
// want any of the scripts final "scriptEnding()" or pending "update()" methods from accessing
|
|
// any application state after we leave this stopAllScripts() method
|
|
scriptEngine->waitTillDoneRunning();
|
|
|
|
// If the script is stopped, we can remove it from our set
|
|
i.remove();
|
|
}
|
|
}
|
|
_stoppingAllScripts = false;
|
|
_allScriptsMutex.unlock();
|
|
}
|
|
|
|
|
|
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) {
|
|
|
|
_doneRunningThisScript = false; // NOTE: this is static, we serialize our waiting for scripts to finish
|
|
|
|
// NOTE: waitTillDoneRunning() will be called on the main Application thread, inside of stopAllScripts()
|
|
// we want the application thread to continue to process events, because the scripts will likely need to
|
|
// marshall messages across to the main thread. For example if they access Settings or Meny in any of their
|
|
// shutdown code.
|
|
while (!_doneRunningThisScript) {
|
|
|
|
// process events for the main application thread, allowing invokeMethod calls to pass between threads
|
|
QCoreApplication::processEvents();
|
|
}
|
|
}
|
|
}
|
|
|
|
QString ScriptEngine::getFilename() const {
|
|
QStringList fileNameParts = _fileNameString.split("/");
|
|
QString lastPart;
|
|
if (!fileNameParts.isEmpty()) {
|
|
lastPart = fileNameParts.last();
|
|
}
|
|
return lastPart;
|
|
}
|
|
|
|
|
|
void ScriptEngine::setIsAvatar(bool isAvatar) {
|
|
_isAvatar = isAvatar;
|
|
|
|
if (_isAvatar && !_avatarIdentityTimer) {
|
|
// set up the avatar timers
|
|
_avatarIdentityTimer = new QTimer(this);
|
|
_avatarBillboardTimer = new QTimer(this);
|
|
|
|
// connect our slot
|
|
connect(_avatarIdentityTimer, &QTimer::timeout, this, &ScriptEngine::sendAvatarIdentityPacket);
|
|
connect(_avatarBillboardTimer, &QTimer::timeout, this, &ScriptEngine::sendAvatarBillboardPacket);
|
|
|
|
// start the timers
|
|
_avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS);
|
|
_avatarBillboardTimer->start(AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS);
|
|
}
|
|
|
|
if (!_isAvatar) {
|
|
delete _avatarIdentityTimer;
|
|
_avatarIdentityTimer = NULL;
|
|
delete _avatarBillboardTimer;
|
|
_avatarBillboardTimer = NULL;
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::setAvatarData(AvatarData* avatarData, const QString& objectName) {
|
|
_avatarData = avatarData;
|
|
|
|
// remove the old Avatar property, if it exists
|
|
globalObject().setProperty(objectName, QScriptValue());
|
|
|
|
// give the script engine the new Avatar script property
|
|
registerGlobalObject(objectName, _avatarData);
|
|
}
|
|
|
|
void ScriptEngine::setAvatarHashMap(AvatarHashMap* avatarHashMap, const QString& objectName) {
|
|
// remove the old Avatar property, if it exists
|
|
globalObject().setProperty(objectName, QScriptValue());
|
|
|
|
// give the script engine the new avatar hash map
|
|
registerGlobalObject(objectName, avatarHashMap);
|
|
}
|
|
|
|
bool ScriptEngine::setScriptContents(const QString& scriptContents, const QString& fileNameString) {
|
|
if (_isRunning) {
|
|
return false;
|
|
}
|
|
_scriptContents = scriptContents;
|
|
_fileNameString = fileNameString;
|
|
return true;
|
|
}
|
|
|
|
void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) {
|
|
if (_isRunning) {
|
|
return;
|
|
}
|
|
|
|
_fileNameString = scriptURL.toString();
|
|
_isReloading = 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();
|
|
emit scriptLoaded(_fileNameString);
|
|
} else {
|
|
qCDebug(scriptengine) << "ERROR Loading file:" << _fileNameString << "line:" << __LINE__;
|
|
emit errorLoadingScript(_fileNameString);
|
|
}
|
|
} else {
|
|
bool isPending;
|
|
auto scriptCache = DependencyManager::get<ScriptCache>();
|
|
scriptCache->getScript(url, this, isPending, reload);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) {
|
|
_scriptContents = scriptContents;
|
|
emit scriptLoaded(_fileNameString);
|
|
}
|
|
|
|
void ScriptEngine::errorInLoadingScript(const QUrl& url) {
|
|
qCDebug(scriptengine) << "ERROR Loading file:" << url.toString() << "line:" << __LINE__;
|
|
emit errorLoadingScript(_fileNameString); // ??
|
|
}
|
|
|
|
void ScriptEngine::init() {
|
|
if (_isInitialized) {
|
|
return; // only initialize once
|
|
}
|
|
|
|
_isInitialized = true;
|
|
|
|
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
|
entityScriptingInterface->init();
|
|
|
|
// register various meta-types
|
|
registerMetaTypes(this);
|
|
registerMIDIMetaTypes(this);
|
|
registerEventTypes(this);
|
|
registerMenuItemProperties(this);
|
|
registerAnimationTypes(this);
|
|
registerAvatarTypes(this);
|
|
registerAudioMetaTypes(this);
|
|
|
|
if (_controllerScriptingInterface) {
|
|
_controllerScriptingInterface->registerControllerTypes(this);
|
|
}
|
|
|
|
qScriptRegisterMetaType(this, EntityItemPropertiesToScriptValue, EntityItemPropertiesFromScriptValueHonorReadOnly);
|
|
qScriptRegisterMetaType(this, EntityItemIDtoScriptValue, EntityItemIDfromScriptValue);
|
|
qScriptRegisterMetaType(this, RayToEntityIntersectionResultToScriptValue, RayToEntityIntersectionResultFromScriptValue);
|
|
qScriptRegisterSequenceMetaType<QVector<QUuid>>(this);
|
|
qScriptRegisterSequenceMetaType<QVector<EntityItemID>>(this);
|
|
|
|
qScriptRegisterSequenceMetaType<QVector<glm::vec2> >(this);
|
|
qScriptRegisterSequenceMetaType<QVector<glm::quat> >(this);
|
|
qScriptRegisterSequenceMetaType<QVector<QString> >(this);
|
|
|
|
QScriptValue xmlHttpRequestConstructorValue = newFunction(XMLHttpRequestClass::constructor);
|
|
globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue);
|
|
|
|
QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor);
|
|
globalObject().setProperty("WebSocket", webSocketConstructorValue);
|
|
|
|
QScriptValue printConstructorValue = newFunction(debugPrint);
|
|
globalObject().setProperty("print", printConstructorValue);
|
|
|
|
QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor);
|
|
globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue);
|
|
|
|
qScriptRegisterMetaType(this, injectorToScriptValue, injectorFromScriptValue);
|
|
qScriptRegisterMetaType(this, inputControllerToScriptValue, inputControllerFromScriptValue);
|
|
qScriptRegisterMetaType(this, avatarDataToScriptValue, avatarDataFromScriptValue);
|
|
qScriptRegisterMetaType(this, animationDetailsToScriptValue, animationDetailsFromScriptValue);
|
|
qScriptRegisterMetaType(this, webSocketToScriptValue, webSocketFromScriptValue);
|
|
qScriptRegisterMetaType(this, qWSCloseCodeToScriptValue, qWSCloseCodeFromScriptValue);
|
|
qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue);
|
|
|
|
registerGlobalObject("Script", this);
|
|
registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());
|
|
registerGlobalObject("Controller", _controllerScriptingInterface);
|
|
registerGlobalObject("Entities", entityScriptingInterface.data());
|
|
registerGlobalObject("Quat", &_quatLibrary);
|
|
registerGlobalObject("Vec3", &_vec3Library);
|
|
registerGlobalObject("Uuid", &_uuidLibrary);
|
|
registerGlobalObject("AnimationCache", DependencyManager::get<AnimationCache>().data());
|
|
|
|
// constants
|
|
globalObject().setProperty("TREE_SCALE", newVariant(QVariant(TREE_SCALE)));
|
|
}
|
|
|
|
QScriptValue ScriptEngine::registerGlobalObject(const QString& name, QObject* object) {
|
|
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(QScriptValue parent, const QString& name, QScriptEngine::FunctionSignature fun, int numArguments) {
|
|
QScriptValue scriptFun = newFunction(fun, numArguments);
|
|
parent.setProperty(name, scriptFun);
|
|
}
|
|
|
|
void ScriptEngine::registerGetterSetter(const QString& name, QScriptEngine::FunctionSignature getter,
|
|
QScriptEngine::FunctionSignature setter, QScriptValue object) {
|
|
QScriptValue setterFunction = newFunction(setter, 1);
|
|
QScriptValue getterFunction = newFunction(getter);
|
|
|
|
if (!object.isNull()) {
|
|
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<QScriptValueList()> 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 (!_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 (_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,
|
|
[=](const EntityItemID& entityID) {
|
|
_registeredHandlers.remove(entityID);
|
|
});
|
|
|
|
// Two common cases of event handler, differing only in argument signature.
|
|
auto makeSingleEntityHandler = [=](const QString& eventName) -> std::function<void(const EntityItemID&)> {
|
|
return [=](const EntityItemID& entityItemID) -> void {
|
|
generalHandler(entityItemID, eventName, [=]() -> QScriptValueList {
|
|
return QScriptValueList() << entityItemID.toScriptValue(this);
|
|
});
|
|
};
|
|
};
|
|
auto makeMouseHandler = [=](const QString& eventName) -> std::function<void(const EntityItemID&, const MouseEvent&)> {
|
|
return [=](const EntityItemID& entityItemID, const MouseEvent& event) -> void {
|
|
generalHandler(entityItemID, eventName, [=]() -> QScriptValueList {
|
|
return QScriptValueList() << entityItemID.toScriptValue(this) << event.toScriptValue(this);
|
|
});
|
|
};
|
|
};
|
|
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,
|
|
[=](const EntityItemID& idA, const EntityItemID& idB, const Collision& collision) {
|
|
generalHandler(idA, "collisionWithEntity", [=]() {
|
|
return QScriptValueList () << idA.toScriptValue(this) << idB.toScriptValue(this) << collisionToScriptValue(this, collision);
|
|
});
|
|
});
|
|
}
|
|
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().
|
|
}
|
|
|
|
|
|
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();
|
|
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
|
|
}
|
|
|
|
_evaluatesPending++;
|
|
QScriptValue result = QScriptEngine::evaluate(program, fileName, lineNumber);
|
|
if (hasUncaughtException()) {
|
|
int line = uncaughtExceptionLineNumber();
|
|
qCDebug(scriptengine) << "Uncaught exception at (" << _fileNameString << " : " << fileName << ") line" << line << ": " << result.toString();
|
|
}
|
|
_evaluatesPending--;
|
|
emit evaluationFinished(result, hasUncaughtException());
|
|
clearExceptions();
|
|
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?
|
|
|
|
if (!_isInitialized) {
|
|
init();
|
|
}
|
|
_isRunning = true;
|
|
_isFinished = false;
|
|
emit runningStateChanged();
|
|
|
|
QScriptValue result = evaluate(_scriptContents);
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
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<const int16_t*>(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<const char*>(nextSoundOutput), numAvailableSamples * sizeof(int16_t));
|
|
}
|
|
|
|
// write audio packet to AudioMixer nodes
|
|
auto nodeList = DependencyManager::get<NodeList>();
|
|
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;
|
|
|
|
if (hasUncaughtException()) {
|
|
int line = uncaughtExceptionLineNumber();
|
|
qCDebug(scriptengine) << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << uncaughtException().toString();
|
|
emit errorMessage("Uncaught exception at (" + _fileNameString + ") line" + QString::number(line) + ":" + uncaughtException().toString());
|
|
clearExceptions();
|
|
}
|
|
|
|
if (!_isFinished) {
|
|
emit update(deltaTime);
|
|
}
|
|
lastUpdate = now;
|
|
|
|
}
|
|
|
|
stopAllTimers(); // make sure all our timers are stopped if the script is ending
|
|
emit scriptEnding();
|
|
|
|
// kill the avatar identity timer
|
|
delete _avatarIdentityTimer;
|
|
|
|
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?
|
|
}
|
|
}
|
|
|
|
emit finished(_fileNameString);
|
|
|
|
_isRunning = false;
|
|
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;
|
|
emit runningStateChanged();
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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) {
|
|
urls.append(resolvePath(file));
|
|
}
|
|
|
|
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());
|
|
emit reloadScript(url.toString(), false);
|
|
} else {
|
|
emit loadScript(url.toString(), false);
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::nodeKilled(SharedNodePointer node) {
|
|
_outgoingScriptAudioSequenceNumbers.remove(node->getUUID());
|
|
}
|