Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 2 files). Show all
Unified · Split
| @@ -485,10 +485,10 @@ void ScriptEngine::removeEventHandler(const EntityItemID& entityID, const QStrin · 484 unmodified lines | |||
| 485 | return; | 485 | return; |
| 486 | } | 486 | } |
| 487 | RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; | 487 | RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; |
| 488 | QScriptValueList& handlersForEvent = handlersOnEntity[eventName]; | 488 | CallbackList& handlersForEvent = handlersOnEntity[eventName]; |
| 489 | // QScriptValue does not have operator==(), so we can't use QList::removeOne and friends. So iterate. | 489 | // QScriptValue does not have operator==(), so we can't use QList::removeOne and friends. So iterate. |
| 490 | for (int i = 0; i < handlersForEvent.count(); ++i) { | 490 | for (int i = 0; i < handlersForEvent.count(); ++i) { |
| 491 | if (handlersForEvent[i].equals(handler)) { | 491 | if (handlersForEvent[i].function.equals(handler)) { |
| 492 | handlersForEvent.removeAt(i); | 492 | handlersForEvent.removeAt(i); |
| 493 | return; // Design choice: since comparison is relatively expensive, just remove the first matching handler. | 493 | return; // Design choice: since comparison is relatively expensive, just remove the first matching handler. |
| 494 | } | 494 | } |
| @@ -516,6 +516,13 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& · 21 unmodified lines | |||
| 516 | // Connect up ALL the handlers to the global entities object's signals. | 516 | // Connect up ALL the handlers to the global entities object's signals. |
| 517 | // (We could go signal by signal, or even handler by handler, but I don't think the efficiency is worth the complexity.) | 517 | // (We could go signal by signal, or even handler by handler, but I don't think the efficiency is worth the complexity.) |
| 518 | auto entities = DependencyManager::get<EntityScriptingInterface>(); | 518 | auto entities = DependencyManager::get<EntityScriptingInterface>(); |
| 519 | // Bug? These handlers are deleted when entityID is deleted, which is nice. | ||
| 520 | // But if they are created by an entity script on a different entity, should they also be deleted when the entity script unloads? | ||
| 521 | // E.g., suppose a bow has an entity script that causes arrows to be created with a potential lifetime greater than the bow, | ||
| 522 | // and that the entity script adds (e.g., collision) handlers to the arrows. Should those handlers fire if the bow is unloaded? | ||
| 523 | // Also, what about when the entity script is REloaded? | ||
| 524 | // For now, we are leaving them around. Changing that would require some non-trivial digging around to find the | ||
| 525 | // handlers that were added while a given currentEntityIdentifier was in place. I don't think this is dangerous. Just perhaps unexpected. -HRS | ||
| 519 | connect(entities.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { | 526 | connect(entities.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { |
| 520 | _registeredHandlers.remove(entityID); | 527 | _registeredHandlers.remove(entityID); |
| 521 | }); | 528 | }); |
| @@ -563,8 +570,9 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& · 41 unmodified lines | |||
| 563 | if (!_registeredHandlers.contains(entityID)) { | 570 | if (!_registeredHandlers.contains(entityID)) { |
| 564 | _registeredHandlers[entityID] = RegisteredEventHandlers(); | 571 | _registeredHandlers[entityID] = RegisteredEventHandlers(); |
| 565 | } | 572 | } |
| 566 | QScriptValueList& handlersForEvent = _registeredHandlers[entityID][eventName]; | 573 | CallbackList& handlersForEvent = _registeredHandlers[entityID][eventName]; |
| 567 | handlersForEvent << handler; // Note that the same handler can be added many times. See removeEntityEventHandler(). | 574 | CallbackData handlerData = {handler, currentEntityIdentifier}; |
| 575 | handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler(). | ||
| 568 | } | 576 | } |
| 569 | 577 | ||
| 570 | 578 | ||
| @@ -705,13 +713,30 @@ void ScriptEngine::run() { · 134 unmodified lines | |||
| 705 | // NOTE: This is private because it must be called on the same thread that created the timers, which is why | 713 | // NOTE: This is private because it must be called on the same thread that created the timers, which is why |
| 706 | // we want to only call it in our own run "shutdown" processing. | 714 | // we want to only call it in our own run "shutdown" processing. |
| 707 | void ScriptEngine::stopAllTimers() { | 715 | void ScriptEngine::stopAllTimers() { |
| 708 | QMutableHashIterator<QTimer*, QScriptValue> i(_timerFunctionMap); | 716 | QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap); |
| 709 | while (i.hasNext()) { | 717 | while (i.hasNext()) { |
| 710 | i.next(); | 718 | i.next(); |
| 711 | QTimer* timer = i.key(); | 719 | QTimer* timer = i.key(); |
| 712 | stopTimer(timer); | 720 | stopTimer(timer); |
| 713 | } | 721 | } |
| 714 | } | 722 | } |
| 723 | void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) { | ||
| 724 | // We could maintain a separate map of entityID => QTimer, but someone will have to prove to me that it's worth the complexity. -HRS | ||
| 725 | QVector<QTimer*> toDelete; | ||
| 726 | QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap); | ||
| 727 | while (i.hasNext()) { | ||
| 728 | i.next(); | ||
| 729 | if (i.value().definingEntityIdentifier != entityID) { | ||
| 730 | continue; | ||
| 731 | } | ||
| 732 | QTimer* timer = i.key(); | ||
| 733 | toDelete << timer; // don't delete while we're iterating. save it. | ||
| 734 | } | ||
| 735 | for (auto timer:toDelete) { // now reap 'em | ||
| 736 | stopTimer(timer); | ||
| 737 | } | ||
| 738 | |||
| 739 | } | ||
| 715 | 740 | ||
| 716 | void ScriptEngine::stop() { | 741 | void ScriptEngine::stop() { |
| 717 | if (!_isFinished) { | 742 | if (!_isFinished) { |
| @@ -743,13 +768,14 @@ void ScriptEngine::callAnimationStateHandler(QScriptValue callback, AnimVariantM · 25 unmodified lines | |||
| 743 | QScriptValue javascriptParameters = parameters.animVariantMapToScriptValue(this, names, useNames); | 768 | QScriptValue javascriptParameters = parameters.animVariantMapToScriptValue(this, names, useNames); |
| 744 | QScriptValueList callingArguments; | 769 | QScriptValueList callingArguments; |
| 745 | callingArguments << javascriptParameters; | 770 | callingArguments << javascriptParameters; |
| 771 | assert(currentEntityIdentifier.isInvalidID()); // No animation state handlers from entity scripts. | ||
| 746 | QScriptValue result = callback.call(QScriptValue(), callingArguments); | 772 | QScriptValue result = callback.call(QScriptValue(), callingArguments); |
| 747 | resultHandler(result); | 773 | resultHandler(result); |
| 748 | } | 774 | } |
| 749 | 775 | ||
| 750 | void ScriptEngine::timerFired() { | 776 | void ScriptEngine::timerFired() { |
| 751 | QTimer* callingTimer = reinterpret_cast<QTimer*>(sender()); | 777 | QTimer* callingTimer = reinterpret_cast<QTimer*>(sender()); |
| 752 | QScriptValue timerFunction = _timerFunctionMap.value(callingTimer); | 778 | CallbackData timerData = _timerFunctionMap.value(callingTimer); |
| 753 | 779 | ||
| 754 | if (!callingTimer->isActive()) { | 780 | if (!callingTimer->isActive()) { |
| 755 | // this timer is done, we can kill it | 781 | // this timer is done, we can kill it |
| @@ -758,11 +784,12 @@ void ScriptEngine::timerFired() { · 2 unmodified lines | |||
| 758 | } | 784 | } |
| 759 | 785 | ||
| 760 | // call the associated JS function, if it exists | 786 | // call the associated JS function, if it exists |
| 761 | if (timerFunction.isValid()) { | 787 | if (timerData.function.isValid()) { |
| 762 | timerFunction.call(); | 788 | callWithEnvironment(timerData.definingEntityIdentifier, timerData.function, timerData.function, QScriptValueList()); |
| 763 | } | 789 | } |
| 764 | } | 790 | } |
| 765 | 791 | ||
| 792 | |||
| 766 | QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { | 793 | QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { |
| 767 | // create the timer, add it to the map, and start it | 794 | // create the timer, add it to the map, and start it |
| 768 | QTimer* newTimer = new QTimer(this); | 795 | QTimer* newTimer = new QTimer(this); |
| @@ -773,7 +800,8 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int · 4 unmodified lines | |||
| 773 | // make sure the timer stops when the script does | 800 | // make sure the timer stops when the script does |
| 774 | connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); | 801 | connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); |
| 775 | 802 | ||
| 776 | _timerFunctionMap.insert(newTimer, function); | 803 | CallbackData timerData = {function, currentEntityIdentifier}; |
| 804 | _timerFunctionMap.insert(newTimer, timerData); | ||
| 777 | 805 | ||
| 778 | newTimer->start(intervalMS); | 806 | newTimer->start(intervalMS); |
| 779 | return newTimer; | 807 | return newTimer; |
| @@ -859,6 +887,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 79 unmodified lines | |||
| 859 | } | 887 | } |
| 860 | 888 | ||
| 861 | BatchLoader* loader = new BatchLoader(urls); | 889 | BatchLoader* loader = new BatchLoader(urls); |
| 890 | EntityItemID capturedEntityIdentifier = currentEntityIdentifier; | ||
| 862 | 891 | ||
| 863 | auto evaluateScripts = [=](const QMap<QUrl, QString>& data) { | 892 | auto evaluateScripts = [=](const QMap<QUrl, QString>& data) { |
| 864 | auto parentURL = _parentURL; | 893 | auto parentURL = _parentURL; |
| @@ -870,13 +899,16 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 5 unmodified lines | |||
| 870 | // Set the parent url so that path resolution will be relative | 899 | // Set the parent url so that path resolution will be relative |
| 871 | // to this script's url during its initial evaluation | 900 | // to this script's url during its initial evaluation |
| 872 | _parentURL = url.toString(); | 901 | _parentURL = url.toString(); |
| 873 | QScriptValue result = evaluate(contents, url.toString()); | 902 | auto operation = [&]() { |
| 903 | evaluate(contents, url.toString()); | ||
| 904 | }; | ||
| 905 | doWithEnvironment(capturedEntityIdentifier, operation); | ||
| 874 | } | 906 | } |
| 875 | } | 907 | } |
| 876 | _parentURL = parentURL; | 908 | _parentURL = parentURL; |
| 877 | 909 | ||
| 878 | if (callback.isFunction()) { | 910 | if (callback.isFunction()) { |
| 879 | QScriptValue(callback).call(); | 911 | callWithEnvironment(capturedEntityIdentifier, QScriptValue(callback), QScriptValue(), QScriptValueList()); |
| 880 | } | 912 | } |
| 881 | 913 | ||
| 882 | loader->deleteLater(); | 914 | loader->deleteLater(); |
| @@ -917,6 +949,11 @@ void ScriptEngine::load(const QString& loadFile) { · 34 unmodified lines | |||
| 917 | << "loadFile:" << loadFile << "parent script:" << getFilename(); | 949 | << "loadFile:" << loadFile << "parent script:" << getFilename(); |
| 918 | return; // bail early | 950 | return; // bail early |
| 919 | } | 951 | } |
| 952 | if (!currentEntityIdentifier.isInvalidID()) { | ||
| 953 | qCWarning(scriptengine) << "Script.load() from entity script is ignored... " | ||
| 954 | << "loadFile:" << loadFile << "parent script:" << getFilename(); | ||
| 955 | return; // bail early | ||
| 956 | } | ||
| 920 | 957 | ||
| 921 | QUrl url = resolvePath(loadFile); | 958 | QUrl url = resolvePath(loadFile); |
| 922 | if (_isReloading) { | 959 | if (_isReloading) { |
| @@ -933,7 +970,7 @@ void ScriptEngine::load(const QString& loadFile) { · 10 unmodified lines | |||
| 933 | } | 970 | } |
| 934 | 971 | ||
| 935 | // Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those args | 972 | // Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those args |
| 936 | void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHanderArgs) { | 973 | void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHandlerArgs) { |
| 937 | if (QThread::currentThread() != thread()) { | 974 | if (QThread::currentThread() != thread()) { |
| 938 | qDebug() << "*** ERROR *** ScriptEngine::forwardHandlerCall() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; | 975 | qDebug() << "*** ERROR *** ScriptEngine::forwardHandlerCall() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; |
| 939 | assert(false); | 976 | assert(false); |
| @@ -946,10 +983,13 @@ void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QStrin · 6 unmodified lines | |||
| 946 | if (!handlersOnEntity.contains(eventName)) { | 983 | if (!handlersOnEntity.contains(eventName)) { |
| 947 | return; | 984 | return; |
| 948 | } | 985 | } |
| 949 | QScriptValueList handlersForEvent = handlersOnEntity[eventName]; | 986 | CallbackList handlersForEvent = handlersOnEntity[eventName]; |
| 950 | if (!handlersForEvent.isEmpty()) { | 987 | if (!handlersForEvent.isEmpty()) { |
| 951 | for (int i = 0; i < handlersForEvent.count(); ++i) { | 988 | for (int i = 0; i < handlersForEvent.count(); ++i) { |
| 952 | handlersForEvent[i].call(QScriptValue(), eventHanderArgs); | 989 | // handlersForEvent[i] can tonain many handlers that may have each been added by different interface or entity scripts, |
| 990 | // and the entity scripts may be for entities other than the one this is a handler for. | ||
| 991 | // Fortunately, the definingEntityIdentifier captured the entity script id (if any) when the handler was added. | ||
| 992 | callWithEnvironment(handlersForEvent[i].definingEntityIdentifier, handlersForEvent[i].function, QScriptValue(), eventHandlerArgs); | ||
| 953 | } | 993 | } |
| 954 | } | 994 | } |
| 955 | } | 995 | } |
| @@ -1055,9 +1095,13 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 99 unmodified lines | |||
| 1055 | QString file = QUrl(scriptOrURL).toLocalFile(); | 1095 | QString file = QUrl(scriptOrURL).toLocalFile(); |
| 1056 | lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch(); | 1096 | lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch(); |
| 1057 | } | 1097 | } |
| 1098 | QScriptValue entityScriptConstructor, entityScriptObject; | ||
| 1099 | auto initialization = [&]{ | ||
| 1100 | entityScriptConstructor = evaluate(contents, fileName); | ||
| 1101 | entityScriptObject = entityScriptConstructor.construct(); | ||
| 1102 | }; | ||
| 1103 | doWithEnvironment(entityID, initialization); | ||
| 1058 | 1104 | ||
| 1059 | QScriptValue entityScriptConstructor = evaluate(contents, fileName); | ||
| 1060 | QScriptValue entityScriptObject = entityScriptConstructor.construct(); | ||
| 1061 | EntityScriptDetails newDetails = { scriptOrURL, entityScriptObject, lastModified }; | 1105 | EntityScriptDetails newDetails = { scriptOrURL, entityScriptObject, lastModified }; |
| 1062 | _entityScripts[entityID] = newDetails; | 1106 | _entityScripts[entityID] = newDetails; |
| 1063 | if (isURL) { | 1107 | if (isURL) { |
| @@ -1087,6 +1131,7 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { · 23 unmodified lines | |||
| 1087 | if (_entityScripts.contains(entityID)) { | 1131 | if (_entityScripts.contains(entityID)) { |
| 1088 | callEntityScriptMethod(entityID, "unload"); | 1132 | callEntityScriptMethod(entityID, "unload"); |
| 1089 | _entityScripts.remove(entityID); | 1133 | _entityScripts.remove(entityID); |
| 1134 | stopAllTimersForEntityScript(entityID); | ||
| 1090 | } | 1135 | } |
| 1091 | } | 1136 | } |
| 1092 | 1137 | ||
| @@ -1142,6 +1187,32 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { · 49 unmodified lines | |||
| 1142 | recurseGuard = false; | 1187 | recurseGuard = false; |
| 1143 | } | 1188 | } |
| 1144 | 1189 | ||
| 1190 | // Execute operation in the appropriate context for (the possibly empty) entityID. | ||
| 1191 | // Even if entityID is supplied as currentEntityIdentifier, this still documents the source | ||
| 1192 | // of the code being executed (e.g., if we ever sandbox different entity scripts, or provide different | ||
| 1193 | // global values for different entity scripts). | ||
| 1194 | void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, std::function<void()> operation) { | ||
| 1195 | EntityItemID oldIdentifier = currentEntityIdentifier; | ||
| 1196 | currentEntityIdentifier = entityID; | ||
| 1197 | |||
| 1198 | #if DEBUG_CURRENT_ENTITY | ||
| 1199 | QScriptValue oldData = this->globalObject().property("debugEntityID"); | ||
| 1200 | this->globalObject().setProperty("debugEntityID", entityID.toScriptValue(this)); // Make the entityID available to javascript as a global. | ||
| 1201 | operation(); | ||
| 1202 | this->globalObject().setProperty("debugEntityID", oldData); | ||
| 1203 | #else | ||
| 1204 | operation(); | ||
| 1205 | #endif | ||
| 1206 | |||
| 1207 | currentEntityIdentifier = oldIdentifier; | ||
| 1208 | } | ||
| 1209 | void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, QScriptValue function, QScriptValue thisObject, QScriptValueList args) { | ||
| 1210 | auto operation = [&]() { | ||
| 1211 | function.call(thisObject, args); | ||
| 1212 | }; | ||
| 1213 | doWithEnvironment(entityID, operation); | ||
| 1214 | } | ||
| 1215 | |||
| 1145 | void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params) { | 1216 | void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params) { |
| 1146 | if (QThread::currentThread() != thread()) { | 1217 | if (QThread::currentThread() != thread()) { |
| 1147 | #ifdef THREAD_DEBUGGING | 1218 | #ifdef THREAD_DEBUGGING |
| @@ -1168,7 +1239,7 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 20 unmodified lines | |||
| 1168 | QScriptValueList args; | 1239 | QScriptValueList args; |
| 1169 | args << entityID.toScriptValue(this); | 1240 | args << entityID.toScriptValue(this); |
| 1170 | args << qScriptValueFromSequence(this, params); | 1241 | args << qScriptValueFromSequence(this, params); |
| 1171 | entityScript.property(methodName).call(entityScript, args); | 1242 | callWithEnvironment(entityID, entityScript.property(methodName), entityScript, args); |
| 1172 | } | 1243 | } |
| 1173 | 1244 | ||
| 1174 | } | 1245 | } |
| @@ -1200,7 +1271,7 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 25 unmodified lines | |||
| 1200 | QScriptValueList args; | 1271 | QScriptValueList args; |
| 1201 | args << entityID.toScriptValue(this); | 1272 | args << entityID.toScriptValue(this); |
| 1202 | args << event.toScriptValue(this); | 1273 | args << event.toScriptValue(this); |
| 1203 | entityScript.property(methodName).call(entityScript, args); | 1274 | callWithEnvironment(entityID, entityScript.property(methodName), entityScript, args); |
| 1204 | } | 1275 | } |
| 1205 | } | 1276 | } |
| 1206 | } | 1277 | } |
| @@ -1234,7 +1305,7 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 27 unmodified lines | |||
| 1234 | args << entityID.toScriptValue(this); | 1305 | args << entityID.toScriptValue(this); |
| 1235 | args << otherID.toScriptValue(this); | 1306 | args << otherID.toScriptValue(this); |
| 1236 | args << collisionToScriptValue(this, collision); | 1307 | args << collisionToScriptValue(this, collision); |
| 1237 | entityScript.property(methodName).call(entityScript, args); | 1308 | callWithEnvironment(entityID, entityScript.property(methodName), entityScript, args); |
| 1238 | } | 1309 | } |
| 1239 | } | 1310 | } |
| 1240 | } | 1311 | } |