diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 3937d5f799..68464fef91 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -84,7 +84,7 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::get()->setPacketSender(&_entityEditSender); DependencyManager::set(); - DependencyManager::set(); + DependencyManager::set()->instantiate(); DependencyManager::registerInheritance(); @@ -511,6 +511,7 @@ void Agent::executeScript() { DependencyManager::set(_entityViewer.getTree()); + DependencyManager::get()->runScriptInitializers(_scriptEngine); _scriptEngine->run(); Frame::clearFrameHandler(AUDIO_FRAME_TYPE); diff --git a/assignment-client/src/AssignmentClientApp.cpp b/assignment-client/src/AssignmentClientApp.cpp index a87200dc5b..2fa052e244 100644 --- a/assignment-client/src/AssignmentClientApp.cpp +++ b/assignment-client/src/AssignmentClientApp.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "Assignment.h" #include "AssignmentClient.h" @@ -240,6 +241,7 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : QThread::currentThread()->setObjectName("main thread"); DependencyManager::registerInheritance(); + DependencyManager::set(); if (numForks || minForks || maxForks) { AssignmentClientMonitor* monitor = new AssignmentClientMonitor(numForks, minForks, maxForks, diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index dde8a01b26..6bbf4a0e3b 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ EntityServer::EntityServer(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set()->instantiate(); DependencyManager::registerInheritance(); DependencyManager::set(); diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 514eb62380..ea002a27c6 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -64,7 +64,7 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); + DependencyManager::set()->instantiate(); DependencyManager::registerInheritance(); @@ -462,7 +462,7 @@ void EntityScriptServer::resetEntitiesScriptEngine() { _entityViewer.getTree()->update(); }); - + scriptEngines->runScriptInitializers(newEngine); newEngine->runInThread(); auto newEngineSP = qSharedPointerCast(newEngine); DependencyManager::get()->setEntitiesScriptEngine(newEngineSP); diff --git a/cmake/macros/SetupHifiClientServerPlugin.cmake b/cmake/macros/SetupHifiClientServerPlugin.cmake index bc66484c30..1ce0b0ca6e 100644 --- a/cmake/macros/SetupHifiClientServerPlugin.cmake +++ b/cmake/macros/SetupHifiClientServerPlugin.cmake @@ -7,7 +7,8 @@ # macro(SETUP_HIFI_CLIENT_SERVER_PLUGIN) set(${TARGET_NAME}_SHARED 1) - setup_hifi_library(${ARGV}) + set(PLUGIN_SUBFOLDER ${ARGN}) + setup_hifi_library() if (BUILD_CLIENT) add_dependencies(interface ${TARGET_NAME}) @@ -27,6 +28,11 @@ macro(SETUP_HIFI_CLIENT_SERVER_PLUGIN) set(SERVER_PLUGIN_PATH "plugins") endif() + if (PLUGIN_SUBFOLDER) + set(CLIENT_PLUGIN_PATH "${CLIENT_PLUGIN_PATH}/${PLUGIN_SUBFOLDER}") + set(SERVER_PLUGIN_PATH "${SERVER_PLUGIN_PATH}/${PLUGIN_SUBFOLDER}") + endif() + if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_GENERATOR STREQUAL "Unix Makefiles") set(CLIENT_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/${CLIENT_PLUGIN_PATH}/") set(SERVER_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/assignment-client/${SERVER_PLUGIN_PATH}/") diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index cc7a6929a2..9af99f927d 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -1156,7 +1156,17 @@ FunctionEnd Section "-Core installation" - ;The following delete blocks are temporary and can be removed once users who had the initial installer have updated + ; 2016-02-25 - The following delete blocks are temporary and can be removed once users who had the initial installer have updated + ; 2019-09-10 - (3 and a half years later) Sure they are buddy. Sure they are. + + ; MessageBox MB_OK|MB_ICONEXCLAMATION "installer type is @INSTALLER_TYPE@" + + ;Delete any server executables that might have been installed by bad versions of the client-only installer, but ONLY if we are a client-only installer + ${If} "@INSTALLER_TYPE@" == "client_only" + ; MessageBox MB_OK|MB_ICONEXCLAMATION "trying to delete server binaries" + Delete "$INSTDIR\assignment-client.exe" + Delete "$INSTDIR\domain-server.exe" + ${EndIf} ;Delete any server-console files installed before it was placed in sub-folder Delete "$INSTDIR\server-console.exe" diff --git a/interface/resources/html/tabletHelp.html b/interface/resources/html/tabletHelp.html index 1140f045db..e83c0cc388 100644 --- a/interface/resources/html/tabletHelp.html +++ b/interface/resources/html/tabletHelp.html @@ -1,4 +1,4 @@ - + @@ -77,9 +77,9 @@ var handControllerImageURL = null; var index = 0; var count = 3; - var handControllerRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/vr-controls.html#vr-controls"; - var keyboardRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/desktop.html#movement-controls"; - var gamepadRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/vr-controls.html#gamepad"; + var handControllerRefURL = "https://docs.highfidelity.com/explore/get-started/vr-controls.html#vr-controls"; + var keyboardRefURL = "https://docs.highfidelity.com/explore/get-started/desktop.html#movement-controls"; + var gamepadRefURL = "https://docs.highfidelity.com/explore/get-started/vr-controls.html#gamepad"; function showKbm() { document.getElementById("main_image").setAttribute("src", "img/tablet-help-keyboard.jpg"); diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index d3bf6aca0a..fccba12a8a 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -266,6 +266,7 @@ Rectangle { labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.warnWhenMuted; + visible: bar.currentIndex !== 0; onClicked: { AudioScriptingInterface.warnWhenMuted = checked; checked = Qt.binding(function() { return AudioScriptingInterface.warnWhenMuted; }); // restore binding @@ -277,8 +278,8 @@ Rectangle { id: audioLevelSwitch height: root.switchHeight; switchWidth: root.switchWidth; - anchors.top: warnMutedSwitch.bottom - anchors.topMargin: 24 + anchors.top: warnMutedSwitch.visible ? warnMutedSwitch.bottom : parent.top + anchors.topMargin: bar.currentIndex === 0 ? 0 : 24 anchors.left: parent.left labelTextOn: qsTr("Audio Level Meter"); labelTextSize: 16; diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml index a7a0d461a2..b1f62633e7 100644 --- a/interface/resources/qml/hifi/dialogs/security/Security.qml +++ b/interface/resources/qml/hifi/dialogs/security/Security.qml @@ -240,9 +240,96 @@ Rectangle { } } + // -- Plugin Permissions -- + Item { + id: kpiContainer; + anchors.top: accountContainer.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + + Rectangle { + id: kpiHeaderContainer; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 55; + color: hifi.colors.baseGrayHighlight; + + HifiStylesUit.RalewaySemiBold { + text: "Plugin Permissions"; + anchors.fill: parent; + anchors.leftMargin: 20; + color: hifi.colors.white; + size: 18; + } + } + + Item { + id: kpiScriptContainer; + anchors.top: kpiHeaderContainer.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: 80; + + HifiControlsUit.CheckBox { + id: kpiScriptCheckbox; + readonly property string kpiSettingsKey: "private/enableScriptingPlugins" + checked: Settings.getValue(kpiSettingsKey, false); + text: "Enable custom script plugins (requires restart)" + // Anchors + anchors.verticalCenter: parent.verticalCenter; + anchors.left: parent.left; + anchors.leftMargin: 20; + boxSize: 24; + labelFontSize: 18; + colorScheme: hifi.colorSchemes.dark + color: hifi.colors.white; + width: 300; + onCheckedChanged: Settings.setValue(kpiSettingsKey, checked); + } + + HifiStylesUit.RalewaySemiBold { + id: kpiScriptHelp; + text: '[?]'; + // Anchors + anchors.verticalCenter: parent.verticalCenter; + anchors.left: kpiScriptCheckbox.right; + width: 30; + height: 30; + // Text size + size: 18; + // Style + color: hifi.colors.blueHighlight; + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + parent.color = hifi.colors.blueAccent; + } + onExited: { + parent.color = hifi.colors.blueHighlight; + } + onClicked: { + lightboxPopup.titleText = "Script Plugin Infrastructure by Kasen"; + lightboxPopup.bodyText = "Toggles the activation of scripting plugins in the 'plugins/scripting' folder. \n\n" + + "Created by https://kasen.io/"; + lightboxPopup.button1text = "OK"; + lightboxPopup.button1method = function() { + lightboxPopup.visible = false; + } + lightboxPopup.visible = true; + } + } + } + } + } + + Item { id: walletContainer; - anchors.top: accountContainer.bottom; + anchors.top: kpiContainer.bottom; anchors.left: parent.left; anchors.right: parent.right; height: childrenRect.height; diff --git a/interface/resources/qml/hifi/simplifiedUI/helpApp/about/HelpAbout.qml b/interface/resources/qml/hifi/simplifiedUI/helpApp/about/HelpAbout.qml index 4a36232029..a4a6575d1e 100644 --- a/interface/resources/qml/hifi/simplifiedUI/helpApp/about/HelpAbout.qml +++ b/interface/resources/qml/hifi/simplifiedUI/helpApp/about/HelpAbout.qml @@ -167,7 +167,7 @@ Flickable { Component.onCompleted: { var cpu = JSON.parse(PlatformInfo.getCPU(0)); var cpuModel = cpu.model; - if (cpuModel.length === 0) { + if (!cpuModel || cpuModel.length === 0) { cpuModel = "Unknown"; } @@ -213,7 +213,7 @@ Flickable { Component.onCompleted: { var gpu = JSON.parse(PlatformInfo.getGPU(PlatformInfo.getMasterGPU())); var gpuModel = gpu.model; - if (gpuModel.length === 0) { + if (!gpuModel || gpuModel.length === 0) { gpuModel = "Unknown"; } @@ -327,7 +327,7 @@ Flickable { var cpu = JSON.parse(PlatformInfo.getCPU(0)); var cpuModel = cpu.model; - if (cpuModel.length === 0) { + if (!cpuModel || cpuModel.length === 0) { cpuModel = "Unknown"; } @@ -338,7 +338,7 @@ Flickable { var gpu = JSON.parse(PlatformInfo.getGPU(PlatformInfo.getMasterGPU())); var gpuModel = gpu.model; - if (gpuModel.length === 0) { + if (!gpuModel || gpuModel.length === 0) { gpuModel = "Unknown"; } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b318f07d8f..b719f26c68 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -810,6 +810,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { } // Tell the plugin manager about our statically linked plugins + DependencyManager::set(); DependencyManager::set(); auto pluginManager = PluginManager::getInstance(); pluginManager->setInputPluginProvider([] { return getInputPlugins(); }); @@ -859,7 +860,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { #endif DependencyManager::set(); DependencyManager::set(ScriptEngine::CLIENT_SCRIPT, defaultScriptsOverrideOption); - DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -3425,7 +3425,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("Window", DependencyManager::get().data()); surfaceContext->setContextProperty("Desktop", DependencyManager::get().data()); surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); - surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + surfaceContext->setContextProperty("Settings", new QMLSettingsScriptingInterface(surfaceContext)); surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); @@ -3541,7 +3541,7 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona surfaceContext->setContextProperty("offscreenFlags", flags); surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + surfaceContext->setContextProperty("Settings", new QMLSettingsScriptingInterface(surfaceContext)); surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); surfaceContext->setContextProperty("Performance", new PerformanceScriptingInterface()); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8c6292681b..193de2792d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -725,45 +725,51 @@ Menu::Menu() { DependencyManager::get().data(), SLOT(setForceCoarsePicking(bool))); // Developer > Crash >>> - MenuWrapper* crashMenu = developerMenu->addMenu("Crash"); + bool result = false; + const QString HIFI_SHOW_DEVELOPER_CRASH_MENU("HIFI_SHOW_DEVELOPER_CRASH_MENU"); + result = QProcessEnvironment::systemEnvironment().contains(HIFI_SHOW_DEVELOPER_CRASH_MENU); + if (result) { + MenuWrapper* crashMenu = developerMenu->addMenu("Crash"); - // Developer > Crash > Display Crash Options - addCheckableActionToQMenuAndActionHash(crashMenu, MenuOption::DisplayCrashOptions, 0, true); + // Developer > Crash > Display Crash Options + addCheckableActionToQMenuAndActionHash(crashMenu, MenuOption::DisplayCrashOptions, 0, true); - addActionToQMenuAndActionHash(crashMenu, MenuOption::DeadlockInterface, 0, qApp, SLOT(deadlockApplication())); - addActionToQMenuAndActionHash(crashMenu, MenuOption::UnresponsiveInterface, 0, qApp, SLOT(unresponsiveApplication())); + addActionToQMenuAndActionHash(crashMenu, MenuOption::DeadlockInterface, 0, qApp, SLOT(deadlockApplication())); + addActionToQMenuAndActionHash(crashMenu, MenuOption::UnresponsiveInterface, 0, qApp, SLOT(unresponsiveApplication())); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunction); - connect(action, &QAction::triggered, qApp, []() { crash::pureVirtualCall(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunctionThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::pureVirtualCall).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunction); + connect(action, &QAction::triggered, qApp, []() { crash::pureVirtualCall(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunctionThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::pureVirtualCall).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFree); - connect(action, &QAction::triggered, qApp, []() { crash::doubleFree(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFreeThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doubleFree).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFree); + connect(action, &QAction::triggered, qApp, []() { crash::doubleFree(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFreeThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doubleFree).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbort); - connect(action, &QAction::triggered, qApp, []() { crash::doAbort(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbortThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doAbort).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbort); + connect(action, &QAction::triggered, qApp, []() { crash::doAbort(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbortThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doAbort).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereference); - connect(action, &QAction::triggered, qApp, []() { crash::nullDeref(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereferenceThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::nullDeref).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereference); + connect(action, &QAction::triggered, qApp, []() { crash::nullDeref(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereferenceThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::nullDeref).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccess); - connect(action, &QAction::triggered, qApp, []() { crash::outOfBoundsVectorCrash(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccessThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::outOfBoundsVectorCrash).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccess); + connect(action, &QAction::triggered, qApp, []() { crash::outOfBoundsVectorCrash(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccessThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::outOfBoundsVectorCrash).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFault); - connect(action, &QAction::triggered, qApp, []() { crash::newFault(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFaultThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::newFault).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFault); + connect(action, &QAction::triggered, qApp, []() { crash::newFault(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFaultThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::newFault).join(); }); - addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOnShutdown, 0, qApp, SLOT(crashOnShutdown())); + addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOnShutdown, 0, qApp, SLOT(crashOnShutdown())); + } + // Developer > Show Statistics addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats, 0, true); diff --git a/interface/src/scripting/SettingsScriptingInterface.cpp b/interface/src/scripting/SettingsScriptingInterface.cpp index c47d8e1da1..13eddddb1f 100644 --- a/interface/src/scripting/SettingsScriptingInterface.cpp +++ b/interface/src/scripting/SettingsScriptingInterface.cpp @@ -38,6 +38,14 @@ void SettingsScriptingInterface::setValue(const QString& setting, const QVariant if (getValue(setting) == value) { return; } + if (setting.startsWith("private/")) { + if (_restrictPrivateValues) { + qWarning() << "SettingsScriptingInterface::setValue -- restricted write: " << setting << value; + return; + } else { + qInfo() << "SettingsScriptingInterface::setValue -- allowing restricted write: " << setting << value; + } + } // Make a deep-copy of the string. // Dangling pointers can occur with QStrings that are implicitly shared from a QScriptEngine. QString deepCopy = QString::fromUtf16(setting.utf16()); diff --git a/interface/src/scripting/SettingsScriptingInterface.h b/interface/src/scripting/SettingsScriptingInterface.h index ff00299fd0..f321c41e76 100644 --- a/interface/src/scripting/SettingsScriptingInterface.h +++ b/interface/src/scripting/SettingsScriptingInterface.h @@ -27,7 +27,6 @@ class SettingsScriptingInterface : public QObject { Q_OBJECT - SettingsScriptingInterface() { }; public: static SettingsScriptingInterface* getInstance(); @@ -67,6 +66,16 @@ public slots: signals: void valueChanged(const QString& setting, const QVariant& value); + +protected: + SettingsScriptingInterface(QObject* parent = nullptr) : QObject(parent) { }; + bool _restrictPrivateValues { true }; +}; + +class QMLSettingsScriptingInterface : public SettingsScriptingInterface { + Q_OBJECT +public: + QMLSettingsScriptingInterface(QObject* parent) : SettingsScriptingInterface(parent) { _restrictPrivateValues = false; } }; #endif // hifi_SettingsScriptingInterface_h diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 9d6a368e1c..70290fe283 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -246,6 +246,12 @@ void ModelBaker::bakeSourceCopy() { // Begin hfm baking baker.run(); + const auto& errors = baker.getDracoErrors(); + if (std::find(errors.cbegin(), errors.cend(), true) != errors.cend()) { + handleError("Failed to finalize the baking of a draco Geometry node from model " + _modelURL.toString()); + return; + } + _hfmModel = baker.getHFMModel(); _materialMapping = baker.getMaterialMapping(); dracoMeshes = baker.getDracoMeshes(); @@ -437,8 +443,7 @@ void ModelBaker::abort() { bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { if (dracoMeshBytes.isEmpty()) { - handleError("Failed to finalize the baking of a draco Geometry node"); - return false; + handleWarning("Empty mesh detected in model: '" + _modelURL.toString() + "'. It will be included in the baked output."); } FBXNode dracoNode; diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index 9d0fe53e3c..55adec5786 100644 --- a/libraries/baking/src/OBJBaker.h +++ b/libraries/baking/src/OBJBaker.h @@ -28,7 +28,7 @@ protected: private: void createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& hfmModel, const hifi::ByteArray& dracoMesh); - void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel); + void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel); NodeID nextNodeID() { return _nodeID++; } NodeID _nodeID { 0 }; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index fd82567a94..a8ef41643a 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include #include #include @@ -146,7 +146,7 @@ int EntityTreeRenderer::_entitiesScriptEngineCount = 0; void EntityTreeRenderer::resetEntitiesScriptEngine() { _entitiesScriptEngine = scriptEngineFactory(ScriptEngine::ENTITY_CLIENT_SCRIPT, NO_SCRIPT, QString("about:Entities %1").arg(++_entitiesScriptEngineCount)); - _scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine); + DependencyManager::get()->runScriptInitializers(_entitiesScriptEngine); _entitiesScriptEngine->runInThread(); auto entitiesScriptEngineProvider = qSharedPointerCast(_entitiesScriptEngine); auto entityScriptingInterface = DependencyManager::get(); diff --git a/libraries/entities/src/EntityEditFilters.cpp b/libraries/entities/src/EntityEditFilters.cpp index a763c0f146..a222ca8216 100644 --- a/libraries/entities/src/EntityEditFilters.cpp +++ b/libraries/entities/src/EntityEditFilters.cpp @@ -14,6 +14,7 @@ #include #include +#include QList EntityEditFilters::getZonesByPosition(glm::vec3& position) { QList zones; @@ -258,7 +259,13 @@ void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) { if (hasCorrectSyntax(program)) { // create a QScriptEngine for this script QScriptEngine* engine = new QScriptEngine(); - engine->evaluate(scriptContents); + engine->setObjectName("filter:" + entityID.toString()); + engine->setProperty("type", "edit_filter"); + engine->setProperty("fileName", urlString); + engine->setProperty("entityID", entityID); + engine->globalObject().setProperty("Script", engine->newQObject(engine)); + DependencyManager::get()->runScriptInitializers(engine); + engine->evaluate(scriptContents, urlString); if (!hadUncaughtExceptions(*engine, urlString)) { // put the engine in the engine map (so we don't leak them, etc...) FilterData filterData; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 53afb34de5..598ac17510 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1075,16 +1075,13 @@ void EntityItem::setMass(float mass) { void EntityItem::setHref(QString value) { auto href = value.toLower(); - - // If the string has something and doesn't start with with "hifi://" it shouldn't be set - // We allow the string to be empty, because that's the initial state of this property - if (!value.isEmpty() && - !(value.toLower().startsWith("hifi://")) && - !(value.toLower().startsWith("file://")) - // TODO: serverless-domains will eventually support http and https also - ) { - return; - } + // Let's let the user set the value of this property to anything, then let consumers of the property + // decide what to do with it. Currently, the only in-engine consumers are `EntityTreeRenderer::mousePressEvent()` + // and `OtherAvatar::handleChangedAvatarEntityData()` (to remove the href property from others' avatar entities). + // + // We want this property to be as flexible as possible. The value of this property _should_ only be values that can + // be handled by `AddressManager::handleLookupString()`. That function will return `false` and not do + // anything if the value of this property isn't something that function can handle. withWriteLock([&] { _href = value; }); diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h index 484a10aa3b..888e562bca 100644 --- a/libraries/hfm/src/hfm/HFM.h +++ b/libraries/hfm/src/hfm/HFM.h @@ -56,7 +56,7 @@ const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; using ShapeVertices = std::vector; // The version of the Draco mesh binary data itself. See also: FBX_DRACO_MESH_VERSION in FBX.h -static const int DRACO_MESH_VERSION = 2; +static const int DRACO_MESH_VERSION = 3; static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000; static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES; diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index c896613df5..47a8db82b8 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -120,7 +120,7 @@ namespace baker { class BakerEngineBuilder { public: using Input = VaryingSet3; - using Output = VaryingSet4, std::vector>>; + using Output = VaryingSet5, std::vector, std::vector>>; using JobModel = Task::ModelIO; void build(JobModel& model, const Varying& input, Varying& output) { const auto& hfmModelIn = input.getN(0); @@ -168,7 +168,8 @@ namespace baker { const auto buildDracoMeshInputs = BuildDracoMeshTask::Input(meshesIn, normalsPerMesh, tangentsPerMesh).asVarying(); const auto buildDracoMeshOutputs = model.addJob("BuildDracoMesh", buildDracoMeshInputs); const auto dracoMeshes = buildDracoMeshOutputs.getN(0); - const auto materialList = buildDracoMeshOutputs.getN(1); + const auto dracoErrors = buildDracoMeshOutputs.getN(1); + const auto materialList = buildDracoMeshOutputs.getN(2); // Parse flow data const auto flowData = model.addJob("ParseFlowData", mapping); @@ -181,7 +182,7 @@ namespace baker { const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices, flowData).asVarying(); const auto hfmModelOut = model.addJob("BuildModel", buildModelInputs); - output = Output(hfmModelOut, materialMapping, dracoMeshes, materialList); + output = Output(hfmModelOut, materialMapping, dracoMeshes, dracoErrors, materialList); } }; @@ -212,7 +213,11 @@ namespace baker { return _engine->getOutput().get().get2(); } - std::vector> Baker::getDracoMaterialLists() const { + std::vector Baker::getDracoErrors() const { return _engine->getOutput().get().get3(); } + + std::vector> Baker::getDracoMaterialLists() const { + return _engine->getOutput().get().get4(); + } }; diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 6f74cb646e..9780484fa4 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -33,6 +33,7 @@ namespace baker { hfm::Model::Pointer getHFMModel() const; MaterialMapping getMaterialMapping() const; const std::vector& getDracoMeshes() const; + std::vector getDracoErrors() const; // This is a ByteArray and not a std::string because the character sequence can contain the null character (particularly for FBX materials) std::vector> getDracoMaterialLists() const; diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index 25a45cefe5..12347c30b1 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -51,7 +51,7 @@ std::vector createMaterialList(const hfm::Mesh& mesh) { return materialList; } -std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::vector& normals, const std::vector& tangents, const std::vector& materialList) { +std::tuple, bool> createDracoMesh(const hfm::Mesh& mesh, const std::vector& normals, const std::vector& tangents, const std::vector& materialList) { Q_ASSERT(normals.size() == 0 || (int)normals.size() == mesh.vertices.size()); Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); @@ -68,7 +68,7 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v } if (numTriangles == 0) { - return std::unique_ptr(); + return std::make_tuple(std::unique_ptr(), false); } draco::TriangleSoupMeshBuilder meshBuilder; @@ -184,7 +184,7 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v if (!dracoMesh) { qCWarning(model_baker) << "Failed to finalize the baking of a draco Geometry node"; - return std::unique_ptr(); + return std::make_tuple(std::unique_ptr(), true); } // we need to modify unique attribute IDs for custom attributes @@ -201,7 +201,7 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX); } - return dracoMesh; + return std::make_tuple(std::move(dracoMesh), false); } #endif // not Q_OS_ANDROID @@ -218,9 +218,13 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp const auto& normalsPerMesh = input.get1(); const auto& tangentsPerMesh = input.get2(); auto& dracoBytesPerMesh = output.edit0(); - auto& materialLists = output.edit1(); + auto& dracoErrorsPerMesh = output.edit1(); + auto& materialLists = output.edit2(); dracoBytesPerMesh.reserve(meshes.size()); + // vector is an exception to the std::vector conventions as it is a bit field + // So a bool reference to an element doesn't work + dracoErrorsPerMesh.resize(meshes.size()); materialLists.reserve(meshes.size()); for (size_t i = 0; i < meshes.size(); i++) { const auto& mesh = meshes[i]; @@ -231,7 +235,10 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp materialLists.push_back(createMaterialList(mesh)); const auto& materialList = materialLists.back(); - auto dracoMesh = createDracoMesh(mesh, normals, tangents, materialList); + bool dracoError; + std::unique_ptr dracoMesh; + std::tie(dracoMesh, dracoError) = createDracoMesh(mesh, normals, tangents, materialList); + dracoErrorsPerMesh[i] = dracoError; if (dracoMesh) { draco::Encoder encoder; diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h index 0e33be3c41..ac9ad648ab 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h @@ -34,7 +34,7 @@ class BuildDracoMeshTask { public: using Config = BuildDracoMeshConfig; using Input = baker::VaryingSet3, baker::NormalsPerMesh, baker::TangentsPerMesh>; - using Output = baker::VaryingSet2, std::vector>>; + using Output = baker::VaryingSet3, std::vector, std::vector>>; using JobModel = baker::Job::ModelIO; void configure(const Config& config); diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index b9a3e6f61e..44f42caec2 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -85,6 +85,13 @@ namespace { const QString& CACHE_ERROR_MESSAGE{ "AssetClient::Error: %1 %2" }; } +/**jsdoc + * Cache status value returned by {@link Assets.getCacheStatus}. + * @typedef {object} Assets.GetCacheStatusResult + * @property {string} cacheDirectory - The path of the cache directory. + * @property {number} cacheSize - The current cache size, in bytes. + * @property {number} maximumCacheSize - The maximum cache size, in bytes. + */ MiniPromise::Promise AssetClient::cacheInfoRequestAsync(MiniPromise::Promise deferred) { if (!deferred) { deferred = makePromise(__FUNCTION__); // create on caller's thread @@ -106,6 +113,20 @@ MiniPromise::Promise AssetClient::cacheInfoRequestAsync(MiniPromise::Promise def return deferred; } +/**jsdoc + * Information on an asset in the cache. Value returned by {@link Assets.queryCacheMeta} and included in the data returned by + * {@link Assets.loadFromCache}. + * @typedef {object} Assets.CacheItemMetaData + * @property {object} [attributes] - The attributes that are stored with this cache item. Not used. + * @property {Date} [expirationDate] - The date and time when the meta data expires. An invalid date means "never expires". + * @property {boolean} isValid - true if the item specified in the URL is in the cache, false if + * it isn't. + * @property {Date} [lastModified] - The date and time when the meta data was last modified. + * @property {object} [rawHeaders] - The raw headers that are set in the meta data. Not used. + * @property {boolean} [saveToDisk] - true if the cache item is allowed to be store on disk, + * false if it isn't. + * @property {string} [url|metaDataURL] - The ATP URL of the cached item. + */ MiniPromise::Promise AssetClient::queryCacheMetaAsync(const QUrl& url, MiniPromise::Promise deferred) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "queryCacheMetaAsync", Q_ARG(const QUrl&, url), Q_ARG(MiniPromise::Promise, deferred)); @@ -202,6 +223,24 @@ namespace { } } +/**jsdoc + * Last-modified and expiry times for a cache item. + * @typedef {object} Assets.SaveToCacheHeaders + * @property {string} [expires] - The date and time the cache value expires, in the format: + * "ddd, dd MMM yyyy HH:mm:ss". The default value is an invalid date, representing "never expires". + * @property {string} [last-modified] - The date and time the cache value was last modified, in the format: + * "ddd, dd MMM yyyy HH:mm:ss". The default value is the current date and time. + */ +/**jsdoc + * Information on saving asset data to the cache with {@link Assets.saveToCache}. + * @typedef {object} Assets.SaveToCacheResult + * @property {number} [byteLength] - The size of the cached data, in bytes. + * @property {Date} [expirationDate] - The date and time that the cache item expires. An invalid date means "never expires". + * @property {Date} [lastModified] - The date and time that the cache item was last modified. + * @property {string} [metaDataURL] - The URL associated with the cache item. + * @property {boolean} [success] - true if the save to cache request was successful. + * @property {string} [url] - The URL associated with the cache item. + */ MiniPromise::Promise AssetClient::saveToCacheAsync(const QUrl& url, const QByteArray& data, const QVariantMap& headers, MiniPromise::Promise deferred) { if (!deferred) { deferred = makePromise(__FUNCTION__); // create on caller's thread diff --git a/libraries/networking/src/Assignment.cpp b/libraries/networking/src/Assignment.cpp index 71a3cfb269..735f6dbc74 100644 --- a/libraries/networking/src/Assignment.cpp +++ b/libraries/networking/src/Assignment.cpp @@ -154,7 +154,7 @@ const char* Assignment::typeToString(Assignment::Type type) { QDebug operator<<(QDebug debug, const Assignment &assignment) { debug.nospace() << "UUID: " << qPrintable(assignment.getUUID().toString()) << - ", Type: " << assignment.getType(); + ", Type: " << assignment.getTypeName() << " (" << assignment.getType() << ")"; if (!assignment.getPool().isEmpty()) { debug << ", Pool: " << assignment.getPool(); diff --git a/libraries/networking/src/BaseAssetScriptingInterface.cpp b/libraries/networking/src/BaseAssetScriptingInterface.cpp index b231339e51..2a98dbf3c3 100644 --- a/libraries/networking/src/BaseAssetScriptingInterface.cpp +++ b/libraries/networking/src/BaseAssetScriptingInterface.cpp @@ -68,6 +68,17 @@ Promise BaseAssetScriptingInterface::queryCacheMeta(const QUrl& url) { return assetClient()->queryCacheMetaAsync(url, makePromise(__FUNCTION__)); } +/**jsdoc + * Data and information returned by {@link Assets.loadFromCache}. + * @typedef {object} Assets.LoadFromCacheResult + * @property {number} [byteLength] - The number of bytes in the retrieved data. + * @property {string} [contentType] - The automatically detected MIME type of the content. + * @property {ArrayBuffer} data - The data bytes. + * @property {Assets.CacheItemMetaData} metadata - Information on the cache item. + * @property {string|object|ArrayBuffer} [response] - The content of the response. + * @property {Assets.ResponseType} responseType - The type of the content in response. + * @property {string} url - The URL of the cache item. + */ Promise BaseAssetScriptingInterface::loadFromCache(const QUrl& url, bool decompress, const QString& responseType) { QVariantMap metaData = { { "_type", "cache" }, diff --git a/libraries/networking/src/BaseAssetScriptingInterface.h b/libraries/networking/src/BaseAssetScriptingInterface.h index 497f627421..7d118e1979 100644 --- a/libraries/networking/src/BaseAssetScriptingInterface.h +++ b/libraries/networking/src/BaseAssetScriptingInterface.h @@ -24,6 +24,22 @@ class BaseAssetScriptingInterface : public QObject { Q_OBJECT public: + + /**jsdoc + *

Types of response that {@link Assets.decompressData}, {@link Assets.getAsset}, or {@link Assets.loadFromCache} may + * provide.

+ * + * + * + * + * + * + * + * + * + *
ValueDescription
"arraybuffer"A binary ArrayBuffer object.
"json"A parsed JSON object.
"text"UTF-8 decoded string value.
+ * @typedef {string} Assets.ResponseType + */ const QStringList RESPONSE_TYPES{ "text", "arraybuffer", "json" }; using Promise = MiniPromise::Promise; QSharedPointer assetClient(); @@ -33,51 +49,62 @@ public: public slots: /**jsdoc + * Checks whether a string is a valid path. Note: A valid path must start with a "/". * @function Assets.isValidPath - * @param {string} input - * @returns {boolean} + * @param {string} path - The path to check. + * @returns {boolean} true if the path is a valid path, false if it isn't. */ bool isValidPath(QString input) { return AssetUtils::isValidPath(input); } /**jsdoc + * Checks whether a string is a valid path and filename. Note: A valid path and filename must start with a "/" + * but must not end with a "/". * @function Assets.isValidFilePath - * @param {string} input - * @returns {boolean} + * @param {string} path - The path to check. + * @returns {boolean} true if the path is a valid file path, false if it isn't. */ bool isValidFilePath(QString input) { return AssetUtils::isValidFilePath(input); } /**jsdoc + * Gets the normalized ATP URL for a path or hash: ensures that it has "atp:" at the start. * @function Assets.getATPUrl - * @param {string} input - * @returns {string} + * @param {string} url - The URL to normalize. + * @returns {string} The normalized ATP URL. */ QUrl getATPUrl(QString input) { return AssetUtils::getATPUrl(input); } /**jsdoc + * Gets the SHA256 hexadecimal hash portion of an asset server URL. * @function Assets.extractAssetHash - * @param {string} input - * @returns {string} + * @param {string} url - The URL to get the SHA256 hexadecimal hash from. + * @returns {string} The SHA256 hexadecimal hash portion of the URL if present and valid, "" otherwise. */ QString extractAssetHash(QString input) { return AssetUtils::extractAssetHash(input); } /**jsdoc + * Checks whether a string is a valid SHA256 hexadecimal hash, i.e., 64 hexadecimal characters. * @function Assets.isValidHash - * @param {string} input - * @returns {boolean} + * @param {string} hash - The hash to check. + * @returns {boolean} true if the hash is a valid SHA256 hexadecimal string, false if it isn't. */ bool isValidHash(QString input) { return AssetUtils::isValidHash(input); } /**jsdoc + * Calculates the SHA256 hash of given data. * @function Assets.hashData - * @param {} data - * @returns {object} + * @param {string|ArrayBuffer} data - The data to calculate the hash of. + * @returns {ArrayBuffer} The SHA256 hash of the data. */ QByteArray hashData(const QByteArray& data) { return AssetUtils::hashData(data); } /**jsdoc + * Calculates the SHA256 hash of given data, in hexadecimal format. * @function Assets.hashDataHex - * @param {} data - * @returns {string} + * @param {string|ArrayBuffer} data - The data to calculate the hash of. + * @returns {string} The SHA256 hash of the data, in hexadecimal format. + * @example Calculate the hash of some text. + * var text = "Hello world!"; + * print("Hash: " + Assets.hashDataHex(text)); */ QString hashDataHex(const QByteArray& data) { return hashData(data).toHex(); } diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp index b8a8f65080..e680b22fdc 100644 --- a/libraries/networking/src/ThreadedAssignment.cpp +++ b/libraries/networking/src/ThreadedAssignment.cpp @@ -29,6 +29,9 @@ ThreadedAssignment::ThreadedAssignment(ReceivedMessage& message) : _domainServerTimer(this), _statsTimer(this) { + // use as a temporary targetName name until commonInit can be called later + LogHandler::getInstance().setTargetName(QString("<%1>").arg(getTypeName())); + static const int STATS_TIMEOUT_MS = 1000; _statsTimer.setInterval(STATS_TIMEOUT_MS); // 1s, Qt::CoarseTimer acceptable connect(&_statsTimer, &QTimer::timeout, this, &ThreadedAssignment::sendStatsPacket); diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 4c01517346..20cb30dbd8 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -546,7 +546,6 @@ void Socket::handleStateChanged(QAbstractSocket::SocketState socketState) { void Socket::handleRemoteAddressChange(HifiSockAddr previousAddress, HifiSockAddr currentAddress) { { Lock connectionsLock(_connectionsHashMutex); - _connectionsHash.erase(currentAddress); const auto connectionIter = _connectionsHash.find(previousAddress); if (connectionIter != _connectionsHash.end()) { @@ -554,18 +553,16 @@ void Socket::handleRemoteAddressChange(HifiSockAddr previousAddress, HifiSockAdd _connectionsHash.erase(connectionIter); connection->setDestinationAddress(currentAddress); _connectionsHash[currentAddress] = move(connection); - } - } + connectionsLock.unlock(); - { - Lock sequenceNumbersLock(_unreliableSequenceNumbersMutex); - _unreliableSequenceNumbers.erase(currentAddress); + Lock sequenceNumbersLock(_unreliableSequenceNumbersMutex); + const auto sequenceNumbersIter = _unreliableSequenceNumbers.find(previousAddress); + if (sequenceNumbersIter != _unreliableSequenceNumbers.end()) { + auto sequenceNumbers = sequenceNumbersIter->second; + _unreliableSequenceNumbers.erase(sequenceNumbersIter); + _unreliableSequenceNumbers[currentAddress] = sequenceNumbers; + } - const auto sequenceNumbersIter = _unreliableSequenceNumbers.find(previousAddress); - if (sequenceNumbersIter != _unreliableSequenceNumbers.end()) { - auto sequenceNumbers = sequenceNumbersIter->second; - _unreliableSequenceNumbers.erase(sequenceNumbersIter); - _unreliableSequenceNumbers[currentAddress] = sequenceNumbers; } } } diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 0d0209e35f..8f1184904e 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -84,6 +84,11 @@ bool isDisabled(QJsonObject metaData) { return false; } +int PluginManager::instantiate() { + auto loaders = getLoadedPlugins(); + return std::count_if(loaders.begin(), loaders.end(), [](const auto& loader) { return (bool)loader->instance(); }); +} + auto PluginManager::getLoadedPlugins() const -> const LoaderList& { static std::once_flag once; static LoaderList loadedPlugins; @@ -105,6 +110,16 @@ bool isDisabled(QJsonObject metaData) { pluginDir.setNameFilters(QStringList() << "libplugins_lib*.so"); #endif auto candidates = pluginDir.entryList(); + + if (_enableScriptingPlugins.get()) { + QDir scriptingPluginDir{ pluginDir }; + scriptingPluginDir.cd("scripting"); + qCDebug(plugins) << "Loading scripting plugins from " << scriptingPluginDir.path(); + for (auto plugin : scriptingPluginDir.entryList()) { + candidates << "scripting/" + plugin; + } + } + for (auto plugin : candidates) { qCDebug(plugins) << "Attempting plugin" << qPrintable(plugin); QSharedPointer loader(new QPluginLoader(pluginPath + plugin)); @@ -139,6 +154,8 @@ bool isDisabled(QJsonObject metaData) { qCDebug(plugins) << " " << qPrintable(loader->errorString()); } } + } else { + qWarning() << "pluginPath does not exit..." << pluginDir; } }); return loadedPlugins; diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index 1a578c7406..eb377a2c8e 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -10,6 +10,7 @@ #include #include +#include #include "Forward.h" @@ -38,6 +39,7 @@ public: void saveSettings(); void setContainer(PluginContainer* container) { _container = container; } + int instantiate(); void shutdown(); // Application that have statically linked plugins can expose them to the plugin manager with these function @@ -69,6 +71,9 @@ private: using LoaderList = QList; const LoaderList& getLoadedPlugins() const; + Setting::Handle _enableScriptingPlugins { + "private/enableScriptingPlugins", (bool)qgetenv("enableScriptingPlugins").toInt() + }; }; // TODO: we should define this value in CMake, and then use CMake diff --git a/libraries/script-engine/src/AssetScriptingInterface.cpp b/libraries/script-engine/src/AssetScriptingInterface.cpp index 689b4b89ea..54b570e256 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.cpp +++ b/libraries/script-engine/src/AssetScriptingInterface.cpp @@ -81,6 +81,11 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu setMappingRequest->start(); } +/**jsdoc + * The success or failure of an {@link Assets.downloadData} call. + * @typedef {object} Assets.DownloadDataError + * @property {string} errorMessage - "" if the download was successful, otherwise a description of the error. + */ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) { // FIXME: historically this API method failed silently when given a non-atp prefixed // urlString (or if the AssetRequest failed). @@ -219,20 +224,31 @@ void AssetScriptingInterface::deleteAsset(QScriptValue options, QScriptValue sco } /**jsdoc - * @typedef {string} Assets.GetOptions.ResponseType - *

Available responseType values for use with @{link Assets.getAsset} and @{link Assets.loadFromCache} configuration option.

- * - * - * - * - * - * - * - * - * - *
responseTypetypeof response value
"text"contents returned as utf-8 decoded String value
"arraybuffer"contents as a binary ArrayBuffer object
"json"contents as a parsed JSON object
+ * Source and download options for {@link Assets.getAsset}. + * @typedef {object} Assets.GetOptions + * @property {boolean} [decompress=false] - true to gunzip decompress the downloaded data. Synonym: + * compressed. + * @property {Assets.ResponseType} [responseType="text"] - The desired result type. + * @property {string} url - The mapped path or hash to download. May have a leading "atp:". + */ +/**jsdoc + * Result value returned by {@link Assets.getAsset}. + * @typedef {object} Assets.GetResult + * @property {number} [byteLength] - The number of bytes in the downloaded content in response. + * @property {boolean} cached - true if the item was retrieved from the cache, false if it was + * downloaded. + * @property {string} [contentType] - The automatically detected MIME type of the content. + * @property {boolean} [decompressed] - true if the content was decompressed, false if it wasn't. + * @property {string} [hash] - The hash for the downloaded asset. + * @property {string} [hashURL] - The ATP URL of the hash file. + * @property {string} [path] - The path for the asset, if a path was requested. Otherwise, undefined. + * @property {string|object|ArrayBuffer} [response] - The downloaded content. + * @property {Assets.ResponseType} [responseType] - The type of the downloaded content in response. + * @property {string} [url] - The URL of the asset requested: the path with leading "atp:" if a path was + * requested, otherwise the requested URL. + * @property {boolean} [wasRedirected] - true if the downloaded data is the baked version of the asset, + * false if it isn't baked. */ - void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { JS_VERIFY(options.isObject() || options.isString(), "expected request options Object or URL as first parameter"); @@ -283,6 +299,22 @@ void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope, } } +/**jsdoc + * Source options for {@link Assets.resolveAsset}. + * @typedef {object} Assets.ResolveOptions + * @property {string} url - The hash or path to resolve. May have a leading "atp:". + */ +/**jsdoc + * Result value returned by {@link Assets.resolveAsset}. + *

Note: If resolving a hash, a file of that hash need not be present on the asset server for the hash to resolve.

+ * @typedef {object} Assets.ResolveResult + * @property {string} [hash] - The hash of the asset. + * @property {string} [hashURL] - The url of the asset's hash file, with leading atp:. + * @property {string} [path] - The path to the asset. + * @property {string} [url] - The URL of the asset. + * @property {boolean} [wasRedirected] - true if the resolved data is for the baked version of the asset, + * false if it isn't. + */ void AssetScriptingInterface::resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { const QString& URL{ "url" }; @@ -295,6 +327,21 @@ void AssetScriptingInterface::resolveAsset(QScriptValue options, QScriptValue sc jsPromiseReady(getAssetInfo(asset), scope, callback); } +/**jsdoc + * Content and decompression options for {@link Assets.decompressData}. + * @typedef {object} Assets.DecompressOptions + * @property {ArrayBuffer} data - The data to decompress. + * @property {Assets.ResponseType} [responseType=text] - The type of decompressed data to return. + */ +/**jsdoc + * Result value returned by {@link Assets.decompressData}. + * @typedef {object} Assets.DecompressResult + * @property {number} [byteLength] - The number of bytes in the decompressed data. + * @property {string} [contentType] - The MIME type of the decompressed data. + * @property {boolean} [decompressed] - true if the data is decompressed. + * @property {string|object|ArrayBuffer} [response] - The decompressed data. + * @property {Assets.ResponseType} [responseType] - The type of the decompressed data in response. + */ void AssetScriptingInterface::decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback) { auto data = options.property("data"); QByteArray dataByteArray = qscriptvalue_cast(data); @@ -319,6 +366,23 @@ namespace { const int32_t DEFAULT_GZIP_COMPRESSION_LEVEL = -1; const int32_t MAX_GZIP_COMPRESSION_LEVEL = 9; } + +/**jsdoc + * Content and compression options for {@link Assets.compressData}. + * @typedef {object} Assets.CompressOptions + * @property {string|ArrayBuffer} data - The data to compress. + * @property {number} level - The compression level, range -19. -1 means + * use the default gzip compression level, 0 means no compression, and 9 means maximum + * compression. + */ +/**jsdoc + * Result value returned by {@link Assets.compressData}. + * @typedef {object} Assets.CompressResult + * @property {number} [byteLength] - The number of bytes in the compressed data. + * @property {boolean} [compressed] - true if the data is compressed. + * @property {string} [contentType] - The MIME type of the compressed data, i.e., "application/gzip". + * @property {ArrayBuffer} [data] - The compressed data. + */ void AssetScriptingInterface::compressData(QScriptValue options, QScriptValue scope, QScriptValue callback) { auto data = options.property("data").isValid() ? options.property("data") : options; QByteArray dataByteArray = data.isString() ? data.toString().toUtf8() : qscriptvalue_cast(data); @@ -327,6 +391,27 @@ void AssetScriptingInterface::compressData(QScriptValue options, QScriptValue sc jsPromiseReady(compressBytes(dataByteArray, level), scope, callback); } +/**jsdoc + * Content and upload options for {@link Assets.putAsset}. + * @typedef {object} Assets.PutOptions + * @property {boolean} [compress=false] - true to gzip compress the content for upload and storage, + * false to upload and store the data without gzip compression. Synonym: compressed. + * @property {string|ArrayBuffer} data - The content to upload. + * @property {string} [path] - A user-friendly path for the file in the asset server. May have a leading + * "atp:". IF not specified, no path-to-hash mapping is set. + *

Note: The asset server destroys any unmapped SHA256-named file at server restart. Either set the mapping path + * with this property or use {@link Assets.setMapping} to set a path-to-hash mapping for the uploaded file.

+ */ +/**jsdoc + * Result value returned by {@link Assets.putAsset}. + * @typedef {object} Assets.PutResult + * @property {number} [byteLength] - The number of bytes in the hash file stored on the asset server. + * @property {boolean} [compressed] - true if the content stored is gzip compressed. + * @property {string} [contentType] - "application/gzip" if the content stored is gzip compressed. + * @property {string} [hash] - The SHA256 hash of the content. + * @property {string} [url] - The atp: URL of the content: using the path if specified, otherwise the hash. + * @property {string} [path] - The uploaded content's mapped path, if specified. + */ void AssetScriptingInterface::putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { auto compress = options.property("compress").toBool() || options.property("compressed").toBool(); auto data = options.isObject() ? options.property("data") : options; @@ -377,12 +462,27 @@ void AssetScriptingInterface::putAsset(QScriptValue options, QScriptValue scope, } } +/**jsdoc + * Source for {@link Assets.queryCacheMeta}. + * @typedef {object} Assets.QueryCacheMetaOptions + * @property {string} url - The URL of the cached asset to get information on. Must start with "atp:" or + * "cache:". + */ void AssetScriptingInterface::queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback) { QString url = options.isString() ? options.toString() : options.property("url").toString(); JS_VERIFY(QUrl(url).isValid(), QString("Invalid URL '%1'").arg(url)); jsPromiseReady(Parent::queryCacheMeta(url), scope, callback); } +/**jsdoc + * Source and retrieval options for {@link Assets.loadFromCache}. + * @typedef {object} Assets.LoadFromCacheOptions + * @property {boolean} [decompress=false] - true to gunzip decompress the cached data. Synonym: + * compressed. + * @property {Assets.ResponseType} [responseType=text] - The desired result type. + * @property {string} url - The URL of the asset to load from cache. Must start with "atp:" or + * "cache:". + */ void AssetScriptingInterface::loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback) { QString url, responseType; bool decompress = false; @@ -417,6 +517,14 @@ bool AssetScriptingInterface::canWriteCacheValue(const QUrl& url) { return true; } +/**jsdoc + * The data to save to the cache and cache options for {@link Assets.saveToCache}. + * @typedef {object} Assets.SaveToCacheOptions + * @property {string|ArrayBuffer} data - The data to save to the cache. + * @property {Assets.SaveToCacheHeaders} [headers] - The last-modified and expiry times for the cache item. + * @property {string} [url] - The URL to associate with the cache item. Must start with "atp:" or + * "cache:". If not specified, the URL is "atp:" followed by the SHA256 hash of the content. + */ void AssetScriptingInterface::saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback) { JS_VERIFY(options.isObject(), QString("expected options object as first parameter not: %1").arg(options.toVariant().typeName())); diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index 07d681ca88..5da3c51a08 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -25,7 +25,14 @@ #include /**jsdoc - * The Assets API allows you to communicate with the Asset Browser. + * The Assets API provides facilities for interacting with the domain's asset server and the client cache. + *

Assets are stored in the asset server in files with SHA256 names. These files are mapped to user-friendly URLs of the + * format: atp:/path/filename. The assets may optionally be baked, in which case a request for the original + * unbaked version of the asset is automatically redirected to the baked version. The asset data may optionally be stored as + * compressed.

+ *

The client cache can be access directly, using "atp:" or "cache:" URLs. Interface, avatar, and + * assignment client scripts can write to the cache. All script types can read from the cache.

+ * * @namespace Assets * * @hifi-interface @@ -41,251 +48,490 @@ public: AssetScriptingInterface(QObject* parent = nullptr); /**jsdoc - * Upload content to the connected domain's asset server. - * @function Assets.uploadData - * @static - * @param data {string} content to upload - * @param callback {Assets~uploadDataCallback} called when upload is complete + * Called when an {@link Assets.uploadData} call is complete. + * @callback Assets~uploadDataCallback + * @param {string} url - The raw URL of the file that the content is stored in, with atp: as the scheme and + * the SHA256 hash as the filename (with no extension). + * @param {string} hash - The SHA256 hash of the content. */ /**jsdoc - * Called when uploadData is complete - * @callback Assets~uploadDataCallback - * @param {string} url - * @param {string} hash + * Uploads content to the asset server, storing it in a SHA256-named file. + *

Note: The asset server destroys any unmapped SHA256-named file at server restart. Use {@link Assets.setMapping} to + * set a path-to-hash mapping for the new file.

+ * @function Assets.uploadData + * @param {string} data - The content to upload. + * @param {Assets~uploadDataCallback} callback - The function to call upon completion. + * @example Store a string in the asset server. + * Assets.uploadData("Hello world!", function (url, hash) { + * print("URL: " + url); // atp:0a1b...9g + * Assets.setMapping("/assetsExamples/helloWorld.txt", hash, function (error) { + * if (error) { + * print("ERROR: Could not set mapping!"); + * return; + * } + * }); + * }); */ Q_INVOKABLE void uploadData(QString data, QScriptValue callback); /**jsdoc - * Download data from the connected domain's asset server. - * @function Assets.downloadData - * @param url {string} URL of asset to download, must be ATP scheme URL. - * @param callback {Assets~downloadDataCallback} + * Called when an {@link Assets.downloadData} call is complete. + * @callback Assets~downloadDataCallback + * @param {string} data - The content that was downloaded. + * @param {Assets.DownloadDataError} error - The success or failure of the download. */ /**jsdoc - * Called when downloadData is complete - * @callback Assets~downloadDataCallback - * @param data {string} content that was downloaded + * Downloads content from the asset server, from a SHA256-named file. + * @function Assets.downloadData + * @param {string} url - The raw URL of asset to download: atp: followed by the assets's SHA256 hash. + * @param {Assets~downloadDataCallback} callback - The function to call upon completion. + * @example Store and retrieve a string from the asset server. + * var assetURL; + * + * // Store the string. + * Assets.uploadData("Hello world!", function (url, hash) { + * assetURL = url; + * print("url: " + assetURL); // atp:a0g89... + * Assets.setMapping("/assetsExamples/helloWorld.txt", hash, function (error) { + * if (error) { + * print("ERROR: Could not set mapping!"); + * return; + * } + * }); + * }); + * + * // Retrieve the string. + * Script.setTimeout(function () { + * Assets.downloadData(assetURL, function (data, error) { + * print("Downloaded data: " + data); + * print("Error: " + JSON.stringify(error)); + * }); + * }, 1000); */ - Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete); + Q_INVOKABLE void downloadData(QString url, QScriptValue callback); /**jsdoc - * Sets up a path to hash mapping within the connected domain's asset server - * @function Assets.setMapping - * @param path {string} - * @param hash {string} - * @param callback {Assets~setMappingCallback} + * Called when an {@link Assets.setMapping} call is complete. + * @callback Assets~setMappingCallback + * @param {string} error - null if the path-to-hash mapping was set, otherwise a description of the error. */ /**jsdoc - * Called when setMapping is complete - * @callback Assets~setMappingCallback - * @param {string} error + * Sets a path-to-hash mapping within the asset server. + * @function Assets.setMapping + * @param {string} path - A user-friendly path for the file in the asset server, without leading "atp:". + * @param {string} hash - The hash in the asset server. + * @param {Assets~setMappingCallback} callback - The function to call upon completion. */ Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback); /**jsdoc - * Look up a path to hash mapping within the connected domain's asset server - * @function Assets.getMapping - * @param path {string} - * @param callback {Assets~getMappingCallback} + * Called when an {@link Assets.getMapping} call is complete. + * @callback Assets~getMappingCallback + * @param {string} error - null if the path was found, otherwise a description of the error. + * @param {string} hash - The hash value if the path was found, "" if it wasn't. */ /**jsdoc - * Called when getMapping is complete. - * @callback Assets~getMappingCallback - * @param assetID {string} hash value if found, else an empty string - * @param error {string} error description if the path could not be resolved; otherwise a null value. + * Gets the hash for a path within the asset server. The hash is for the unbaked or baked version of the + * asset, according to the asset server setting for the particular path. + * @function Assets.getMapping + * @param {string} path - The path to a file in the asset server to get the hash of. + * @param {Assets~getMappingCallback} callback - The function to call upon completion. + * @example Report the hash of an asset server item. + * var assetPath = Window.browseAssets(); + * if (assetPath) { + * var mapping = Assets.getMapping(assetPath, function (error, hash) { + * print("Asset: " + assetPath); + * print("- hash: " + hash); + * print("- error: " + error); + * }); + * } */ Q_INVOKABLE void getMapping(QString path, QScriptValue callback); /**jsdoc - * @function Assets.setBakingEnabled - * @param path {string} - * @param enabled {boolean} - * @param callback {} + * Called when an {@link Assets.setBakingEnabled} call is complete. + * @callback Assets~setBakingEnabledCallback + * @param {string} error - null if baking was successfully enabled or disabled, otherwise a description of the + * error. */ /**jsdoc - * Called when setBakingEnabled is complete. - * @callback Assets~setBakingEnabledCallback + * Sets whether or not to bake an asset in the asset server. + * @function Assets.setBakingEnabled + * @param {string} path - The path to a file in the asset server. + * @param {boolean} enabled - true to enable baking of the asset, false to disable. + * @param {Assets~setBakingEnabledCallback} callback - The function to call upon completion. */ + // Note: Second callback parameter not documented because it's always {}. Q_INVOKABLE void setBakingEnabled(QString path, bool enabled, QScriptValue callback); #if (PR_BUILD || DEV_BUILD) /** * This function is purely for development purposes, and not meant for use in a - * production context. It is not a public-facing API, so it should not contain jsdoc. + * production context. It is not a public-facing API, so it should not have JSDoc. */ Q_INVOKABLE void sendFakedHandshake(); #endif /**jsdoc - * Request Asset data from the ATP Server - * @function Assets.getAsset - * @param {URL|Assets.GetOptions} options An atp: style URL, hash, or relative mapped path; or an {@link Assets.GetOptions} object with request parameters - * @param {Assets~getAssetCallback} scope A scope callback function to receive (error, results) values - * @param {function} [callback=undefined] + * Details of a callback function. + * @typedef {object} Assets.CallbackDetails + * @property {object} scope - The scope that the callback function is defined in. This object is bound to + * this when the function is called. + * @property {Assets~compressDataCallback|Assets~decompressDataCallback|Assets~getAssetCallback + * |Assets~getCacheStatusCallback|Assets~loadFromCacheCallback|Assets~putAssetCallback|Assets~queryCacheMetaCallback + * |Assets~resolveAssetCallback|Assets~saveToCacheCallback} + * callback - The function to call upon completion. May be an inline function or a function identifier. If a function + * identifier, it must be a member of scope. */ /**jsdoc - * A set of properties that can be passed to {@link Assets.getAsset}. - * @typedef {object} Assets.GetOptions - * @property {string} [url] an "atp:" style URL, hash, or relative mapped path to fetch - * @property {string} [responseType=text] the desired reponse type (text | arraybuffer | json) - * @property {boolean} [decompress=false] whether to attempt gunzip decompression on the fetched data - * See: {@link Assets.putAsset} and its .compress=true option - */ - - /**jsdoc - * Called when Assets.getAsset is complete. + * Called when an {@link Assets.getAsset} call is complete. * @callback Assets~getAssetCallback - * @param {string} error - contains error message or null value if no error occured fetching the asset - * @param {Asset~getAssetResult} result - result object containing, on success containing asset metadata and contents + * @param {string} error - null if the content was downloaded, otherwise a description of the error. + * @param {Assets.GetResult} result - Information on and the content downloaded. */ - /**jsdoc - * Result value returned by {@link Assets.getAsset}. - * @typedef {object} Assets~getAssetResult - * @property {string} [url] the resolved "atp:" style URL for the fetched asset - * @property {string} [hash] the resolved hash for the fetched asset - * @property {string|ArrayBuffer|Object} [response] response data (possibly converted per .responseType value) - * @property {string} [responseType] response type (text | arraybuffer | json) - * @property {string} [contentType] detected asset mime-type (autodetected) - * @property {number} [byteLength] response data size in bytes - * @property {number} [decompressed] flag indicating whether data was decompressed + * Downloads content from the asset server. + * @function Assets.getAsset + * @param {string|Assets.GetOptions} source - What to download and download options. If a string, the mapped path or hash + * to download, optionally including a leading "atp:". + * @param {object|Assets.CallbackDetails|Assets~getAssetCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~getAssetCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Retrieve a string from the asset server. + * Assets.getAsset( + * { + * url: "/assetsExamples/helloWorld.txt", + * responseType: "text" + * }, + * function (error, result) { + * if (error) { + * print("ERROR: Data not downloaded"); + * } else { + * print("Data: " + result.response); + * } + * } + * ); */ - Q_INVOKABLE void getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * Upload Asset data to the ATP Server + * Called when an {@link Assets.putAsset} call is complete. + * @callback Assets~putAssetCallback + * @param {string} error - null if the content was uploaded and any path-to-hash mapping set, otherwise a + * description of the error. + * @param {Assets.PutResult} result - Information on the content uploaded. + */ + /**jsdoc + * Uploads content to the asset server and sets a path-to-hash mapping. * @function Assets.putAsset - * @param {Assets.PutOptions} options A PutOptions object with upload parameters - * @param {Assets~putAssetCallback} scope[callback] A scoped callback function invoked with (error, results) - * @param {function} [callback=undefined] + * @param {string|Assets.PutOptions} options - The content to upload and upload options. If a string, the value of the + * string is uploaded but a path-to-hash mapping is not set. + * @param {object|Assets.CallbackDetails|Assets~putAssetCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~putAssetCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Store a string in the asset server. + * Assets.putAsset( + * { + * data: "Hello world!", + * path: "/assetsExamples/helloWorld.txt" + * }, + * function (error, result) { + * if (error) { + * print("ERROR: Data not uploaded or mapping not set"); + * } else { + * print("URL: " + result.url); // atp:/assetsExamples/helloWorld.txt + * } + * } + * ); */ - - /**jsdoc - * A set of properties that can be passed to {@link Assets.putAsset}. - * @typedef {object} Assets.PutOptions - * @property {ArrayBuffer|string} [data] byte buffer or string value representing the new asset's content - * @property {string} [path=null] ATP path mapping to automatically create (upon successful upload to hash) - * @property {boolean} [compress=false] whether to gzip compress data before uploading - */ - - /**jsdoc - * Called when Assets.putAsset is complete. - * @callback Assets~puttAssetCallback - * @param {string} error - contains error message (or null value if no error occured while uploading/mapping the new asset) - * @param {Asset~putAssetResult} result - result object containing error or result status of asset upload - */ - - /**jsdoc - * Result value returned by {@link Assets.putAsset}. - * @typedef {object} Assets~putAssetResult - * @property {string} [url] the resolved "atp:" style URL for the uploaded asset (based on .path if specified, otherwise on the resulting ATP hash) - * @property {string} [path] the uploaded asset's resulting ATP path (or undefined if no path mapping was assigned) - * @property {string} [hash] the uploaded asset's resulting ATP hash - * @property {boolean} [compressed] flag indicating whether the data was compressed before upload - * @property {number} [byteLength] flag indicating final byte size of the data uploaded to the ATP server - */ - Q_INVOKABLE void putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.deleteAsset - * @param {} options - * @param {} scope - * @param {} [callback = ""] + * Called when an {@link Assets.deleteAsset} call is complete. + *

Not implemented: This type is not implemented yet.

+ * @callback Assets~deleteAssetCallback + * @param {string} error - null if the content was deleted, otherwise a description of the error. + * @param {Assets.DeleteResult} result - Information on the content deleted. */ - - Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); - /**jsdoc - * @function Assets.resolveAsset - * @param {} options - * @param {} scope - * @param {} [callback = ""] + * Deletes content from the asset server. + *

Not implemented: This method is not implemented yet.

+ * @function Assets.deleteAsset + * @param {Assets.DeleteOptions} options - The content to delete and delete options. + * @param {object} scope - The scope that the callback function is defined in. + * @param {Assets~deleteAssetCallback} callback - The function to call upon completion. */ + Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + /**jsdoc + * Called when an {@link Assets.resolveAsset} call is complete. + * @callback Assets~resolveAssetCallback + * @param {string} error - null if the asset hash or path was resolved, otherwise a description of the error. + * @param {Assets.ResolveResult} result - Information on the hash or path resolved. + */ + /**jsdoc + * Resolves and returns information on a hash or a path in the asset server. + * @function Assets.resolveAsset + * @param {string|Assets.ResolveOptions} source - The hash or path to resolve if a string, otherwise an object specifying + * what to resolve. If a string, it may have a leading "atp:". + * @param {object|Assets.CallbackDetails|Assets~resolveAssetCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~resolveAssetCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Get the hash and URL for a path. + * Assets.resolveAsset( + * "/assetsExamples/helloWorld.txt", + * function (error, result) { + * if (error) { + * print("ERROR: " + error); + * } else { + * print("Hash: " + result.hash); + * print("URL: " + result.url); + * } + * } + * ); + */ Q_INVOKABLE void resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.decompressData - * @param {} options - * @param {} scope - * @param {} [callback = ""] + * Called when an {@link Assets.decompressData} call is complete. + * @callback Assets~decompressDataCallback + * @param {string} error - null if the data was successfully compressed, otherwise a description of the error. + * @param {Assets.DecompressResult} result - Information on and the decompressed data. + */ + /**jsdoc + * Decompresses data in memory using gunzip. + * @function Assets.decompressData + * @param {Assets.DecompressOptions} source - What to decompress and decompression options. + * @param {object|Assets.CallbackDetails|Assets~decompressDataCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~decompressDataCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

*/ - Q_INVOKABLE void decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.compressData - * @param {} options - * @param {} scope - * @param {} [callback = ""] + * Called when an {@link Assets.compressData} call is complete. + * @callback Assets~compressDataCallback + * @param {string} error - null if the data was successfully compressed, otherwise a description of the error. + * @param {Assets.CompressResult} result - Information on and the compressed data. + */ + /**jsdoc + * Compresses data in memory using gzip. + * @function Assets.compressData + * @param {string|ArrayBuffer|Assets.CompressOptions} source - What to compress and compression options. If a string or + * ArrayBuffer, the data to compress. + * @param {object|Assets.CallbackDetails|Assets~compressDataCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~compressDataCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

*/ - Q_INVOKABLE void compressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc + * Initializes the cache if it isn't already initialized. * @function Assets.initializeCache - * @returns {boolean} + * @returns {boolean} true if the cache is initialized, false if it isn't. */ - Q_INVOKABLE bool initializeCache() { return Parent::initializeCache(); } /**jsdoc + * Checks whether the script can write to the cache. * @function Assets.canWriteCacheValue - * @param {string} url - * @returns {boolean} + * @param {string} url - Not used. + * @returns {boolean} true if the script is an Interface, avatar, or assignment client script, + * false if the script is a client entity or server entity script. + * @example Report whether the script can write to the cache. + * print("Can write to cache: " + Assets.canWriteCacheValue(null)); */ - Q_INVOKABLE bool canWriteCacheValue(const QUrl& url); /**jsdoc - * @function Assets.getCacheStatus - * @param {} scope - * @param {} [callback=undefined] + * Called when a {@link Assets.getCacheStatus} call is complete. + * @callback Assets~getCacheStatusCallback + * @param {string} error - null if the cache status was retrieved without error, otherwise a description of + * the error. + * @param {Assets.GetCacheStatusResult} result - Details of the current cache status. + */ + /**jsdoc + * Gets the current cache status. + * @function Assets.getCacheStatus + * @param {object|Assets.CallbackDetails|Assets~getCacheStatusCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~getCacheStatusCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Report the cache status. + * Assets.getCacheStatus(function (error, status) { + * print("Cache status"); + * print("- Error: " + error); + * print("- Status: " + JSON.stringify(status)); + * }); */ - Q_INVOKABLE void getCacheStatus(QScriptValue scope, QScriptValue callback = QScriptValue()) { jsPromiseReady(Parent::getCacheStatus(), scope, callback); } /**jsdoc - * @function Assets.queryCacheMeta - * @param {} options - * @param {} scope - * @param {} [callback=undefined] + * Called when {@link Assets.queryCacheMeta} is complete. + * @callback Assets~queryCacheMetaCallback + * @param {string} error - null if the URL has a valid cache entry, otherwise a description of the error. + * @param {Assets.CacheItemMetaData} result - Information on an asset in the cache. + */ + /**jsdoc + * Gets information about the status of an asset in the cache. + * @function Assets.queryCacheMeta + * @param {string|Assets.QueryCacheMetaOptions} path - The URL of the cached asset to get information on if a string, + * otherwise an object specifying the cached asset to get information on. The URL must start with "atp:" + * or "cache:". + * @param {object|Assets.CallbackDetails|Assets~queryCacheMetaCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~queryCacheMetaCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Report details of a string store in the cache. + * Assets.queryCacheMeta( + * "cache:/cacheExample/helloCache.txt", + * function (error, result) { + * if (error) { + * print("Error: " + error); + * } else { + * print("Success:"); + * print("- URL: " + result.url); + * print("- isValid: " + result.isValid); + * print("- saveToDisk: " + result.saveToDisk); + * print("- expirationDate: " + result.expirationDate); + * } + * } + * ); */ - Q_INVOKABLE void queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.loadFromCache - * @param {} options - * @param {} scope - * @param {} [callback=undefined] + * Called when an {@link Assets.loadFromCache} call is complete. + * @callback Assets~loadFromCacheCallback + * @param {string} error - null if the cache item was successfully retrieved, otherwise a description of the + * error. + * @param {Assets.LoadFromCacheResult} result - Information on and the retrieved data. + */ + /**jsdoc + * Retrieves data from the cache directly, without downloading it. + * @function Assets.loadFromCache + * @param {string|Assets.LoadFromCacheOptions} options - The URL of the asset to load from the cache if a string, otherwise + * an object specifying the asset to load from the cache and load options. The URL must start with "atp:" + * or "cache:". + * @param {object|Assets.CallbackDetails|Assets~loadFromCacheCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~loadFromCacheCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Retrieve a string from the cache. + * Assets.loadFromCache( + * "cache:/cacheExample/helloCache.txt", + * function (error, result) { + * if (error) { + * print("Error: " + error); + * } else { + * print("Success:"); + * print("- Response: " + result.response); + * print("- Content type: " + result.contentType); + * print("- Number of bytes: " + result.byteLength); + * print("- Bytes: " + [].slice.call(new Uint8Array(result.data), 0, result.byteLength)); + * print("- URL: " + result.url); + * } + * } + * ); */ - Q_INVOKABLE void loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.saveToCache - * @param {} options - * @param {} scope - * @param {} [callback=undefined] + * Called when an {@link Assets.saveToCache} call is complete. + * @callback Assets~saveToCacheCallback + * @param {string} error - null if the asset data was successfully saved to the cache, otherwise a description + * of the error. + * @param {Assets.SaveToCacheResult} result - Information on the cached data. + */ + /**jsdoc + * Saves asset data to the cache directly, without downloading it from a URL. + *

Note: Can only be used in Interface, avatar, and assignment client scripts.

+ * @function Assets.saveToCache + * @param {Assets.SaveToCacheOptions} options - The data to save to the cache and cache options. + * @param {object|Assets.CallbackDetails|Assets~saveToCacheCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~saveToCacheCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Save a string in the cache. + * Assets.saveToCache( + * { + * url: "cache:/cacheExample/helloCache.txt", + * data: "Hello cache" + * }, + * function (error, result) { + * if (error) { + * print("Error: " + error); + * } else { + * print("Success:"); + * print("- Bytes: " + result.byteLength); + * print("- URL: " + result.url); + * } + * } + * ); */ - Q_INVOKABLE void saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc + * Saves asset data to the cache directly, without downloading it from a URL. + *

Note: Can only be used in Interface, avatar, and assignment client scripts.

* @function Assets.saveToCache - * @param {} url - * @param {} data - * @param {} metadata - * @param {} scope - * @param {} [callback=undefined] + * @param {string} url - The URL to associate with the cache item. Must start with "atp:" or + * "cache:". + * @param {string|ArrayBuffer} data - The data to save to the cache. + * @param {Assets.SaveToCacheHeaders} headers - The last-modified and expiry times for the cache item. + * @param {object|Assets.CallbackDetails|Assets~saveToCacheCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~saveToCacheCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

*/ - Q_INVOKABLE void saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& metadata, QScriptValue scope, QScriptValue callback = QScriptValue()); protected: diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 80036d6ad9..8337d1911a 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -237,6 +237,11 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const } } +QString ScriptEngine::getTypeAsString() const { + auto value = QVariant::fromValue(_type).toString(); + return value.isEmpty() ? "unknown" : value.toLower(); +} + QString ScriptEngine::getContext() const { switch (_context) { case CLIENT_SCRIPT: diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 3ffef16844..94381ede02 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -122,6 +122,8 @@ public: class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider { Q_OBJECT Q_PROPERTY(QString context READ getContext) + Q_PROPERTY(QString type READ getTypeAsString) + Q_PROPERTY(QString fileName MEMBER _fileNameString CONSTANT) public: enum Context { @@ -138,6 +140,7 @@ public: AGENT, AVATAR }; + Q_ENUM(Type) static int processLevelMaxRetries; ScriptEngine(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString("about:ScriptEngine")); @@ -636,6 +639,7 @@ public: void setType(Type type) { _type = type; }; Type getType() { return _type; }; + QString getTypeAsString() const; bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget bool isRunning() const { return _isRunning; } // used by ScriptWidget diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 508a26302e..fd2ebe300d 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -136,24 +136,6 @@ QUrl expandScriptUrl(const QUrl& rawScriptURL) { QObject* scriptsModel(); -bool NativeScriptInitializers::registerNativeScriptInitializer(NativeScriptInitializer initializer) { - return registerScriptInitializer([initializer](ScriptEnginePointer engine) { - initializer(qobject_cast(engine.data())); - }); -} - -bool NativeScriptInitializers::registerScriptInitializer(ScriptInitializer initializer) { - if (auto scriptEngines = DependencyManager::get().data()) { - scriptEngines->registerScriptInitializer(initializer); - return true; - } - return false; -} - -void ScriptEngines::registerScriptInitializer(ScriptInitializer initializer) { - _scriptInitializers.push_back(initializer); -} - void ScriptEngines::addScriptEngine(ScriptEnginePointer engine) { if (!_isStopped) { QMutexLocker locker(&_allScriptsMutex); @@ -590,12 +572,8 @@ void ScriptEngines::quitWhenFinished() { } int ScriptEngines::runScriptInitializers(ScriptEnginePointer scriptEngine) { - int ii=0; - for (auto initializer : _scriptInitializers) { - ii++; - initializer(scriptEngine); - } - return ii; + auto nativeCount = DependencyManager::get()->runScriptInitializers(scriptEngine.data()); + return nativeCount + ScriptInitializerMixin::runScriptInitializers(scriptEngine); } void ScriptEngines::launchScriptEngine(ScriptEnginePointer scriptEngine) { diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 484ccd8932..df736e101d 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -48,13 +48,8 @@ class ScriptEngine; * scripts directory of the Interface installation. * Read-only. */ -class NativeScriptInitializers : public ScriptInitializerMixin { -public: - bool registerNativeScriptInitializer(NativeScriptInitializer initializer) override; - bool registerScriptInitializer(ScriptInitializer initializer) override; -}; -class ScriptEngines : public QObject, public Dependency { +class ScriptEngines : public QObject, public Dependency, public ScriptInitializerMixin { Q_OBJECT Q_PROPERTY(ScriptsModel* scriptsModel READ scriptsModel CONSTANT) @@ -62,11 +57,9 @@ class ScriptEngines : public QObject, public Dependency { Q_PROPERTY(QString debugScriptUrl READ getDebugScriptUrl WRITE setDebugScriptUrl) public: - using ScriptInitializer = ScriptInitializerMixin::ScriptInitializer; - ScriptEngines(ScriptEngine::Context context, const QUrl& defaultScriptsOverride = QUrl()); - void registerScriptInitializer(ScriptInitializer initializer); - int runScriptInitializers(ScriptEnginePointer engine); + int runScriptInitializers(ScriptEnginePointer engine) override; + void loadScripts(); void saveScripts(); @@ -347,7 +340,6 @@ protected: QHash _scriptEnginesHash; QSet _allKnownScriptEngines; QMutex _allScriptsMutex; - std::list _scriptInitializers; ScriptsModel _scriptsModel; ScriptsModelFilter _scriptsModelFilter; std::atomic _isStopped { false }; diff --git a/libraries/shared/src/shared/ScriptInitializerMixin.h b/libraries/shared/src/shared/ScriptInitializerMixin.h index 50de553b0b..cefa33c2d3 100644 --- a/libraries/shared/src/shared/ScriptInitializerMixin.h +++ b/libraries/shared/src/shared/ScriptInitializerMixin.h @@ -9,30 +9,38 @@ #pragma once #include +#include #include #include "../DependencyManager.h" class QScriptEngine; class ScriptEngine; -class ScriptInitializerMixin : public QObject, public Dependency { - Q_OBJECT +template class ScriptInitializerMixin { +public: + using ScriptInitializer = std::function; + virtual void registerScriptInitializer(ScriptInitializer initializer) { + InitializerLock lock(_scriptInitializerMutex); + _scriptInitializers.push_back(initializer); + } + virtual int runScriptInitializers(T engine) { + InitializerLock lock(_scriptInitializerMutex); + return std::count_if(_scriptInitializers.begin(), _scriptInitializers.end(), + [engine](auto initializer){ initializer(engine); return true; } + ); + } + virtual ~ScriptInitializerMixin() {} +protected: + std::mutex _scriptInitializerMutex; + using InitializerLock = std::lock_guard; + std::list _scriptInitializers; +}; + +class ScriptInitializers : public ScriptInitializerMixin, public Dependency { public: // Lightweight `QScriptEngine*` initializer (only depends on built-in Qt components) // example registration: - // eg: [&](QScriptEngine* engine) -> bool { + // eg: [&](QScriptEngine* engine) { // engine->globalObject().setProperties("API", engine->newQObject(...instance...)) - // return true; - // } - using NativeScriptInitializer = std::function; - virtual bool registerNativeScriptInitializer(NativeScriptInitializer initializer) = 0; - - // Heavyweight `ScriptEngine*` initializer (tightly coupled to Interface and script-engine library internals) - // eg: [&](ScriptEnginePointer scriptEngine) -> bool { - // engine->registerGlobalObject("API", ...instance..); - // return true; - // } - using ScriptEnginePointer = QSharedPointer; - using ScriptInitializer = std::function; - virtual bool registerScriptInitializer(ScriptInitializer initializer) { return false; }; + // }; }; diff --git a/libraries/task/src/task/Config.h b/libraries/task/src/task/Config.h index 8accba9e1f..71d48c9a18 100644 --- a/libraries/task/src/task/Config.h +++ b/libraries/task/src/task/Config.h @@ -90,6 +90,17 @@ public: using Config = JobConfig; }; +/**jsdoc + * @namespace Workload + * + * @hifi-interface + * @hifi-client-entity + * @hifi-avatar + * + * @property {number} cpuRunTime - Read-only. + * @property {boolean} enabled + * @property {number} branch + */ // A default Config is always on; to create an enableable Config, use the ctor JobConfig(bool enabled) class JobConfig : public QObject { Q_OBJECT @@ -139,7 +150,7 @@ public: double getCPURunTime() const { return _msCPURunTime; } /**jsdoc - * @function Render.getConfig + * @function Workload.getConfig * @param {string} name * @returns {object} */ @@ -162,19 +173,19 @@ public: // Describe the node graph data connections of the associated Job/Task /**jsdoc - * @function JobConfig.isTask + * @function Workload.isTask * @returns {boolean} */ Q_INVOKABLE bool isTask() const { return _isTask; } /**jsdoc - * @function JobConfig.isSwitch + * @function Workload.isSwitch * @returns {boolean} */ Q_INVOKABLE bool isSwitch() const { return _isSwitch; } /**jsdoc - * @function JobConfig.getSubConfigs + * @function Workload.getSubConfigs * @returns {object[]} */ Q_INVOKABLE QObjectList getSubConfigs() const { @@ -187,13 +198,13 @@ public: } /**jsdoc - * @function JobConfig.getNumSubs + * @function Workload.getNumSubs * @returns {number} */ Q_INVOKABLE int getNumSubs() const { return getSubConfigs().size(); } /**jsdoc - * @function JobConfig.getSubConfig + * @function Workload.getSubConfig * @param {number} index * @returns {object} */ @@ -214,7 +225,7 @@ public slots: /**jsdoc * @function Workload.load - * @param {object} map + * @param {object} json */ void load(const QJsonObject& val) { qObjectFromJsonValue(val, *this); emit loaded(); } diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 4a0f52e272..1150e27cac 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -43,3 +43,7 @@ set(DIR "pcmCodec") add_subdirectory(${DIR}) set(DIR "hifiCodec") add_subdirectory(${DIR}) + +# example plugins +set(DIR "KasenAPIExample") +add_subdirectory(${DIR}) diff --git a/plugins/KasenAPIExample/CMakeLists.txt b/plugins/KasenAPIExample/CMakeLists.txt new file mode 100644 index 0000000000..96ac84e10d --- /dev/null +++ b/plugins/KasenAPIExample/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME KasenAPIExample) +setup_hifi_client_server_plugin(scripting) +link_hifi_libraries(shared plugins avatars networking graphics gpu) diff --git a/plugins/KasenAPIExample/src/ExampleScriptPlugin.h b/plugins/KasenAPIExample/src/ExampleScriptPlugin.h new file mode 100644 index 0000000000..a6af105e0e --- /dev/null +++ b/plugins/KasenAPIExample/src/ExampleScriptPlugin.h @@ -0,0 +1,56 @@ +// +// ExampleScriptPlugin.h +// plugins/KasenAPIExample/src +// +// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com +// Copyright 2019 Kasen IO +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// Supporting file containing all QtScript specific integration. + +#ifndef EXAMPLE_SCRIPT_PLUGIN_H +#define EXAMPLE_SCRIPT_PLUGIN_H + +#if DEV_BUILD +#pragma message("QtScript is deprecated see: doc.qt.io/qt-5/topics-scripting.html") +#endif +#include + +#include +#include +#include + +namespace example { + +extern const QLoggingCategory& logger; + +inline void setGlobalInstance(QScriptEngine* engine, const QString& name, QObject* object) { + auto value = engine->newQObject(object, QScriptEngine::QtOwnership); + engine->globalObject().setProperty(name, value); + qCDebug(logger) << "setGlobalInstance" << name << engine->property("fileName"); +} + +class ScriptPlugin : public QObject { + Q_OBJECT + QString _version; + Q_PROPERTY(QString version MEMBER _version CONSTANT) +protected: + inline ScriptPlugin(const QString& name, const QString& version) : _version(version) { + setObjectName(name); + if (!DependencyManager::get()) { + qCWarning(logger) << "COULD NOT INITIALIZE (ScriptInitializers unavailable)" << qApp << this; + return; + } + qCWarning(logger) << "registering w/ScriptInitializerMixin..." << DependencyManager::get().data(); + DependencyManager::get()->registerScriptInitializer( + [this](QScriptEngine* engine) { setGlobalInstance(engine, objectName(), this); }); + } +public slots: + inline QString toString() const { return QString("[%1 version=%2]").arg(objectName()).arg(_version); } +}; + +} // namespace example + +#endif \ No newline at end of file diff --git a/plugins/KasenAPIExample/src/KasenAPIExample.cpp b/plugins/KasenAPIExample/src/KasenAPIExample.cpp new file mode 100644 index 0000000000..d566d11376 --- /dev/null +++ b/plugins/KasenAPIExample/src/KasenAPIExample.cpp @@ -0,0 +1,139 @@ +// +// KasenAPIExample.cpp +// plugins/KasenAPIExample/src +// +// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com +// Copyright 2019 Kasen IO +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// Example of prototyping new JS APIs by leveraging the existing plugin system. + +#include "ExampleScriptPlugin.h" + +#include +#include +#include +#include +#include + +#include +#include + +namespace custom_api_example { + +QLoggingCategory logger{ "custom_api_example" }; + +class KasenAPIExample : public example::ScriptPlugin { + Q_OBJECT + Q_PLUGIN_METADATA(IID "KasenAPIExample" FILE "plugin.json") +public: + KasenAPIExample() : example::ScriptPlugin("KasenAPIExample", "0.0.1") { + qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread(); + } + +public slots: + /**jsdoc + * Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms + * @example Measure current setTimeout accuracy. + * var expected = 1000; + * var start = KasenAPIExample.now(); + * Script.setTimeout(function () { + * var elapsed = (KasenAPIExample.now() - start)/1000; + * print("expected (ms):", expected, "actual (ms):", elapsed); + * }, expected); + */ + QVariant now() const { + return usecTimestampNow(); + } + + /**jsdoc + * Returns the available blendshape names for an avatar. + * @example Get blendshape names + * print(JSON.stringify(KasenAPIExample.getBlendshapeNames(MyAvatar.sessionUUID))); + */ + QStringList getBlendshapeNames(const QUuid& avatarID) const { + QVector out; + if (auto head = getAvatarHead(avatarID)) { + for (const auto& kv : head->getBlendshapeMap().toStdMap()) { + if (kv.second >= out.size()) out.resize(kv.second+1); + out[kv.second] = kv.first; + } + } + return out.toList(); + } + + /**jsdoc + * Returns a key-value object with active (non-zero) blendshapes. + * eg: { JawOpen: 1.0, ... } + * @example Get active blendshape map + * print(JSON.stringify(KasenAPIExample.getActiveBlendshapes(MyAvatar.sessionUUID))); + */ + QVariant getActiveBlendshapes(const QUuid& avatarID) const { + if (auto head = getAvatarHead(avatarID)) { + return head->toJson()["blendShapes"].toVariant(); + } + return {}; + } + + QVariant getBlendshapeMapping(const QUuid& avatarID) const { + QVariantMap out; + if (auto head = getAvatarHead(avatarID)) { + for (const auto& kv : head->getBlendshapeMap().toStdMap()) { + out[kv.first] = kv.second; + } + } + return out; + } + + QVariant getBlendshapes(const QUuid& avatarID) const { + QVariantMap result; + if (auto head = getAvatarHead(avatarID)) { + QStringList names = getBlendshapeNames(avatarID); + auto states = head->getBlendshapeStates(); + result = { + { "base", zipNonZeroValues(names, states.base) }, + { "summed", zipNonZeroValues(names, states.summed) }, + { "transient", zipNonZeroValues(names, states.transient) }, + }; + } + return result; + } + +private: + static QVariantMap zipNonZeroValues(const QStringList& keys, const QVector& values) { + QVariantMap out; + for (int i=1; i < values.size(); i++) { + if (fabs(values[i]) > 1.0e-6) { + out[keys.value(i)] = values[i]; + } + } + return out; + } + struct _HeadHelper : public HeadData { + QMap getBlendshapeMap() const { + return _blendshapeLookupMap; + } + struct States { QVector base, summed, transient; }; + States getBlendshapeStates() const { + return { + _blendshapeCoefficients, + _summedBlendshapeCoefficients, + _transientBlendshapeCoefficients + }; + } + }; + static const _HeadHelper* getAvatarHead(const QUuid& avatarID) { + auto avatars = DependencyManager::get(); + auto avatar = avatars ? avatars->getAvatarBySessionID(avatarID) : nullptr; + auto head = avatar ? avatar->getHeadData() : nullptr; + return reinterpret_cast(head); + } +}; + +} + +const QLoggingCategory& example::logger{ custom_api_example::logger }; + +#include "KasenAPIExample.moc" diff --git a/plugins/KasenAPIExample/src/plugin.json b/plugins/KasenAPIExample/src/plugin.json new file mode 100644 index 0000000000..3e6931deec --- /dev/null +++ b/plugins/KasenAPIExample/src/plugin.json @@ -0,0 +1,21 @@ +{ + "name":"Kasen JS API Example", + "version": 1, + "package": { + "author": "Revofire", + "homepage": "www.realities.dev", + "version": "0.0.1", + "engines": { + "hifi-interface": ">= 0.83.0", + "hifi-assignment-client": ">= 0.83.0" + }, + "config": { + "client": true, + "entity_client": true, + "entity_server": true, + "edit_filter": true, + "agent": true, + "avatar": true + } + } +} diff --git a/scripts/simplifiedUI/simplifiedEmote/emojiApp/resources/sounds/emojiPopSound2.wav b/scripts/simplifiedUI/simplifiedEmote/emojiApp/resources/sounds/emojiPopSound2.wav deleted file mode 100644 index 3d917997a7..0000000000 Binary files a/scripts/simplifiedUI/simplifiedEmote/emojiApp/resources/sounds/emojiPopSound2.wav and /dev/null differ diff --git a/scripts/simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js b/scripts/simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js index 1b16843b09..d8db41e488 100644 --- a/scripts/simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js +++ b/scripts/simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js @@ -48,7 +48,6 @@ var UTF_CODE = 0; // Only plays a sound if it is downloaded. // Only plays one sound at a time. var emojiCreateSound = SoundCache.getSound(Script.resolvePath('resources/sounds/emojiPopSound1.wav')); -var emojiDestroySound = SoundCache.getSound(Script.resolvePath('resources/sounds/emojiPopSound2.wav')); var injector; var DEFAULT_VOLUME = 0.01; var local = false; @@ -326,9 +325,7 @@ function playPopAnimation() { if (popType === "in") { currentPopScale = MIN_POP_SCALE; } else { - // Start with the pop sound on the out currentPopScale = finalInPopScale ? finalInPopScale : MAX_POP_SCALE; - playSound(emojiDestroySound, DEFAULT_VOLUME, MyAvatar.position, true); } } diff --git a/scripts/simplifiedUI/simplifiedEmote/simplifiedEmote.js b/scripts/simplifiedUI/simplifiedEmote/simplifiedEmote.js index f76ef72a33..189608fafd 100644 --- a/scripts/simplifiedUI/simplifiedEmote/simplifiedEmote.js +++ b/scripts/simplifiedUI/simplifiedEmote/simplifiedEmote.js @@ -16,11 +16,13 @@ // ************************************* // #region dependencies + // The information needed to properly use the sprite sheets and get the general information // about the emojis var emojiList = Script.require("./emojiApp/resources/modules/emojiList.js"); var customEmojiList = Script.require("./emojiApp/resources/modules/customEmojiList.js"); + // #endregion // ************************************* // END dependencies @@ -181,6 +183,7 @@ function maybeClearClapSoundInterval() { } } + // URLs for this fn are relative to SimplifiedEmoteIndicator.qml function toggleReaction(reaction) { var reactionEnding = reactionsBegun.indexOf(reaction) > -1; @@ -192,6 +195,7 @@ function toggleReaction(reaction) { } } + function maybeDeleteRemoteIndicatorTimeout() { if (restoreEmoteIndicatorTimeout) { Script.clearTimeout(restoreEmoteIndicatorTimeout); @@ -199,6 +203,7 @@ function maybeDeleteRemoteIndicatorTimeout() { } } + var reactionsBegun = []; var pointReticle = null; var mouseMoveEventsConnected = false; @@ -229,6 +234,7 @@ function beginReactionWrapper(reaction) { } } + // Checks to see if there are any reticle entities already to delete function deleteOldReticles() { MyAvatar.getAvatarEntitiesVariant() @@ -313,6 +319,7 @@ function triggerReactionWrapper(reaction) { }, WAIT_TO_RESTORE_EMOTE_INDICATOR_ICON_MS); } + function maybeClearReticleUpdateLimiterTimeout() { if (reticleUpdateRateLimiterTimer) { Script.clearTimeout(reticleUpdateRateLimiterTimer); @@ -393,6 +400,7 @@ function onMessageFromEmoteAppBar(message) { } } + function getEmojiURLFromCode(code) { var emojiObject = emojiList[emojiCodeMap[code]]; var emojiFilename; @@ -405,6 +413,7 @@ function getEmojiURLFromCode(code) { return "../../emojiApp/resources/images/emojis/52px/" + emojiFilename; } + function updateEmoteIndicatorIcon(iconURL) { emoteAppBarWindow.sendToQml({ "source": "simplifiedEmote.js", @@ -451,7 +460,10 @@ function keyPressHandler(event) { } else if (event.text === RAISE_HAND_KEY) { toggleReaction("raiseHand"); } else if (event.text === APPLAUD_KEY) { - toggleReaction("applaud"); + // Make sure this doesn't get triggered if you are flying, falling, or jumping + if (!MyAvatar.isInAir()) { + toggleReaction("applaud"); + } } else if (event.text === POINT_KEY) { toggleReaction("point"); } else if (event.text === EMOTE_WINDOW && !(Settings.getValue("io.highfidelity.isEditing", false))) { @@ -639,6 +651,7 @@ function unload() { // ************************************* // #region EMOJI_UTILITY + var EMOJI_52_BASE_URL = "../../resources/images/emojis/52px/"; function selectedEmoji(code) { emojiAPI.addEmoji(code); @@ -744,6 +757,7 @@ function toggleEmojiApp() { emojiAPI.registerAvimojiQMLWindow(emojiAppWindow); } + // #endregion // ************************************* // END EMOJI_MAIN diff --git a/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml b/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml index 787ccadd62..bbd1d4d735 100644 --- a/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml +++ b/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml @@ -45,7 +45,8 @@ Rectangle { } Behavior on requestedWidth { - enabled: true + enabled: false // Set this to `true` once we have a different windowing system that better supports on-screen widgets + // like the Emote Indicator. SmoothedAnimation { duration: 220 } } diff --git a/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css b/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css index a33cff15e4..2386f88586 100644 --- a/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css +++ b/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css @@ -23,7 +23,7 @@ ********************************************************************/ * { - box-sizing: border-box + box-sizing: border-box; } html @@ -38,11 +38,12 @@ body font-weight: 400; color: #000000; letter-spacing: 0.5px; + font-size: 0.95rem; + line-height: 20px; } p { font-size: 0.95rem; - line-height: 20px; } section @@ -128,7 +129,6 @@ table { thead { border-color: #d8e1d9; background:#d8e1d9; - text-align: left; } table tr { @@ -146,6 +146,7 @@ td { article table thead tr th, article table tbody tr td, article table tbody tr td p { font-size: .89rem; line-height: 20px; + text-align: left; } article table thead tr th, article table tbody tr td { @@ -199,6 +200,7 @@ nav { padding-left: 20px; padding-right: 20px; box-sizing: border-box; + line-height: 12px; } nav::-webkit-scrollbar { @@ -378,12 +380,12 @@ nav > h2 > a { tt, code, kbd, samp { font-family: Consolas, Monaco, 'Andale Mono', monospace; - font-size: 0.9rem; + font-size: 1.05em; } .name, .signature { font-family: Consolas, Monaco, 'Andale Mono', monospace; - font-size: 0.9rem; + font-size: 1.05em; } img { @@ -422,7 +424,6 @@ header { display: block; text-align: center; font-size: 90%; - margin-top: -20px; } .variation { @@ -537,7 +538,7 @@ header { .prettyprint code { - font-size: 0.7rem; + font-size: 0.9em; line-height: 18px; display: block; padding: 4px 12px; diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl index 2a5e863d6d..5c149fa434 100644 --- a/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl @@ -92,35 +92,33 @@ -

Example 1? 's':'' ?>

- - +

Description

- -

Classes

- -

- - - -

Example 1? 's':'' ?>

+ +

Classes

+ +

+ + + + diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index 67dafe5a16..07549530ce 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -125,7 +125,7 @@ exports.handlers = { if (rows.length > 0) { var availableIn = "

Supported Script Types: " + rows.join(" • ") + "

"; - e.doclet.description = (e.doclet.description ? e.doclet.description : "") + availableIn; + e.doclet.description = availableIn + (e.doclet.description ? e.doclet.description : ""); } }