first cut at moving entity scripts into ScriptEngine

This commit is contained in:
Brad Hefta-Gaub 2015-09-14 15:13:43 -07:00
parent 56118e4204
commit 18fbf896f1
6 changed files with 283 additions and 316 deletions

View file

@ -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<EntityItemID> 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<EntityTree*>(_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<ScriptCache>();
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<ScriptCache>();
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<SceneScriptingInterface>().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<EntityTree*>(_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) {

View file

@ -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<EntityItemID>& 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<EntityItemID, EntityScriptDetails> _entityScripts;
//QHash<EntityItemID, EntityScriptDetails> _entityScripts;
void playEntityCollisionSound(const QUuid& myNodeID, EntityTree* entityTree, const EntityItemID& id, const Collision& collision);

View file

@ -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<ResourceRequest*>(sender());
QUrl url = req->getUrl();
QList<contentAvailableCallback> 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();
}

View file

@ -20,23 +20,33 @@ public:
virtual void errorInLoadingScript(const QUrl& url) = 0;
};
using contentAvailableCallback = std::function<void(const QString& scriptOrURL, const QString& contents, bool isURL, bool contentAvailable)>;
/// 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<QUrl, contentAvailableCallback> _contentCallbacks;
QHash<QUrl, QString> _scriptCache;
QMultiMap<QUrl, ScriptUser*> _scriptUsers;
QSet<QUrl> _badScripts;

View file

@ -16,6 +16,7 @@
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <QScriptEngine>
#include <QScriptValue>
#include <AudioConstants.h>
#include <AudioEffectOptions.h>
@ -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>();
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);
}
}
}

View file

@ -40,6 +40,12 @@ const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0f / 60.0f) * 1000 * 1
typedef QHash<QString, QScriptValueList> 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<QUrl> _includedURLs;
bool _wantSignals = true;
QHash<EntityItemID, EntityScriptDetails> _entityScripts;
private:
void init();
QString getFilename() const;