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/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/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/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/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/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/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/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/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/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 + } + } +}