From ce9d5dfc648758d2f952a5ec375f6b479b9e2f61 Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Thu, 23 Feb 2023 23:45:36 +0100 Subject: [PATCH 1/7] Add more Doxygen docs Doesn't work right in ScriptManager, looks like a conflict with JSDoc --- libraries/script-engine/src/ScriptEngine.h | 114 ++++++++++++++++-- libraries/script-engine/src/ScriptManager.h | 104 ++++++++++++++-- libraries/script-engine/src/ScriptProgram.h | 72 ++++++++++- .../src/v8/ScriptProgramV8Wrapper.h | 3 + tests/script-engine/src/ScriptEngineTests.cpp | 2 + 5 files changed, 265 insertions(+), 30 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 81f4d340a7..e110befc69 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -5,6 +5,7 @@ // Created by Brad Hefta-Gaub on 12/14/13. // Copyright 2013 High Fidelity, Inc. // Copyright 2020 Vircadia contributors. +// Copyright 2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -44,48 +45,126 @@ template inline T scriptvalue_cast(const ScriptValue& value); -/// [ScriptInterface] Provides an engine-independent interface for QScriptEngine +/** + * @brief Provides an engine-independent interface for a scripting engine + * + * Each script engine is strictly single threaded. + * + * This class only provides an interface to the underlying scripting engine, and doesn't + * provide the full environment needed to execute scripts. + * + * To execute scripts that have access to the API, use ScriptManager. + */ class ScriptEngine { public: typedef ScriptValue (*FunctionSignature)(ScriptContext*, ScriptEngine*); typedef ScriptValue (*MarshalFunction)(ScriptEngine*, const void*); typedef bool (*DemarshalFunction)(const ScriptValue&, QVariant &dest); + /** + * @brief Who owns a given object + * + */ enum ValueOwnership { - QtOwnership = 0, - ScriptOwnership = 1, - AutoOwnership = 2, + QtOwnership = 0, /** Object is managed by Qt */ + ScriptOwnership = 1, /** Object is managed by the script */ + AutoOwnership = 2, /** Ownership is determined automatically */ }; + /** + * @brief Which part of an object is exposed to the script + * + */ enum QObjectWrapOption { - //ExcludeChildObjects = 0x0001, // The script object will not expose child objects as properties. - ExcludeSuperClassMethods = 0x0002, // The script object will not expose signals and slots inherited from the superclass. - ExcludeSuperClassProperties = 0x0004, // The script object will not expose properties inherited from the superclass. + //ExcludeChildObjects = 0x0001, /** The script object will not expose child objects as properties. */ + ExcludeSuperClassMethods = 0x0002, /** The script object will not expose signals and slots inherited from the superclass. */ + ExcludeSuperClassProperties = 0x0004, /** The script object will not expose properties inherited from the superclass. */ ExcludeSuperClassContents = ExcludeSuperClassMethods | ExcludeSuperClassProperties, - //ExcludeDeleteLater = 0x0010, // The script object will not expose the QObject::deleteLater() slot. - ExcludeSlots = 0x0020, // The script object will not expose the QObject's slots. - AutoCreateDynamicProperties = 0x0100, // Properties that don't already exist in the QObject will be created as dynamic properties of that object, rather than as properties of the script object. - PreferExistingWrapperObject = 0x0200, // If a wrapper object with the requested configuration already exists, return that object. - SkipMethodsInEnumeration = 0x0008, // Don't include methods (signals and slots) when enumerating the object's properties. + //ExcludeDeleteLater = 0x0010, /** The script object will not expose the QObject::deleteLater() slot. */ + ExcludeSlots = 0x0020, /** The script object will not expose the QObject's slots. */ + AutoCreateDynamicProperties = 0x0100, /** Properties that don't already exist in the QObject will be created as dynamic properties of that object, rather than as properties of the script object. */ + PreferExistingWrapperObject = 0x0200, /** If a wrapper object with the requested configuration already exists, return that object. */ + SkipMethodsInEnumeration = 0x0008, /** Don't include methods (signals and slots) when enumerating the object's properties. */ }; Q_DECLARE_FLAGS(QObjectWrapOptions, QObjectWrapOption); public: + /** + * @brief Stops the currently running script + * + */ virtual void abortEvaluation() = 0; + + /** + * @brief Clears uncaughtException and related + * + */ virtual void clearExceptions() = 0; + virtual ScriptValue cloneUncaughtException(const QString& detail = QString()) = 0; + + /** + * @brief Context of the currently running script + * + * This allows getting a backtrace, the local variables of the currently running function, etc. + * + * @return ScriptContext* + */ virtual ScriptContext* currentContext() const = 0; + + /** + * @brief Runs a script + * + * This may be called several times during the lifetime of a scripting engine, with the + * side effects accumulating. + * + * @param program Code to run + * @param fileName Name of the script, for informational purposes + * @return ScriptValue Return value of the script when it finishes running. + */ virtual ScriptValue evaluate(const QString& program, const QString& fileName = QString()) = 0; + + /** + * @brief Evaluates a pre-compiled program + * + * @param program Program to evaluaate + * @return ScriptValue + */ virtual ScriptValue evaluate(const ScriptProgramPointer &program) = 0; + + + /** + * @brief Evaluate a script in a separate environment + * + * Used for evaluating included scripts + * + * @param locals Local variables available to the script + * @param program Code to run + * @return ScriptValue + */ virtual ScriptValue evaluateInClosure(const ScriptValue& locals, const ScriptProgramPointer& program) = 0; + + /** + * @brief Global namespace, containing all the public APIs + * + * @return ScriptValue + */ virtual ScriptValue globalObject() { Q_ASSERT(false); return ScriptValue(); } virtual bool hasUncaughtException() const = 0; + + /** + * @brief Whether a script is currently being evaluated + * + * @return true A script is currently being evaluated + * @return false No script is being evaluated + */ virtual bool isEvaluating() const = 0; //virtual ScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1) = 0; - virtual ScriptValue checkScriptSyntax(ScriptProgramPointer program) = 0; + + virtual ScriptValue checkScriptSyntax(ScriptProgramPointer program) = 0; virtual ScriptValue makeError(const ScriptValue& other = ScriptValue(), const QString& type = "Error") = 0; virtual ScriptManager* manager() const = 0; virtual bool maybeEmitUncaughtException(const QString& debugHint = QString()) = 0; @@ -107,6 +186,15 @@ public: virtual ScriptValue newValue(const char* value) = 0; virtual ScriptValue newVariant(const QVariant& value) = 0; virtual ScriptValue nullValue() = 0; + + + /** + * @brief Causes an exception to be raised in the currently executing script + * + * @param exception + * @return true + * @return false + */ virtual bool raiseException(const ScriptValue& exception) = 0; virtual void registerEnum(const QString& enumName, QMetaEnum newEnum) = 0; virtual void registerFunction(const QString& name, FunctionSignature fun, int numArguments = -1) = 0; diff --git a/libraries/script-engine/src/ScriptManager.h b/libraries/script-engine/src/ScriptManager.h index 08b3842877..a3326c8e1d 100644 --- a/libraries/script-engine/src/ScriptManager.h +++ b/libraries/script-engine/src/ScriptManager.h @@ -112,6 +112,8 @@ public: #define STATIC_SCRIPT_INITIALIZER(init) \ static ScriptManager::StaticInitializerNode static_script_initializer_(init); + + /**jsdoc * The Script API provides facilities for working with scripts. * @@ -144,6 +146,21 @@ public: * Read-only. * @property {Script.ResourceBuckets} ExternalPaths - External resource buckets. */ + +/** + * @brief Manages a single scripting engine + * + * This class manages and sets up a single scripting engine to make it execute scripts. + * + * It passes the objects needed to expose the public API, provides console access and error + * reporting and event management. + * + * This manipulates a single underlying instance of ScriptEngine. + * + * TODO: Split this class apart, create a ScriptManagerScriptingInterface to handle the JS API + * side separately. + * + */ class ScriptManager : public QObject, public EntitiesScriptEngineProvider, public std::enable_shared_from_this { Q_OBJECT Q_PROPERTY(QString context READ getContext) @@ -153,6 +170,10 @@ public: static const QString SCRIPT_EXCEPTION_FORMAT; static const QString SCRIPT_BACKTRACE_SEP; + /** + * @brief Context of the script + * + */ enum Context { CLIENT_SCRIPT, ENTITY_CLIENT_SCRIPT, @@ -190,16 +211,34 @@ public: inline StaticTypesInitializerNode(ScriptManagerInitializer&& pInit) : init(std::move(pInit)),prev(nullptr) { registerNewStaticTypesInitializer(this); } }; static void registerNewStaticTypesInitializer(StaticTypesInitializerNode* dest); - /// run the script in a dedicated thread. This will have the side effect of evalulating - /// the current script contents and calling run(). Callers will likely want to register the script with external - /// services before calling this. + + /** + * @brief Run the script in a dedicated thread + * + * This will have the side effect of evaluating the current script contents and calling run(). + * Callers will likely want to register the script with external services before calling this. + */ void runInThread(); - /// run the script in the callers thread, exit when stop() is called. + /** + * @brief Run the script in the caller's thread, exit when stop() is called. + * + */ void run(); + + /** + * @brief Get the filename of the running script + * + * @return QString Filename + */ QString getFilename() const; + /** + * @brief Underlying scripting engine + * + * @return ScriptEnginePointer Scripting engine + */ inline ScriptEnginePointer engine() { return _engine; } QList getListOfEntityScriptIDs(); @@ -225,16 +264,29 @@ public: // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts Q_INVOKABLE void stop(bool marshal = false); - // Stop any evaluating scripts and wait for the scripting thread to finish. + /** + * @brief Stop any evaluating scripts and wait for the scripting thread to finish. + * + * @param shutdown True if we are currently shutting down + */ void waitTillDoneRunning(bool shutdown = false); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // NOTE - these are NOT intended to be public interfaces available to scripts, the are only Q_INVOKABLE so we can // properly ensure they are only called on the correct thread - /// if the script engine is not already running, this will download the URL and start the process of seting it up - /// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed - /// to scripts. we may not need this to be invokable + + /** + * @brief Load a script from a given URL + * + * If the script engine is not already running, this will download the URL and start the process of seting it up + * to run. + * @note This is used by Application currently to load the url. We don't really want it to be exposed + * to scripts. we may not need this to be invokable + * + * @param scriptURL URL where to load the script from + * @param reload + */ void loadURL(const QUrl& scriptURL, bool reload); bool hasValidScriptSuffix(const QString& scriptFileName); @@ -648,9 +700,26 @@ public: void disconnectNonEssentialSignals(); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // These are currently used by Application to track if a script is user loaded or not. Consider finding a solution - // inside of Application so that the ScriptManager class is not polluted by this notion + + /** + * @brief Set whether this script was user-loaded + * + * This is used by Application to track if a script is user loaded or not. + * @note Consider finding a solution inside of Application so that the ScriptManager class is not polluted by this notion + * + * @param isUserLoaded Script is user-loaded. + */ void setUserLoaded(bool isUserLoaded) { _isUserLoaded = isUserLoaded; } + + /** + * @brief Whether the script was user-loaded. + * + * This is used by Application to track if a script is user loaded or not. + * @note Consider finding a solution inside of Application so that the ScriptManager class is not polluted by this notion + * + * @return true + * @return false + */ bool isUserLoaded() const { return _isUserLoaded; } void setQuitWhenFinished(const bool quitWhenFinished) { _quitWhenFinished = quitWhenFinished; } @@ -669,10 +738,21 @@ public: void setScriptEngines(QSharedPointer& scriptEngines) { _scriptEngines = scriptEngines; } - // call all the registered event handlers on an entity for the specified name. + + /** + * @brief Call all the registered event handlers on an entity for the specified name. + * + * @param entityID + * @param eventName + * @param eventHanderArgs + */ void forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, const ScriptValueList& eventHanderArgs); - // remove all event handlers for the specified entityID (i.e. the entity is being removed) + /** + * @brief Remove all event handlers for the specified entityID (i.e. the entity is being removed) + * + * @param entityID + */ void removeAllEventHandlers(const EntityItemID& entityID); diff --git a/libraries/script-engine/src/ScriptProgram.h b/libraries/script-engine/src/ScriptProgram.h index 3392595432..2de3f96d6a 100644 --- a/libraries/script-engine/src/ScriptProgram.h +++ b/libraries/script-engine/src/ScriptProgram.h @@ -4,6 +4,7 @@ // // Created by Heather Anderson on 5/2/21. // Copyright 2021 Vircadia contributors. +// Copyright 2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -22,32 +23,93 @@ class ScriptSyntaxCheckResult; using ScriptProgramPointer = std::shared_ptr; using ScriptSyntaxCheckResultPointer = std::shared_ptr; -/// [ScriptInterface] Provides an engine-independent interface for QScriptProgram +/** + * @brief Engine-independent representation of a script program + * + * This is an analog of QScriptProgram from Qt5. + * + * It's used to pre-compile scripts, and to check their syntax. + * + */ class ScriptProgram { public: virtual ScriptSyntaxCheckResultPointer checkSyntax() = 0; //It cannot be const anymore because V8 doesn't have separate syntax checking function + + /** + * @brief Returns the filename associated with this program. + * + * @return QString + */ virtual QString fileName() const = 0; + + /** + * @brief Returns the source code of this program. + * + * @return QString + */ virtual QString sourceCode() const = 0; protected: ~ScriptProgram() {} // prevent explicit deletion of base class }; -/// [ScriptInterface] Provides an engine-independent interface for QScriptSyntaxCheckResult + +/** + * @brief Engine-independent representation of a script syntax check + * + * This is an analog of QScriptSyntaxCheckResult from Qt5. + * + * + */ class ScriptSyntaxCheckResult { public: + + /** + * @brief State of the syntax check + * + */ enum State { - Error = 0, - Intermediate = 1, - Valid = 2 + Error = 0, /** The program contains a syntax error. */ + Intermediate = 1, /** The program is incomplete. */ + Valid = 2 /** The program is a syntactically correct program. */ }; public: + + /** + * @brief Returns the error column number of this ScriptSyntaxCheckResult, or -1 if there is no error. + * + * @return int + */ virtual int errorColumnNumber() const = 0; + + /** + * @brief Returns the error line number of this ScriptSyntaxCheckResult, or -1 if there is no error. + * + * @return int + */ virtual int errorLineNumber() const = 0; + + /** + * @brief Returns the error message of this ScriptSyntaxCheckResult, or an empty string if there is no error. + * + * @return QString + */ virtual QString errorMessage() const = 0; + + /** + * @brief + * + * @return QString + */ virtual QString errorBacktrace() const = 0; + + /** + * @brief Returns the state of this ScriptSyntaxCheckResult. + * + * @return State + */ virtual State state() const = 0; protected: diff --git a/libraries/script-engine/src/v8/ScriptProgramV8Wrapper.h b/libraries/script-engine/src/v8/ScriptProgramV8Wrapper.h index c61b83f231..324655bbb4 100644 --- a/libraries/script-engine/src/v8/ScriptProgramV8Wrapper.h +++ b/libraries/script-engine/src/v8/ScriptProgramV8Wrapper.h @@ -23,6 +23,9 @@ #include "ScriptEngineV8.h" #include "V8Types.h" + +// V8TODO: This class is likely unnecessary, and it'd be enough +// to just use a non-abstract version of ScriptSyntaxCheckResult instead. class ScriptSyntaxCheckResultV8Wrapper final : public ScriptSyntaxCheckResult { public: // construction inline ScriptSyntaxCheckResultV8Wrapper() : _errorColumnNumber(0), _errorLineNumber(0), _errorMessage("Not compiled"), _state(ScriptSyntaxCheckResult::Error) {} diff --git a/tests/script-engine/src/ScriptEngineTests.cpp b/tests/script-engine/src/ScriptEngineTests.cpp index a06a67be99..c0836499d1 100644 --- a/tests/script-engine/src/ScriptEngineTests.cpp +++ b/tests/script-engine/src/ScriptEngineTests.cpp @@ -102,4 +102,6 @@ void ScriptEngineTests::scriptTest() { //ac->shutdownScripting(); + + //TODO: Add a test for Script.require(JSON) } From 8f67725219fd37f2caa8ddad7cf986fbdea33037 Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Sat, 25 Feb 2023 16:55:58 +0100 Subject: [PATCH 2/7] Some minor improvements to Doxygen generation Enable graphs, use SVG --- tools/doxygen/Doxyfile | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tools/doxygen/Doxyfile b/tools/doxygen/Doxyfile index 90ddb030bb..21cf125e9d 100644 --- a/tools/doxygen/Doxyfile +++ b/tools/doxygen/Doxyfile @@ -973,7 +973,12 @@ EXCLUDE_SYMLINKS = NO # Note that the wildcards are matched against the file with absolute path, so to # exclude all test directories for example use the pattern */test/* -EXCLUDE_PATTERNS = + +# The ScriptingInterface files are a JS interface, and documented in JSDoc +# instead of Doxygen. + +EXCLUDE_PATTERNS = *ScriptingInterface.h \ + *ScriptingInterface.cpp # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names # (namespaces, classes, functions, etc.) that should be excluded from the @@ -2354,7 +2359,7 @@ DIA_PATH = # and usage relations if the target is undocumented or is not a class. # The default value is: YES. -HIDE_UNDOC_RELATIONS = YES +HIDE_UNDOC_RELATIONS = NO # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz (see: @@ -2363,7 +2368,7 @@ HIDE_UNDOC_RELATIONS = YES # set to NO # The default value is: NO. -HAVE_DOT = NO +HAVE_DOT = YES # The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed # to run in parallel. When set to 0 doxygen will base this on the number of @@ -2545,7 +2550,7 @@ DIRECTORY_GRAPH = YES # The default value is: png. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_IMAGE_FORMAT = png +DOT_IMAGE_FORMAT = svg # If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to # enable generation of interactive SVG images that allow zooming and panning. @@ -2557,7 +2562,7 @@ DOT_IMAGE_FORMAT = png # The default value is: NO. # This tag requires that the tag HAVE_DOT is set to YES. -INTERACTIVE_SVG = NO +INTERACTIVE_SVG = YES # The DOT_PATH tag can be used to specify the path where the dot tool can be # found. If left blank, it is assumed the dot tool can be found in the path. From 551633a7d6886e80ca730ead773c812f5cc0078c Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Sat, 25 Feb 2023 16:56:48 +0100 Subject: [PATCH 3/7] Create ScriptManagerScriptingInterface to move off the scripting API into its own class Add a lot of documentation --- libraries/script-engine/src/ScriptManager.cpp | 33 +- libraries/script-engine/src/ScriptManager.h | 1370 +++++++++++------ .../src/ScriptManagerScriptingInterface.h | 717 +++++++++ 3 files changed, 1591 insertions(+), 529 deletions(-) create mode 100644 libraries/script-engine/src/ScriptManagerScriptingInterface.h diff --git a/libraries/script-engine/src/ScriptManager.cpp b/libraries/script-engine/src/ScriptManager.cpp index 23fdc96692..2feb6e9dd2 100644 --- a/libraries/script-engine/src/ScriptManager.cpp +++ b/libraries/script-engine/src/ScriptManager.cpp @@ -56,6 +56,7 @@ #include "ScriptProgram.h" #include "ScriptValueIterator.h" #include "ScriptValueUtils.h" +#include "ScriptManagerScriptingInterface.h" #include @@ -270,6 +271,8 @@ ScriptManager::ScriptManager(Context context, const QString& scriptContents, con break; } + _scriptingInterface = std::make_shared(this); + if (isEntityServerScript()) { qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1"; processLevelMaxRetries = 1; @@ -683,7 +686,7 @@ void ScriptManager::init() { // NOTE: You do not want to end up creating new instances of singletons here. They will be on the ScriptManager thread // and are likely to be unusable if we "reset" the ScriptManager by creating a new one (on a whole new thread). - scriptEngine->registerGlobalObject("Script", this); + scriptEngine->registerGlobalObject("Script", _scriptingInterface.get()); { // set up Script.require.resolve and Script.require.cache @@ -827,7 +830,7 @@ void ScriptManager::run() { // (because we're a client script) hifi::scripting::setLocalAccessSafeThread(true); } - + //_engine->enterIsolateOnThisThread(); @@ -1878,32 +1881,6 @@ void ScriptManager::loadEntityScript(const EntityItemID& entityID, const QString }, forceRedownload); } -/*@jsdoc - * Triggered when the script starts for a user. See also, {@link Script.entityScriptPreloadFinished}. - *

Note: Can only be connected to via this.preload = function (...) { ... } in the entity script.

- *

Supported Script Types: Client Entity Scripts • Server Entity Scripts

- * @function Entities.preload - * @param {Uuid} entityID - The ID of the entity that the script is running in. - * @returns {Signal} - * @example Get the ID of the entity that a client entity script is running in. - * var entityScript = (function () { - * this.entityID = Uuid.NULL; - * - * this.preload = function (entityID) { - * this.entityID = entityID; - * print("Entity ID: " + this.entityID); - * }; - * }); - * - * var entityID = Entities.addEntity({ - * type: "Box", - * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5 })), - * dimensions: { x: 0.5, y: 0.5, z: 0.5 }, - * color: { red: 255, green: 0, blue: 0 }, - * script: "(" + entityScript + ")", // Could host the script on a Web server instead. - * lifetime: 300 // Delete after 5 minutes. - * }); - */ // The JSDoc is for the callEntityScriptMethod() call in this method. // since all of these operations can be asynch we will always do the actual work in the response handler // for the download diff --git a/libraries/script-engine/src/ScriptManager.h b/libraries/script-engine/src/ScriptManager.h index a3326c8e1d..e23766679e 100644 --- a/libraries/script-engine/src/ScriptManager.h +++ b/libraries/script-engine/src/ScriptManager.h @@ -56,21 +56,49 @@ static const int DEFAULT_ENTITY_PPS_PER_SCRIPT = 900; class ScriptEngine; class ScriptEngines; class ScriptManager; +class ScriptManagerScriptingInterface; + using ScriptEnginePointer = std::shared_ptr; using ScriptManagerPointer = std::shared_ptr; +using ScriptManagerScriptingInterfacePointer = std::shared_ptr; using ScriptValueList = QList; Q_DECLARE_METATYPE(ScriptManagerPointer) const int QTREGISTER_QTimerStar = qRegisterMetaType(); + +/** + * @brief Callback data for addEventHandler + * + */ class CallbackData { public: + /** + * @brief Function to call + * + */ ScriptValue function; + + /** + * @brief Entity ID + * + */ EntityItemID definingEntityIdentifier; + + /** + * @brief Sandbox URL for the script + * + */ QUrl definingSandboxURL; }; + +/** + * @brief DeferredLoadEntity + * @deprecated This appars unused + * + */ class DeferredLoadEntity { public: EntityItemID entityID; @@ -78,12 +106,45 @@ public: //bool forceRedownload; }; +/** + * @brief Entity with available script contents + * + */ struct EntityScriptContentAvailable { + /** + * @brief Entity ID + * + */ EntityItemID entityID; + + /** + * @brief URL to the script, or the actual script if embedded in the URL field + * + */ QString scriptOrURL; + + /** + * @brief Contents of the script + * + */ QString contents; + + /** + * @brief Whether scriptOrURL contains an URL + * + */ bool isURL; + + /** + * @brief Whether the request has been successful + * + */ bool success; + + /** + * @brief Status as text + * + */ QString status; }; @@ -92,16 +153,54 @@ typedef std::unordered_map EntityScr typedef QList CallbackList; typedef QHash RegisteredEventHandlers; + +/** + * @brief Details about an entity script + * + */ class EntityScriptDetails { public: + + /** + * @brief Current status + * + */ EntityScriptStatus status { EntityScriptStatus::PENDING }; - // If status indicates an error, this contains a human-readable string giving more information about the error. + /** + * @brief Error information + * + * If status indicates an error, this contains a human-readable string giving more information about the error. + * + */ QString errorInfo { "" }; + + /** + * @brief The source code of the script + * + */ QString scriptText { "" }; + + /** + * @brief The return value of the script + * + */ ScriptValue scriptObject{ ScriptValue() }; + + /** + * @brief Last modified time of the underlying script file + * + * This is used to determine if the script needs reloading when it changes on disk. + */ int64_t lastModified { 0 }; + + /** + * @brief URL under which the script is allowed to have access + * + * The script is allowed access below this URL (eg, sub-directories), but + * not to the parent context. + */ QUrl definingSandboxURL { QUrl("about:EntityScript") }; }; @@ -114,38 +213,6 @@ public: -/**jsdoc - * The Script API provides facilities for working with scripts. - * - * @namespace Script - * - * @hifi-interface - * @hifi-client-entity - * @hifi-avatar - * @hifi-server-entity - * @hifi-assignment-client - * - * @property {string} context - The context that the script is running in: - *
    - *
  • "client": An Interface or avatar script.
  • - *
  • "entity_client": A client entity script.
  • - *
  • "entity_server": A server entity script.
  • - *
  • "agent": An assignment client script.
  • - *
- * Read-only. - * @property {string} type - The type of script that is running: - *
    - *
  • "client": An Interface script.
  • - *
  • "entity_client": A client entity script.
  • - *
  • "avatar": An avatar script.
  • - *
  • "entity_server": A server entity script.
  • - *
  • "agent": An assignment client script.
  • - *
- * Read-only. - * @property {string} filename - The filename of the script file. - * Read-only. - * @property {Script.ResourceBuckets} ExternalPaths - External resource buckets. - */ /** * @brief Manages a single scripting engine @@ -157,9 +224,12 @@ public: * * This manipulates a single underlying instance of ScriptEngine. * - * TODO: Split this class apart, create a ScriptManagerScriptingInterface to handle the JS API - * side separately. + * Part of this class' functionality exists only to provide helper functions to the scripts that are + * run by the scripting engine, and shouldn't be considered part of the C++ API. Those are the functions + * in the "Script support methods", "Module support methods", "Entity Script methods", and "Scripting signals" sections. * + * The script-facing interface is in ScriptManagerScriptingInterface and documented in JSDoc + * as the Script class. */ class ScriptManager : public QObject, public EntitiesScriptEngineProvider, public std::enable_shared_from_this { Q_OBJECT @@ -175,17 +245,66 @@ public: * */ enum Context { + /** + * @brief Client script. + * Allowed to access local HTML files on UI created from C++ calls. + * + */ CLIENT_SCRIPT, + + /** + * @brief Entity client script + * + */ ENTITY_CLIENT_SCRIPT, + + /** + * @brief Entity server script + * + */ ENTITY_SERVER_SCRIPT, + + /** + * @brief Agent script + * + */ AGENT_SCRIPT }; + /** + * @brief Type of the script + * + */ enum Type { + /** + * @brief Client + * + */ CLIENT, + + /** + * @brief Entity client + * Receives the update event. + */ ENTITY_CLIENT, + + /** + * @brief Entity server + * Receives the update event + * + */ ENTITY_SERVER, + + /** + * @brief Agent script + * + */ AGENT, + + /** + * @brief Avatar script + * + */ AVATAR }; Q_ENUM(Type); @@ -217,6 +336,11 @@ public: * * This will have the side effect of evaluating the current script contents and calling run(). * Callers will likely want to register the script with external services before calling this. + * + * This function will return immediately, and work will continue on the newly created thread. + * + * @note Can't be called twice. + * @note The underlying thread is not accessible. */ void runInThread(); @@ -228,7 +352,7 @@ public: /** - * @brief Get the filename of the running script + * @brief Get the filename of the running script, without the path. * * @return QString Filename */ @@ -245,460 +369,547 @@ public: bool isStopped() const; - /**jsdoc - * Stops and unloads the current script. - *

Warning: If an assignment client script, the script gets restarted after stopping.

- * @function Script.stop - * @param {boolean} [marshal=false] - Marshal. - *

Deprecated: This parameter is deprecated and will be removed.

- * @example Stop a script after 5s. - * Script.setInterval(function () { - * print("Hello"); - * }, 1000); + + /** + * @name Script support functions * - * Script.setTimeout(function () { - * Script.stop(true); - * }, 5000); + * These functions exist to support the scripting API + */ + + ///@{ + + /** + * @brief Stops and unloads the current script. + * + * @note This is part of the public scripting API for Agent scripts and local scripts, but not for EntityScripts + * @param marshal Deprecated */ - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts Q_INVOKABLE void stop(bool marshal = false); + ///@} + /** * @brief Stop any evaluating scripts and wait for the scripting thread to finish. * - * @param shutdown True if we are currently shutting down + * @param shutdown True if we are currently shutting down. Setting this to true will allow + * processing events emitted during the script's shutdown, such as scripts saving settings. + * + * @note This function has an internal timeout, and will forcefully abort the script if it + * takes too long. */ void waitTillDoneRunning(bool shutdown = false); - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // NOTE - these are NOT intended to be public interfaces available to scripts, the are only Q_INVOKABLE so we can - // properly ensure they are only called on the correct thread - - /** * @brief Load a script from a given URL * * If the script engine is not already running, this will download the URL and start the process of seting it up * to run. - * @note This is used by Application currently to load the url. We don't really want it to be exposed - * to scripts. we may not need this to be invokable * - * @param scriptURL URL where to load the script from - * @param reload + * + * @param scriptURL URL where to load the script from. Can be http, https, atp, or file protocol. The file extension + * has to pass hasValidScriptSuffix(). + * @param reload Load the script again even if it's in the cache. + * + * @note For file:// URLs, only URLs under the default scripts location are allowed. + * @see PathUtils::defaultScriptsLocation */ void loadURL(const QUrl& scriptURL, bool reload); + + /** + * @brief Determines whether a script filename has the right suffix + * + * + * @param scriptFileName + * @return true When the script has the right file extension (eg, .js) + * @return false Otherwise + */ bool hasValidScriptSuffix(const QString& scriptFileName); - /**jsdoc - * Gets the context that the script is running in: Interface/avatar, client entity, server entity, or assignment client. - * @function Script.getContext - * @returns {string} The context that the script is running in: - *
    - *
  • "client": An Interface or avatar script.
  • - *
  • "entity_client": A client entity script.
  • - *
  • "entity_server": A server entity script.
  • - *
  • "agent": An assignment client script.
  • - *
+ + /** + * @name Script support methods + * + * These functions exist to support the scripting API + */ + + ///@{ + + /** + * @brief Gets the context that the script is running in: Interface/avatar, client entity, server entity, or assignment client. + * + * @note This is part of the public JS API + * @return QString */ Q_INVOKABLE QString getContext() const; - /**jsdoc - * Checks whether the script is running as an Interface or avatar script. - * @function Script.isClientScript - * @returns {boolean} true if the script is running as an Interface or avatar script, false if it - * isn't. + /** + * @brief Checks whether the script is running as an Interface or avatar script. + * @note This is part of the public JS API + * @return bool */ Q_INVOKABLE bool isClientScript() const { return _context == CLIENT_SCRIPT; } - /**jsdoc - * Checks whether the application was compiled as a debug build. - * @function Script.isDebugMode - * @returns {boolean} true if the application was compiled as a debug build, false if it was - * compiled as a release build. + /** + * @brief Checks whether the application was compiled as a debug build. + * @note This is part of the public JS API + * @return bool */ Q_INVOKABLE bool isDebugMode() const; - /**jsdoc - * Checks whether the script is running as a client entity script. - * @function Script.isEntityClientScript - * @returns {boolean} true if the script is running as a client entity script, false if it isn't. + /** + * @brief Checks whether the script is running as a client entity script. + * + * @return bool */ Q_INVOKABLE bool isEntityClientScript() const { return _context == ENTITY_CLIENT_SCRIPT; } - /**jsdoc - * Checks whether the script is running as a server entity script. - * @function Script.isEntityServerScript - * @returns {boolean} true if the script is running as a server entity script, false if it isn't. + + /** + * @brief Checks whether the script is running as a server entity script. + * + * @return bool */ Q_INVOKABLE bool isEntityServerScript() const { return _context == ENTITY_SERVER_SCRIPT; } - /**jsdoc - * Checks whether the script is running as an assignment client script. - * @function Script.isAgentScript - * @returns {boolean} true if the script is running as an assignment client script, false if it - * isn't. + /** + * @brief Checks whether the script is running as an assignment client script. + * + * @return bool */ Q_INVOKABLE bool isAgentScript() const { return _context == AGENT_SCRIPT; } - /*@jsdoc - * registers a global object by name. - * @function Script.registerValue - * @param {string} valueName - * @param {value} value + /** + * @brief Registers a global object by name. + * + * @param valueName + * @param value */ - /// registers a global object by name Q_INVOKABLE void registerValue(const QString& valueName, ScriptValue value); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // NOTE - these are intended to be public interfaces available to scripts - /**jsdoc - * @function Script.formatExecption - * @param {object} exception - Exception. - * @param {boolean} inludeExtendeDetails - Include extended details. - * @returns {string} String. - * @deprecated This function is deprecated and will be removed. + /** + * @brief Format an exception and return it as a string + * + * @param exception Exception object, containing the exception information. + * @param includeExtendedDetails Include additional troubleshooting information from the "detail" property, if there's one + * @return QString A multi-line string containing the formatted exception */ Q_INVOKABLE QString formatException(const ScriptValue& exception, bool includeExtendedDetails); - /**jsdoc - * Adds a function to the list of functions called when a particular event occurs on a particular entity. - *

See also, the {@link Entities} API.

- * @function Script.addEventHandler - * @param {Uuid} entityID - The ID of the entity. - * @param {Script.EntityEvent} eventName - The name of the event. - * @param {Script~entityEventCallback|Script~pointerEventCallback|Script~collisionEventCallback} handler - The function to - * call when the event occurs on the entity. It can be either the name of a function or an in-line definition. - * @example Report when a mouse press occurs on a particular entity. - * var entityID = Entities.addEntity({ - * type: "Box", - * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5 })), - * dimensions: { x: 0.5, y: 0.5, z: 0.5 }, - * lifetime: 300 // Delete after 5 minutes. - * }); + + + /** + * @brief Adds a function to the list of functions called when a particular event occurs on a particular entity. * - * function reportMousePress(entityID, event) { - * print("Mouse pressed on entity: " + JSON.stringify(event)); - * } + * @param entityID Entity ID + * @param eventName Name of the event + * @param handler Event handler * - * Script.addEventHandler(entityID, "mousePressOnEntity", reportMousePress); + * @note The same handler can be added multiple times. */ Q_INVOKABLE void addEventHandler(const EntityItemID& entityID, const QString& eventName, const ScriptValue& handler); - /**jsdoc - * Removes a function from the list of functions called when an entity event occurs on a particular entity. - *

See also, the {@link Entities} API.

- * @function Script.removeEventHandler - * @param {Uuid} entityID - The ID of the entity. - * @param {Script.EntityEvent} eventName - The name of the entity event. - * @param {function} handler - The name of the function to no longer call when the entity event occurs on the entity. + /** + * @brief Removes a function from the list of functions called when an entity event occurs on a particular entity. + * + * @param entityID Entity ID + * @param eventName Name if the event + * @param handler Event handler */ Q_INVOKABLE void removeEventHandler(const EntityItemID& entityID, const QString& eventName, const ScriptValue& handler); - /**jsdoc - * Starts running another script in Interface, if it isn't already running. The script is not automatically loaded next + + /** + * @brief Starts running another script in Interface, if it isn't already running. The script is not automatically loaded next * time Interface starts. - *

Supported Script Types: Interface Scripts • Avatar Scripts

- *

See also, {@link ScriptDiscoveryService.loadScript}.

- * @function Script.load - * @param {string} filename - The URL of the script to load. This can be relative to the current script's URL. - * @example Load a script from another script. - * // First file: scriptA.js - * print("This is script A"); * - * // Second file: scriptB.js - * print("This is script B"); - * Script.load("scriptA.js"); + * The script is loaded as a stand-alone script. * - * // If you run scriptB.js you should see both scripts in the Running Scripts dialog. - * // And you should see the following output: - * // This is script B - * // This is script A + * @param loadfile File to load + * @warning In practice this seems equivalent to calling loadScript or reloadScript. It reacts to _isReloading in an odd-looking manner. + * Is this function superfluous? */ Q_INVOKABLE void load(const QString& loadfile); - /**jsdoc - * Includes JavaScript from other files in the current script. If a callback is specified, the files are loaded and - * included asynchronously, otherwise they are included synchronously (i.e., script execution blocks while the files are - * included). - * @function Script.include - * @variation 0 - * @param {string[]} filenames - The URLs of the scripts to include. Each can be relative to the current script. - * @param {function} [callback=null] - The function to call back when the scripts have been included. It can be either the - * name of a function or an in-line definition. + /** + * @brief Includes JavaScript from other files in the current script. + * + * @param includeFiles + * @param callback */ Q_INVOKABLE void include(const QStringList& includeFiles, const ScriptValue& callback = ScriptValue()); - /**jsdoc - * Includes JavaScript from another file in the current script. If a callback is specified, the file is loaded and included - * asynchronously, otherwise it is included synchronously (i.e., script execution blocks while the file is included). - * @function Script.include - * @param {string} filename - The URL of the script to include. It can be relative to the current script. - * @param {function} [callback=null] - The function to call back when the script has been included. It can be either the - * name of a function or an in-line definition. - * @example Include a script file asynchronously. - * // First file: scriptA.js - * print("This is script A"); + /** + * @brief Includes JavaScript from another file in the current script. * - * // Second file: scriptB.js - * print("This is script B"); - * Script.include("scriptA.js", function () { - * print("Script A has been included"); - * }); - * - * // If you run scriptB.js you should see only scriptB.js in the running scripts list. - * // And you should see the following output: - * // This is script B - * // This is script A - * // Script A has been included + * @param includeFile + * @param callback */ Q_INVOKABLE void include(const QString& includeFile, const ScriptValue& callback = ScriptValue()); - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // MODULE related methods + ///@} - /**jsdoc - * Provides access to methods or objects provided in an external JavaScript or JSON file. - * See {@link https://docs.overte.org/script/js-tips.html} for further details. - * @function Script.require - * @param {string} module - The module to use. May be a JavaScript file, a JSON file, or the name of a system module such - * as "appUi" (i.e., the "appUi.js" system module JavaScript file). - * @returns {object|array} The value assigned to module.exports in the JavaScript file, or the value defined - * in the JSON file. + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + + /** + * @name Module support methods + * + */ + ///@{ + + /** + * @brief Provides access to methods or objects provided in an external JavaScript or JSON file. + * + * Implements CommonJS/Node.js like require/module support + * + * @param moduleId Module to load + * @return ScriptValue */ Q_INVOKABLE ScriptValue require(const QString& moduleId); - /**jsdoc - * @function Script.resetModuleCache - * @param {boolean} [deleteScriptCache=false] - Delete script cache. - * @deprecated This function is deprecated and will be removed. + /** + * @brief Resets the module cache + * + * @deprecated + * @param deleteScriptCache */ Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false); + /** + * @brief The current parent module from the running JS script + * + * + * @return ScriptValue Module. May be null or empty. + */ ScriptValue currentModule(); + + /** + * @brief Replaces or adds "module" to "parent.children[]" array + * + * This is an internal use function used as a part of the 'require' implementation. + * + * @param module Module to register + * @param parent Parent + * @return true Registration successful + * @return false Registration failed, if the parent isn't a valid module + */ bool registerModuleWithParent(const ScriptValue& module, const ScriptValue& parent); + + /** + * @brief creates a new JS "module" Object with default metadata properties + * + * This imitates what is provided by https://nodejs.org/api/modules.html + * + * @param modulePath File path to the module + * @param parent Parent module + * @return ScriptValue Created module object + */ ScriptValue newModule(const QString& modulePath, const ScriptValue& parent = ScriptValue()); + + /** + * @brief Synchronously fetch a module's source code + * + * The return value is a map containing the following fields: + * + * * "status" -- A string indicating the status of the operation + * * "success" -- A true or false value indicating success or failure + * * "url" -- The URL of the source. May not be present. + * * "contents" -- The contents of the source. May not be present. + * + * @param modulePath Path to the module's source code + * @param forceDownload Force a redownload even if the source is already in the cache + * @return QVariantMap The result of the operation + */ QVariantMap fetchModuleSource(const QString& modulePath, const bool forceDownload = false); + + /** + * @brief Evaluate a pending module object using the fetched source code + * + * @param module Module object + * @param sourceCode Source code to evaluate + * @return ScriptValue The result of evaluating the source code + */ ScriptValue instantiateModule(const ScriptValue& module, const QString& sourceCode); + /** + * @brief Evaluate a program in the underlying scripting engine + * + * This simply calls to ScriptEngine::evaluate() + * + * @param program Source of the program + * @param fileName Filename it was obtained from + * @return ScriptValue Result of the evaluation + */ ScriptValue evaluate(const QString& program, const QString& fileName = QString()); - /**jsdoc - * Calls a function repeatedly, at a set interval. - * @function Script.setInterval - * @param {function} function - The function to call. This can be either the name of a function or an in-line definition. - * @param {number} interval - The interval at which to call the function, in ms. - * @returns {object} A handle to the interval timer. This can be used in {@link Script.clearInterval}. - * @example Print a message every second. - * Script.setInterval(function () { - * print("Interval timer fired"); - * }, 1000); + /** + * @brief Calls a function repeatedly, at a set interval. + * + * @param function + * @param intervalMS + * @return QTimer* */ Q_INVOKABLE QTimer* setInterval(const ScriptValue& function, int intervalMS); - /**jsdoc - * Calls a function once, after a delay. - * @function Script.setTimeout - * @param {function} function - The function to call. This can be either the name of a function or an in-line definition. - * @param {number} timeout - The delay after which to call the function, in ms. - * @returns {object} A handle to the timeout timer. This can be used in {@link Script.clearTimeout}. - * @example Print a message once, after a second. - * Script.setTimeout(function () { - * print("Timeout timer fired"); - * }, 1000); + + /** + * @brief Calls a function once, after a delay. + * + * @param function + * @param timeoutMS + * @return QTimer* */ Q_INVOKABLE QTimer* setTimeout(const ScriptValue& function, int timeoutMS); - /**jsdoc - * Stops an interval timer set by {@link Script.setInterval|setInterval}. - * @function Script.clearInterval - * @param {object} timer - The interval timer to stop. - * @example Stop an interval timer. - * // Print a message every second. - * var timer = Script.setInterval(function () { - * print("Interval timer fired"); - * }, 1000); + /** + * @brief Stops an interval timer * - * // Stop the timer after 10 seconds. - * Script.setTimeout(function () { - * print("Stop interval timer"); - * Script.clearInterval(timer); - * }, 10000); + * @param timer */ Q_INVOKABLE void clearInterval(QTimer* timer) { stopTimer(timer); } - // Overloaded version is needed in case the timer has expired + + /** + * @brief Stops an interval timer + * + * Overloaded version is needed in case the timer has expired + * @param timer + */ Q_INVOKABLE void clearInterval(QVariantMap timer) { ; } - /**jsdoc - * Stops a timeout timer set by {@link Script.setTimeout|setTimeout}. - * @function Script.clearTimeout - * @param {object} timer - The timeout timer to stop. - * @example Stop a timeout timer. - * // Print a message after two seconds. - * var timer = Script.setTimeout(function () { - * print("Timer fired"); - * }, 2000); + /** + * @brief Stops a timeout timer * - * // Uncomment the following line to stop the timer from firing. - * //Script.clearTimeout(timer); + * @param timer */ Q_INVOKABLE void clearTimeout(QTimer* timer) { stopTimer(timer); } - // Overloaded version is needed in case the timer has expired + + /** + * @brief Stops a timeout timer + * Overloaded version is needed in case the timer has expired + * + * @param timer + */ + Q_INVOKABLE void clearTimeout(QVariantMap timer) { ; } - /**jsdoc - * Prints a message to the program log and emits {@link Script.printedMessage}. - *

Alternatively, you can use {@link print} or one of the {@link console} API methods.

- * @function Script.print - * @param {string} message - The message to print. + + /** + * @brief Prints a message to the program log + * + * @param message */ Q_INVOKABLE void print(const QString& message); - /**jsdoc - * Resolves a relative path to an absolute path. The relative path is relative to the script's location. - * @function Script.resolvePath - * @param {string} path - The relative path to resolve. - * @returns {string} The absolute path. - * @example Report the directory and filename of the running script. - * print(Script.resolvePath("")); - * @example Report the directory of the running script. - * print(Script.resolvePath(".")); - * @example Report the path to a file located relative to the running script. - * print(Script.resolvePath("../assets/sounds/hello.wav")); + /** + * @brief Resolves a relative path to an absolute path. The relative path is relative to the script's location. + * + * @param path + * @return QUrl */ Q_INVOKABLE QUrl resolvePath(const QString& path) const; - /**jsdoc - * Gets the path to the resources directory for QML files. - * @function Script.resourcesPath - * @returns {string} The path to the resources directory for QML files. + /** + * @brief Gets the path to the resources directory for QML files. + * + * @return QUrl */ Q_INVOKABLE QUrl resourcesPath() const; - /**jsdoc - * Starts timing a section of code in order to send usage data about it to Vircadia. Shouldn't be used outside of the + /** + * @brief Starts timing a section of code in order to send usage data about it to Vircadia. Shouldn't be used outside of the * standard scripts. - * @function Script.beginProfileRange - * @param {string} label - A name that identifies the section of code. + * @param label */ Q_INVOKABLE void beginProfileRange(const QString& label) const; - /**jsdoc - * Finishes timing a section of code in order to send usage data about it to Vircadia. Shouldn't be used outside of - * the standard scripts. - * @function Script.endProfileRange - * @param {string} label - A name that identifies the section of code. + /** + * @brief Finishes timing a section of code in order to send usage data about it to Vircadia. Shouldn't be used outside of + * the standard scripts + * @param label */ Q_INVOKABLE void endProfileRange(const QString& label) const; + + ///@} + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Entity Script Related methods - /**jsdoc - * Checks whether an entity has an entity script running. - * @function Script.isEntityScriptRunning - * @param {Uuid} entityID - The ID of the entity. - * @returns {boolean} true if the entity has an entity script running, false if it doesn't. + + /** + * @name Entity Script methods + * + */ + + ///@{ + + /** + * @brief Checks whether an entity has an entity script running. + * + * @param entityID + * @return bool */ Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) { QReadLocker locker { &_entityScriptsLock }; auto it = _entityScripts.constFind(entityID); return it != _entityScripts.constEnd() && it->status == EntityScriptStatus::RUNNING; } + + /** + * @brief Clone the details of an entity script + * + * @param entityID Entity ID + * @return QVariant Copy of the details + */ QVariant cloneEntityScriptDetails(const EntityItemID& entityID); + + + /** + * @brief Get the details of a local entity script + * + * Same as cloneEntityScriptDetails, only as a QFuture. + * + * @param entityID Entity ID + * @return QFuture + */ QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) override; - /**jsdoc - * Manually runs the JavaScript garbage collector which reclaims memory by disposing of objects that are no longer - * reachable. - * @function Script.requestGarbageCollection + + /** + * @brief Manually runs the JavaScript garbage collector which reclaims memory by disposing of objects that are no longer reachable + * */ Q_INVOKABLE void requestGarbageCollection(); - /**jsdoc - * Prints out current backtrace to the log. - * @function Script.logBacktrace - * @param {string} title - Title added to the printed out backtrace. + /** + * @brief Prints out current backtrace to the log. + * + * @param title */ Q_INVOKABLE void logBacktrace(const QString &title); - /*@jsdoc - * @function Script.loadEntityScript - * @param {Uuid} entityID - Entity ID. - * @param {string} script - Script. - * @param {boolean} forceRedownload - Force re-download. - * @deprecated This function is deprecated and will be removed. + /** + * @brief Load an entity script + * + * @param entityID + * @param entityScript + * @param forceRedownload */ Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); - /**jsdoc - * @function Script.unloadEntityScript - * @param {Uuid} entityID - Entity ID. - * @param {boolean} [shouldRemoveFromMap=false] - Should remove from map. - * @deprecated This function is deprecated and will be removed. + /** + * @brief Unload an entity script + * + * @param entityID + * @param shouldRemoveFromMap */ Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method - /**jsdoc - * @function Script.unloadAllEntityScripts - * @param {boolean} [blockingCall=false] - Wait for completion if call moved to another thread. - * @deprecated This function is deprecated and will be removed. + + /** + * @brief Unload all entity scripts + * + * @param blockingCall */ Q_INVOKABLE void unloadAllEntityScripts(bool blockingCall = false); - /**jsdoc - * Calls a method in an entity script. - * @function Script.callEntityScriptMethod - * @param {Uuid} entityID - The ID of the entity running the entity script. - * @param {string} methodName - The name of the method to call. - * @param {string[]} [parameters=[]] - The parameters to call the specified method with. - * @param {Uuid} [remoteCallerID=Uuid.NULL] - An ID that identifies the caller. + /** + * @brief Call a method on an entity script + * + * @param entityID + * @param methodName + * @param params + * @param remoteCallerID */ Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList(), const QUuid& remoteCallerID = QUuid()) override; - /**jsdoc - * Calls a method in an entity script. - * @function Script.callEntityScriptMethod - * @param {Uuid} entityID - Entity ID. - * @param {string} methodName - Method name. - * @param {PointerEvent} event - Pointer event. - * @deprecated This function is deprecated and will be removed. + /** + * @brief Call a method on an entity script + * + * @param entityID + * @param methodName + * @param event */ Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const PointerEvent& event); - /**jsdoc - * Calls a method in an entity script. - * @function Script.callEntityScriptMethod - * @param {Uuid} entityID - Entity ID. - * @param {string} methodName - Method name. - * @param {Uuid} otherID - Other entity ID. - * @param {Collision} collision - Collision. - * @deprecated This function is deprecated and will be removed. - */ + + /** + * @brief Call a method on an entity script + * + * @param entityID + * @param methodName + * @param otherID + * @param collision + */ Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision); - /**jsdoc - * @function Script.generateUUID - * @returns {Uuid} A new UUID. - * @deprecated This function is deprecated and will be removed. Use {@link Uuid(0).generate|Uuid.generate} instead. - */ - Q_INVOKABLE QUuid generateUUID() { return QUuid::createUuid(); } + /** + * @brief Set the script type + * + * @param type Type of this script + */ void setType(Type type) { _type = type; }; + + /** + * @brief Returns the script type + * + * @return Type of this script + */ Type getType() { return _type; }; + + /** + * @brief Returns the type of the script as a string + * + * @return QString A string describing the script's type + */ QString getTypeAsString() const; + /** + * @brief Whether the script has finished running + * + * The finished status is set by stop() + * @return true The script has finished + * @return false The script is running + */ bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget + + /** + * @brief Whether the script is running + * + * @return true The script is running + * @return false The script is not running + */ bool isRunning() const { return _isRunning; } // used by ScriptWidget // this is used by code in ScriptEngines.cpp during the "reload all" operation + /** + * @brief Whether this ScriptManager is stopping. Once this is true, it stays true. + * + * @return true We are stopping + * @return false We are not stopping + */ bool isStopping() const { return _isStopping; } + /** + * @brief Disconnect all signals, except essential ones + * + * This disconnects all signals, except the destroyed() and finished() handlers that + * are needed for cleanup. + */ void disconnectNonEssentialSignals(); + + ///@} + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** @@ -722,26 +933,125 @@ public: */ bool isUserLoaded() const { return _isUserLoaded; } + /** + * @brief Set whether to quit when finished + * + * This is used by ScriptEngines + * + * @param quitWhenFinished + */ void setQuitWhenFinished(const bool quitWhenFinished) { _quitWhenFinished = quitWhenFinished; } + + /** + * @brief Whether to quit when finished + * + * This is read by ScriptEngines + * + * @return true + * @return false + */ bool isQuitWhenFinished() const { return _quitWhenFinished; } + /** + * @brief Set a function that determines whether to emit update events + * + * Function func will be called from run() to determine whether update() will be issued. + * The update() event will be called if func() returns true. + * + * @param func Function that determines whether update() events will be issued. + */ void setEmitScriptUpdatesFunction(std::function func) { _emitScriptUpdates = func; } + /** + * @brief Logs a script error message and emits an errorMessage event + * + * Emits errorMessage() + * + * @param message Message to send to the log + */ void scriptErrorMessage(const QString& message); + + /** + * @brief Logs a script warning message and emits an warningMessage event + * + * Emits warningMessage() + * + * @param message Message to send to the log + */ void scriptWarningMessage(const QString& message); + + /** + * @brief Logs a script info message and emits an infoMessage event + * + * Emits infoMessage() + * + * @param message Message to send to the log + */ void scriptInfoMessage(const QString& message); + + /** + * @brief Logs a script printed message and emits an printedMessage event + * + * These are messages scripts provide by calling the print function. + * Emits printedMessage() + * + * @param message Message to send to the log + */ + void scriptPrintedMessage(const QString& message); + + /** + * @brief Clears the debug log window + * + * This only emits clearDebugWindow() + * + */ void clearDebugLogWindow(); + + /** + * @brief Get the number of running entity scripts + * + * @return int Number of scripts with the status EntityScriptStatus::RUNNING + */ int getNumRunningEntityScripts() const; + + /** + * @brief Retrieves the details about an entity script + * + * @param entityID Entity ID + * @param details Returned details + * @return true If the entity ID was found + * @return false If the entity ID wasn't found. details will be unmodified. + */ bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const; + + /** + * @brief Whether there are script details for a given entity ID + * + * @param entityID Entity ID + * @return true There is an entity script for this entity + * @return false There's no entity script + */ bool hasEntityScriptDetails(const EntityItemID& entityID) const; + /** + * @brief Set a shared pointer to the ScriptEngines class + * + * This is used to ask ScriptEngines whether the system is being stopped. + * Setting this is optional. + * + * isStopped() is implemented by asking ScriptEngines. + * + * @param scriptEngines ScriptEngines class + */ void setScriptEngines(QSharedPointer& scriptEngines) { _scriptEngines = scriptEngines; } /** * @brief Call all the registered event handlers on an entity for the specified name. * + * Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those args + * * @param entityID * @param eventName * @param eventHanderArgs @@ -751,7 +1061,7 @@ public: /** * @brief Remove all event handlers for the specified entityID (i.e. the entity is being removed) * - * @param entityID + * @param entityID Entity ID */ void removeAllEventHandlers(const EntityItemID& entityID); @@ -765,223 +1075,190 @@ public: */ ScriptValue getReturnValue() const { return _returnValue; } - /**jsdoc - * Gets the URL for an asset in an external resource bucket. (The location where the bucket is hosted may change over time - * but this method will return the asset's current URL.) - * @function Script.getExternalPath - * @param {Script.ResourceBucket} bucket - The external resource bucket that the asset is in. - * @param {string} path - The path within the external resource bucket where the asset is located. - *

Normally, this should start with a path or filename to be appended to the bucket URL. - * Alternatively, it can be a relative path starting with ./ or ../, to navigate within the - * resource bucket's URL.

- * @Returns {string} The URL of an external asset. - * @example Report the URL of a default particle. - * print(Script.getExternalPath(Script.ExternalPaths.Assets, "Bazaar/Assets/Textures/Defaults/Interface/default_particle.png")); - * @example Report the root directory where the Vircadia assets are located. - * print(Script.getExternalPath(Script.ExternalPaths.Assets, ".")); + + /** + * @brief Gets the URL for an asset in an external resource bucket. + * + * @param bucket + * @param path + * @return Q_INVOKABLE */ Q_INVOKABLE QString getExternalPath(ExternalResource::Bucket bucket, const QString& path); public slots: - /**jsdoc - * @function Script.updateMemoryCost - * @param {number} deltaSize - Delta size. - * @deprecated This function is deprecated and will be removed. + /** + * @brief Script.updateMemoryCost + * + * Sends a memory cost update to the underlying scripting engine + * + * @param deltaSize Difference in memory usage + * @deprecated Deprecated */ - void updateMemoryCost(const qint64&); + void updateMemoryCost(const qint64 &deltaSize); signals: - /**jsdoc - * @function Script.scriptLoaded - * @param {string} filename - File name. - * @returns {Signal} - * @deprecated This signal is deprecated and will be removed. + + /** + * @name Scripting events + * + */ + ///@{ + + /** + * @brief Script.scriptLoaded + * @deprecated + * @param scriptFilename */ void scriptLoaded(const QString& scriptFilename); - /**jsdoc - * @function Script.errorLoadingScript - * @param {string} filename - File name. - * @returns {Signal} - * @deprecated This signal is deprecated and will be removed. + + /** + * @brief Script.errorLoadingScript + * @deprecated + * @param scriptFilename */ void errorLoadingScript(const QString& scriptFilename); - /**jsdoc - * Triggered frequently at a system-determined interval. - * @function Script.update - * @param {number} deltaTime - The time since the last update, in s. - * @returns {Signal} - * @example Report script update intervals. - * Script.update.connect(function (deltaTime) { - * print("Update: " + deltaTime); - * }); + + /** + * @brief Triggered frequently at a system-determined interval. + * + * @param deltaTime */ void update(float deltaTime); - /**jsdoc - * Triggered when the script is stopping. - * @function Script.scriptEnding - * @returns {Signal} - * @example Report when a script is stopping. - * print("Script started"); + + + /** + * @brief Triggered when the script is stopping. * - * Script.scriptEnding.connect(function () { - * print("Script ending"); - * }); - * - * Script.setTimeout(function () { - * print("Stopping script"); - * Script.stop(); - * }, 1000); */ void scriptEnding(); - /**jsdoc - * @function Script.finished - * @param {string} filename - File name. - * @param {object} engine - Engine. - * @returns {Signal} - * @deprecated This signal is deprecated and will be removed. + + + /** + * @brief Script.finished + * + * @param fileNameString */ void finished(const QString& fileNameString, ScriptManagerPointer); - /**jsdoc - * Triggered when the script prints a message to the program log via {@link print}, {@link Script.print}, - * {@link console.log}, {@link console.debug}, {@link console.group}, {@link console.groupEnd}, {@link console.time}, or - * {@link console.timeEnd}. - * @function Script.printedMessage - * @param {string} message - The message. - * @param {string} scriptName - The name of the script that generated the message. - * @returns {Signal} + /** + * @brief Triggered when the script prints a message to the program log + * + * @param message + * @param scriptName */ void printedMessage(const QString& message, const QString& scriptName); - /**jsdoc - * Triggered when the script generates an error, {@link console.error} or {@link console.exception} is called, or - * {@link console.assert} is called and fails. - * @function Script.errorMessage - * @param {string} message - The error message. - * @param {string} scriptName - The name of the script that generated the error message. - * @returns {Signal} + + /** + * @brief Triggered when the script generates an error + * + * @param message + * @param scriptName */ void errorMessage(const QString& message, const QString& scriptName); - /**jsdoc - * Triggered when the script generates a warning or {@link console.warn} is called. - * @function Script.warningMessage - * @param {string} message - The warning message. - * @param {string} scriptName - The name of the script that generated the warning message. - * @returns {Signal} + + + /** + * @brief Triggered when the script generates a warning + * + * @param message + * @param scriptName */ void warningMessage(const QString& message, const QString& scriptName); - /**jsdoc - * Triggered when the script generates an information message or {@link console.info} is called. - * @function Script.infoMessage - * @param {string} message - The information message. - * @param {string} scriptName - The name of the script that generated the information message. - * @returns {Signal} + + /** + * @brief Triggered when the script generates an information message + * + * @param message + * @param scriptName */ void infoMessage(const QString& message, const QString& scriptName); - /**jsdoc - * Triggered when the running state of the script changes, e.g., from running to stopping. - * @function Script.runningStateChanged - * @returns {Signal} + + /** + * @brief Triggered when the running state of the script changes, e.g., from running to stopping. + * */ void runningStateChanged(); - /**jsdoc - * @function Script.clearDebugWindow - * @returns {Signal} - * @deprecated This signal is deprecated and will be removed. + + /** + * @brief Script.clearDebugWindow + * @deprecated */ void clearDebugWindow(); - /**jsdoc - * @function Script.loadScript - * @param {string} scriptName - Script name. - * @param {boolean} isUserLoaded - Is user loaded. - * @returns {Signal} - * @deprecated This signal is deprecated and will be removed. + + /** + * @brief Script.loadScript + * @deprecated + * @param scriptName + * @param isUserLoaded */ void loadScript(const QString& scriptName, bool isUserLoaded); - /**jsdoc - * @function Script.reloadScript - * @param {string} scriptName - Script name. - * @param {boolean} isUserLoaded - Is user loaded. - * @returns {Signal} - * @deprecated This signal is deprecated and will be removed. + + /** + * @brief Script.reloadScript + * + * @param scriptName + * @param isUserLoaded */ void reloadScript(const QString& scriptName, bool isUserLoaded); - /**jsdoc - * Triggered when the script has stopped. - * @function Script.doneRunning - * @returns {Signal} + /** + * @brief Triggered when the script has stopped. + * */ void doneRunning(); - /**jsdoc - * @function Script.entityScriptDetailsUpdated - * @returns {Signal} - * @deprecated This signal is deprecated and will be removed. + /** + * @brief Emitted when an entity script is added or removed, or when the status of an entity + * script is updated (goes from RUNNING to ERROR_RUNNING_SCRIPT, for example) + * */ - // Emitted when an entity script is added or removed, or when the status of an entity - // script is updated (goes from RUNNING to ERROR_RUNNING_SCRIPT, for example) void entityScriptDetailsUpdated(); - /**jsdoc - * Triggered when the script starts for the user. See also, {@link Entities.preload}. - *

Supported Script Types: Client Entity Scripts • Server Entity Scripts

- * @function Script.entityScriptPreloadFinished - * @param {Uuid} entityID - The ID of the entity that the script is running in. - * @returns {Signal} - * @example Get the ID of the entity that a client entity script is running in. - * var entityScript = function () { - * this.entityID = Uuid.NULL; - * }; + /** + * @brief Emitted when an entity script has finished running preload * - * Script.entityScriptPreloadFinished.connect(function (entityID) { - * this.entityID = entityID; - * print("Entity ID: " + this.entityID); - * }); - * - * var entityID = Entities.addEntity({ - * type: "Box", - * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5 })), - * dimensions: { x: 0.5, y: 0.5, z: 0.5 }, - * color: { red: 255, green: 0, blue: 0 }, - * script: "(" + entityScript + ")", // Could host the script on a Web server instead. - * lifetime: 300 // Delete after 5 minutes. - * }); + * @param entityID */ - // Emitted when an entity script has finished running preload void entityScriptPreloadFinished(const EntityItemID& entityID); - /**jsdoc - * Triggered when a script generates an unhandled exception. - * @function Script.unhandledException - * @param {object} exception - The details of the exception. - * @returns {Signal} - * @example Report the details of an unhandled exception. - * Script.unhandledException.connect(function (exception) { - * print("Unhandled exception: " + JSON.stringify(exception)); - * }); - * var properties = JSON.parse("{ x: 1"); // Invalid JSON string. + + /** + * @brief Triggered when a script generates an unhandled exception. + * + * @param exception */ void unhandledException(const ScriptValue& exception); - // Triggered once before the first call to Script.addEventHandler happens on this ScriptManager - // connections assumed to use Qt::DirectConnection; not for use by scripts + ///@} + + /** + * @brief Triggered once before the first call to Script.addEventHandler happens on this ScriptManager + * connections assumed to use Qt::DirectConnection + * + * @note not for use by scripts + */ void attachDefaultEventHandlers(); - // Triggered repeatedly in the scripting loop to ensure entity edit messages get processed properly - // connections assumed to use Qt::DirectConnection; not for use by scripts + /** + * @brief Triggered repeatedly in the scripting loop to ensure entity edit messages get processed properly + * connections assumed to use Qt::DirectConnection + * + * @note Not for use by scripts + * @param wait + */ void releaseEntityPacketSenderMessages(bool wait); protected: @@ -990,54 +1267,139 @@ protected: void init(); - /**jsdoc - * @function Script.executeOnScriptThread - * @param {function} function - Function. - * @param {ConnectionType} [type=2] - Connection type. - * @deprecated This function is deprecated and will be removed. + /** + * @brief executeOnScriptThread + * + * @deprecated + * @param function + * @param type */ Q_INVOKABLE void executeOnScriptThread(std::function function, const Qt::ConnectionType& type = Qt::QueuedConnection ); - /**jsdoc - * @function Script._requireResolve - * @param {string} module - Module. - * @param {string} [relativeTo=""] - Relative to. - * @returns {string} Result. - * @deprecated This function is deprecated and will be removed. + /** + * @brief Script._requireResolve + * + * @note this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general; + * then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;" + * + * @deprecated + * @param moduleId + * @param relativeTo + * @return QString */ - // note: this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general; - // then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;" Q_INVOKABLE QString _requireResolve(const QString& moduleId, const QString& relativeTo = QString()); + /** + * @brief Log an exception + * + * This both sends an exception to the log as an error message, and returns the formatted + * text as a string. + * + * @param exception Exception + * @return QString Exception formatted as a string + */ QString logException(const ScriptValue& exception); void timerFired(); void stopAllTimers(); void stopAllTimersForEntityScript(const EntityItemID& entityID); void refreshFileScript(const EntityItemID& entityID); + + /** + * @brief Updates the status of an entity script + * + * Emits entityScriptDetailsUpdated() + * + * @param entityID Entity ID + * @param status Status + * @param errorInfo Description of the error, if any + */ void updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus& status, const QString& errorInfo = QString()); + + + /** + * @brief Set the details for an entity script + * + * @param entityID Entity ID + * @param details Details + */ void setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details); + + /** + * @brief Set the parent URL, used to resolve relative paths + * + * Relative paths are resolved respect of this URL + * + * @param parentURL Parent URL + */ void setParentURL(const QString& parentURL) { _parentURL = parentURL; } + /** + * @brief Creates a timer with the specified interval + * + * @param function Function to call when the interval elapses + * @param intervalMS Interval in milliseconds + * @param isSingleShot Whether the timer happens continuously or a single time + * @return QTimer* + */ QTimer* setupTimerWithInterval(const ScriptValue& function, int intervalMS, bool isSingleShot); + + /** + * @brief Stops a timer + * + * @param timer Timer to stop + */ void stopTimer(QTimer* timer); QHash _registeredHandlers; - /**jsdoc - * @function Script.entityScriptContentAvailable - * @param {Uuid} entityID - Entity ID. - * @param {string} scriptOrURL - Path. - * @param {string} contents - Contents. - * @param {boolean} isURL - Is a URL. - * @param {boolean} success - Success. - * @param {string} status - Status. - * @deprecated This function is deprecated and will be removed. + + /** + * @brief Script.entityScriptContentAvailable + * + * @param entityID + * @param scriptOrURL + * @param contents + * @param isURL + * @param success + * @param status */ Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString& status); EntityItemID currentEntityIdentifier; // Contains the defining entity script entity id during execution, if any. Empty for interface script execution. QUrl currentSandboxURL; // The toplevel url string for the entity script that loaded the code being executed, else empty. + + /** + * @brief Execute operation in the appropriate context for (the possibly empty) entityID. + * Even if entityID is supplied as currentEntityIdentifier, this still documents the source + * of the code being executed (e.g., if we ever sandbox different entity scripts, or provide different + * global values for different entity scripts). + * + * @param entityID Entity ID, may be null + * @param sandboxURL Sandbox URL + * @param operation Operation to call + */ void doWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, std::function operation); + + /** + * @brief Execute operation in the appropriate context for (the possibly empty) entityID. + * + * This is equivalent to doWithEnvironment(), only with separate arguments for the function, this object and arguments. + * + * This is a convenience function, which performs: + * + * @code {.cpp} + * auto operation = [&]() { + * function.call(thisObject, args); + * }; + * doWithEnvironment(entityID, sandboxURL, operation); + * @endcode + * + * @param entityID Entity ID, may be null + * @param sandboxURL Sandbox URL + * @param function Function to call + * @param thisObject "this" object to use for the call + * @param args Arguments + */ void callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, const ScriptValue& function, const ScriptValue& thisObject, const ScriptValueList& args); Context _context; @@ -1085,11 +1447,17 @@ protected: QWeakPointer _scriptEngines; + + // For debugging performance issues int _timerCallCounter{ 0 }; double _totalTimeInTimerEvents_s{ 0.0 }; + ScriptManagerScriptingInterfacePointer _scriptingInterface; + friend ScriptManagerPointer newScriptManager(Context context, const QString& scriptContents, const QString& fileNameString); + friend class ScriptManagerScriptingInterface; + }; ScriptManagerPointer newScriptManager(ScriptManager::Context context, diff --git a/libraries/script-engine/src/ScriptManagerScriptingInterface.h b/libraries/script-engine/src/ScriptManagerScriptingInterface.h new file mode 100644 index 0000000000..1de1b39c54 --- /dev/null +++ b/libraries/script-engine/src/ScriptManagerScriptingInterface.h @@ -0,0 +1,717 @@ +// +// ScriptManagerScriptingInterface.h +// libraries/script-engine/src +// +// Created by Dale Glass on 24/02/2023. +// Copyright 2022 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include +#include "ScriptManager.h" + + +/**jsdoc + * The Script API provides facilities for working with scripts. + * + * @namespace Script + * + * @hifi-interface + * @hifi-client-entity + * @hifi-avatar + * @hifi-server-entity + * @hifi-assignment-client + * + * @property {string} context - The context that the script is running in: + *
    + *
  • "client": An Interface or avatar script.
  • + *
  • "entity_client": A client entity script.
  • + *
  • "entity_server": A server entity script.
  • + *
  • "agent": An assignment client script.
  • + *
+ * Read-only. + * @property {string} type - The type of script that is running: + *
    + *
  • "client": An Interface script.
  • + *
  • "entity_client": A client entity script.
  • + *
  • "avatar": An avatar script.
  • + *
  • "entity_server": A server entity script.
  • + *
  • "agent": An assignment client script.
  • + *
+ * Read-only. + * @property {string} filename - The filename of the script file. + * Read-only. + * @property {Script.ResourceBuckets} ExternalPaths - External resource buckets. + */ + +class ScriptManagerScriptingInterface : public QObject { + Q_OBJECT +public: + ScriptManagerScriptingInterface(ScriptManager *parent) : QObject(parent), _manager(parent) { + + connect(_manager, &ScriptManager::scriptLoaded, this, &ScriptManagerScriptingInterface::scriptLoaded); + connect(_manager, &ScriptManager::errorLoadingScript, this, &ScriptManagerScriptingInterface::errorLoadingScript); + connect(_manager, &ScriptManager::update, this, &ScriptManagerScriptingInterface::update); + connect(_manager, &ScriptManager::scriptEnding, this, &ScriptManagerScriptingInterface::scriptEnding); + connect(_manager, &ScriptManager::finished, this, &ScriptManagerScriptingInterface::finished); + connect(_manager, &ScriptManager::printedMessage, this, &ScriptManagerScriptingInterface::printedMessage); + connect(_manager, &ScriptManager::errorMessage, this, &ScriptManagerScriptingInterface::errorMessage); + connect(_manager, &ScriptManager::warningMessage, this, &ScriptManagerScriptingInterface::warningMessage); + connect(_manager, &ScriptManager::infoMessage, this, &ScriptManagerScriptingInterface::infoMessage); + connect(_manager, &ScriptManager::runningStateChanged, this, &ScriptManagerScriptingInterface::runningStateChanged); + connect(_manager, &ScriptManager::clearDebugWindow, this, &ScriptManagerScriptingInterface::clearDebugWindow); + connect(_manager, &ScriptManager::loadScript, this, &ScriptManagerScriptingInterface::loadScript); + connect(_manager, &ScriptManager::doneRunning, this, &ScriptManagerScriptingInterface::doneRunning); + connect(_manager, &ScriptManager::entityScriptDetailsUpdated, this, &ScriptManagerScriptingInterface::entityScriptDetailsUpdated); + connect(_manager, &ScriptManager::entityScriptPreloadFinished, this, &ScriptManagerScriptingInterface::entityScriptPreloadFinished); + connect(_manager, &ScriptManager::unhandledException, this, &ScriptManagerScriptingInterface::unhandledException); + } + + /**jsdoc + * Stops and unloads the current script. + *

Warning: If an assignment client script, the script gets restarted after stopping.

+ * @function Script.stop + * @param {boolean} [marshal=false] - Marshal. + *

Deprecated: This parameter is deprecated and will be removed.

+ * @example Stop a script after 5s. + * Script.setInterval(function () { + * print("Hello"); + * }, 1000); + * + * Script.setTimeout(function () { + * Script.stop(true); + * }, 5000); + */ + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts + Q_INVOKABLE void stop(bool marshal = false) { _manager->stop(marshal); } + + /**jsdoc + * Gets the context that the script is running in: Interface/avatar, client entity, server entity, or assignment client. + * @function Script.getContext + * @returns {string} The context that the script is running in: + *
    + *
  • "client": An Interface or avatar script.
  • + *
  • "entity_client": A client entity script.
  • + *
  • "entity_server": A server entity script.
  • + *
  • "agent": An assignment client script.
  • + *
+ */ + Q_INVOKABLE QString getContext() const { return _manager->getContext(); } + + + /**jsdoc + * Checks whether the script is running as an Interface or avatar script. + * @function Script.isClientScript + * @returns {boolean} true if the script is running as an Interface or avatar script, false if it + * isn't. + */ + Q_INVOKABLE bool isClientScript() const { return _manager->isClientScript(); } + + /**jsdoc + * Checks whether the application was compiled as a debug build. + * @function Script.isDebugMode + * @returns {boolean} true if the application was compiled as a debug build, false if it was + * compiled as a release build. + */ + Q_INVOKABLE bool isDebugMode() const { return _manager->isDebugMode(); } + + /**jsdoc + * Checks whether the script is running as a client entity script. + * @function Script.isEntityClientScript + * @returns {boolean} true if the script is running as a client entity script, false if it isn't. + */ + Q_INVOKABLE bool isEntityClientScript() const { return _manager->isEntityClientScript(); } + + /**jsdoc + * Checks whether the script is running as a server entity script. + * @function Script.isEntityServerScript + * @returns {boolean} true if the script is running as a server entity script, false if it isn't. + */ + Q_INVOKABLE bool isEntityServerScript() const { return _manager->isEntityServerScript(); } + + /**jsdoc + * Checks whether the script is running as an assignment client script. + * @function Script.isAgentScript + * @returns {boolean} true if the script is running as an assignment client script, false if it + * isn't. + */ + Q_INVOKABLE bool isAgentScript() const { return _manager->isAgentScript(); } + + /*@jsdoc + * registers a global object by name. + * @function Script.registerValue + * @param {string} valueName + * @param {value} value + */ + /// registers a global object by name + Q_INVOKABLE void registerValue(const QString& valueName, ScriptValue value) { _manager->registerValue(valueName, value); } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // NOTE - these are intended to be public interfaces available to scripts + + /**jsdoc + * @function Script.formatExecption + * @param {object} exception - Exception. + * @param {boolean} inludeExtendeDetails - Include extended details. + * @returns {string} String. + * @deprecated This function is deprecated and will be removed. + */ + Q_INVOKABLE QString formatException(const ScriptValue& exception, bool includeExtendedDetails) { return _manager->formatException(exception, includeExtendedDetails); } + + /**jsdoc + * Adds a function to the list of functions called when a particular event occurs on a particular entity. + *

See also, the {@link Entities} API.

+ * @function Script.addEventHandler + * @param {Uuid} entityID - The ID of the entity. + * @param {Script.EntityEvent} eventName - The name of the event. + * @param {Script~entityEventCallback|Script~pointerEventCallback|Script~collisionEventCallback} handler - The function to + * call when the event occurs on the entity. It can be either the name of a function or an in-line definition. + * @example Report when a mouse press occurs on a particular entity. + * var entityID = Entities.addEntity({ + * type: "Box", + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5 })), + * dimensions: { x: 0.5, y: 0.5, z: 0.5 }, + * lifetime: 300 // Delete after 5 minutes. + * }); + * + * function reportMousePress(entityID, event) { + * print("Mouse pressed on entity: " + JSON.stringify(event)); + * } + * + * Script.addEventHandler(entityID, "mousePressOnEntity", reportMousePress); + */ + Q_INVOKABLE void addEventHandler(const EntityItemID& entityID, const QString& eventName, const ScriptValue& handler) { _manager->addEventHandler(entityID, eventName, handler); } + + /**jsdoc + * Removes a function from the list of functions called when an entity event occurs on a particular entity. + *

See also, the {@link Entities} API.

+ * @function Script.removeEventHandler + * @param {Uuid} entityID - The ID of the entity. + * @param {Script.EntityEvent} eventName - The name of the entity event. + * @param {function} handler - The name of the function to no longer call when the entity event occurs on the entity. + */ + Q_INVOKABLE void removeEventHandler(const EntityItemID& entityID, const QString& eventName, const ScriptValue& handler) { _manager->removeEventHandler(entityID, eventName, handler); } + + /**jsdoc + * Starts running another script in Interface, if it isn't already running. The script is not automatically loaded next + * time Interface starts. + *

Supported Script Types: Interface Scripts • Avatar Scripts

+ *

See also, {@link ScriptDiscoveryService.loadScript}.

+ * @function Script.load + * @param {string} filename - The URL of the script to load. This can be relative to the current script's URL. + * @example Load a script from another script. + * // First file: scriptA.js + * print("This is script A"); + * + * // Second file: scriptB.js + * print("This is script B"); + * Script.load("scriptA.js"); + * + * // If you run scriptB.js you should see both scripts in the Running Scripts dialog. + * // And you should see the following output: + * // This is script B + * // This is script A + */ + Q_INVOKABLE void load(const QString& loadfile) { _manager->load(loadfile); } + + /**jsdoc + * Includes JavaScript from other files in the current script. If a callback is specified, the files are loaded and + * included asynchronously, otherwise they are included synchronously (i.e., script execution blocks while the files are + * included). + * @function Script.include + * @variation 0 + * @param {string[]} filenames - The URLs of the scripts to include. Each can be relative to the current script. + * @param {function} [callback=null] - The function to call back when the scripts have been included. It can be either the + * name of a function or an in-line definition. + */ + Q_INVOKABLE void include(const QStringList& includeFiles, const ScriptValue& callback = ScriptValue()) { _manager->include(includeFiles, callback);}; + + /**jsdoc + * Includes JavaScript from another file in the current script. If a callback is specified, the file is loaded and included + * asynchronously, otherwise it is included synchronously (i.e., script execution blocks while the file is included). + * @function Script.include + * @param {string} filename - The URL of the script to include. It can be relative to the current script. + * @param {function} [callback=null] - The function to call back when the script has been included. It can be either the + * name of a function or an in-line definition. + * @example Include a script file asynchronously. + * // First file: scriptA.js + * print("This is script A"); + * + * // Second file: scriptB.js + * print("This is script B"); + * Script.include("scriptA.js", function () { + * print("Script A has been included"); + * }); + * + * // If you run scriptB.js you should see only scriptB.js in the running scripts list. + * // And you should see the following output: + * // This is script B + * // This is script A + * // Script A has been included + */ + Q_INVOKABLE void include(const QString& includeFile, const ScriptValue& callback = ScriptValue()) { _manager->include( includeFile, callback); }; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // MODULE related methods + + /**jsdoc + * Provides access to methods or objects provided in an external JavaScript or JSON file. + * See {@link https://docs.overte.org/script/js-tips.html} for further details. + * @function Script.require + * @param {string} module - The module to use. May be a JavaScript file, a JSON file, or the name of a system module such + * as "appUi" (i.e., the "appUi.js" system module JavaScript file). + * @returns {object|array} The value assigned to module.exports in the JavaScript file, or the value defined + * in the JSON file. + */ + Q_INVOKABLE ScriptValue require(const QString& moduleId) { return _manager->require(moduleId); } + + /**jsdoc + * @function Script.resetModuleCache + * @param {boolean} [deleteScriptCache=false] - Delete script cache. + * @deprecated This function is deprecated and will be removed. + */ + Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false) { _manager->resetModuleCache(deleteScriptCache);} + + /**jsdoc + * Calls a function repeatedly, at a set interval. + * @function Script.setInterval + * @param {function} function - The function to call. This can be either the name of a function or an in-line definition. + * @param {number} interval - The interval at which to call the function, in ms. + * @returns {object} A handle to the interval timer. This can be used in {@link Script.clearInterval}. + * @example Print a message every second. + * Script.setInterval(function () { + * print("Interval timer fired"); + * }, 1000); + */ + Q_INVOKABLE QTimer* setInterval(const ScriptValue& function, int intervalMS) { return _manager->setInterval(function, intervalMS); } + + /**jsdoc + * Calls a function once, after a delay. + * @function Script.setTimeout + * @param {function} function - The function to call. This can be either the name of a function or an in-line definition. + * @param {number} timeout - The delay after which to call the function, in ms. + * @returns {object} A handle to the timeout timer. This can be used in {@link Script.clearTimeout}. + * @example Print a message once, after a second. + * Script.setTimeout(function () { + * print("Timeout timer fired"); + * }, 1000); + */ + Q_INVOKABLE QTimer* setTimeout(const ScriptValue& function, int timeoutMS) { return _manager->setTimeout(function, timeoutMS); }; + + /**jsdoc + * Stops an interval timer set by {@link Script.setInterval|setInterval}. + * @function Script.clearInterval + * @param {object} timer - The interval timer to stop. + * @example Stop an interval timer. + * // Print a message every second. + * var timer = Script.setInterval(function () { + * print("Interval timer fired"); + * }, 1000); + * + * // Stop the timer after 10 seconds. + * Script.setTimeout(function () { + * print("Stop interval timer"); + * Script.clearInterval(timer); + * }, 10000); + */ + Q_INVOKABLE void clearInterval(QTimer* timer) { _manager->clearInterval(timer); } + + // Overloaded version is needed in case the timer has expired + Q_INVOKABLE void clearInterval(QVariantMap timer) { ; } + + /**jsdoc + * Stops a timeout timer set by {@link Script.setTimeout|setTimeout}. + * @function Script.clearTimeout + * @param {object} timer - The timeout timer to stop. + * @example Stop a timeout timer. + * // Print a message after two seconds. + * var timer = Script.setTimeout(function () { + * print("Timer fired"); + * }, 2000); + * + * // Uncomment the following line to stop the timer from firing. + * //Script.clearTimeout(timer); + */ + Q_INVOKABLE void clearTimeout(QTimer* timer) { _manager->clearTimeout(timer); } + + // Overloaded version is needed in case the timer has expired + Q_INVOKABLE void clearTimeout(QVariantMap timer) { ; } + + /**jsdoc + * Prints a message to the program log and emits {@link Script.printedMessage}. + *

Alternatively, you can use {@link print} or one of the {@link console} API methods.

+ * @function Script.print + * @param {string} message - The message to print. + */ + Q_INVOKABLE void print(const QString& message) { _manager->print(message); } + + /**jsdoc + * Resolves a relative path to an absolute path. The relative path is relative to the script's location. + * @function Script.resolvePath + * @param {string} path - The relative path to resolve. + * @returns {string} The absolute path. + * @example Report the directory and filename of the running script. + * print(Script.resolvePath("")); + * @example Report the directory of the running script. + * print(Script.resolvePath(".")); + * @example Report the path to a file located relative to the running script. + * print(Script.resolvePath("../assets/sounds/hello.wav")); + */ + Q_INVOKABLE QUrl resolvePath(const QString& path) const { return _manager->resolvePath(path);} + + /**jsdoc + * Gets the path to the resources directory for QML files. + * @function Script.resourcesPath + * @returns {string} The path to the resources directory for QML files. + */ + Q_INVOKABLE QUrl resourcesPath() const { return _manager->resourcesPath(); } + + /**jsdoc + * Starts timing a section of code in order to send usage data about it to Vircadia. Shouldn't be used outside of the + * standard scripts. + * @function Script.beginProfileRange + * @param {string} label - A name that identifies the section of code. + */ + Q_INVOKABLE void beginProfileRange(const QString& label) const { _manager->beginProfileRange(label); } + + /**jsdoc + * Finishes timing a section of code in order to send usage data about it to Vircadia. Shouldn't be used outside of + * the standard scripts. + * @function Script.endProfileRange + * @param {string} label - A name that identifies the section of code. + */ + Q_INVOKABLE void endProfileRange(const QString& label) const { _manager->endProfileRange(label); } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Entity Script Related methods + + /**jsdoc + * Checks whether an entity has an entity script running. + * @function Script.isEntityScriptRunning + * @param {Uuid} entityID - The ID of the entity. + * @returns {boolean} true if the entity has an entity script running, false if it doesn't. + */ + Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) { return _manager->isEntityScriptRunning(entityID); } + + /**jsdoc + * Manually runs the JavaScript garbage collector which reclaims memory by disposing of objects that are no longer + * reachable. + * @function Script.requestGarbageCollection + */ + Q_INVOKABLE void requestGarbageCollection() { _manager->requestGarbageCollection(); } + + /**jsdoc + * Prints out current backtrace to the log. + * @function Script.logBacktrace + * @param {string} title - Title added to the printed out backtrace. + */ + Q_INVOKABLE void logBacktrace(const QString &title) { _manager->logBacktrace(title); } + + /*@jsdoc + * @function Script.loadEntityScript + * @param {Uuid} entityID - Entity ID. + * @param {string} script - Script. + * @param {boolean} forceRedownload - Force re-download. + * @deprecated This function is deprecated and will be removed. + */ + Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { _manager->loadEntityScript(entityID, entityScript, forceRedownload); } + + /**jsdoc + * @function Script.unloadEntityScript + * @param {Uuid} entityID - Entity ID. + * @param {boolean} [shouldRemoveFromMap=false] - Should remove from map. + * @deprecated This function is deprecated and will be removed. + */ + Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false) { _manager->unloadEntityScript(entityID, shouldRemoveFromMap); } + + /**jsdoc + * @function Script.unloadAllEntityScripts + * @param {boolean} [blockingCall=false] - Wait for completion if call moved to another thread. + * @deprecated This function is deprecated and will be removed. + */ + Q_INVOKABLE void unloadAllEntityScripts(bool blockingCall = false) { _manager->unloadAllEntityScripts(blockingCall); } + + /**jsdoc + * Calls a method in an entity script. + * @function Script.callEntityScriptMethod + * @param {Uuid} entityID - The ID of the entity running the entity script. + * @param {string} methodName - The name of the method to call. + * @param {string[]} [parameters=[]] - The parameters to call the specified method with. + * @param {Uuid} [remoteCallerID=Uuid.NULL] - An ID that identifies the caller. + */ + Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, + const QStringList& params = QStringList(), + const QUuid& remoteCallerID = QUuid()) { _manager->callEntityScriptMethod(entityID, methodName, params, remoteCallerID); } + + /**jsdoc + * Calls a method in an entity script. + * @function Script.callEntityScriptMethod + * @param {Uuid} entityID - Entity ID. + * @param {string} methodName - Method name. + * @param {PointerEvent} event - Pointer event. + * @deprecated This function is deprecated and will be removed. + */ + Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const PointerEvent& event) { _manager->callEntityScriptMethod(entityID, methodName, event); } + + /**jsdoc + * Calls a method in an entity script. + * @function Script.callEntityScriptMethod + * @param {Uuid} entityID - Entity ID. + * @param {string} methodName - Method name. + * @param {Uuid} otherID - Other entity ID. + * @param {Collision} collision - Collision. + * @deprecated This function is deprecated and will be removed. + */ + Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) { _manager->callEntityScriptMethod(entityID, methodName, otherID, collision);} + + /**jsdoc + * @function Script.generateUUID + * @returns {Uuid} A new UUID. + * @deprecated This function is deprecated and will be removed. Use {@link Uuid(0).generate|Uuid.generate} instead. + */ + Q_INVOKABLE QUuid generateUUID() { return QUuid::createUuid(); } + + /**jsdoc + * Gets the URL for an asset in an external resource bucket. (The location where the bucket is hosted may change over time + * but this method will return the asset's current URL.) + * @function Script.getExternalPath + * @param {Script.ResourceBucket} bucket - The external resource bucket that the asset is in. + * @param {string} path - The path within the external resource bucket where the asset is located. + *

Normally, this should start with a path or filename to be appended to the bucket URL. + * Alternatively, it can be a relative path starting with ./ or ../, to navigate within the + * resource bucket's URL.

+ * @Returns {string} The URL of an external asset. + * @example Report the URL of a default particle. + * print(Script.getExternalPath(Script.ExternalPaths.Assets, "Bazaar/Assets/Textures/Defaults/Interface/default_particle.png")); + * @example Report the root directory where the Vircadia assets are located. + * print(Script.getExternalPath(Script.ExternalPaths.Assets, ".")); + */ + Q_INVOKABLE QString getExternalPath(ExternalResource::Bucket bucket, const QString& path) { return _manager->getExternalPath(bucket, path); } + +signals: + + /**jsdoc + * @function Script.scriptLoaded + * @param {string} filename - File name. + * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. + */ + void scriptLoaded(const QString& scriptFilename); + + /**jsdoc + * @function Script.errorLoadingScript + * @param {string} filename - File name. + * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. + */ + void errorLoadingScript(const QString& scriptFilename); + + /**jsdoc + * Triggered frequently at a system-determined interval. + * @function Script.update + * @param {number} deltaTime - The time since the last update, in s. + * @returns {Signal} + * @example Report script update intervals. + * Script.update.connect(function (deltaTime) { + * print("Update: " + deltaTime); + * }); + */ + void update(float deltaTime); + + /**jsdoc + * Triggered when the script is stopping. + * @function Script.scriptEnding + * @returns {Signal} + * @example Report when a script is stopping. + * print("Script started"); + * + * Script.scriptEnding.connect(function () { + * print("Script ending"); + * }); + * + * Script.setTimeout(function () { + * print("Stopping script"); + * Script.stop(); + * }, 1000); + */ + void scriptEnding(); + + /**jsdoc + * @function Script.finished + * @param {string} filename - File name. + * @param {object} engine - Engine. + * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. + */ + void finished(const QString& fileNameString, ScriptManagerPointer); + + /**jsdoc + * Triggered when the script prints a message to the program log via {@link print}, {@link Script.print}, + * {@link console.log}, {@link console.debug}, {@link console.group}, {@link console.groupEnd}, {@link console.time}, or + * {@link console.timeEnd}. + * @function Script.printedMessage + * @param {string} message - The message. + * @param {string} scriptName - The name of the script that generated the message. + * @returns {Signal} + */ + void printedMessage(const QString& message, const QString& scriptName); + + /**jsdoc + * Triggered when the script generates an error, {@link console.error} or {@link console.exception} is called, or + * {@link console.assert} is called and fails. + * @function Script.errorMessage + * @param {string} message - The error message. + * @param {string} scriptName - The name of the script that generated the error message. + * @returns {Signal} + */ + void errorMessage(const QString& message, const QString& scriptName); + + /**jsdoc + * Triggered when the script generates a warning or {@link console.warn} is called. + * @function Script.warningMessage + * @param {string} message - The warning message. + * @param {string} scriptName - The name of the script that generated the warning message. + * @returns {Signal} + */ + void warningMessage(const QString& message, const QString& scriptName); + + /**jsdoc + * Triggered when the script generates an information message or {@link console.info} is called. + * @function Script.infoMessage + * @param {string} message - The information message. + * @param {string} scriptName - The name of the script that generated the information message. + * @returns {Signal} + */ + void infoMessage(const QString& message, const QString& scriptName); + + /**jsdoc + * Triggered when the running state of the script changes, e.g., from running to stopping. + * @function Script.runningStateChanged + * @returns {Signal} + */ + void runningStateChanged(); + + /**jsdoc + * @function Script.clearDebugWindow + * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. + */ + void clearDebugWindow(); + + /**jsdoc + * @function Script.loadScript + * @param {string} scriptName - Script name. + * @param {boolean} isUserLoaded - Is user loaded. + * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. + */ + void loadScript(const QString& scriptName, bool isUserLoaded); + + /**jsdoc + * @function Script.reloadScript + * @param {string} scriptName - Script name. + * @param {boolean} isUserLoaded - Is user loaded. + * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. + */ + void reloadScript(const QString& scriptName, bool isUserLoaded); + + /**jsdoc + * Triggered when the script has stopped. + * @function Script.doneRunning + * @returns {Signal} + */ + void doneRunning(); + + /**jsdoc + * @function Script.entityScriptDetailsUpdated + * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. + */ + // Emitted when an entity script is added or removed, or when the status of an entity + // script is updated (goes from RUNNING to ERROR_RUNNING_SCRIPT, for example) + void entityScriptDetailsUpdated(); + + /**jsdoc + * Triggered when the script starts for the user. See also, {@link Entities.preload}. + *

Supported Script Types: Client Entity Scripts • Server Entity Scripts

+ * @function Script.entityScriptPreloadFinished + * @param {Uuid} entityID - The ID of the entity that the script is running in. + * @returns {Signal} + * @example Get the ID of the entity that a client entity script is running in. + * var entityScript = function () { + * this.entityID = Uuid.NULL; + * }; + * + * Script.entityScriptPreloadFinished.connect(function (entityID) { + * this.entityID = entityID; + * print("Entity ID: " + this.entityID); + * }); + * + * var entityID = Entities.addEntity({ + * type: "Box", + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5 })), + * dimensions: { x: 0.5, y: 0.5, z: 0.5 }, + * color: { red: 255, green: 0, blue: 0 }, + * script: "(" + entityScript + ")", // Could host the script on a Web server instead. + * lifetime: 300 // Delete after 5 minutes. + * }); + */ + // Emitted when an entity script has finished running preload + void entityScriptPreloadFinished(const EntityItemID& entityID); + + /**jsdoc + * Triggered when a script generates an unhandled exception. + * @function Script.unhandledException + * @param {object} exception - The details of the exception. + * @returns {Signal} + * @example Report the details of an unhandled exception. + * Script.unhandledException.connect(function (exception) { + * print("Unhandled exception: " + JSON.stringify(exception)); + * }); + * var properties = JSON.parse("{ x: 1"); // Invalid JSON string. + */ + void unhandledException(const ScriptValue& exception); + + +protected: + /**jsdoc + * @function Script.executeOnScriptThread + * @param {function} function - Function. + * @param {ConnectionType} [type=2] - Connection type. + * @deprecated This function is deprecated and will be removed. + */ + Q_INVOKABLE void executeOnScriptThread(std::function function, const Qt::ConnectionType& type = Qt::QueuedConnection ) { _manager->executeOnScriptThread(function, type);} + + /**jsdoc + * @function Script._requireResolve + * @param {string} module - Module. + * @param {string} [relativeTo=""] - Relative to. + * @returns {string} Result. + * @deprecated This function is deprecated and will be removed. + */ + // note: this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general; + // then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;" + Q_INVOKABLE QString _requireResolve(const QString& moduleId, const QString& relativeTo = QString()) { return _manager->_requireResolve(moduleId, relativeTo); } + + /**jsdoc + * @function Script.entityScriptContentAvailable + * @param {Uuid} entityID - Entity ID. + * @param {string} scriptOrURL - Path. + * @param {string} contents - Contents. + * @param {boolean} isURL - Is a URL. + * @param {boolean} success - Success. + * @param {string} status - Status. + * @deprecated This function is deprecated and will be removed. + */ + Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString& status); + + +private: + ScriptManager *_manager; +}; \ No newline at end of file From 59d6efea2dcdf42d9fe05228addd34c496d1437e Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Sat, 25 Feb 2023 19:24:26 +0100 Subject: [PATCH 4/7] A bit more documentation --- libraries/script-engine/src/ScriptManager.h | 87 ++++++++++++++++++--- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/libraries/script-engine/src/ScriptManager.h b/libraries/script-engine/src/ScriptManager.h index e23766679e..030dff4003 100644 --- a/libraries/script-engine/src/ScriptManager.h +++ b/libraries/script-engine/src/ScriptManager.h @@ -230,6 +230,29 @@ public: * * The script-facing interface is in ScriptManagerScriptingInterface and documented in JSDoc * as the Script class. + * + * The ScriptManager provides the following functionality to scripts: + * + * * A math library: Quat, Vec3, Mat4 + * * UUID generation: Uuid + * * Filesystem access: File + * * Console access: console, print + * * Resource access: Resource, Asset, Resources, ExternalPaths + * * Scripting system management: Script + * * Module loading: require + * * Web access: XMLHttpRequest, WebSocket + * * Other miscellaneous functionality. + * + * @note + * Technically, the ScriptManager isn't generic enough -- it implements things that imitate + * Node.js for examine in the module loading code, which makes it JS specific. This code + * should probably be moved into the JS ScriptEngine class instead. + * + * The EntityScript functionality might also benefit from being split off into a separate + * class, for better organization. + * + * Some more functionality can be shifted to ScriptManagerScriptingInterface, since + * it only provides services to scripts and isn't called from C++. */ class ScriptManager : public QObject, public EntitiesScriptEngineProvider, public std::enable_shared_from_this { Q_OBJECT @@ -536,14 +559,24 @@ public: /** * @brief Includes JavaScript from other files in the current script. * - * @param includeFiles - * @param callback + * If a callback is specified, the included files will be loaded asynchronously and the callback will be called + * when all of the files have finished loading. + * If no callback is specified, the included files will be loaded synchronously and will block execution until + * all of the files have finished loading. + * + * @param includeFiles List of files to include + * @param callback Callback to call when the files have finished loading. */ Q_INVOKABLE void include(const QStringList& includeFiles, const ScriptValue& callback = ScriptValue()); /** * @brief Includes JavaScript from another file in the current script. * + * If a callback is specified, the included files will be loaded asynchronously and the callback will be called + * when all of the files have finished loading. + * If no callback is specified, the included files will be loaded synchronously and will block execution until + * all of the files have finished loading. + * * @param includeFile * @param callback */ @@ -650,9 +683,11 @@ public: /** * @brief Calls a function repeatedly, at a set interval. * - * @param function - * @param intervalMS - * @return QTimer* + * @note This is a JS API service. + * + * @param function Function to call + * @param intervalMS Interval at which to call the function, in ms + * @return QTimer* A pointer to the timer */ Q_INVOKABLE QTimer* setInterval(const ScriptValue& function, int intervalMS); @@ -660,16 +695,18 @@ public: /** * @brief Calls a function once, after a delay. * - * @param function - * @param timeoutMS - * @return QTimer* + * @note This is a JS API service. + * + * @param function Function to call + * @param timeoutMS How long to wait before calling the function, in ms + * @return QTimer* A pointer to the timer */ Q_INVOKABLE QTimer* setTimeout(const ScriptValue& function, int timeoutMS); /** * @brief Stops an interval timer * - * @param timer + * @param timer Timer to stop */ Q_INVOKABLE void clearInterval(QTimer* timer) { stopTimer(timer); } @@ -677,14 +714,15 @@ public: * @brief Stops an interval timer * * Overloaded version is needed in case the timer has expired - * @param timer + * + * @param timer Timer to stop */ Q_INVOKABLE void clearInterval(QVariantMap timer) { ; } /** * @brief Stops a timeout timer * - * @param timer + * @param timer Timer to stop */ Q_INVOKABLE void clearTimeout(QTimer* timer) { stopTimer(timer); } @@ -692,7 +730,7 @@ public: * @brief Stops a timeout timer * Overloaded version is needed in case the timer has expired * - * @param timer + * @param timer Timer to stop */ Q_INVOKABLE void clearTimeout(QVariantMap timer) { ; } @@ -1265,6 +1303,11 @@ protected: // Is called by the constructor, bceause all types need to be registered before method discovery with ScriptObjectV8Proxy::investigate() void initMetaTypes(); + /** + * @brief Initializes the underlying scripting engine + * + * This sets up the scripting engine with the default APIs + */ void init(); /** @@ -1460,9 +1503,29 @@ protected: }; +/** + * @brief Creates a new ScriptManager + * + * @param context Context in which scripts will run + * @param scriptContents Contents of the script to run + * @param fileNameString Filename for the script + * @return ScriptManagerPointer + */ ScriptManagerPointer newScriptManager(ScriptManager::Context context, const QString& scriptContents, const QString& fileNameString); + +/** + * @brief Creates a new ScriptManager and adds it to ScriptEngines + * + * Same as newScriptManager, but it additionally registers the new + * ScriptManager with ScriptEngines. + * + * @param context Context in which scripts will run + * @param scriptContents Contents of the script + * @param fileNameString Filename of the script + * @return ScriptManagerPointer + */ ScriptManagerPointer scriptManagerFactory(ScriptManager::Context context, const QString& scriptContents, const QString& fileNameString); From fe0b903293cba2996d6d4c949bc73c79577ad76d Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Sun, 26 Feb 2023 13:41:43 +0100 Subject: [PATCH 5/7] More Doxygen for ScriptEngine --- libraries/script-engine/src/ScriptEngine.h | 125 ++++++++++++++++++--- 1 file changed, 112 insertions(+), 13 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index e110befc69..3d5e1f39a0 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -66,9 +66,23 @@ public: * */ enum ValueOwnership { - QtOwnership = 0, /** Object is managed by Qt */ - ScriptOwnership = 1, /** Object is managed by the script */ - AutoOwnership = 2, /** Ownership is determined automatically */ + /** + * @brief Object is managed by Qt + * + */ + QtOwnership = 0, + + /** + * @brief Object is managed by the script + * + */ + ScriptOwnership = 1, + + /** + * @brief Ownership is determined automatically + * + */ + AutoOwnership = 2, }; /** @@ -76,15 +90,56 @@ public: * */ enum QObjectWrapOption { - //ExcludeChildObjects = 0x0001, /** The script object will not expose child objects as properties. */ - ExcludeSuperClassMethods = 0x0002, /** The script object will not expose signals and slots inherited from the superclass. */ - ExcludeSuperClassProperties = 0x0004, /** The script object will not expose properties inherited from the superclass. */ + + /** + * @brief The script object will not expose child objects as properties. + * + */ + //ExcludeChildObjects = 0x0001, + + /** + * @brief The script object will not expose signals and slots inherited from the superclass. + * + */ + ExcludeSuperClassMethods = 0x0002, + + /** + * @brief The script object will not expose properties inherited from the superclass. + * + */ + ExcludeSuperClassProperties = 0x0004, + + /** + * @brief The script object will not expose the QObject::deleteLater() slot. + * + */ ExcludeSuperClassContents = ExcludeSuperClassMethods | ExcludeSuperClassProperties, - //ExcludeDeleteLater = 0x0010, /** The script object will not expose the QObject::deleteLater() slot. */ - ExcludeSlots = 0x0020, /** The script object will not expose the QObject's slots. */ - AutoCreateDynamicProperties = 0x0100, /** Properties that don't already exist in the QObject will be created as dynamic properties of that object, rather than as properties of the script object. */ - PreferExistingWrapperObject = 0x0200, /** If a wrapper object with the requested configuration already exists, return that object. */ - SkipMethodsInEnumeration = 0x0008, /** Don't include methods (signals and slots) when enumerating the object's properties. */ + + //ExcludeDeleteLater = 0x0010, + + /** + * @brief The script object will not expose the QObject's slots. + * + */ + ExcludeSlots = 0x0020, + + /** + * @brief Properties that don't already exist in the QObject will be created as dynamic properties of that object, rather than as properties of the script object. + * + */ + AutoCreateDynamicProperties = 0x0100, + + /** + * @brief If a wrapper object with the requested configuration already exists, return that object. + * + */ + PreferExistingWrapperObject = 0x0200, + + /** + * @brief Don't include methods (signals and slots) when enumerating the object's properties. + * + */ + SkipMethodsInEnumeration = 0x0008, }; Q_DECLARE_FLAGS(QObjectWrapOptions, QObjectWrapOption); @@ -101,6 +156,12 @@ public: */ virtual void clearExceptions() = 0; + /** + * @brief Creates a clone of the current exception + * + * @param detail Additional text to add to the report + * @return ScriptValue Result + */ virtual ScriptValue cloneUncaughtException(const QString& detail = QString()) = 0; /** @@ -145,14 +206,24 @@ public: virtual ScriptValue evaluateInClosure(const ScriptValue& locals, const ScriptProgramPointer& program) = 0; /** - * @brief Global namespace, containing all the public APIs + * @brief Global object which holds all the functions and variables available everywhere * - * @return ScriptValue + * This is a JavaScript concept, https://javascript.info/global-object + * + * @note This may not belong in the base class. + * @return ScriptValue Global Object */ virtual ScriptValue globalObject() { Q_ASSERT(false); return ScriptValue(); } + + /** + * @brief Whether the script has an uncaught exception + * + * @return true There is an uncaught exception + * @return false There's no exception + */ virtual bool hasUncaughtException() const = 0; /** @@ -164,8 +235,36 @@ public: virtual bool isEvaluating() const = 0; //virtual ScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1) = 0; + /** + * @brief Check a program for syntax errors + * + * Returns an object with at least the following properties: + * * fileName + * * lineNumber + * * stack + * * formatted + * + * @param program Program to check + * @return ScriptValue Result + * + * @note It could be a good improvement to redo this to return a struct instead. + */ virtual ScriptValue checkScriptSyntax(ScriptProgramPointer program) = 0; + + /** + * @brief Creates a ScriptValue that contains an error + * + * @param other + * @param type + * @return ScriptValue + */ virtual ScriptValue makeError(const ScriptValue& other = ScriptValue(), const QString& type = "Error") = 0; + + /** + * @brief Pointer to the ScriptManager that controls this scripting engine + * + * @return ScriptManager* ScriptManager + */ virtual ScriptManager* manager() const = 0; virtual bool maybeEmitUncaughtException(const QString& debugHint = QString()) = 0; virtual ScriptValue newArray(uint length = 0) = 0; From 349f07a35aaff90aacad9a38c24d933944984bee Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Sun, 26 Feb 2023 14:14:25 +0100 Subject: [PATCH 6/7] Add a ScriptManagerScriptingInterface.cpp, will be useful later. --- .../src/ScriptManagerScriptingInterface.cpp | 33 +++++++++++++++++++ .../src/ScriptManagerScriptingInterface.h | 24 +++----------- 2 files changed, 38 insertions(+), 19 deletions(-) create mode 100644 libraries/script-engine/src/ScriptManagerScriptingInterface.cpp diff --git a/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp b/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp new file mode 100644 index 0000000000..9b0efa71e8 --- /dev/null +++ b/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp @@ -0,0 +1,33 @@ +// +// ScriptManagerScriptingInterface.h +// libraries/script-engine/src +// +// Created by Dale Glass on 24/02/2023. +// Copyright 2022 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ScriptManager.h" +#include "ScriptManagerScriptingInterface.h" + + + ScriptManagerScriptingInterface::ScriptManagerScriptingInterface(ScriptManager *parent): QObject(parent), _manager(parent) { + connect(_manager, &ScriptManager::scriptLoaded, this, &ScriptManagerScriptingInterface::scriptLoaded); + connect(_manager, &ScriptManager::errorLoadingScript, this, &ScriptManagerScriptingInterface::errorLoadingScript); + connect(_manager, &ScriptManager::update, this, &ScriptManagerScriptingInterface::update); + connect(_manager, &ScriptManager::scriptEnding, this, &ScriptManagerScriptingInterface::scriptEnding); + connect(_manager, &ScriptManager::finished, this, &ScriptManagerScriptingInterface::finished); + connect(_manager, &ScriptManager::printedMessage, this, &ScriptManagerScriptingInterface::printedMessage); + connect(_manager, &ScriptManager::errorMessage, this, &ScriptManagerScriptingInterface::errorMessage); + connect(_manager, &ScriptManager::warningMessage, this, &ScriptManagerScriptingInterface::warningMessage); + connect(_manager, &ScriptManager::infoMessage, this, &ScriptManagerScriptingInterface::infoMessage); + connect(_manager, &ScriptManager::runningStateChanged, this, &ScriptManagerScriptingInterface::runningStateChanged); + connect(_manager, &ScriptManager::clearDebugWindow, this, &ScriptManagerScriptingInterface::clearDebugWindow); + connect(_manager, &ScriptManager::loadScript, this, &ScriptManagerScriptingInterface::loadScript); + connect(_manager, &ScriptManager::doneRunning, this, &ScriptManagerScriptingInterface::doneRunning); + connect(_manager, &ScriptManager::entityScriptDetailsUpdated, this, &ScriptManagerScriptingInterface::entityScriptDetailsUpdated); + connect(_manager, &ScriptManager::entityScriptPreloadFinished, this, &ScriptManagerScriptingInterface::entityScriptPreloadFinished); + connect(_manager, &ScriptManager::unhandledException, this, &ScriptManagerScriptingInterface::unhandledException); + } \ No newline at end of file diff --git a/libraries/script-engine/src/ScriptManagerScriptingInterface.h b/libraries/script-engine/src/ScriptManagerScriptingInterface.h index 1de1b39c54..b46c3cf4f0 100644 --- a/libraries/script-engine/src/ScriptManagerScriptingInterface.h +++ b/libraries/script-engine/src/ScriptManagerScriptingInterface.h @@ -51,26 +51,11 @@ class ScriptManagerScriptingInterface : public QObject { Q_OBJECT public: - ScriptManagerScriptingInterface(ScriptManager *parent) : QObject(parent), _manager(parent) { + ScriptManagerScriptingInterface(ScriptManager *parent); + + virtual ~ScriptManagerScriptingInterface() { - connect(_manager, &ScriptManager::scriptLoaded, this, &ScriptManagerScriptingInterface::scriptLoaded); - connect(_manager, &ScriptManager::errorLoadingScript, this, &ScriptManagerScriptingInterface::errorLoadingScript); - connect(_manager, &ScriptManager::update, this, &ScriptManagerScriptingInterface::update); - connect(_manager, &ScriptManager::scriptEnding, this, &ScriptManagerScriptingInterface::scriptEnding); - connect(_manager, &ScriptManager::finished, this, &ScriptManagerScriptingInterface::finished); - connect(_manager, &ScriptManager::printedMessage, this, &ScriptManagerScriptingInterface::printedMessage); - connect(_manager, &ScriptManager::errorMessage, this, &ScriptManagerScriptingInterface::errorMessage); - connect(_manager, &ScriptManager::warningMessage, this, &ScriptManagerScriptingInterface::warningMessage); - connect(_manager, &ScriptManager::infoMessage, this, &ScriptManagerScriptingInterface::infoMessage); - connect(_manager, &ScriptManager::runningStateChanged, this, &ScriptManagerScriptingInterface::runningStateChanged); - connect(_manager, &ScriptManager::clearDebugWindow, this, &ScriptManagerScriptingInterface::clearDebugWindow); - connect(_manager, &ScriptManager::loadScript, this, &ScriptManagerScriptingInterface::loadScript); - connect(_manager, &ScriptManager::doneRunning, this, &ScriptManagerScriptingInterface::doneRunning); - connect(_manager, &ScriptManager::entityScriptDetailsUpdated, this, &ScriptManagerScriptingInterface::entityScriptDetailsUpdated); - connect(_manager, &ScriptManager::entityScriptPreloadFinished, this, &ScriptManagerScriptingInterface::entityScriptPreloadFinished); - connect(_manager, &ScriptManager::unhandledException, this, &ScriptManagerScriptingInterface::unhandledException); } - /**jsdoc * Stops and unloads the current script. *

Warning: If an assignment client script, the script gets restarted after stopping.

@@ -709,7 +694,8 @@ protected: * @param {string} status - Status. * @deprecated This function is deprecated and will be removed. */ - Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString& status); + Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString& status) + { _manager->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success, status); } private: From 82e4e6087c048552acd5b565a18ee1cfa6893d46 Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Sun, 26 Feb 2023 15:23:19 +0100 Subject: [PATCH 7/7] Review fixes --- libraries/script-engine/src/ScriptManager.h | 4 ++-- .../script-engine/src/ScriptManagerScriptingInterface.cpp | 2 +- libraries/script-engine/src/ScriptManagerScriptingInterface.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/script-engine/src/ScriptManager.h b/libraries/script-engine/src/ScriptManager.h index 030dff4003..99d78623ad 100644 --- a/libraries/script-engine/src/ScriptManager.h +++ b/libraries/script-engine/src/ScriptManager.h @@ -759,14 +759,14 @@ public: Q_INVOKABLE QUrl resourcesPath() const; /** - * @brief Starts timing a section of code in order to send usage data about it to Vircadia. Shouldn't be used outside of the + * @brief Starts timing a section of code in order to send usage data about it to Overte. Shouldn't be used outside of the * standard scripts. * @param label */ Q_INVOKABLE void beginProfileRange(const QString& label) const; /** - * @brief Finishes timing a section of code in order to send usage data about it to Vircadia. Shouldn't be used outside of + * @brief Finishes timing a section of code in order to send usage data about it to Overte. Shouldn't be used outside of * the standard scripts * @param label */ diff --git a/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp b/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp index 9b0efa71e8..3343203a2e 100644 --- a/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp +++ b/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp @@ -3,7 +3,7 @@ // libraries/script-engine/src // // Created by Dale Glass on 24/02/2023. -// Copyright 2022 Overte e.V. +// Copyright 2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/libraries/script-engine/src/ScriptManagerScriptingInterface.h b/libraries/script-engine/src/ScriptManagerScriptingInterface.h index b46c3cf4f0..086e8bee6c 100644 --- a/libraries/script-engine/src/ScriptManagerScriptingInterface.h +++ b/libraries/script-engine/src/ScriptManagerScriptingInterface.h @@ -3,7 +3,7 @@ // libraries/script-engine/src // // Created by Dale Glass on 24/02/2023. -// Copyright 2022 Overte e.V. +// Copyright 2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html