diff --git a/examples/dice.js b/examples/dice.js index f64aa647ad..2aefcf90fe 100644 --- a/examples/dice.js +++ b/examples/dice.js @@ -46,7 +46,7 @@ var diceButton = Overlays.addOverlay("image", { var GRAVITY = -3.5; var LIFETIME = 300; // NOTE: angularVelocity is in radians/sec -var maxAngularSpeed = Math.PI; +var MAX_ANGULAR_SPEED = Math.PI; function shootDice(position, velocity) { for (var i = 0; i < NUMBER_OF_DICE; i++) { @@ -56,7 +56,7 @@ function shootDice(position, velocity) { position: position, velocity: velocity, rotation: Quat.fromPitchYawRollDegrees(Math.random() * 360, Math.random() * 360, Math.random() * 360), - angularVelocity: { x: Math.random() * maxAngularSpeed, y: Math.random() * maxAngularSpeed, z: Math.random() * maxAngularSpeed }, + angularVelocity: { x: Math.random() * MAX_ANGULAR_SPEED, y: Math.random() * MAX_ANGULAR_SPEED, z: Math.random() * MAX_ANGULAR_SPEED }, lifetime: LIFETIME, gravity: { x: 0, y: GRAVITY, z: 0 }, shapeType: "box", diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5753449075..eb427737e1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -76,7 +76,7 @@ #include #include #include -//#include +#include #include #include #include @@ -221,7 +221,7 @@ bool setupEssentials(int& argc, char** argv) { auto addressManager = DependencyManager::set(); auto nodeList = DependencyManager::set(NodeType::Agent, listenPort); auto geometryCache = DependencyManager::set(); - //auto scriptCache = DependencyManager::set(); + auto scriptCache = DependencyManager::set(); auto soundCache = DependencyManager::set(); auto glowEffect = DependencyManager::set(); auto faceshift = DependencyManager::set(); @@ -608,6 +608,7 @@ Application::~Application() { Menu::getInstance()->deleteLater(); + _physicsEngine.setCharacterController(NULL); _myAvatar = NULL; ModelEntityItem::cleanupLoadedAnimations(); @@ -619,7 +620,7 @@ Application::~Application() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); - //DependencyManager::destroy(); + DependencyManager::destroy(); DependencyManager::destroy(); QThread* nodeThread = DependencyManager::get()->thread(); @@ -1523,7 +1524,7 @@ void Application::idle() { } // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing - // details if we're in ExtraDebugging mode. However, the ::update() and it's subcomponents will show their timing + // details if we're in ExtraDebugging mode. However, the ::update() and its subcomponents will show their timing // details normally. bool showWarnings = getLogger()->extraDebugging(); PerformanceWarning warn(showWarnings, "idle()"); @@ -2539,7 +2540,7 @@ bool Application::isHMDMode() const { QRect Application::getDesirableApplicationGeometry() { QRect applicationGeometry = getWindow()->geometry(); - // If our parent window is on the HMD, then don't use it's geometry, instead use + // If our parent window is on the HMD, then don't use its geometry, instead use // the "main screen" geometry. HMDToolsDialog* hmdTools = DependencyManager::get()->getHMDToolsDialog(); if (hmdTools && hmdTools->hasHMDScreen()) { @@ -3375,7 +3376,7 @@ void Application::nodeKilled(SharedNodePointer node) { void Application::trackIncomingOctreePacket(const QByteArray& packet, const SharedNodePointer& sendingNode, bool wasStatsPacket) { - // Attempt to identify the sender from it's address. + // Attempt to identify the sender from its address. if (sendingNode) { QUuid nodeUUID = sendingNode->getUUID(); @@ -3444,7 +3445,7 @@ int Application::parseOctreeStats(const QByteArray& packet, const SharedNodePoin } // store jurisdiction details for later use // This is bit of fiddling is because JurisdictionMap assumes it is the owner of the values used to construct it - // but OctreeSceneStats thinks it's just returning a reference to it's contents. So we need to make a copy of the + // but OctreeSceneStats thinks it's just returning a reference to its contents. So we need to make a copy of the // details from the OctreeSceneStats to construct the JurisdictionMap JurisdictionMap jurisdictionMap; jurisdictionMap.copyContents(temp.getJurisdictionRoot(), temp.getJurisdictionEndNodes()); @@ -3664,17 +3665,56 @@ bool Application::askToSetAvatarUrl(const QString& url) { msgBox.exec(); return false; } - - QString message = "Would you like to use this model for part of avatar:\n" + url; + + // Download the FST file, to attempt to determine its model type + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest = QNetworkRequest(url); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + QNetworkReply* reply = networkAccessManager.get(networkRequest); + qDebug() << "Downloading avatar file at " << url; + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + QByteArray fstContents = reply->readAll(); + delete reply; + QVariantHash fstMapping = FSTReader::readMapping(fstContents); + + FSTReader::ModelType modelType = FSTReader::predictModelType(fstMapping); + QMessageBox msgBox; - msgBox.setIcon(QMessageBox::Question); msgBox.setWindowTitle("Set Avatar"); - msgBox.setText(message); + QPushButton* headButton = NULL; + QPushButton* bodyButton = NULL; + QPushButton* bodyAndHeadButton = NULL; + + QString message; + QString typeInfo; + switch (modelType) { + case FSTReader::HEAD_MODEL: + message = QString("Would you like to use '") + fstMapping["name"].toString() + QString("' for your avatar head?"); + headButton = msgBox.addButton(tr("Yes"), QMessageBox::ActionRole); + break; - QPushButton* headButton = msgBox.addButton(tr("Head"), QMessageBox::ActionRole); - QPushButton* bodyButton = msgBox.addButton(tr("Body"), QMessageBox::ActionRole); - QPushButton* bodyAndHeadButton = msgBox.addButton(tr("Body + Head"), QMessageBox::ActionRole); + case FSTReader::BODY_ONLY_MODEL: + message = QString("Would you like to use '") + fstMapping["name"].toString() + QString("' for your avatar body?"); + bodyButton = msgBox.addButton(tr("Yes"), QMessageBox::ActionRole); + break; + + case FSTReader::HEAD_AND_BODY_MODEL: + message = QString("Would you like to use '") + fstMapping["name"].toString() + QString("' for your avatar?"); + bodyAndHeadButton = msgBox.addButton(tr("Yes"), QMessageBox::ActionRole); + break; + + default: + message = QString("Would you like to use '") + fstMapping["name"].toString() + QString("' for some part of your avatar head?"); + headButton = msgBox.addButton(tr("Use for Head"), QMessageBox::ActionRole); + bodyButton = msgBox.addButton(tr("Use for Body"), QMessageBox::ActionRole); + bodyAndHeadButton = msgBox.addButton(tr("Use for Body and Head"), QMessageBox::ActionRole); + break; + } + + msgBox.setText(message); msgBox.addButton(QMessageBox::Cancel); msgBox.exec(); @@ -3687,6 +3727,11 @@ bool Application::askToSetAvatarUrl(const QString& url) { } else if (msgBox.clickedButton() == bodyButton) { qDebug() << "Chose to use for body: " << url; _myAvatar->setSkeletonModelURL(url); + // if the head is empty, reset it to the default head. + if (_myAvatar->getFaceModelURLString().isEmpty()) { + _myAvatar->setFaceModelURL(DEFAULT_HEAD_MODEL_URL); + UserActivityLogger::getInstance().changedModel("head", DEFAULT_HEAD_MODEL_URL.toString()); + } UserActivityLogger::getInstance().changedModel("skeleton", url); _myAvatar->sendIdentityPacket(); } else if (msgBox.clickedButton() == bodyAndHeadButton) { diff --git a/interface/src/Menu.h b/interface/src/Menu.h index a4e644f20f..63dab63711 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -246,7 +246,7 @@ namespace MenuOption { const QString ToolWindow = "Tool Window"; const QString TransmitterDrive = "Transmitter Drive"; const QString TurnWithHead = "Turn using Head"; - const QString PackageModel = "Package Model"; + const QString PackageModel = "Package Model..."; const QString Visage = "Visage"; const QString Wireframe = "Wireframe"; } diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index f552d67a98..787c21a2ef 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -85,7 +85,7 @@ bool ModelPackager::loadModel() { return false; } qDebug() << "Reading FST file : " << _modelFile.filePath(); - _mapping = readMapping(fst.readAll()); + _mapping = FSTReader::readMapping(fst.readAll()); fst.close(); _fbxInfo = QFileInfo(_modelFile.path() + "/" + _mapping.value(FILENAME_FIELD).toString()); @@ -119,21 +119,23 @@ bool ModelPackager::editProperties() { return false; } _mapping = properties.getMapping(); - - // Make sure that a mapping for the root joint has been specified - QVariantHash joints = _mapping.value(JOINT_FIELD).toHash(); - if (!joints.contains("jointRoot")) { - qWarning() << QString("%1 root joint not configured for skeleton.").arg(_modelFile.fileName()); + + if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) { + // Make sure that a mapping for the root joint has been specified + QVariantHash joints = _mapping.value(JOINT_FIELD).toHash(); + if (!joints.contains("jointRoot")) { + qWarning() << QString("%1 root joint not configured for skeleton.").arg(_modelFile.fileName()); - QString message = "Your did not configure a root joint for your skeleton model.\n\nThe upload will be canceled."; - QMessageBox msgBox; - msgBox.setWindowTitle("Model Upload"); - msgBox.setText(message); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Warning); - msgBox.exec(); + QString message = "Your did not configure a root joint for your skeleton model.\n\nPackaging will be canceled."; + QMessageBox msgBox; + msgBox.setWindowTitle("Model Packager"); + msgBox.setText(message); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setIcon(QMessageBox::Warning); + msgBox.exec(); - return false; + return false; + } } return true; @@ -183,7 +185,7 @@ bool ModelPackager::zipModel() { // Copy FST QFile fst(tempDir.path() + "/" + nameField + ".fst"); if (fst.open(QIODevice::WriteOnly)) { - fst.write(writeMapping(_mapping)); + fst.write(FSTReader::writeMapping(_mapping)); fst.close(); } else { qDebug() << "Couldn't write FST file" << fst.fileName(); @@ -204,6 +206,18 @@ bool ModelPackager::zipModel() { } void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const FBXGeometry& geometry) { + + bool isBodyType = _modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL; + + // mixamo files - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will + // be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file + bool likelyMixamoFile = geometry.applicationName == "mixamo.com" || + (geometry.blendshapeChannelNames.contains("BrowsDown_Right") && + geometry.blendshapeChannelNames.contains("MouthOpen") && + geometry.blendshapeChannelNames.contains("Blink_Left") && + geometry.blendshapeChannelNames.contains("Blink_Right") && + geometry.blendshapeChannelNames.contains("Squint_Right")); + if (!mapping.contains(NAME_FIELD)) { mapping.insert(NAME_FIELD, QFileInfo(filename).baseName()); } @@ -232,39 +246,40 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename if (!joints.contains("jointNeck")) { joints.insert("jointNeck", geometry.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck"); } - if (!joints.contains("jointRoot")) { - joints.insert("jointRoot", "Hips"); - } - if (!joints.contains("jointLean")) { - joints.insert("jointLean", "Spine"); + + if (isBodyType) { + if (!joints.contains("jointRoot")) { + joints.insert("jointRoot", "Hips"); + } + if (!joints.contains("jointLean")) { + joints.insert("jointLean", "Spine"); + } + if (!joints.contains("jointLeftHand")) { + joints.insert("jointLeftHand", "LeftHand"); + } + if (!joints.contains("jointRightHand")) { + joints.insert("jointRightHand", "RightHand"); + } } + if (!joints.contains("jointHead")) { - const char* topName = (geometry.applicationName == "mixamo.com") ? "HeadTop_End" : "HeadEnd"; + const char* topName = likelyMixamoFile ? "HeadTop_End" : "HeadEnd"; joints.insert("jointHead", geometry.jointIndices.contains(topName) ? topName : "Head"); } - if (!joints.contains("jointLeftHand")) { - joints.insert("jointLeftHand", "LeftHand"); - } - if (!joints.contains("jointRightHand")) { - joints.insert("jointRightHand", "RightHand"); - } + mapping.insert(JOINT_FIELD, joints); - if (!mapping.contains(FREE_JOINT_FIELD)) { - mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); + + if (isBodyType) { + if (!mapping.contains(FREE_JOINT_FIELD)) { + mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); + } } - // mixamo blendshapes - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will - // be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file - bool likelyMixamoFile = geometry.applicationName == "mixamo.com" || - (geometry.blendshapeChannelNames.contains("BrowsDown_Right") && - geometry.blendshapeChannelNames.contains("MouthOpen") && - geometry.blendshapeChannelNames.contains("Blink_Left") && - geometry.blendshapeChannelNames.contains("Blink_Right") && - geometry.blendshapeChannelNames.contains("Squint_Right")); - + // If there are no blendshape mappings, and we detect that this is likely a mixamo file, + // then we can add the default mixamo to "faceshift" mappings if (!mapping.contains(BLENDSHAPE_FIELD) && likelyMixamoFile) { QVariantHash blendshapes; blendshapes.insertMulti("BrowsD_L", QVariantList() << "BrowsDown_Left" << 1.0); diff --git a/interface/src/ModelPackager.h b/interface/src/ModelPackager.h index 2c90395e56..c681ae436f 100644 --- a/interface/src/ModelPackager.h +++ b/interface/src/ModelPackager.h @@ -35,7 +35,7 @@ private: QFileInfo _modelFile; QFileInfo _fbxInfo; - ModelType _modelType; + FSTReader::ModelType _modelType; QString _texDir; QVariantHash _mapping; diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp index 81fe9ce7fd..28a50205c9 100644 --- a/interface/src/ModelPropertiesDialog.cpp +++ b/interface/src/ModelPropertiesDialog.cpp @@ -25,7 +25,7 @@ #include "ModelPropertiesDialog.h" -ModelPropertiesDialog::ModelPropertiesDialog(ModelType modelType, const QVariantHash& originalMapping, +ModelPropertiesDialog::ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, const QString& basePath, const FBXGeometry& geometry) : _modelType(modelType), _originalMapping(originalMapping), @@ -46,8 +46,8 @@ _geometry(geometry) _scale->setMaximum(FLT_MAX); _scale->setSingleStep(0.01); - if (_modelType != ENTITY_MODEL) { - if (_modelType == ATTACHMENT_MODEL) { + if (_modelType != FSTReader::ENTITY_MODEL) { + if (_modelType == FSTReader::ATTACHMENT_MODEL) { QHBoxLayout* translation = new QHBoxLayout(); form->addRow("Translation:", translation); translation->addWidget(_translationX = createTranslationBox()); @@ -63,7 +63,7 @@ _geometry(geometry) form->addRow("Right Eye Joint:", _rightEyeJoint = createJointBox()); form->addRow("Neck Joint:", _neckJoint = createJointBox()); } - if (_modelType == SKELETON_MODEL) { + if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) { form->addRow("Root Joint:", _rootJoint = createJointBox()); form->addRow("Lean Joint:", _leanJoint = createJointBox()); form->addRow("Head Joint:", _headJoint = createJointBox()); @@ -89,8 +89,14 @@ _geometry(geometry) reset(); } + +QString ModelPropertiesDialog::getType() const { + return FSTReader::getNameFromType(_modelType); +} + QVariantHash ModelPropertiesDialog::getMapping() const { QVariantHash mapping = _originalMapping; + mapping.insert(TYPE_FIELD, getType()); mapping.insert(NAME_FIELD, _name->text()); mapping.insert(TEXDIR_FIELD, _textureDirectory->text()); mapping.insert(SCALE_FIELD, QString::number(_scale->value())); @@ -102,9 +108,9 @@ QVariantHash ModelPropertiesDialog::getMapping() const { } mapping.insert(JOINT_INDEX_FIELD, jointIndices); - if (_modelType != ENTITY_MODEL) { + if (_modelType != FSTReader::ENTITY_MODEL) { QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); - if (_modelType == ATTACHMENT_MODEL) { + if (_modelType == FSTReader::ATTACHMENT_MODEL) { glm::vec3 pivot; if (_pivotAboutCenter->isChecked()) { pivot = (_geometry.meshExtents.minimum + _geometry.meshExtents.maximum) * 0.5f; @@ -121,7 +127,9 @@ QVariantHash ModelPropertiesDialog::getMapping() const { insertJointMapping(joints, "jointEyeRight", _rightEyeJoint->currentText()); insertJointMapping(joints, "jointNeck", _neckJoint->currentText()); } - if (_modelType == SKELETON_MODEL) { + + + if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) { insertJointMapping(joints, "jointRoot", _rootJoint->currentText()); insertJointMapping(joints, "jointLean", _leanJoint->currentText()); insertJointMapping(joints, "jointHead", _headJoint->currentText()); @@ -151,8 +159,8 @@ void ModelPropertiesDialog::reset() { QVariantHash jointHash = _originalMapping.value(JOINT_FIELD).toHash(); - if (_modelType != ENTITY_MODEL) { - if (_modelType == ATTACHMENT_MODEL) { + if (_modelType != FSTReader::ENTITY_MODEL) { + if (_modelType == FSTReader::ATTACHMENT_MODEL) { _translationX->setValue(_originalMapping.value(TRANSLATION_X_FIELD).toDouble()); _translationY->setValue(_originalMapping.value(TRANSLATION_Y_FIELD).toDouble()); _translationZ->setValue(_originalMapping.value(TRANSLATION_Z_FIELD).toDouble()); @@ -164,7 +172,8 @@ void ModelPropertiesDialog::reset() { setJointText(_rightEyeJoint, jointHash.value("jointEyeRight").toString()); setJointText(_neckJoint, jointHash.value("jointNeck").toString()); } - if (_modelType == SKELETON_MODEL) { + + if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) { setJointText(_rootJoint, jointHash.value("jointRoot").toString()); setJointText(_leanJoint, jointHash.value("jointLean").toString()); setJointText(_headJoint, jointHash.value("jointHead").toString()); diff --git a/interface/src/ModelPropertiesDialog.h b/interface/src/ModelPropertiesDialog.h index 5af4d173f1..11abc5ab54 100644 --- a/interface/src/ModelPropertiesDialog.h +++ b/interface/src/ModelPropertiesDialog.h @@ -15,6 +15,7 @@ #include #include +#include #include "ui/ModelsBrowser.h" @@ -28,7 +29,7 @@ class ModelPropertiesDialog : public QDialog { Q_OBJECT public: - ModelPropertiesDialog(ModelType modelType, const QVariantHash& originalMapping, + ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, const QString& basePath, const FBXGeometry& geometry); QVariantHash getMapping() const; @@ -43,8 +44,9 @@ private: QComboBox* createJointBox(bool withNone = true) const; QDoubleSpinBox* createTranslationBox() const; void insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const; + QString getType() const; - ModelType _modelType; + FSTReader::ModelType _modelType; QVariantHash _originalMapping; QString _basePath; FBXGeometry _geometry; diff --git a/interface/src/ModelSelector.cpp b/interface/src/ModelSelector.cpp index c55d77dc00..8e130cec1a 100644 --- a/interface/src/ModelSelector.cpp +++ b/interface/src/ModelSelector.cpp @@ -18,8 +18,9 @@ #include "ModelSelector.h" -static const QString AVATAR_HEAD_STRING = "Avatar Head"; -static const QString AVATAR_BODY_STRING = "Avatar Body"; +static const QString AVATAR_HEAD_STRING = "Avatar Head Only"; +static const QString AVATAR_BODY_STRING = "Avatar Body Only"; +static const QString AVATAR_HEAD_AND_BODY_STRING = "Avatar Body with Head"; static const QString AVATAR_ATTACHEMENT_STRING = "Avatar Attachment"; static const QString ENTITY_MODEL_STRING = "Entity Model"; @@ -36,6 +37,7 @@ ModelSelector::ModelSelector() { _modelType = new QComboBox(this); _modelType->addItem(AVATAR_HEAD_STRING); _modelType->addItem(AVATAR_BODY_STRING); + _modelType->addItem(AVATAR_HEAD_AND_BODY_STRING); _modelType->addItem(AVATAR_ATTACHEMENT_STRING); _modelType->addItem(ENTITY_MODEL_STRING); form->addRow("Model Type:", _modelType); @@ -50,17 +52,19 @@ QFileInfo ModelSelector::getFileInfo() const { return _modelFile; } -ModelType ModelSelector::getModelType() const { +FSTReader::ModelType ModelSelector::getModelType() const { QString text = _modelType->currentText(); if (text == AVATAR_HEAD_STRING) { - return HEAD_MODEL; + return FSTReader::HEAD_MODEL; } else if (text == AVATAR_BODY_STRING) { - return SKELETON_MODEL; + return FSTReader::BODY_ONLY_MODEL; + } else if (text == AVATAR_HEAD_AND_BODY_STRING) { + return FSTReader::HEAD_AND_BODY_MODEL; } else if (text == AVATAR_ATTACHEMENT_STRING) { - return ATTACHMENT_MODEL; + return FSTReader::ATTACHMENT_MODEL; } else if (text == ENTITY_MODEL_STRING) { - return ENTITY_MODEL; + return FSTReader::ENTITY_MODEL; } else { Q_UNREACHABLE(); } diff --git a/interface/src/ModelSelector.h b/interface/src/ModelSelector.h index aaa35e01c3..0ac3df5963 100644 --- a/interface/src/ModelSelector.h +++ b/interface/src/ModelSelector.h @@ -29,7 +29,7 @@ public: ModelSelector(); QFileInfo getFileInfo() const; - ModelType getModelType() const; + FSTReader::ModelType getModelType() const; public slots: virtual void accept(); diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index a5b8128d1e..6ed3e48274 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -638,7 +638,7 @@ QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QS /// \param const QString& nameFilter filter to filter filenames /// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` QScriptValue WindowScriptingInterface::showS3Browse(const QString& nameFilter) { - ModelsBrowser browser(ENTITY_MODEL); + ModelsBrowser browser(FSTReader::ENTITY_MODEL); if (nameFilter != "") { browser.setNameFilter(nameFilter); } diff --git a/interface/src/ui/AttachmentsDialog.cpp b/interface/src/ui/AttachmentsDialog.cpp index 58b46e5bbf..2a6ff2b2b1 100644 --- a/interface/src/ui/AttachmentsDialog.cpp +++ b/interface/src/ui/AttachmentsDialog.cpp @@ -164,7 +164,7 @@ AttachmentData AttachmentPanel::getAttachmentData() const { } void AttachmentPanel::chooseModelURL() { - ModelsBrowser modelBrowser(ATTACHMENT_MODEL, this); + ModelsBrowser modelBrowser(FSTReader::ATTACHMENT_MODEL, this); connect(&modelBrowser, SIGNAL(selected(QString)), SLOT(setModelURL(const QString&))); modelBrowser.browse(); } diff --git a/interface/src/ui/ModelsBrowser.cpp b/interface/src/ui/ModelsBrowser.cpp index 070c6a85cc..91de4e36ac 100644 --- a/interface/src/ui/ModelsBrowser.cpp +++ b/interface/src/ui/ModelsBrowser.cpp @@ -28,7 +28,7 @@ #include "ModelsBrowser.h" -const char* MODEL_TYPE_NAMES[] = { "entities", "heads", "skeletons", "attachments" }; +const char* MODEL_TYPE_NAMES[] = { "entities", "heads", "skeletons", "skeletons", "attachments" }; static const QString S3_URL = "http://s3.amazonaws.com/hifi-public"; static const QString PUBLIC_URL = "http://public.highfidelity.io"; @@ -71,7 +71,7 @@ static const QString propertiesIds[MODEL_METADATA_COUNT] = { "Tags" }; -ModelsBrowser::ModelsBrowser(ModelType modelsType, QWidget* parent) : +ModelsBrowser::ModelsBrowser(FSTReader::ModelType modelsType, QWidget* parent) : QWidget(parent, Qt::WindowStaysOnTopHint), _handler(new ModelHandler(modelsType)) { @@ -184,7 +184,7 @@ void ModelsBrowser::browse() { } -ModelHandler::ModelHandler(ModelType modelsType, QWidget* parent) : +ModelHandler::ModelHandler(FSTReader::ModelType modelsType, QWidget* parent) : QObject(parent), _initiateExit(false), _type(modelsType), diff --git a/interface/src/ui/ModelsBrowser.h b/interface/src/ui/ModelsBrowser.h index 0c8bb59c85..2cb3c67991 100644 --- a/interface/src/ui/ModelsBrowser.h +++ b/interface/src/ui/ModelsBrowser.h @@ -16,21 +16,16 @@ #include #include -class QNetworkReply; +#include -enum ModelType { - ENTITY_MODEL, - HEAD_MODEL, - SKELETON_MODEL, - ATTACHMENT_MODEL -}; +class QNetworkReply; extern const char* MODEL_TYPE_NAMES[]; class ModelHandler : public QObject { Q_OBJECT public: - ModelHandler(ModelType modelsType, QWidget* parent = NULL); + ModelHandler(FSTReader::ModelType modelsType, QWidget* parent = NULL); void lockModel() { _lock.lockForRead(); } QStandardItemModel* getModel() { return &_model; } @@ -51,7 +46,7 @@ private slots: private: bool _initiateExit; - ModelType _type; + FSTReader::ModelType _type; QReadWriteLock _lock; QStandardItemModel _model; QString _nameFilter; @@ -66,7 +61,7 @@ class ModelsBrowser : public QWidget { Q_OBJECT public: - ModelsBrowser(ModelType modelsType, QWidget* parent = NULL); + ModelsBrowser(FSTReader::ModelType modelsType, QWidget* parent = NULL); QString getSelectedFile() { return _selectedFile; } signals: diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index a07de371a2..4a9165ae44 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -67,13 +67,13 @@ void PreferencesDialog::setSkeletonUrl(QString modelUrl) { } void PreferencesDialog::openHeadModelBrowser() { - ModelsBrowser modelBrowser(HEAD_MODEL); + ModelsBrowser modelBrowser(FSTReader::HEAD_MODEL); connect(&modelBrowser, &ModelsBrowser::selected, this, &PreferencesDialog::setHeadUrl); modelBrowser.browse(); } void PreferencesDialog::openBodyModelBrowser() { - ModelsBrowser modelBrowser(SKELETON_MODEL); + ModelsBrowser modelBrowser(FSTReader::HEAD_AND_BODY_MODEL); connect(&modelBrowser, &ModelsBrowser::selected, this, &PreferencesDialog::setSkeletonUrl); modelBrowser.browse(); } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 3faa06fc53..058e02b507 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -110,14 +110,30 @@ void EntityTreeRenderer::shutdown() { _shuttingDown = true; } +void EntityTreeRenderer::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) { + if (_waitingOnPreload.contains(url)) { + QList entityIDs = _waitingOnPreload.values(url); + _waitingOnPreload.remove(url); + foreach(EntityItemID entityID, entityIDs) { + checkAndCallPreload(entityID); + } + } +} -QScriptValue EntityTreeRenderer::loadEntityScript(const EntityItemID& entityItemID) { +void EntityTreeRenderer::errorInLoadingScript(const QUrl& url) { + if (_waitingOnPreload.contains(url)) { + _waitingOnPreload.remove(url); + } +} + +QScriptValue EntityTreeRenderer::loadEntityScript(const EntityItemID& entityItemID, bool isPreload) { EntityItem* entity = static_cast(_tree)->findEntityByEntityItemID(entityItemID); - return loadEntityScript(entity); + return loadEntityScript(entity, isPreload); } -QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorText, bool& isURL) { +QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorText, bool& isURL, bool& isPending, QUrl& urlOut) { + isPending = false; QUrl url(scriptMaybeURLorText); // If the url is not valid, this must be script text... @@ -126,6 +142,7 @@ QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorTe return scriptMaybeURLorText; } isURL = true; + urlOut = url; QString scriptContents; // assume empty @@ -148,20 +165,11 @@ QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorTe qDebug() << "ERROR Loading file:" << fileName; } } else { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = QNetworkRequest(url); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(networkRequest); - qDebug() << "Downloading script at" << url; - QEventLoop loop; - QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - loop.exec(); - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 200) { - scriptContents = reply->readAll(); - } else { - qDebug() << "ERROR Loading file:" << url.toString(); + auto scriptCache = DependencyManager::get(); + + if (!scriptCache->isInBadScriptList(url)) { + scriptContents = scriptCache->getScript(url, this, isPending); } - delete reply; } } @@ -169,7 +177,7 @@ QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorTe } -QScriptValue EntityTreeRenderer::loadEntityScript(EntityItem* entity) { +QScriptValue EntityTreeRenderer::loadEntityScript(EntityItem* entity, bool isPreload) { if (_shuttingDown) { return QScriptValue(); // since we're shutting down, we don't load any more scripts } @@ -203,7 +211,24 @@ QScriptValue EntityTreeRenderer::loadEntityScript(EntityItem* entity) { } bool isURL = false; // loadScriptContents() will tell us if this is a URL or just text. - QString scriptContents = loadScriptContents(entityScript, isURL); + bool isPending = false; + QUrl url; + QString scriptContents = loadScriptContents(entityScript, isURL, isPending, url); + + if (isPending && isPreload && isURL) { + _waitingOnPreload.insert(url, entityID); + + } + + auto scriptCache = DependencyManager::get(); + + 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) { @@ -211,6 +236,9 @@ QScriptValue EntityTreeRenderer::loadEntityScript(EntityItem* entity) { qDebug() << " " << syntaxCheck.errorMessage() << ":" << syntaxCheck.errorLineNumber() << syntaxCheck.errorColumnNumber(); qDebug() << " SCRIPT:" << entityScript; + + scriptCache->addScriptToBadScriptList(url); + return QScriptValue(); // invalid script } @@ -223,6 +251,9 @@ QScriptValue EntityTreeRenderer::loadEntityScript(EntityItem* entity) { qDebug() << "EntityTreeRenderer::loadEntityScript() entity:" << entityID; qDebug() << " NOT CONSTRUCTOR"; qDebug() << " SCRIPT:" << entityScript; + + scriptCache->addScriptToBadScriptList(url); + return QScriptValue(); // invalid script } else { entityScriptConstructor = _entitiesScriptEngine->evaluate(scriptContents); @@ -920,7 +951,7 @@ void EntityTreeRenderer::entitySciptChanging(const EntityItemID& entityID) { void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID) { if (_tree && !_shuttingDown) { // load the entity script if needed... - QScriptValue entityScript = loadEntityScript(entityID); + QScriptValue entityScript = loadEntityScript(entityID, true); // is preload! if (entityScript.property("preload").isValid()) { QScriptValueList entityArgs = createEntityArgs(entityID); entityScript.property("preload").call(entityScript, entityArgs); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index c4aa5a42d0..12d5f6e166 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -16,6 +16,7 @@ #include // for RayToEntityIntersectionResult #include #include +#include class Model; class ScriptEngine; @@ -31,7 +32,7 @@ public: }; // Generic client side Octree renderer class. -class EntityTreeRenderer : public OctreeRenderer, public EntityItemFBXService { +class EntityTreeRenderer : public OctreeRenderer, public EntityItemFBXService, public ScriptUser { Q_OBJECT public: EntityTreeRenderer(bool wantScripts, AbstractViewStateInterface* viewState, @@ -84,6 +85,9 @@ 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); + signals: void mousePressOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); void mouseMoveOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); @@ -138,10 +142,10 @@ private: ScriptEngine* _entitiesScriptEngine; ScriptEngine* _sandboxScriptEngine; - QScriptValue loadEntityScript(EntityItem* entity); - QScriptValue loadEntityScript(const EntityItemID& entityItemID); + QScriptValue loadEntityScript(EntityItem* entity, bool isPreload = false); + QScriptValue loadEntityScript(const EntityItemID& entityItemID, bool isPreload = false); QScriptValue getPreviouslyLoadedEntityScript(const EntityItemID& entityItemID); - QString loadScriptContents(const QString& scriptMaybeURLorText, bool& isURL); + QString loadScriptContents(const QString& scriptMaybeURLorText, bool& isURL, bool& isPending, QUrl& url); QScriptValueList createMouseEventArgs(const EntityItemID& entityID, QMouseEvent* event, unsigned int deviceID); QScriptValueList createMouseEventArgs(const EntityItemID& entityID, const MouseEvent& mouseEvent); @@ -157,6 +161,8 @@ private: bool _dontDoPrecisionPicking; bool _shuttingDown = false; + + QMultiMap _waitingOnPreload; }; diff --git a/libraries/fbx/src/FSTReader.cpp b/libraries/fbx/src/FSTReader.cpp index f1ffe11996..6a5cc2bd53 100644 --- a/libraries/fbx/src/FSTReader.cpp +++ b/libraries/fbx/src/FSTReader.cpp @@ -13,7 +13,7 @@ #include "FSTReader.h" -QVariantHash parseMapping(QIODevice* device) { +QVariantHash FSTReader::parseMapping(QIODevice* device) { QVariantHash properties; QByteArray line; @@ -48,13 +48,13 @@ QVariantHash parseMapping(QIODevice* device) { return properties; } -QVariantHash readMapping(const QByteArray& data) { +QVariantHash FSTReader::readMapping(const QByteArray& data) { QBuffer buffer(const_cast(&data)); buffer.open(QIODevice::ReadOnly); - return parseMapping(&buffer); + return FSTReader::parseMapping(&buffer); } -void writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) { +void FSTReader::writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) { QByteArray key = it.key().toUtf8() + " = "; QVariantHash hashValue = it.value().toHash(); if (hashValue.isEmpty()) { @@ -76,8 +76,8 @@ void writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) { } } -QByteArray writeMapping(const QVariantHash& mapping) { - static const QStringList PREFERED_ORDER = QStringList() << NAME_FIELD << SCALE_FIELD << FILENAME_FIELD +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 << BLENDSHAPE_FIELD << JOINT_INDEX_FIELD; QBuffer buffer; @@ -96,4 +96,76 @@ QByteArray writeMapping(const QVariantHash& mapping) { } } return buffer.data(); -} \ No newline at end of file +} + +QHash FSTReader::_typesToNames; +QString FSTReader::getNameFromType(ModelType modelType) { + if (_typesToNames.size() == 0) { + _typesToNames[ENTITY_MODEL] = "entity"; + _typesToNames[HEAD_MODEL] = "head"; + _typesToNames[BODY_ONLY_MODEL] = "body"; + _typesToNames[HEAD_AND_BODY_MODEL] = "body+head"; + _typesToNames[ATTACHMENT_MODEL] = "attachment"; + } + return _typesToNames[modelType]; +} + +QHash FSTReader::_namesToTypes; +FSTReader::ModelType FSTReader::getTypeFromName(const QString& name) { + if (_namesToTypes.size() == 0) { + _namesToTypes["entity"] = ENTITY_MODEL; + _namesToTypes["head"] = HEAD_MODEL ; + _namesToTypes["body"] = BODY_ONLY_MODEL; + _namesToTypes["body+head"] = HEAD_AND_BODY_MODEL; + _namesToTypes["attachment"] = ATTACHMENT_MODEL; + } + return _namesToTypes[name]; +} + +FSTReader::ModelType FSTReader::predictModelType(const QVariantHash& mapping) { + + QVariantHash joints; + + if (mapping.contains("joint") && mapping["joint"].type() == QVariant::Hash) { + joints = mapping["joint"].toHash(); + } + + // if the mapping includes the type hint... then we trust the mapping + if (mapping.contains(TYPE_FIELD)) { + return FSTReader::getTypeFromName(mapping[TYPE_FIELD].toString()); + } + + // check for blendshapes + bool hasBlendshapes = mapping.contains(BLENDSHAPE_FIELD); + + // a Head needs to have these minimum fields... + //joint = jointEyeLeft = EyeL = 1 + //joint = jointEyeRight = EyeR = 1 + //joint = jointNeck = Head = 1 + bool hasHeadMinimum = joints.contains("jointNeck") && joints.contains("jointEyeLeft") && joints.contains("jointEyeRight"); + + // a Body needs to have these minimum fields... + //joint = jointRoot = Hips + //joint = jointLean = Spine + //joint = jointNeck = Neck + //joint = jointHead = HeadTop_End + + bool hasBodyMinimumJoints = joints.contains("jointRoot") && joints.contains("jointLean") && joints.contains("jointNeck") + && joints.contains("jointHead"); + + bool isLikelyHead = hasBlendshapes || hasHeadMinimum; + + if (isLikelyHead && hasBodyMinimumJoints) { + return HEAD_AND_BODY_MODEL; + } + + if (isLikelyHead) { + return HEAD_MODEL; + } + + if (hasBodyMinimumJoints) { + return BODY_ONLY_MODEL; + } + + return ENTITY_MODEL; +} diff --git a/libraries/fbx/src/FSTReader.h b/libraries/fbx/src/FSTReader.h index 59559dea74..5752a224c6 100644 --- a/libraries/fbx/src/FSTReader.h +++ b/libraries/fbx/src/FSTReader.h @@ -12,9 +12,11 @@ #ifndef hifi_FSTReader_h #define hifi_FSTReader_h +#include #include static const QString NAME_FIELD = "name"; +static const QString TYPE_FIELD = "type"; static const QString FILENAME_FIELD = "filename"; static const QString TEXDIR_FIELD = "texdir"; static const QString LOD_FIELD = "lod"; @@ -27,10 +29,35 @@ static const QString JOINT_FIELD = "joint"; static const QString FREE_JOINT_FIELD = "freeJoint"; static const QString BLENDSHAPE_FIELD = "bs"; -/// Reads an FST mapping from the supplied data. -QVariantHash readMapping(const QByteArray& data); +class FSTReader { +public: -/// Writes an FST mapping to a byte array. -QByteArray writeMapping(const QVariantHash& mapping); + enum ModelType { + ENTITY_MODEL, + HEAD_MODEL, + BODY_ONLY_MODEL, + HEAD_AND_BODY_MODEL, + ATTACHMENT_MODEL + }; + + /// Reads an FST mapping from the supplied data. + static QVariantHash readMapping(const QByteArray& data); + + /// Writes an FST mapping to a byte array. + static QByteArray writeMapping(const QVariantHash& mapping); + + /// Predicts the type of model by examining the mapping + static ModelType predictModelType(const QVariantHash& mapping); + + static QString getNameFromType(ModelType modelType); + static FSTReader::ModelType getTypeFromName(const QString& name); + +private: + static void writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it); + static QVariantHash parseMapping(QIODevice* device); + + static QHash _typesToNames; + static QHash _namesToTypes; +}; #endif // hifi_FSTReader_h \ No newline at end of file diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 148746a76e..626a1ea3a9 100644 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -241,6 +241,12 @@ CharacterController::CharacterController(AvatarData* avatarData) { } CharacterController::~CharacterController() { + delete _ghostObject; + _ghostObject = NULL; + delete _convexShape; + _convexShape = NULL; + // make sure you remove this Character from its DynamicsWorld before reaching this spot + assert(_dynamicsWorld == NULL); } btPairCachingGhostObject* CharacterController::getGhostObject() { diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index aabf3dc331..139a954b22 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -28,6 +28,9 @@ PhysicsEngine::PhysicsEngine(const glm::vec3& offset) } PhysicsEngine::~PhysicsEngine() { + if (_characterController) { + _characterController->setDynamicsWorld(NULL); + } // TODO: delete engine components... if we ever plan to create more than one instance delete _collisionConfig; delete _collisionDispatcher; @@ -614,8 +617,14 @@ bool PhysicsEngine::updateObjectHard(btRigidBody* body, ObjectMotionState* motio } void PhysicsEngine::setCharacterController(CharacterController* character) { - if (!_characterController) { + if (_characterController != character) { lock(); + if (_characterController) { + // remove the character from the DynamicsWorld immediately + _characterController->setDynamicsWorld(NULL); + _characterController = NULL; + } + // the character will be added to the DynamicsWorld later _characterController = character; unlock(); } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index f8fc4633cc..c74f714e8d 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -2132,7 +2132,7 @@ void NetworkGeometry::downloadFinished(QNetworkReply* reply) { QUrl url = reply->url(); if (url.path().toLower().endsWith(".fst")) { // it's a mapping file; parse it and get the mesh filename - _mapping = readMapping(reply->readAll()); + _mapping = FSTReader::readMapping(reply->readAll()); reply->deleteLater(); QString filename = _mapping.value("filename").toString(); if (filename.isNull()) { diff --git a/libraries/render-utils/src/TextRenderer.cpp b/libraries/render-utils/src/TextRenderer.cpp index 9238db91e3..75448f90cc 100644 --- a/libraries/render-utils/src/TextRenderer.cpp +++ b/libraries/render-utils/src/TextRenderer.cpp @@ -359,8 +359,12 @@ void Font::setupGL() { // FIXME there has to be a cleaner way of doing this QStringList Font::tokenizeForWrapping(const QString & str) const { QStringList result; - foreach(const QString & token1, str.split(" ", QString::SkipEmptyParts)) { + foreach(const QString & token1, str.split(" ")) { bool lineFeed = false; + if (token1.isEmpty()) { + result << token1; + continue; + } foreach(const QString & token2, token1.split("\n")) { if (lineFeed) { result << "\n"; diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp new file mode 100644 index 0000000000..c0ffc7b4e7 --- /dev/null +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -0,0 +1,76 @@ +// +// ScriptCache.cpp +// libraries/script-engine/src +// +// Created by Brad Hefta-Gaub on 2015-03-30 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include + +#include +#include + +#include "ScriptCache.h" + +ScriptCache::ScriptCache(QObject* parent) { + // nothing to do here... +} + +QString ScriptCache::getScript(const QUrl& url, ScriptUser* scriptUser, bool& isPending) { + QString scriptContents; + if (_scriptCache.contains(url)) { + qDebug() << "Found script in cache:" << url.toString(); + scriptContents = _scriptCache[url]; + scriptUser->scriptContentsAvailable(url, scriptContents); + isPending = false; + } else { + isPending = true; + bool alreadyWaiting = _scriptUsers.contains(url); + _scriptUsers.insert(url, scriptUser); + + if (alreadyWaiting) { + qDebug() << "Already downloading script at:" << url.toString(); + } else { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest = QNetworkRequest(url); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + qDebug() << "Downloading script at:" << url.toString(); + QNetworkReply* reply = networkAccessManager.get(networkRequest); + connect(reply, &QNetworkReply::finished, this, &ScriptCache::scriptDownloaded); + } + } + return scriptContents; +} + +void ScriptCache::scriptDownloaded() { + QNetworkReply* reply = qobject_cast(sender()); + QUrl url = reply->url(); + QList scriptUsers = _scriptUsers.values(url); + _scriptUsers.remove(url); + + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 200) { + _scriptCache[url] = reply->readAll(); + qDebug() << "Done downloading script at:" << url.toString(); + + foreach(ScriptUser* user, scriptUsers) { + user->scriptContentsAvailable(url, _scriptCache[url]); + } + } else { + qDebug() << "ERROR Loading file:" << reply->url().toString(); + foreach(ScriptUser* user, scriptUsers) { + user->errorInLoadingScript(url); + } + } + reply->deleteLater(); +} + + diff --git a/libraries/script-engine/src/ScriptCache.h b/libraries/script-engine/src/ScriptCache.h new file mode 100644 index 0000000000..16bf04c53e --- /dev/null +++ b/libraries/script-engine/src/ScriptCache.h @@ -0,0 +1,44 @@ +// +// ScriptCache.h +// libraries/script-engine/src +// +// Created by Brad Hefta-Gaub on 2015-03-30 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptCache_h +#define hifi_ScriptCache_h + +#include + +class ScriptUser { +public: + virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents) = 0; + virtual void errorInLoadingScript(const QUrl& url) = 0; +}; + +/// Interface for loading scripts +class ScriptCache : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + QString getScript(const QUrl& url, ScriptUser* scriptUser, bool& isPending); + void addScriptToBadScriptList(const QUrl& url) { _badScripts.insert(url); } + bool isInBadScriptList(const QUrl& url) { return _badScripts.contains(url); } + +private slots: + void scriptDownloaded(); + +private: + ScriptCache(QObject* parent = NULL); + + QHash _scriptCache; + QMultiMap _scriptUsers; + QSet _badScripts; +}; + +#endif // hifi_ScriptCache_h \ No newline at end of file diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 831db73a0a..c31f5be46a 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -34,6 +34,7 @@ #include "EventTypes.h" #include "MenuItemProperties.h" #include "ScriptAudioInjector.h" +#include "ScriptCache.h" #include "ScriptEngine.h" #include "TypedArrays.h" #include "XMLHttpRequestClass.h" @@ -275,31 +276,26 @@ void ScriptEngine::loadURL(const QUrl& scriptURL) { _scriptContents = in.readAll(); emit scriptLoaded(_fileNameString); } else { - qDebug() << "ERROR Loading file:" << _fileNameString; + qDebug() << "ERROR Loading file:" << _fileNameString << "line:" << __LINE__; emit errorLoadingScript(_fileNameString); } } else { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = QNetworkRequest(url); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(networkRequest); - connect(reply, &QNetworkReply::finished, this, &ScriptEngine::handleScriptDownload); + bool isPending; + auto scriptCache = DependencyManager::get(); + scriptCache->getScript(url, this, isPending); + } } } -void ScriptEngine::handleScriptDownload() { - QNetworkReply* reply = qobject_cast(sender()); - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 200) { - _scriptContents = reply->readAll(); - emit scriptLoaded(_fileNameString); - } else { - qDebug() << "ERROR Loading file:" << reply->url().toString(); - emit errorLoadingScript(_fileNameString); - } - - reply->deleteLater(); +void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) { + _scriptContents = scriptContents; + emit scriptLoaded(_fileNameString); +} + +void ScriptEngine::errorInLoadingScript(const QUrl& url) { + qDebug() << "ERROR Loading file:" << url.toString() << "line:" << __LINE__; + emit errorLoadingScript(_fileNameString); // ?? } void ScriptEngine::init() { @@ -765,7 +761,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac for (QUrl url : urls) { QString contents = data[url]; if (contents.isNull()) { - qDebug() << "Error loading file: " << url; + qDebug() << "Error loading file: " << url << "line:" << __LINE__; } else { QScriptValue result = evaluate(contents, url.toString()); } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 84d2527bd0..45e56850a7 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -28,6 +28,7 @@ #include "ArrayBufferClass.h" #include "AudioScriptingInterface.h" #include "Quat.h" +#include "ScriptCache.h" #include "ScriptUUID.h" #include "Vec3.h" @@ -35,7 +36,7 @@ const QString NO_SCRIPT(""); const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0 / 60.0f) * 1000 * 1000) + 0.5); -class ScriptEngine : public QScriptEngine { +class ScriptEngine : public QScriptEngine, public ScriptUser { Q_OBJECT public: ScriptEngine(const QString& scriptContents = NO_SCRIPT, @@ -94,6 +95,9 @@ public: void waitTillDoneRunning(); + virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents); + virtual void errorInLoadingScript(const QUrl& url); + public slots: void loadURL(const QUrl& scriptURL); void stop(); @@ -160,8 +164,6 @@ private: ArrayBufferClass* _arrayBufferClass; QHash _outgoingScriptAudioSequenceNumbers; -private slots: - void handleScriptDownload(); private: static QSet _allKnownScriptEngines; diff --git a/libraries/shared/src/StreamUtils.cpp b/libraries/shared/src/StreamUtils.cpp index 96967648ed..f58115d288 100644 --- a/libraries/shared/src/StreamUtils.cpp +++ b/libraries/shared/src/StreamUtils.cpp @@ -143,4 +143,12 @@ QDebug& operator<<(QDebug& dbg, const glm::mat4& m) { return dbg << " ]}"; } +QDebug& operator<<(QDebug& dbg, const QVariantHash& v) { + dbg.nospace() << "["; + for (QVariantHash::const_iterator it = v.constBegin(); it != v.constEnd(); it++) { + dbg << it.key() << ":" << it.value(); + } + return dbg << " ]"; +} + #endif // QT_NO_DEBUG_STREAM diff --git a/libraries/shared/src/StreamUtils.h b/libraries/shared/src/StreamUtils.h index b9823a6743..fd22f7c068 100644 --- a/libraries/shared/src/StreamUtils.h +++ b/libraries/shared/src/StreamUtils.h @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -54,6 +55,7 @@ QDebug& operator<<(QDebug& s, const glm::vec3& v); QDebug& operator<<(QDebug& s, const glm::vec4& v); QDebug& operator<<(QDebug& s, const glm::quat& q); QDebug& operator<<(QDebug& s, const glm::mat4& m); +QDebug& operator<<(QDebug& dbg, const QVariantHash& v); #endif // QT_NO_DEBUG_STREAM #endif // hifi_StreamUtils_h