diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 21e056d914..edce63bafd 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1269,6 +1269,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(scriptEngines, &ScriptEngines::scriptsReloading, scriptEngines, [this] { getEntities()->reloadEntityScripts(); + loadAvatarScripts(getMyAvatar()->getScriptUrls()); }, Qt::QueuedConnection); connect(scriptEngines, &ScriptEngines::scriptLoadError, @@ -4835,6 +4836,31 @@ void Application::init() { }, Qt::QueuedConnection); } +void Application::loadAvatarScripts(const QVector& urls) { + auto scriptEngines = DependencyManager::get(); + auto runningScripts = scriptEngines->getRunningScripts(); + for (auto url : urls) { + int index = runningScripts.indexOf(url); + if (index < 0) { + auto scriptEnginePointer = scriptEngines->loadScript(url, false); + if (scriptEnginePointer) { + scriptEnginePointer->setType(ScriptEngine::Type::AVATAR); + } + } + } +} + +void Application::unloadAvatarScripts() { + auto scriptEngines = DependencyManager::get(); + auto urls = scriptEngines->getRunningScripts(); + for (auto url : urls) { + auto scriptEngine = scriptEngines->getScriptEngine(url); + if (scriptEngine->getType() == ScriptEngine::Type::AVATAR) { + scriptEngines->stopScript(url, false); + } + } +} + void Application::updateLOD(float deltaTime) const { PerformanceTimer perfTimer("LOD"); // adjust it unless we were asked to disable this feature, or if we're currently in throttleRendering mode diff --git a/interface/src/Application.h b/interface/src/Application.h index ea590545c2..a477cf33b9 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -290,6 +290,9 @@ public: void replaceDomainContent(const QString& url); + void loadAvatarScripts(const QVector& urls); + void unloadAvatarScripts(); + #if defined(Q_OS_ANDROID) void enterBackground(); void enterForeground(); diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index 5f4c7526e0..0e34eebc80 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -156,9 +156,11 @@ bool ModelPackager::zipModel() { QByteArray nameField = _mapping.value(NAME_FIELD).toByteArray(); tempDir.mkpath(nameField + "/textures"); + tempDir.mkpath(nameField + "/scripts"); QDir fbxDir(tempDir.path() + "/" + nameField); QDir texDir(fbxDir.path() + "/textures"); - + QDir scriptDir(fbxDir.path() + "/scripts"); + // Copy textures listTextures(); if (!_textures.empty()) { @@ -166,6 +168,23 @@ bool ModelPackager::zipModel() { _texDir = _modelFile.path() + "/" + texdirField; copyTextures(_texDir, texDir); } + + // Copy scripts + QByteArray scriptField = _mapping.value(SCRIPT_FIELD).toByteArray(); + _mapping.remove(SCRIPT_FIELD); + if (scriptField.size() > 1) { + tempDir.mkpath(nameField + "/scripts"); + _scriptDir = _modelFile.path() + "/" + scriptField; + QDir wdir = QDir(_scriptDir); + _mapping.remove(SCRIPT_FIELD); + wdir.setSorting(QDir::Name | QDir::Reversed); + auto list = wdir.entryList(QDir::NoDotAndDotDot | QDir::AllEntries); + for (auto script : list) { + auto sc = tempDir.relativeFilePath(scriptDir.path()) + "/" + QUrl(script).fileName(); + _mapping.insertMulti(SCRIPT_FIELD, sc); + } + copyDirectoryContent(wdir, scriptDir); + } // Copy LODs QVariantHash lodField = _mapping.value(LOD_FIELD).toHash(); @@ -189,7 +208,10 @@ bool ModelPackager::zipModel() { // Correct FST _mapping[FILENAME_FIELD] = tempDir.relativeFilePath(newPath); _mapping[TEXDIR_FIELD] = tempDir.relativeFilePath(texDir.path()); - + + for (auto multi : _mapping.values(SCRIPT_FIELD)) { + multi.fromValue(tempDir.relativeFilePath(scriptDir.path()) + multi.toString()); + } // Copy FST QFile fst(tempDir.path() + "/" + nameField + ".fst"); if (fst.open(QIODevice::WriteOnly)) { @@ -237,7 +259,9 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename if (!mapping.contains(TEXDIR_FIELD)) { mapping.insert(TEXDIR_FIELD, "."); } - + if (!mapping.contains(SCRIPT_FIELD)) { + mapping.insert(SCRIPT_FIELD, "."); + } // mixamo/autodesk defaults if (!mapping.contains(SCALE_FIELD)) { mapping.insert(SCALE_FIELD, 1.0); diff --git a/interface/src/ModelPackager.h b/interface/src/ModelPackager.h index 10942833f9..60b3825c4d 100644 --- a/interface/src/ModelPackager.h +++ b/interface/src/ModelPackager.h @@ -37,10 +37,12 @@ private: QFileInfo _fbxInfo; FSTReader::ModelType _modelType; QString _texDir; + QString _scriptDir; QVariantHash _mapping; std::unique_ptr _geometry; QStringList _textures; + QStringList _scripts; }; diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp index ae352974ae..35b07aa2b2 100644 --- a/interface/src/ModelPropertiesDialog.cpp +++ b/interface/src/ModelPropertiesDialog.cpp @@ -43,6 +43,9 @@ _geometry(geometry) form->addRow("Texture Directory:", _textureDirectory = new QPushButton()); connect(_textureDirectory, SIGNAL(clicked(bool)), SLOT(chooseTextureDirectory())); + form->addRow("Script Directory:", _scriptDirectory = new QPushButton()); + connect(_scriptDirectory, SIGNAL(clicked(bool)), SLOT(chooseScriptDirectory())); + form->addRow("Scale:", _scale = new QDoubleSpinBox()); _scale->setMaximum(FLT_MAX); _scale->setSingleStep(0.01); @@ -100,6 +103,7 @@ QVariantHash ModelPropertiesDialog::getMapping() const { mapping.insert(TYPE_FIELD, getType()); mapping.insert(NAME_FIELD, _name->text()); mapping.insert(TEXDIR_FIELD, _textureDirectory->text()); + mapping.insert(SCRIPT_FIELD, _scriptDirectory->text()); mapping.insert(SCALE_FIELD, QString::number(_scale->value())); // update the joint indices @@ -157,6 +161,7 @@ void ModelPropertiesDialog::reset() { _name->setText(_originalMapping.value(NAME_FIELD).toString()); _textureDirectory->setText(_originalMapping.value(TEXDIR_FIELD).toString()); _scale->setValue(_originalMapping.value(SCALE_FIELD).toDouble()); + _scriptDirectory->setText(_originalMapping.value(SCRIPT_FIELD).toString()); QVariantHash jointHash = _originalMapping.value(JOINT_FIELD).toHash(); @@ -207,6 +212,20 @@ void ModelPropertiesDialog::chooseTextureDirectory() { _textureDirectory->setText(directory.length() == _basePath.length() ? "." : directory.mid(_basePath.length() + 1)); } +void ModelPropertiesDialog::chooseScriptDirectory() { + QString directory = QFileDialog::getExistingDirectory(this, "Choose Script Directory", + _basePath + "/" + _scriptDirectory->text()); + if (directory.isEmpty()) { + return; + } + if (!directory.startsWith(_basePath)) { + OffscreenUi::asyncWarning(NULL, "Invalid script directory", "Script directory must be child of base path."); + return; + } + _scriptDirectory->setText(directory.length() == _basePath.length() ? "." : directory.mid(_basePath.length() + 1)); +} + + void ModelPropertiesDialog::updatePivotJoint() { _pivotJoint->setEnabled(!_pivotAboutCenter->isChecked()); } diff --git a/interface/src/ModelPropertiesDialog.h b/interface/src/ModelPropertiesDialog.h index 11abc5ab54..e3c2d8ed6a 100644 --- a/interface/src/ModelPropertiesDialog.h +++ b/interface/src/ModelPropertiesDialog.h @@ -37,6 +37,7 @@ public: private slots: void reset(); void chooseTextureDirectory(); + void chooseScriptDirectory(); void updatePivotJoint(); void createNewFreeJoint(const QString& joint = QString()); @@ -52,6 +53,7 @@ private: FBXGeometry _geometry; QLineEdit* _name = nullptr; QPushButton* _textureDirectory = nullptr; + QPushButton* _scriptDirectory = nullptr; QDoubleSpinBox* _scale = nullptr; QDoubleSpinBox* _translationX = nullptr; QDoubleSpinBox* _translationY = nullptr; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 249a765d92..5ce056e9b7 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -121,6 +121,19 @@ MyAvatar::MyAvatar(QThread* thread) : _skeletonModel = std::make_shared(this, nullptr); connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished); + connect(_skeletonModel.get(), &Model::setURLFinished, this, [this](bool success) { + if (success) { + qApp->unloadAvatarScripts(); + _shouldLoadScripts = true; + } + }); + connect(_skeletonModel.get(), &Model::rigReady, this, [this]() { + if (_shouldLoadScripts) { + auto geometry = getSkeletonModel()->getFBXGeometry(); + qApp->loadAvatarScripts(geometry.scripts); + _shouldLoadScripts = false; + } + }); connect(_skeletonModel.get(), &Model::rigReady, this, &Avatar::rigReady); connect(_skeletonModel.get(), &Model::rigReset, this, &Avatar::rigReset); @@ -2839,6 +2852,11 @@ void MyAvatar::setWalkSpeed(float value) { _walkSpeed.set(value); } +QVector MyAvatar::getScriptUrls() { + QVector scripts = _skeletonModel->isLoaded() ? _skeletonModel->getFBXGeometry().scripts : QVector(); + return scripts; +} + glm::vec3 MyAvatar::getPositionForAudio() { glm::vec3 result; switch (_audioListenerMode) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 74f7a3c89f..ac3d3cd2f4 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -985,6 +985,8 @@ public: void setWalkSpeed(float value); float getWalkSpeed() const; + QVector getScriptUrls(); + public slots: /**jsdoc @@ -1322,7 +1324,6 @@ signals: private slots: void leaveDomain(); - protected: virtual void beParentOfChild(SpatiallyNestablePointer newChild) const override; virtual void forgetChild(SpatiallyNestablePointer newChild) const override; @@ -1564,6 +1565,9 @@ private: // max unscaled forward movement speed ThreadSafeValueCache _walkSpeed { DEFAULT_AVATAR_MAX_WALKING_SPEED }; float _walkSpeedScalar { AVATAR_WALK_SPEED_SCALAR }; + + // load avatar scripts once when rig is ready + bool _shouldLoadScripts { false }; }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index a609d85fc8..ce3fc52c3a 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -298,6 +298,7 @@ public: bool hasSkeletonJoints; QVector meshes; + QVector scripts; QHash materials; diff --git a/libraries/fbx/src/FBXReader_Mesh.cpp b/libraries/fbx/src/FBXReader_Mesh.cpp index e8365e38b7..801edddb06 100644 --- a/libraries/fbx/src/FBXReader_Mesh.cpp +++ b/libraries/fbx/src/FBXReader_Mesh.cpp @@ -249,7 +249,7 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn indexToDirect = true; } } - if (indexToDirect && data.normalIndices.isEmpty()) { + if (indexToDirect && data.colorIndices.isEmpty()) { // hack to work around wacky Makehuman exports data.colorsByVertex = true; } diff --git a/libraries/fbx/src/FSTReader.cpp b/libraries/fbx/src/FSTReader.cpp index cc4a919445..d63a5b3cc4 100644 --- a/libraries/fbx/src/FSTReader.cpp +++ b/libraries/fbx/src/FSTReader.cpp @@ -84,7 +84,7 @@ void FSTReader::writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) QByteArray FSTReader::writeMapping(const QVariantHash& mapping) { static const QStringList PREFERED_ORDER = QStringList() << NAME_FIELD << TYPE_FIELD << SCALE_FIELD << FILENAME_FIELD - << TEXDIR_FIELD << JOINT_FIELD << FREE_JOINT_FIELD + << TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD << FREE_JOINT_FIELD << BLENDSHAPE_FIELD << JOINT_INDEX_FIELD; QBuffer buffer; buffer.open(QIODevice::WriteOnly); @@ -92,7 +92,7 @@ QByteArray FSTReader::writeMapping(const QVariantHash& mapping) { for (auto key : PREFERED_ORDER) { auto it = mapping.find(key); if (it != mapping.constEnd()) { - if (key == FREE_JOINT_FIELD) { // writeVariant does not handle strings added using insertMulti. + if (key == FREE_JOINT_FIELD || key == SCRIPT_FIELD) { // writeVariant does not handle strings added using insertMulti. for (auto multi : mapping.values(key)) { buffer.write(key.toUtf8()); buffer.write(" = "); @@ -187,6 +187,26 @@ FSTReader::ModelType FSTReader::predictModelType(const QVariantHash& mapping) { return ENTITY_MODEL; } +QVector FSTReader::getScripts(const QUrl& url, const QVariantHash& mapping) { + + auto fstMapping = mapping.isEmpty() ? downloadMapping(url.toString()) : mapping; + QVector scriptPaths; + if (!fstMapping.value(SCRIPT_FIELD).isNull()) { + auto scripts = fstMapping.values(SCRIPT_FIELD).toVector(); + for (auto &script : scripts) { + QString scriptPath = script.toString(); + if (QUrl(scriptPath).isRelative()) { + if (scriptPath.at(0) == '/') { + scriptPath = scriptPath.right(scriptPath.length() - 1); + } + scriptPath = url.resolved(QUrl(scriptPath)).toString(); + } + scriptPaths.push_back(scriptPath); + } + } + return scriptPaths; +} + QVariantHash FSTReader::downloadMapping(const QString& url) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest networkRequest = QNetworkRequest(url); diff --git a/libraries/fbx/src/FSTReader.h b/libraries/fbx/src/FSTReader.h index 981bae4feb..4a8574f0cf 100644 --- a/libraries/fbx/src/FSTReader.h +++ b/libraries/fbx/src/FSTReader.h @@ -28,6 +28,7 @@ static const QString TRANSLATION_Z_FIELD = "tz"; static const QString JOINT_FIELD = "joint"; static const QString FREE_JOINT_FIELD = "freeJoint"; static const QString BLENDSHAPE_FIELD = "bs"; +static const QString SCRIPT_FIELD = "script"; class FSTReader { public: @@ -49,6 +50,8 @@ public: /// Predicts the type of model by examining the mapping static ModelType predictModelType(const QVariantHash& mapping); + static QVector getScripts(const QUrl& fstUrl, const QVariantHash& mapping = QVariantHash()); + static QString getNameFromType(ModelType modelType); static FSTReader::ModelType getTypeFromName(const QString& name); static QVariantHash downloadMapping(const QString& url); diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index f17cdbb7e8..416920d43f 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -66,6 +66,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { auto mapping = FSTReader::readMapping(data); QString filename = mapping.value("filename").toString(); + if (filename.isNull()) { qCDebug(modelnetworking) << "Mapping file" << _url << "has no \"filename\" field"; finishedLoading(false); @@ -82,6 +83,14 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { _textureBaseUrl = url.resolved(QUrl(".")); } + auto scripts = FSTReader::getScripts(_url, mapping); + if (scripts.size() > 0) { + mapping.remove(SCRIPT_FIELD); + for (auto &scriptPath : scripts) { + mapping.insertMulti(SCRIPT_FIELD, scriptPath); + } + } + auto animGraphVariant = mapping.value("animGraphUrl"); if (animGraphVariant.isValid()) { QUrl fstUrl(animGraphVariant.toString()); @@ -209,6 +218,14 @@ void GeometryReader::run() { throw QString("unsupported format"); } + // Add scripts to fbxgeometry + if (!_mapping.value(SCRIPT_FIELD).isNull()) { + QVariantList scripts = _mapping.values(SCRIPT_FIELD); + for (auto &script : scripts) { + fbxGeometry->scripts.push_back(script.toString()); + } + } + // Ensure the resource has not been deleted auto resource = _resource.toStrongRef(); if (!resource) { diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index c2bacd4949..68f21eea87 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -447,7 +447,12 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { // this case is prevented by setting _ownershipState to UNOWNABLE in EntityMotionState::ctor assert(!(_entity->getClientOnly() && _entity->getOwningAvatarID() != Physics::getSessionUUID())); - if (_entity->dynamicDataNeedsTransmit() || _entity->queryAACubeNeedsUpdate()) { + // shouldSendUpdate() sould NOT be triggering updates to maintain the queryAACube of dynamic entities. + // The server is supposed to predict the transform of such moving things. The client performs a "double prediction" + // where it predicts what the the server is doing, and only sends updates whent the entity's true transform + // differs significantly. That is what the remoteSimulationOutOfSync() logic is all about. + if (_entity->dynamicDataNeedsTransmit() || + (!_entity->getDynamic() && _entity->queryAACubeNeedsUpdate())) { return true; } diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index f0a13cc62b..4915a2dc8b 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -180,6 +180,21 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const // don't delete `ScriptEngines` until all `ScriptEngine`s are gone _scriptEngines(DependencyManager::get()) { + switch (_context) { + case Context::CLIENT_SCRIPT: + _type = Type::CLIENT; + break; + case Context::ENTITY_CLIENT_SCRIPT: + _type = Type::ENTITY_CLIENT; + break; + case Context::ENTITY_SERVER_SCRIPT: + _type = Type::ENTITY_SERVER; + break; + case Context::AGENT_SCRIPT: + _type = Type::AGENT; + break; + } + connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) { if (hasUncaughtException()) { // the engine's uncaughtException() seems to produce much better stack traces here diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 63a4ba4f90..bc7e847cdf 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -103,6 +103,14 @@ public: AGENT_SCRIPT }; + enum Type { + CLIENT, + ENTITY_CLIENT, + ENTITY_SERVER, + AGENT, + AVATAR + }; + static int processLevelMaxRetries; ScriptEngine(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString("about:ScriptEngine")); ~ScriptEngine(); @@ -493,6 +501,9 @@ public: */ Q_INVOKABLE QUuid generateUUID() { return QUuid::createUuid(); } + void setType(Type type) { _type = type; }; + Type getType() { return _type; }; + bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget bool isRunning() const { return _isRunning; } // used by ScriptWidget @@ -724,6 +735,7 @@ protected: void callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args); Context _context; + Type _type; QString _scriptContents; QString _parentURL; std::atomic _isFinished { false }; diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index a788cd9f0e..f2ed296b63 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -427,11 +427,13 @@ bool ScriptEngines::stopScript(const QString& rawScriptURL, bool restart) { if (_scriptEnginesHash.contains(scriptURL)) { ScriptEnginePointer scriptEngine = _scriptEnginesHash[scriptURL]; if (restart) { + bool isUserLoaded = scriptEngine->isUserLoaded(); + ScriptEngine::Type type = scriptEngine->getType(); auto scriptCache = DependencyManager::get(); scriptCache->deleteScript(scriptURL); connect(scriptEngine.data(), &ScriptEngine::finished, - this, [this](QString scriptName, ScriptEnginePointer engine) { - reloadScript(scriptName); + this, [this, isUserLoaded, type](QString scriptName, ScriptEnginePointer engine) { + reloadScript(scriptName, isUserLoaded)->setType(type); }); } scriptEngine->stop(); diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 1200168420..cf081860d8 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -259,7 +259,7 @@ protected slots: protected: friend class ScriptEngine; - void reloadScript(const QString& scriptName) { loadScript(scriptName, true, false, false, true); } + ScriptEnginePointer reloadScript(const QString& scriptName, bool isUserLoaded = true) { return loadScript(scriptName, isUserLoaded, false, false, true); } void removeScriptEngine(ScriptEnginePointer); void onScriptEngineLoaded(const QString& scriptFilename); void onScriptEngineError(const QString& scriptFilename); diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index d53766ab4e..611bd4d84c 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -103,7 +103,7 @@ EntityListTool = function(opts) { var selectedIDs = []; for (var j = 0; j < selectionManager.selections.length; j++) { - selectedIDs.push(selectionManager.selections[j].id); + selectedIDs.push(selectionManager.selections[j]); } var data = {