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 | 485 | return; |
| 486 | 486 | } |
| 487 | 487 | RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; |
| 488 | − QScriptValueList& handlersForEvent = handlersOnEntity[eventName]; | |
| 488 | + CallbackList& handlersForEvent = handlersOnEntity[eventName]; | |
| 489 | 489 | // QScriptValue does not have operator==(), so we can't use QList::removeOne and friends. So iterate. |
| 490 | 490 | for (int i = 0; i < handlersForEvent.count(); ++i) { |
| 491 | − if (handlersForEvent[i].equals(handler)) { | |
| 491 | + if (handlersForEvent[i].function.equals(handler)) { | |
| 492 | 492 | handlersForEvent.removeAt(i); |
| 493 | 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 | 516 | // Connect up ALL the handlers to the global entities object's signals. |
| 517 | 517 | // (We could go signal by signal, or even handler by handler, but I don't think the efficiency is worth the complexity.) |
| 518 | 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 | 526 | connect(entities.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { |
| 520 | 527 | _registeredHandlers.remove(entityID); |
| 521 | 528 | }); |
| @@ -563,8 +570,9 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& · 41 unmodified lines | ||
| 563 | 570 | if (!_registeredHandlers.contains(entityID)) { |
| 564 | 571 | _registeredHandlers[entityID] = RegisteredEventHandlers(); |
| 565 | 572 | } |
| 566 | − QScriptValueList& handlersForEvent = _registeredHandlers[entityID][eventName]; | |
| 567 | − handlersForEvent << handler; // Note that the same handler can be added many times. See removeEntityEventHandler(). | |
| 573 | + CallbackList& handlersForEvent = _registeredHandlers[entityID][eventName]; | |
| 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 | 713 | // NOTE: This is private because it must be called on the same thread that created the timers, which is why |
| 706 | 714 | // we want to only call it in our own run "shutdown" processing. |
| 707 | 715 | void ScriptEngine::stopAllTimers() { |
| 708 | − QMutableHashIterator<QTimer*, QScriptValue> i(_timerFunctionMap); | |
| 716 | + QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap); | |
| 709 | 717 | while (i.hasNext()) { |
| 710 | 718 | i.next(); |
| 711 | 719 | QTimer* timer = i.key(); |
| 712 | 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 | 741 | void ScriptEngine::stop() { |
| 717 | 742 | if (!_isFinished) { |
| @@ -743,13 +768,14 @@ void ScriptEngine::callAnimationStateHandler(QScriptValue callback, AnimVariantM · 25 unmodified lines | ||
| 743 | 768 | QScriptValue javascriptParameters = parameters.animVariantMapToScriptValue(this, names, useNames); |
| 744 | 769 | QScriptValueList callingArguments; |
| 745 | 770 | callingArguments << javascriptParameters; |
| 771 | + assert(currentEntityIdentifier.isInvalidID()); // No animation state handlers from entity scripts. | |
| 746 | 772 | QScriptValue result = callback.call(QScriptValue(), callingArguments); |
| 747 | 773 | resultHandler(result); |
| 748 | 774 | } |
| 749 | 775 | |
| 750 | 776 | void ScriptEngine::timerFired() { |
| 751 | 777 | QTimer* callingTimer = reinterpret_cast<QTimer*>(sender()); |
| 752 | − QScriptValue timerFunction = _timerFunctionMap.value(callingTimer); | |
| 778 | + CallbackData timerData = _timerFunctionMap.value(callingTimer); | |
| 753 | 779 | |
| 754 | 780 | if (!callingTimer->isActive()) { |
| 755 | 781 | // this timer is done, we can kill it |
| @@ -758,11 +784,12 @@ void ScriptEngine::timerFired() { · 2 unmodified lines | ||
| 758 | 784 | } |
| 759 | 785 | |
| 760 | 786 | // call the associated JS function, if it exists |
| 761 | − if (timerFunction.isValid()) { | |
| 762 | − timerFunction.call(); | |
| 787 | + if (timerData.function.isValid()) { | |
| 788 | + callWithEnvironment(timerData.definingEntityIdentifier, timerData.function, timerData.function, QScriptValueList()); | |
| 763 | 789 | } |
| 764 | 790 | } |
| 765 | 791 | |
| 792 | + | |
| 766 | 793 | QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { |
| 767 | 794 | // create the timer, add it to the map, and start it |
| 768 | 795 | QTimer* newTimer = new QTimer(this); |
| @@ -773,7 +800,8 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int · 4 unmodified lines | ||
| 773 | 800 | // make sure the timer stops when the script does |
| 774 | 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 | 806 | newTimer->start(intervalMS); |
| 779 | 807 | return newTimer; |
| @@ -859,6 +887,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 79 unmodified lines | ||
| 859 | 887 | } |
| 860 | 888 | |
| 861 | 889 | BatchLoader* loader = new BatchLoader(urls); |
| 890 | + EntityItemID capturedEntityIdentifier = currentEntityIdentifier; | |
| 862 | 891 | |
| 863 | 892 | auto evaluateScripts = [=](const QMap<QUrl, QString>& data) { |
| 864 | 893 | auto parentURL = _parentURL; |
| @@ -870,13 +899,16 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 5 unmodified lines | ||
| 870 | 899 | // Set the parent url so that path resolution will be relative |
| 871 | 900 | // to this script's url during its initial evaluation |
| 872 | 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 | 908 | _parentURL = parentURL; |
| 877 | 909 | |
| 878 | 910 | if (callback.isFunction()) { |
| 879 | − QScriptValue(callback).call(); | |
| 911 | + callWithEnvironment(capturedEntityIdentifier, QScriptValue(callback), QScriptValue(), QScriptValueList()); | |
| 880 | 912 | } |
| 881 | 913 | |
| 882 | 914 | loader->deleteLater(); |
| @@ -917,6 +949,11 @@ void ScriptEngine::load(const QString& loadFile) { · 34 unmodified lines | ||
| 917 | 949 | << "loadFile:" << loadFile << "parent script:" << getFilename(); |
| 918 | 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 | 958 | QUrl url = resolvePath(loadFile); |
| 922 | 959 | if (_isReloading) { |
| @@ -933,7 +970,7 @@ void ScriptEngine::load(const QString& loadFile) { · 10 unmodified lines | ||
| 933 | 970 | } |
| 934 | 971 | |
| 935 | 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 | 974 | if (QThread::currentThread() != thread()) { |
| 938 | 975 | qDebug() << "*** ERROR *** ScriptEngine::forwardHandlerCall() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; |
| 939 | 976 | assert(false); |
| @@ -946,10 +983,13 @@ void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QStrin · 6 unmodified lines | ||
| 946 | 983 | if (!handlersOnEntity.contains(eventName)) { |
| 947 | 984 | return; |
| 948 | 985 | } |
| 949 | − QScriptValueList handlersForEvent = handlersOnEntity[eventName]; | |
| 986 | + CallbackList handlersForEvent = handlersOnEntity[eventName]; | |
| 950 | 987 | if (!handlersForEvent.isEmpty()) { |
| 951 | 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 | 1095 | QString file = QUrl(scriptOrURL).toLocalFile(); |
| 1056 | 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 | 1105 | EntityScriptDetails newDetails = { scriptOrURL, entityScriptObject, lastModified }; |
| 1062 | 1106 | _entityScripts[entityID] = newDetails; |
| 1063 | 1107 | if (isURL) { |
| @@ -1087,6 +1131,7 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { · 23 unmodified lines | ||
| 1087 | 1131 | if (_entityScripts.contains(entityID)) { |
| 1088 | 1132 | callEntityScriptMethod(entityID, "unload"); |
| 1089 | 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 | 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 | 1216 | void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params) { |
| 1146 | 1217 | if (QThread::currentThread() != thread()) { |
| 1147 | 1218 | #ifdef THREAD_DEBUGGING |
| @@ -1168,7 +1239,7 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 20 unmodified lines | ||
| 1168 | 1239 | QScriptValueList args; |
| 1169 | 1240 | args << entityID.toScriptValue(this); |
| 1170 | 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 | 1271 | QScriptValueList args; |
| 1201 | 1272 | args << entityID.toScriptValue(this); |
| 1202 | 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 | 1305 | args << entityID.toScriptValue(this); |
| 1235 | 1306 | args << otherID.toScriptValue(this); |
| 1236 | 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 | } |