From 9d860a8e8179c33d3439ad322b5d2660f92853a9 Mon Sep 17 00:00:00 2001 From: humbletim Date: Thu, 16 Feb 2017 07:49:56 -0500 Subject: [PATCH 01/43] merge require/module changes into clean branch --- libraries/script-engine/src/ScriptEngine.cpp | 464 ++++++++++++++++++ libraries/script-engine/src/ScriptEngine.h | 19 + .../script-engine/src/ScriptEngineLogging.cpp | 1 + .../script-engine/src/ScriptEngineLogging.h | 1 + 4 files changed, 485 insertions(+) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 73a79f1bc6..8c458f71b7 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -70,6 +70,10 @@ #include "MIDIEvent.h" +const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT { + "com.highfidelity.experimental.enableExtendedModuleCompatbility" +}; + static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable }; @@ -532,6 +536,44 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) { return prototype; } +void ScriptEngine::resetModuleCache(bool deleteScriptCache) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "resetModuleCache"); + return; + } + { + QMutexLocker locker(&_requireLock); + auto jsRequire = globalObject().property("Script").property("require"); + auto cache = jsRequire.property("cache"); + auto cacheMeta = jsRequire.data(); + + if (deleteScriptCache) { + QScriptValueIterator it(cache); + while (it.hasNext()) { + it.next(); + if (it.flags() & QScriptValue::SkipInEnumeration) + continue; + //scriptCache->deleteScript(it.name()); + qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; + cacheMeta.setProperty(it.name(), true); + } + } + //_debugDump("cacheMeta", cacheMeta); + cache = newObject(); + if (!cacheMeta.isObject()) { + cacheMeta = newObject(); + cacheMeta.setProperty("id", "Script.require.cacheMeta"); + cacheMeta.setProperty("type", "cacheMeta"); + jsRequire.setData(cacheMeta); + } + cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration); +#if DEBUG_JS_MODULES + cache.setProperty("__meta__", cacheMeta, HIDDEN_PROP_FLAGS); +#endif + jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable); + } +} + void ScriptEngine::init() { if (_isInitialized) { return; // only initialize once @@ -595,6 +637,15 @@ void ScriptEngine::init() { registerGlobalObject("Script", this); + { + // set up Script.require.resolve and Script.require.cache + auto Script = globalObject().property("Script"); + auto require = Script.property("require"); + auto resolve = Script.property("_requireResolve"); + require.setProperty("resolve", resolve, QScriptValue::ReadOnly | QScriptValue::Undeletable); + resetModuleCache(); + } + registerGlobalObject("Audio", &AudioScriptingInterface::getInstance()); registerGlobalObject("Entities", entityScriptingInterface.data()); registerGlobalObject("Quat", &_quatLibrary); @@ -1304,6 +1355,419 @@ void ScriptEngine::print(const QString& message) { emit printedMessage(message); } +static const auto MAX_MODULE_IDENTIFIER_LEN { 4096 }; +static const auto MAX_MODULE_IDENTIFIER_LOG_LEN { 60 }; + +// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js) +QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { + QUrl defaultScriptsLoc = defaultScriptsLocation(); + QUrl url(moduleId); + + // helper to generate an exception and return a null string + auto resolverException = [=](const QString& detail) -> QString { + currentContext()->throwError( + QString("Cannot find module '%1' (%2)") + .arg(moduleId.left(MAX_MODULE_IDENTIFIER_LOG_LEN)) + .arg(detail)); + return QString(); + }; + + // de-fuzz the input a little by restricting to rational sizes + auto idLength = url.toString().length(); + if (idLength < 1 || idLength > MAX_MODULE_IDENTIFIER_LEN) { + return resolverException( + QString("rejecting invalid module id size (%1 chars [1,%2])") + .arg(idLength).arg(MAX_MODULE_IDENTIFIER_LEN) + ); + } + + // this regex matches: absolute, dotted or path-like URLs + // (ie: the kind of stuff ScriptEngine::resolvePath already handles) + QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)"); + + // this is for module.require (which is a bound version of require that's always relative to the module path) + if (!relativeTo.isEmpty()) { + url = QUrl(relativeTo).resolved(moduleId); + url = resolvePath(url.toString()); + } else if (qualified.match(moduleId).hasMatch()) { + url = resolvePath(moduleId); + } else { + // check if the moduleId refers to a "system" module + QString defaultsPath = defaultScriptsLoc.path(); + QString systemModulePath = QString("%1/modules/%2.js").arg(defaultsPath).arg(moduleId); + url = defaultScriptsLoc; + url.setPath(systemModulePath); + if (!QFileInfo(url.toLocalFile()).isFile()) { + return resolverException("system module not found"); + } + } + + if (url.isRelative()) { + return resolverException("could not resolve module id"); + } + + // if it looks like a local file, verify that it's an allowed path and really a file + if (url.isLocalFile()) { + QFileInfo file(url.toLocalFile()); + QUrl canonical = url; + if (file.exists()) { + canonical.setPath(file.canonicalFilePath()); + } + + bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); + if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { + return resolverException( + QString("path '%1' outside of origin script '%2' '%3'") + .arg(PathUtils::stripFilename(url)) + .arg(PathUtils::stripFilename(currentSandboxURL)) + .arg(canonical.toString()) + ); + } + if (!file.exists()) { + return resolverException("path does not exist: " + url.toLocalFile()); + } + if (!file.isFile()) { + return resolverException("path is not a file: " + url.toLocalFile()); + } + } + + return url.toString(); +} + +// retrieves the current parent module from the JS scope chain +QScriptValue ScriptEngine::currentModule() { + auto jsRequire = globalObject().property("Script").property("require"); + auto cache = jsRequire.property("cache"); + auto candidate = QScriptValue(); + for(auto ctx = currentContext(); ctx && !candidate.isObject(); ctx = ctx->parentContext()) { + QScriptContextInfo contextInfo { ctx }; + candidate = cache.property(contextInfo.fileName()); + } + if (!candidate.isObject()) { + return QScriptValue(); + } + return candidate; +} + +// replaces or adds "module" to "parent.children[]" array +// (for consistency with Node.js and userscript cache invalidation without "cache busters") +bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) { + auto children = parent.property("children"); + if (children.isArray()) { + auto key = module.property("id"); + auto length = children.property("length").toInt32(); + for(int i=0; i < length; i++) { + if (children.property(i).property("id").strictlyEquals(key)) { + qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; + children.setProperty(i, module); + return true; + } + } + qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module"; + children.setProperty(length, module); + return true; + } else if (parent.isValid()) { + qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString(); + } + return false; +} + +// creates a new JS "module" Object with default metadata properties +QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) { + auto closure = newObject(); + auto exports = newObject(); + auto module = newObject(); + qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString(); + + closure.setProperty("module", module, READONLY_PROP_FLAGS); + + // note: this becomes the "exports" free variable, so should not be set read only + closure.setProperty("exports", exports); + + // make the closure available to module instantiation + module.setProperty("__closure__", closure, HIDDEN_PROP_FLAGS); + + // for consistency with Node.js Module + module.setProperty("id", modulePath, READONLY_PROP_FLAGS); + module.setProperty("filename", modulePath, READONLY_PROP_FLAGS); + module.setProperty("exports", exports); // not readonly + module.setProperty("loaded", false, READONLY_PROP_FLAGS); + module.setProperty("parent", parent, READONLY_PROP_FLAGS); + module.setProperty("children", newArray(), READONLY_PROP_FLAGS); + + // module.require is a bound version of require that always resolves relative to that module's path + auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)"); + module.setProperty("require", boundRequire, READONLY_PROP_FLAGS); + + return module; +} + +// synchronously fetch a module's source code using BatchLoader +QScriptValue ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { + using UrlMap = QMap; + auto scriptCache = DependencyManager::get(); + QScriptValue req = newObject(); + qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread(); + + auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) { + auto url = modulePath; + auto status = _status[url]; + auto contents = data[url]; + qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread(); + if (isStopping()) { + req.setProperty("status", "Stopped"); + req.setProperty("success", false); + } else { + req.setProperty("url", url); + req.setProperty("status", status); + req.setProperty("success", ScriptCache::isSuccessStatus(status)); + req.setProperty("contents", contents, HIDDEN_PROP_FLAGS); + } + }; + + if (forceDownload) { + qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath; + scriptCache->deleteScript(modulePath); + } + BatchLoader* loader = new BatchLoader({ modulePath }); + connect(loader, &BatchLoader::finished, this, onload); + connect(this, &QObject::destroyed, loader, &QObject::deleteLater); + // fail faster? (since require() blocks the engine thread while resolving dependencies) + const int MAX_RETRIES = 1; + + loader->start(MAX_RETRIES); + + if (!loader->isFinished()) { + QTimer monitor; + QEventLoop loop; + QObject::connect(loader, &BatchLoader::finished, this, [this, &monitor, &loop]{ + monitor.stop(); + loop.quit(); + }); + + // this helps detect the case where stop() is invoked during the download + // but not seen in time to abort processing in onload()... + connect(&monitor, &QTimer::timeout, this, [this, &loop, &loader]{ + if (isStopping()) { + loop.exit(-1); + } + }); + monitor.start(500); + loop.exec(); + } + loader->deleteLater(); + return req; +} + +// evaluate a pending module object using the fetched source code +QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) { + QScriptValue result; + auto modulePath = module.property("filename").toString(); + auto closure = module.property("__closure__"); + + qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes").arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); + + { + ExceptionEmitter tryCatch(this, __FUNCTION__); + + if (module.property("content-type").toString() == "application/json") { + qCDebug(scriptengine_module) << "... parsing as JSON"; +#ifdef DEV_OTHER_MODULE_JSON_PARSE + auto JSON = globalObject().property("JSON"); + auto parse = JSON.property("parse"); + if (!parse.isFunction()) { + currentContext()->throwValue(makeError("global JSON.parse is not a function", "EvalError")); + return nullValue(); + } + result = parse.call(JSON, QScriptValueList({ sourceCode })); + closure.property("module").setProperty("exports", result); + return result; +#endif + closure.setProperty("__json", sourceCode); + result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath }); + } else { + // scoped vars for consistency with Node.js + closure.setProperty("require", module.property("require")); + closure.setProperty("__filename", modulePath, HIDDEN_PROP_FLAGS); + closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), HIDDEN_PROP_FLAGS); + result = evaluateInClosure(closure, { sourceCode, modulePath }); + } + } + return result; +} + +// CommonJS/Node.js like require/module support +QScriptValue ScriptEngine::require(const QString& moduleId) { + qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_MODULE_IDENTIFIER_LOG_LEN) << ")"; + if (QThread::currentThread() != thread()) { + qCDebug(scriptengine_module) << moduleId << " threads mismatch"; + return nullValue(); + } + + // serialize require calls so the ordering/caching works correctly across multiple Entities + // note: this is a Recursive mutex so nested require's should not affected + QMutexLocker locker(&_requireLock); + // _requireDepth++; + // QObject autoDecrement;connect(&autoDecrement, &QObject::destroyed, this, [this](){ _requireDepth--; }); + + auto jsRequire = globalObject().property("Script").property("require"); + auto cacheMeta = jsRequire.data(); + auto cache = jsRequire.property("cache"); + auto parent = currentModule(); + + auto throwModuleError = [this, &cache](const QString& modulePath, const QScriptValue& error) { + cache.setProperty(modulePath, nullValue()); + if (!error.isNull()) { +#ifdef DEBUG_JS_MODULES + qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); +#endif + currentContext()->throwValue(makeError(error)); + } + return nullValue(); + }; + + // start by resolving the moduleId into a fully-qualified path/URL + QString modulePath = _requireResolve(moduleId); + if (hasUncaughtException()) { + // the resolver already threw an exception -- bail early + return nullValue(); + } + + // check the resolved path against the cache + auto module = cache.property(modulePath); + + // modules get cached in `Script.require.cache` and (similar to Node.js) users can access it + // to inspect particular entries and invalidate them by deleting the key: + // `delete Script.require.cache[Script.require.resolve(moduleId)];` + + // cacheMeta is just used right now to tell deleted keys apart from undefined ones + bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid(); + + // reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load + cacheMeta.setProperty(modulePath, QScriptValue()); + + auto exports = module.property("exports"); + if (!invalidateCache && exports.isObject()) { + // we have found a cacheed module -- just need to possibly register it with current parent + qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)") + .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString()); + registerModuleWithParent(module, parent); + return exports; + } + + // bootstrap / register new empty module + module = newModule(modulePath, parent); + registerModuleWithParent(module, parent); + + // add it to the cache (this is done early so any cyclic dependencies pick up) + cache.setProperty(modulePath, module); + + // download the module source + auto req = fetchModuleSource(modulePath, invalidateCache); + + if (!req.property("success").toBool()) { + auto error = QString("error retrieving script (%1)").arg(req.property("status").toString()); + return throwModuleError(modulePath, error); + } + +#if DEBUG_JS_MODULES + qCDebug(scriptengine_module) << "require.loaded: " << + QUrl(req.property("url").toString()).fileName() << req.property("status").toString(); +#endif + + auto sourceCode = req.property("contents").toString(); + + if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) { + module.setProperty("content-type", "application/json"); + } else { + module.setProperty("content-type", "application/javascript"); + } + + { + // It seems that many JSON sources don't actually put .json in the URL... + // so for now as a workaround users wanting to indicate JSON parsing mode can + // do so by calling with a custom this context, eg: + // + // var ITEMS_URL = 'https://highfidelity.io/api/v1/marketplace/items'; + // var thisObject = { 'content-type': 'application/json' }; + // var items = Script.require.call(thisObject, ITEMS_URL + '?category=everything&sort=recent'); + + auto thisObject = currentContext()->thisObject(); + bool calledWithCustomThis = thisObject.isObject() && + !thisObject.strictlyEquals(globalObject()) && + !thisObject.toQObject(); + + if (calledWithCustomThis) { +#ifdef DEBUG_JS + _debugDump("this", thisObject); +#endif + _applyUserOptions(module, thisObject); + } + } + + // evaluate the module + auto result = instantiateModule(module, sourceCode); + + if (result.isError() && !result.strictlyEquals(module.property("exports"))) { + qCWarning(scriptengine_module) << "-- result.isError --" << result.toString(); + return throwModuleError(modulePath, result); + } + + // mark as fully-loaded + module.setProperty("loaded", true, READONLY_PROP_FLAGS); + + // set up a new reference point for detecting cache key deletion + cacheMeta.setProperty(modulePath, module); + + qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")"; + + return module.property("exports"); +} + +// User-configurable override options +void ScriptEngine::_applyUserOptions(QScriptValue& module, QScriptValue& options) { + if (!options.isValid()) { + return; + } + // options['content-type'] === 'application/json' + // -- allows JSON modules to be used from URLs not ending in .json + if (options.property("content-type").isString()) { + module.setProperty("content-type", options.property("content-type")); + qCDebug(scriptengine_module) << "module['content-type'] =" << module.property("content-type").toString(); + } + + if (ScriptEngine::_enableExtendedModuleCompatbility.get()) { + auto closure = module.property("__closure__"); + + auto maybeSetToExports = [&](const QString& key) { + if (options.property(key).toString() == "exports") { + closure.setProperty(key, module.property("exports")); + qCDebug(scriptengine_module) << "module.closure[" << key << "] = exports"; + } + }; + + // options[{key}] = 'exports' + // several "agnostic" modules in the wild are just one step away from being compatible -- + // they just didn't know not to look specifically for this, self or global for attaching + // things onto. + maybeSetToExports("global"); + maybeSetToExports("self"); + maybeSetToExports("this"); + + // when options is an Object it will get used as the value of "this" during module evaluation + // (which is what one might expect when calling require.call(thisObject, ...)) + if (options.isObject()) { + closure.setProperty("this", options); + } + + // when options.global is an Object it'll get used as the global object (during evaluation only) + if (options.property("global").isObject()) { + closure.setProperty("global", options.property("global")); + qCDebug(scriptengine_module) << "module.closure['global'] = options.global"; + } + } +} + // 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 diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index b988ccfe90..f64dec44c7 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -41,6 +41,7 @@ #include "ScriptCache.h" #include "ScriptUUID.h" #include "Vec3.h" +#include "SettingHandle.h" class QScriptEngineDebugger; @@ -157,6 +158,16 @@ public: Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue()); Q_INVOKABLE void include(const QString& includeFile, QScriptValue callback = QScriptValue()); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // MODULE related methods + Q_INVOKABLE QScriptValue require(const QString& moduleId); + Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false); + QScriptValue currentModule(); + bool registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent); + QScriptValue newModule(const QString& modulePath, const QScriptValue& parent = QScriptValue()); + QScriptValue fetchModuleSource(const QString& modulePath, const bool forceDownload = false); + QScriptValue instantiateModule(const QScriptValue& module, const QString& sourceCode); + Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS); Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS); Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } @@ -237,6 +248,9 @@ signals: protected: void init(); Q_INVOKABLE void executeOnScriptThread(std::function function, const Qt::ConnectionType& type = Qt::QueuedConnection ); + // 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()); QString logException(const QScriptValue& exception); void timerFired(); @@ -272,6 +286,7 @@ protected: QHash _entityScripts; QHash _occupiedScriptURLs; QList _deferredEntityLoads; + QMutex _requireLock { QMutex::Recursive }; bool _isThreaded { false }; QScriptEngineDebugger* _debugger { nullptr }; @@ -295,6 +310,10 @@ protected: std::recursive_mutex _lock; std::chrono::microseconds _totalTimerExecution { 0 }; + + static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT; + Setting::Handle _enableExtendedModuleCompatbility { _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT, false }; + void _applyUserOptions(QScriptValue& module, QScriptValue& options); }; #endif // hifi_ScriptEngine_h diff --git a/libraries/script-engine/src/ScriptEngineLogging.cpp b/libraries/script-engine/src/ScriptEngineLogging.cpp index 2e5d293728..392bc05129 100644 --- a/libraries/script-engine/src/ScriptEngineLogging.cpp +++ b/libraries/script-engine/src/ScriptEngineLogging.cpp @@ -12,3 +12,4 @@ #include "ScriptEngineLogging.h" Q_LOGGING_CATEGORY(scriptengine, "hifi.scriptengine") +Q_LOGGING_CATEGORY(scriptengine_module, "hifi.scriptengine.module") diff --git a/libraries/script-engine/src/ScriptEngineLogging.h b/libraries/script-engine/src/ScriptEngineLogging.h index 0e614dd5bf..62e46632a6 100644 --- a/libraries/script-engine/src/ScriptEngineLogging.h +++ b/libraries/script-engine/src/ScriptEngineLogging.h @@ -15,6 +15,7 @@ #include Q_DECLARE_LOGGING_CATEGORY(scriptengine) +Q_DECLARE_LOGGING_CATEGORY(scriptengine_module) #endif // hifi_ScriptEngineLogging_h From efc61c25ad6bc6a5383d1345c8a5c249f60d2327 Mon Sep 17 00:00:00 2001 From: humbletim Date: Thu, 16 Feb 2017 18:33:34 -0500 Subject: [PATCH 02/43] * use explicit int's for moduleId constants * maxTestConstructorValueSize was already limiting debug output size -- adopt its strategy, use shared MAX_DEBUG_VALUE_LENGTH const --- libraries/script-engine/src/ScriptEngine.cpp | 35 +++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 8c458f71b7..8fb7a88c66 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -74,13 +74,14 @@ const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT { "com.highfidelity.experimental.enableExtendedModuleCompatbility" }; +static const int MAX_MODULE_ID_LENTGH { 4096 }; +static const int MAX_DEBUG_VALUE_LENGTH { 80 }; + static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable }; static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration }; - - static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) @@ -551,8 +552,9 @@ void ScriptEngine::resetModuleCache(bool deleteScriptCache) { QScriptValueIterator it(cache); while (it.hasNext()) { it.next(); - if (it.flags() & QScriptValue::SkipInEnumeration) + if (it.flags() & QScriptValue::SkipInEnumeration) { continue; + } //scriptCache->deleteScript(it.name()); qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; cacheMeta.setProperty(it.name(), true); @@ -1355,29 +1357,31 @@ void ScriptEngine::print(const QString& message) { emit printedMessage(message); } -static const auto MAX_MODULE_IDENTIFIER_LEN { 4096 }; -static const auto MAX_MODULE_IDENTIFIER_LOG_LEN { 60 }; - // Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js) QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { QUrl defaultScriptsLoc = defaultScriptsLocation(); QUrl url(moduleId); // helper to generate an exception and return a null string - auto resolverException = [=](const QString& detail) -> QString { - currentContext()->throwError( + auto resolverException = [=](const QString& detail, const QString& type = "Error") -> QString { + auto displayId = moduleId; + if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { + displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; + } + currentContext()->throwValue(makeError( QString("Cannot find module '%1' (%2)") - .arg(moduleId.left(MAX_MODULE_IDENTIFIER_LOG_LEN)) - .arg(detail)); + .arg(displayId) + .arg(detail), type)); return QString(); }; // de-fuzz the input a little by restricting to rational sizes auto idLength = url.toString().length(); - if (idLength < 1 || idLength > MAX_MODULE_IDENTIFIER_LEN) { + if (idLength < 1 || idLength > MAX_MODULE_ID_LENTGH) { return resolverException( QString("rejecting invalid module id size (%1 chars [1,%2])") - .arg(idLength).arg(MAX_MODULE_IDENTIFIER_LEN) + .arg(idLength).arg(MAX_MODULE_ID_LENTGH), + "RangeError" ); } @@ -1598,7 +1602,7 @@ QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const Q // CommonJS/Node.js like require/module support QScriptValue ScriptEngine::require(const QString& moduleId) { - qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_MODULE_IDENTIFIER_LOG_LEN) << ")"; + qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; if (QThread::currentThread() != thread()) { qCDebug(scriptengine_module) << moduleId << " threads mismatch"; return nullValue(); @@ -2245,9 +2249,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co testConstructorType = "empty"; } QString testConstructorValue = testConstructor.toString(); - const int maxTestConstructorValueSize = 80; - if (testConstructorValue.size() > maxTestConstructorValueSize) { - testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "..."; + if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) { + testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; } auto message = QString("failed to load entity script -- expected a function, got %1, %2") .arg(testConstructorType).arg(testConstructorValue); From 32e450e6c23c50e005edd798ca8f631e4a9ab2ef Mon Sep 17 00:00:00 2001 From: humbletim Date: Thu, 16 Feb 2017 20:19:40 -0500 Subject: [PATCH 03/43] * rework _requireResolve error throws (to avoid using lambda w/default args -- which vc++ didn't care for) * add ExceptionEmitters to require/resolve so errors get logged even if invoked indpendently from a script --- libraries/script-engine/src/ScriptEngine.cpp | 48 ++++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 8fb7a88c66..882fec6c72 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -1361,28 +1361,22 @@ void ScriptEngine::print(const QString& message) { QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { QUrl defaultScriptsLoc = defaultScriptsLocation(); QUrl url(moduleId); + ExceptionEmitter tryCatcher(this, __FUNCTION__); - // helper to generate an exception and return a null string - auto resolverException = [=](const QString& detail, const QString& type = "Error") -> QString { - auto displayId = moduleId; - if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { - displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; - } - currentContext()->throwValue(makeError( - QString("Cannot find module '%1' (%2)") - .arg(displayId) - .arg(detail), type)); - return QString(); - }; + auto displayId = moduleId; + if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { + displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; + } + auto message = QString("Cannot find module '%1' (%2)").arg(displayId); + auto ctx = currentContext(); // de-fuzz the input a little by restricting to rational sizes auto idLength = url.toString().length(); if (idLength < 1 || idLength > MAX_MODULE_ID_LENTGH) { - return resolverException( - QString("rejecting invalid module id size (%1 chars [1,%2])") - .arg(idLength).arg(MAX_MODULE_ID_LENTGH), - "RangeError" - ); + auto details = QString("rejecting invalid module id size (%1 chars [1,%2])") + .arg(idLength).arg(MAX_MODULE_ID_LENTGH); + ctx->throwValue(makeError(message.arg(details), "RangeError")); + return nullptr; } // this regex matches: absolute, dotted or path-like URLs @@ -1402,12 +1396,14 @@ QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& re url = defaultScriptsLoc; url.setPath(systemModulePath); if (!QFileInfo(url.toLocalFile()).isFile()) { - return resolverException("system module not found"); + ctx->throwValue(makeError(message.arg("system module not found"))); + return nullptr; } } if (url.isRelative()) { - return resolverException("could not resolve module id"); + ctx->throwValue(makeError(message.arg("could not resolve module id"))); + return nullptr; } // if it looks like a local file, verify that it's an allowed path and really a file @@ -1420,18 +1416,21 @@ QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& re bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { - return resolverException( + ctx->throwValue(makeError(message.arg( QString("path '%1' outside of origin script '%2' '%3'") .arg(PathUtils::stripFilename(url)) .arg(PathUtils::stripFilename(currentSandboxURL)) .arg(canonical.toString()) - ); + ))); + return nullptr; } if (!file.exists()) { - return resolverException("path does not exist: " + url.toLocalFile()); + ctx->throwValue(makeError(message.arg("path does not exist: " + url.toLocalFile()))); + return nullptr; } if (!file.isFile()) { - return resolverException("path is not a file: " + url.toLocalFile()); + ctx->throwValue(makeError(message.arg("path is not a file: " + url.toLocalFile()))); + return nullptr; } } @@ -1611,6 +1610,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { // serialize require calls so the ordering/caching works correctly across multiple Entities // note: this is a Recursive mutex so nested require's should not affected QMutexLocker locker(&_requireLock); + ExceptionEmitter tryCatcher(this, __FUNCTION__); // _requireDepth++; // QObject autoDecrement;connect(&autoDecrement, &QObject::destroyed, this, [this](){ _requireDepth--; }); @@ -1632,7 +1632,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { // start by resolving the moduleId into a fully-qualified path/URL QString modulePath = _requireResolve(moduleId); - if (hasUncaughtException()) { + if (modulePath.isNull() || tryCatcher.hasPending()) { // the resolver already threw an exception -- bail early return nullValue(); } From e91de1775ec192c610bf3c9596bc9cd0c30d6d53 Mon Sep 17 00:00:00 2001 From: humbletim Date: Thu, 16 Feb 2017 21:10:50 -0500 Subject: [PATCH 04/43] use a fully-qualified initializer when constructing BatchLoader --- libraries/script-engine/src/ScriptEngine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 882fec6c72..09d267f3ba 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -1532,7 +1532,7 @@ QScriptValue ScriptEngine::fetchModuleSource(const QString& modulePath, const bo qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath; scriptCache->deleteScript(modulePath); } - BatchLoader* loader = new BatchLoader({ modulePath }); + BatchLoader* loader = new BatchLoader(QList({ modulePath })); connect(loader, &BatchLoader::finished, this, onload); connect(this, &QObject::destroyed, loader, &QObject::deleteLater); // fail faster? (since require() blocks the engine thread while resolving dependencies) From 9b096513373662b3d88c61a20f6d9f42cb0dfd7c Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 1 Mar 2017 09:11:40 -0500 Subject: [PATCH 05/43] Move BaseScriptEngine from script-engine to shared. --- libraries/{script-engine => shared}/src/BaseScriptEngine.cpp | 0 libraries/{script-engine => shared}/src/BaseScriptEngine.h | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename libraries/{script-engine => shared}/src/BaseScriptEngine.cpp (100%) rename libraries/{script-engine => shared}/src/BaseScriptEngine.h (100%) diff --git a/libraries/script-engine/src/BaseScriptEngine.cpp b/libraries/shared/src/BaseScriptEngine.cpp similarity index 100% rename from libraries/script-engine/src/BaseScriptEngine.cpp rename to libraries/shared/src/BaseScriptEngine.cpp diff --git a/libraries/script-engine/src/BaseScriptEngine.h b/libraries/shared/src/BaseScriptEngine.h similarity index 100% rename from libraries/script-engine/src/BaseScriptEngine.h rename to libraries/shared/src/BaseScriptEngine.h From 40ba8185a06a02f1106d54424032c9b8456bc336 Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 1 Mar 2017 09:14:19 -0500 Subject: [PATCH 06/43] * Update per 21114-part2 changes. * Add explicit thread safety guards. * Add Entities.queryPropertyMetdata for unit testing Entity script module support. * Cleanup / commenting pass. --- .../src/EntityTreeRenderer.cpp | 1 + .../src/EntitiesScriptEngineProvider.h | 4 +- .../entities/src/EntityScriptingInterface.cpp | 109 +++++++ .../entities/src/EntityScriptingInterface.h | 20 ++ libraries/script-engine/src/ScriptEngine.cpp | 280 ++++++++++-------- libraries/script-engine/src/ScriptEngine.h | 13 +- libraries/shared/src/BaseScriptEngine.cpp | 129 ++++++-- libraries/shared/src/BaseScriptEngine.h | 43 ++- 8 files changed, 440 insertions(+), 159 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index bd25bcf905..2c07c368f9 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -146,6 +146,7 @@ void EntityTreeRenderer::clear() { void EntityTreeRenderer::reloadEntityScripts() { _entitiesScriptEngine->unloadAllEntityScripts(); + _entitiesScriptEngine->resetModuleCache(); foreach(auto entity, _entitiesInScene) { if (!entity->getScript().isEmpty()) { _entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true); diff --git a/libraries/entities/src/EntitiesScriptEngineProvider.h b/libraries/entities/src/EntitiesScriptEngineProvider.h index 69bf73e688..d87dd105c2 100644 --- a/libraries/entities/src/EntitiesScriptEngineProvider.h +++ b/libraries/entities/src/EntitiesScriptEngineProvider.h @@ -15,11 +15,13 @@ #define hifi_EntitiesScriptEngineProvider_h #include +#include #include "EntityItemID.h" class EntitiesScriptEngineProvider { public: virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) = 0; + virtual QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) = 0; }; -#endif // hifi_EntitiesScriptEngineProvider_h \ No newline at end of file +#endif // hifi_EntitiesScriptEngineProvider_h diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 540eba4511..df88194f9f 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -10,6 +10,9 @@ // #include "EntityScriptingInterface.h" +#include +#include + #include "EntityItemID.h" #include #include @@ -24,6 +27,7 @@ #include "ModelEntityItem.h" #include "QVariantGLM.h" #include "SimulationOwner.h" +#include "BaseScriptEngine.h" #include "ZoneEntityItem.h" #include "WebEntityItem.h" #include @@ -680,6 +684,111 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) { return client->reloadServerScript(entityID); } +bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto name = property.toString(); + auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); + QPointer engine = dynamic_cast(handler.engine()); + if (!engine) { + qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name; + return false; + } + connect(engine, &QObject::destroyed, this, [=]() { + qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine"); + }); + if (!handler.property("callback").isFunction()) { + qDebug() << "!handler.callback.isFunction" << engine; + engine->raiseException(engine->makeError("callback is not a function", "TypeError")); + return false; + } + if (name == "userData") { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); + QScriptValue err, result; + if (entity) { + auto JSON = engine->globalObject().property("JSON"); + auto parsed = JSON.property("parse").call(JSON, QScriptValueList({ entity->getUserData() })); + if (engine->hasUncaughtException()) { + err = engine->cloneUncaughtException(__FUNCTION__); + engine->clearExceptions(); + } else { + result = parsed; + } + } else { + err = engine->makeError("entity not found"); + } + QFutureWatcher *request = new QFutureWatcher; + connect(request, &QFutureWatcher::finished, engine, [=]() mutable { + if (!engine) { + qCDebug(entities) << "queryPropertyMetadata -- engine destroyed while inflight" << entityID << name; + return; + } + callScopedHandlerObject(handler, err, result); + request->deleteLater(); + }); + request->setFuture(QtConcurrent::run([]() -> QVariant { + QThread::sleep(1); + return 1; + })); + return true; + } else if (name == "script") { + using LocalScriptStatusRequest = QFutureWatcher; + LocalScriptStatusRequest *request = new LocalScriptStatusRequest; + connect(request, &LocalScriptStatusRequest::finished, engine, [=]() mutable { + if (!engine) { + qCDebug(entities) << "queryPropertyMetadata -- engine destroyed while inflight" << entityID << name; + return; + } + auto details = request->result().toMap(); + QScriptValue err, result; + if (details.contains("isError")) { + if (!details.contains("message")) { + details["message"] = details["errorInfo"]; + } + err = engine->makeError(engine->toScriptValue(details)); + } else { + details["success"] = true; + result = engine->toScriptValue(details); + } + callScopedHandlerObject(handler, err, result); + request->deleteLater(); + }); + request->setFuture(_entitiesScriptEngine->getLocalEntityScriptDetails(entityID)); + return true; + } else if (name == "serverScripts") { + auto client = DependencyManager::get(); + auto request = client->createScriptStatusRequest(entityID); + connect(request, &GetScriptStatusRequest::finished, engine, [=](GetScriptStatusRequest* request) mutable { + if (!engine) { + qCDebug(entities) << "queryPropertyMetadata -- engine destroyed while inflight" << entityID << name; + return; + } + QVariantMap details; + details["success"] = request->getResponseReceived(); + details["isRunning"] = request->getIsRunning(); + details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower(); + details["errorInfo"] = request->getErrorInfo(); + + QScriptValue err, result; + if (!details["success"].toBool()) { + if (!details.contains("message") && details.contains("errorInfo")) { + details["message"] = details["errorInfo"]; + } + if (details["message"].toString().isEmpty()) { + details["message"] = "entity server script details not found"; + } + err = engine->makeError(engine->toScriptValue(details)); + } else { + result = engine->toScriptValue(details); + } + callScopedHandlerObject(handler, err, result); + request->deleteLater(); + }); + request->start(); + return true; + } + engine->raiseException(engine->makeError("property has no mapped metadata: " + name)); + return false; +} + bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) { auto client = DependencyManager::get(); auto request = client->createScriptStatusRequest(entityID); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index e9f0637830..2c3c654528 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -211,6 +211,26 @@ public slots: Q_INVOKABLE RayToEntityIntersectionResult findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue()); Q_INVOKABLE bool reloadServerScripts(QUuid entityID); + + /**jsdoc + * Query for the available metadata behind one of an Entity's "magic" properties (eg: `script` and `serverScripts`). + * + * @function Entities.queryPropertyMetadata + * @param {EntityID} entityID The ID of the entity. + * @param {string} property The name of the property extended metadata is wanted for. + * @param {ResultCallback} callback Executes callback(err, result) with the query results. + */ + /**jsdoc + * Query for the available metadata behind one of an Entity's "magic" properties (eg: `script` and `serverScripts`). + * + * @function Entities.queryPropertyMetadata + * @param {EntityID} entityID The ID of the entity. + * @param {string} property The name of the property extended metadata is wanted for. + * @param {Object} thisObject The scoping "this" context that callback will be executed within. + * @param {ResultCallback} callbackOrMethodName Executes thisObject[callbackOrMethodName](err, result) with the query results. + */ + Q_INVOKABLE bool queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue()); + Q_INVOKABLE bool getServerScriptStatus(QUuid entityID, QScriptValue callback); Q_INVOKABLE void setLightsArePickable(bool value); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 09d267f3ba..c7364450a1 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -19,6 +19,9 @@ #include #include +#include +#include + #include #include @@ -74,6 +77,10 @@ const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT { "com.highfidelity.experimental.enableExtendedModuleCompatbility" }; +const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { + "com.highfidelity.experimental.enableExtendedJSExceptions" +}; + static const int MAX_MODULE_ID_LENTGH { 4096 }; static const int MAX_DEBUG_VALUE_LENGTH { 80 }; @@ -146,7 +153,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) } QString ScriptEngine::logException(const QScriptValue& exception) { - auto message = formatException(exception); + auto message = formatException(exception, _enableExtendedJSExceptions.get()); scriptErrorMessage(qPrintable(message)); return message; } @@ -338,7 +345,11 @@ void ScriptEngine::runInThread() { // The thread interface cannot live on itself, and we want to move this into the thread, so // the thread cannot have this as a parent. QThread* workerThread = new QThread(); +#ifdef Q_OS_LINUX + workerThread->setObjectName(QString("js:") + getFilename()); +#else workerThread->setObjectName(QString("Script Thread:") + getFilename()); +#endif moveToThread(workerThread); // NOTE: If you connect any essential signals for proper shutdown or cleanup of @@ -539,41 +550,37 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) { void ScriptEngine::resetModuleCache(bool deleteScriptCache) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "resetModuleCache"); + executeOnScriptThread([=](){ resetModuleCache(deleteScriptCache); }); return; } - { - QMutexLocker locker(&_requireLock); - auto jsRequire = globalObject().property("Script").property("require"); - auto cache = jsRequire.property("cache"); - auto cacheMeta = jsRequire.data(); + auto jsRequire = globalObject().property("Script").property("require"); + auto cache = jsRequire.property("cache"); + auto cacheMeta = jsRequire.data(); - if (deleteScriptCache) { - QScriptValueIterator it(cache); - while (it.hasNext()) { - it.next(); - if (it.flags() & QScriptValue::SkipInEnumeration) { - continue; - } - //scriptCache->deleteScript(it.name()); - qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; - cacheMeta.setProperty(it.name(), true); + if (deleteScriptCache) { + QScriptValueIterator it(cache); + while (it.hasNext()) { + it.next(); + if (it.flags() & QScriptValue::SkipInEnumeration) { + continue; } + //scriptCache->deleteScript(it.name()); + qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; + cacheMeta.setProperty(it.name(), true); } - //_debugDump("cacheMeta", cacheMeta); - cache = newObject(); - if (!cacheMeta.isObject()) { - cacheMeta = newObject(); - cacheMeta.setProperty("id", "Script.require.cacheMeta"); - cacheMeta.setProperty("type", "cacheMeta"); - jsRequire.setData(cacheMeta); - } - cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration); -#if DEBUG_JS_MODULES - cache.setProperty("__meta__", cacheMeta, HIDDEN_PROP_FLAGS); -#endif - jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable); } + cache = newObject(); + if (!cacheMeta.isObject()) { + cacheMeta = newObject(); + cacheMeta.setProperty("id", "Script.require.cacheMeta"); + cacheMeta.setProperty("type", "cacheMeta"); + jsRequire.setData(cacheMeta); + } + cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration); +#if DEBUG_JS_MODULES + cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); +#endif + jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable); } void ScriptEngine::init() { @@ -916,6 +923,11 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler(). } +// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE +QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { + return BaseScriptEngine::evaluateInClosure(closure, program); +} + QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { if (DependencyManager::get()->isStopped()) { return QScriptValue(); // bail early @@ -938,29 +950,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi // Check syntax auto syntaxError = lintScript(sourceCode, fileName); if (syntaxError.isError()) { - if (isEvaluating()) { - currentContext()->throwValue(syntaxError); - } else { + if (!isEvaluating()) { syntaxError.setProperty("detail", "evaluate"); - emit unhandledException(syntaxError); } + raiseException(syntaxError); + maybeEmitUncaughtException(__FUNCTION__); return syntaxError; } QScriptProgram program { sourceCode, fileName, lineNumber }; if (program.isNull()) { // can this happen? auto err = makeError("could not create QScriptProgram for " + fileName); - emit unhandledException(err); + raiseException(err); + maybeEmitUncaughtException(__FUNCTION__); return err; } QScriptValue result; { result = BaseScriptEngine::evaluate(program); - if (!isEvaluating() && hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(__FUNCTION__)); - clearExceptions(); - } + maybeEmitUncaughtException(__FUNCTION__); } return result; } @@ -983,10 +992,7 @@ void ScriptEngine::run() { { evaluate(_scriptContents, _fileNameString); - if (!isEvaluating() && hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(__FUNCTION__)); - clearExceptions(); - } + maybeEmitUncaughtException(__FUNCTION__); } #ifdef _WIN32 // VS13 does not sleep_until unless it uses the system_clock, see: @@ -1359,24 +1365,30 @@ void ScriptEngine::print(const QString& message) { // Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js) QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return QString(); + } QUrl defaultScriptsLoc = defaultScriptsLocation(); QUrl url(moduleId); - ExceptionEmitter tryCatcher(this, __FUNCTION__); auto displayId = moduleId; if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; } auto message = QString("Cannot find module '%1' (%2)").arg(displayId); - auto ctx = currentContext(); + + auto throwResolveError = [&](const QScriptValue& error) -> QString { + raiseException(error); + maybeEmitUncaughtException("require.resolve"); + return nullptr; + }; // de-fuzz the input a little by restricting to rational sizes auto idLength = url.toString().length(); if (idLength < 1 || idLength > MAX_MODULE_ID_LENTGH) { auto details = QString("rejecting invalid module id size (%1 chars [1,%2])") .arg(idLength).arg(MAX_MODULE_ID_LENTGH); - ctx->throwValue(makeError(message.arg(details), "RangeError")); - return nullptr; + return throwResolveError(makeError(message.arg(details), "RangeError")); } // this regex matches: absolute, dotted or path-like URLs @@ -1396,14 +1408,12 @@ QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& re url = defaultScriptsLoc; url.setPath(systemModulePath); if (!QFileInfo(url.toLocalFile()).isFile()) { - ctx->throwValue(makeError(message.arg("system module not found"))); - return nullptr; + return throwResolveError(makeError(message.arg("system module not found"))); } } if (url.isRelative()) { - ctx->throwValue(makeError(message.arg("could not resolve module id"))); - return nullptr; + return throwResolveError(makeError(message.arg("could not resolve module id"))); } // if it looks like a local file, verify that it's an allowed path and really a file @@ -1416,34 +1426,35 @@ QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& re bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { - ctx->throwValue(makeError(message.arg( + return throwResolveError(makeError(message.arg( QString("path '%1' outside of origin script '%2' '%3'") .arg(PathUtils::stripFilename(url)) .arg(PathUtils::stripFilename(currentSandboxURL)) .arg(canonical.toString()) ))); - return nullptr; } if (!file.exists()) { - ctx->throwValue(makeError(message.arg("path does not exist: " + url.toLocalFile()))); - return nullptr; + return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile()))); } if (!file.isFile()) { - ctx->throwValue(makeError(message.arg("path is not a file: " + url.toLocalFile()))); - return nullptr; + return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile()))); } } + maybeEmitUncaughtException(__FUNCTION__); return url.toString(); } // retrieves the current parent module from the JS scope chain QScriptValue ScriptEngine::currentModule() { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } auto jsRequire = globalObject().property("Script").property("require"); auto cache = jsRequire.property("cache"); auto candidate = QScriptValue(); - for(auto ctx = currentContext(); ctx && !candidate.isObject(); ctx = ctx->parentContext()) { - QScriptContextInfo contextInfo { ctx }; + for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) { + QScriptContextInfo contextInfo { c }; candidate = cache.property(contextInfo.fileName()); } if (!candidate.isObject()) { @@ -1459,7 +1470,7 @@ bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QS if (children.isArray()) { auto key = module.property("id"); auto length = children.property("length").toInt32(); - for(int i=0; i < length; i++) { + for (int i=0; i < length; i++) { if (children.property(i).property("id").strictlyEquals(key)) { qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; children.setProperty(i, module); @@ -1488,7 +1499,7 @@ QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptVal closure.setProperty("exports", exports); // make the closure available to module instantiation - module.setProperty("__closure__", closure, HIDDEN_PROP_FLAGS); + module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS); // for consistency with Node.js Module module.setProperty("id", modulePath, READONLY_PROP_FLAGS); @@ -1524,7 +1535,7 @@ QScriptValue ScriptEngine::fetchModuleSource(const QString& modulePath, const bo req.setProperty("url", url); req.setProperty("status", status); req.setProperty("success", ScriptCache::isSuccessStatus(status)); - req.setProperty("contents", contents, HIDDEN_PROP_FLAGS); + req.setProperty("contents", contents, READONLY_HIDDEN_PROP_FLAGS); } }; @@ -1568,73 +1579,54 @@ QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const Q auto modulePath = module.property("filename").toString(); auto closure = module.property("__closure__"); - qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes").arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); + qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes") + .arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); - { - ExceptionEmitter tryCatch(this, __FUNCTION__); - - if (module.property("content-type").toString() == "application/json") { - qCDebug(scriptengine_module) << "... parsing as JSON"; -#ifdef DEV_OTHER_MODULE_JSON_PARSE - auto JSON = globalObject().property("JSON"); - auto parse = JSON.property("parse"); - if (!parse.isFunction()) { - currentContext()->throwValue(makeError("global JSON.parse is not a function", "EvalError")); - return nullValue(); - } - result = parse.call(JSON, QScriptValueList({ sourceCode })); - closure.property("module").setProperty("exports", result); - return result; -#endif - closure.setProperty("__json", sourceCode); - result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath }); - } else { - // scoped vars for consistency with Node.js - closure.setProperty("require", module.property("require")); - closure.setProperty("__filename", modulePath, HIDDEN_PROP_FLAGS); - closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), HIDDEN_PROP_FLAGS); - result = evaluateInClosure(closure, { sourceCode, modulePath }); - } + if (module.property("content-type").toString() == "application/json") { + qCDebug(scriptengine_module) << "... parsing as JSON"; + closure.setProperty("__json", sourceCode); + result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath }); + } else { + // scoped vars for consistency with Node.js + closure.setProperty("require", module.property("require")); + closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS); + closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); + result = evaluateInClosure(closure, { sourceCode, modulePath }); } + maybeEmitUncaughtException(__FUNCTION__); return result; } // CommonJS/Node.js like require/module support QScriptValue ScriptEngine::require(const QString& moduleId) { qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; - if (QThread::currentThread() != thread()) { - qCDebug(scriptengine_module) << moduleId << " threads mismatch"; - return nullValue(); + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); } - // serialize require calls so the ordering/caching works correctly across multiple Entities - // note: this is a Recursive mutex so nested require's should not affected - QMutexLocker locker(&_requireLock); - ExceptionEmitter tryCatcher(this, __FUNCTION__); - // _requireDepth++; - // QObject autoDecrement;connect(&autoDecrement, &QObject::destroyed, this, [this](){ _requireDepth--; }); - auto jsRequire = globalObject().property("Script").property("require"); auto cacheMeta = jsRequire.data(); auto cache = jsRequire.property("cache"); auto parent = currentModule(); - auto throwModuleError = [this, &cache](const QString& modulePath, const QScriptValue& error) { + auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) { cache.setProperty(modulePath, nullValue()); if (!error.isNull()) { #ifdef DEBUG_JS_MODULES qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); #endif - currentContext()->throwValue(makeError(error)); + raiseException(error); } - return nullValue(); + maybeEmitUncaughtException("module"); + return unboundNullValue(); }; // start by resolving the moduleId into a fully-qualified path/URL QString modulePath = _requireResolve(moduleId); - if (modulePath.isNull() || tryCatcher.hasPending()) { + if (modulePath.isNull() || hasUncaughtException()) { // the resolver already threw an exception -- bail early - return nullValue(); + maybeEmitUncaughtException(__FUNCTION__); + return unboundNullValue(); } // check the resolved path against the cache @@ -1656,6 +1648,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)") .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString()); registerModuleWithParent(module, parent); + maybeEmitUncaughtException(__FUNCTION__); return exports; } @@ -1705,7 +1698,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { #ifdef DEBUG_JS _debugDump("this", thisObject); #endif - _applyUserOptions(module, thisObject); + applyUserOptions(module, thisObject); } } @@ -1725,11 +1718,15 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")"; + maybeEmitUncaughtException(__FUNCTION__); return module.property("exports"); } // User-configurable override options -void ScriptEngine::_applyUserOptions(QScriptValue& module, QScriptValue& options) { +void ScriptEngine::applyUserOptions(QScriptValue& module, QScriptValue& options) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } if (!options.isValid()) { return; } @@ -1770,6 +1767,7 @@ void ScriptEngine::_applyUserOptions(QScriptValue& module, QScriptValue& options qCDebug(scriptengine_module) << "module.closure['global'] = options.global"; } } + maybeEmitUncaughtException(__FUNCTION__); } // If a callback is specified, the included files will be loaded asynchronously and the callback will be called @@ -1777,6 +1775,9 @@ void ScriptEngine::_applyUserOptions(QScriptValue& module, QScriptValue& options // If no callback is specified, the included files will be loaded synchronously and will block execution until // all of the files have finished loading. void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } if (DependencyManager::get()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename()); @@ -1886,6 +1887,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } if (DependencyManager::get()->isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); @@ -1955,6 +1959,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const emit entityScriptDetailsUpdated(); } +QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) { + static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) }; + QVariantMap map; + if (entityID.isNull()) { + // TODO: find better way to report JS Error across thread/process boundaries + map["isError"] = true; + map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID"; + } else { +#ifdef DEBUG_ENTITY_STATES + qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread(); +#endif + EntityScriptDetails scriptDetails; + if (getEntityScriptDetails(entityID, scriptDetails)) { +#ifdef DEBUG_ENTITY_STATES + qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread(); +#endif + map["isRunning"] = isEntityScriptRunning(entityID); + map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower(); + map["errorInfo"] = scriptDetails.errorInfo; + map["entityID"] = entityID.toString(); +#ifdef DEBUG_ENTITY_STATES + { + auto debug = QVariantMap(); + debug["script"] = scriptDetails.scriptText; + debug["scriptObject"] = scriptDetails.scriptObject.toVariant(); + debug["lastModified"] = (qlonglong)scriptDetails.lastModified; + debug["sandboxURL"] = scriptDetails.definingSandboxURL; + map["debug"] = debug; + } +#endif + } else { +#ifdef DEBUG_ENTITY_STATES + qDebug() << "!gotEntityScriptDetails" << QThread::currentThread(); +#endif + map["isError"] = true; + map["errorInfo"] = "Entity script details unavailable"; + map["entityID"] = entityID.toString(); + } + } + return map; +} + +QFuture ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) { + return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID); +} + bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { auto it = _entityScripts.constFind(entityID); if (it == _entityScripts.constEnd()) { @@ -2093,10 +2143,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& auto scriptCache = DependencyManager::get(); // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management - QWeakPointer weakRef(sharedFromThis()); + QWeakPointer weakRef(sharedFromThis()); scriptCache->getScriptContents(entityScript, [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { - QSharedPointer strongRef(weakRef); + QSharedPointer strongRef(weakRef); if (!strongRef) { qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!"; return; @@ -2215,13 +2265,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{ - auto context = sandbox.currentContext(); - if (context) { qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")"; // Guard against infinite loops and non-performant code - context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)); - } + sandbox.raiseException( + sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)) + ); }); testConstructor = sandbox.evaluate(program); @@ -2237,7 +2286,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (exception.isError()) { // create a local copy using makeError to decouple from the sandbox engine exception = makeError(exception); - setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } @@ -2288,7 +2337,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (entityScriptObject.isError()) { auto exception = entityScriptObject; - setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } @@ -2420,10 +2469,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s #else operation(); #endif - if (!isEvaluating() && hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__)); - clearExceptions(); - } + maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__); currentEntityIdentifier = oldIdentifier; currentSandboxURL = oldSandboxURL; } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index f64dec44c7..ef6f3b6896 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -79,7 +79,7 @@ public: QUrl definingSandboxURL { QUrl("about:EntityScript") }; }; -class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis { +class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider { Q_OBJECT Q_PROPERTY(QString context READ getContext) public: @@ -138,6 +138,8 @@ public: /// evaluate some code in the context of the ScriptEngine and return the result Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName, int lineNumber = 1); // this is also used by the script tool widget + Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); + /// 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 @@ -181,6 +183,8 @@ public: Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) { return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING; } + QVariant cloneEntityScriptDetails(const EntityItemID& entityID); + QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) override; Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method Q_INVOKABLE void unloadAllEntityScripts(); @@ -286,7 +290,6 @@ protected: QHash _entityScripts; QHash _occupiedScriptURLs; QList _deferredEntityLoads; - QMutex _requireLock { QMutex::Recursive }; bool _isThreaded { false }; QScriptEngineDebugger* _debugger { nullptr }; @@ -312,8 +315,12 @@ protected: std::chrono::microseconds _totalTimerExecution { 0 }; static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT; + static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; + Setting::Handle _enableExtendedModuleCompatbility { _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT, false }; - void _applyUserOptions(QScriptValue& module, QScriptValue& options); + Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; + + void applyUserOptions(QScriptValue& module, QScriptValue& options); }; #endif // hifi_ScriptEngine_h diff --git a/libraries/shared/src/BaseScriptEngine.cpp b/libraries/shared/src/BaseScriptEngine.cpp index 16308c0650..d803e85ed6 100644 --- a/libraries/shared/src/BaseScriptEngine.cpp +++ b/libraries/shared/src/BaseScriptEngine.cpp @@ -10,6 +10,7 @@ // #include "BaseScriptEngine.h" +#include "SharedLogging.h" #include #include @@ -18,18 +19,26 @@ #include #include -#include "ScriptEngineLogging.h" #include "Profile.h" -const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { - "com.highfidelity.experimental.enableExtendedJSExceptions" -}; - const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" }; const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " }; +bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) { + if (QThread::currentThread() == thread) { + return true; + } + qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3") + .arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName()); + qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)"; + return false; +} + // engine-aware JS Error copier and factory QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } auto other = _other; if (other.isString()) { other = newObject(); @@ -41,7 +50,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri } if (!proto.isFunction()) { #ifdef DEBUG_JS_EXCEPTIONS - qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; + qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; #endif proto = globalObject().property("Error"); } @@ -64,6 +73,9 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri // check syntax and when there are issues returns an actual "SyntaxError" with the details QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } const auto syntaxCheck = checkSyntax(sourceCode); if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { auto err = globalObject().property("SyntaxError") @@ -82,13 +94,16 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri } return err; } - return undefinedValue(); + return QScriptValue(); } // this pulls from the best available information to create a detailed snapshot of the current exception QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } if (!hasUncaughtException()) { - return QScriptValue(); + return unboundNullValue(); } auto exception = uncaughtException(); // ensure the error object is engine-local @@ -144,7 +159,10 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail return err; } -QString BaseScriptEngine::formatException(const QScriptValue& exception) { +QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return QString(); + } QString note { "UncaughtException" }; QString result; @@ -156,8 +174,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) { const auto lineNumber = exception.property("lineNumber").toString(); const auto stacktrace = exception.property("stack").toString(); - if (_enableExtendedJSExceptions.get()) { - // This setting toggles display of the hints now being added during the loading process. + if (includeExtendedDetails) { + // Display additional exception / troubleshooting hints that can be added via the custom Error .detail property // Example difference: // [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n... // [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n... @@ -173,14 +191,39 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) { return result; } +bool BaseScriptEngine::raiseException(const QScriptValue& exception) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return false; + } + if (currentContext()) { + // we have an active context / JS stack frame so throw the exception per usual + currentContext()->throwValue(makeError(exception)); + return true; + } else { + // we are within a pure C++ stack frame (ie: being called directly by other C++ code) + // in this case no context information is available so just emit the exception for reporting + emit unhandledException(makeError(exception)); + } + return false; +} + +bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return false; + } + if (!isEvaluating() && hasUncaughtException()) { + emit unhandledException(cloneUncaughtException(debugHint)); + clearExceptions(); + return true; + } + return false; +} + QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { PROFILE_RANGE(script, "evaluateInClosure"); - if (QThread::currentThread() != thread()) { - qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only."; - // note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE - return QScriptValue(); + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); } - const auto fileName = program.fileName(); const auto shortName = QUrl(fileName).fileName(); @@ -189,7 +232,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co auto global = closure.property("global"); if (global.isObject()) { #ifdef DEBUG_JS - qCDebug(scriptengine) << " setting global = closure.global" << shortName; + qCDebug(shared) << " setting global = closure.global" << shortName; #endif oldGlobal = globalObject(); setGlobalObject(global); @@ -200,34 +243,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co auto thiz = closure.property("this"); if (thiz.isObject()) { #ifdef DEBUG_JS - qCDebug(scriptengine) << " setting this = closure.this" << shortName; + qCDebug(shared) << " setting this = closure.this" << shortName; #endif context->setThisObject(thiz); } context->pushScope(closure); #ifdef DEBUG_JS - qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); + qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); #endif { result = BaseScriptEngine::evaluate(program); if (hasUncaughtException()) { auto err = cloneUncaughtException(__FUNCTION__); #ifdef DEBUG_JS_EXCEPTIONS - qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); + qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); err.setProperty("_result", result); #endif result = err; } } #ifdef DEBUG_JS - qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); + qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); #endif popContext(); if (oldGlobal.isValid()) { #ifdef DEBUG_JS - qCDebug(scriptengine) << " restoring global" << shortName; + qCDebug(shared) << " restoring global" << shortName; #endif setGlobalObject(oldGlobal); } @@ -236,7 +279,6 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co } // Lambda - QScriptValue BaseScriptEngine::newLambdaFunction(std::function operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) { auto lambda = new Lambda(this, operation, data); auto object = newQObject(lambda, ownership); @@ -262,26 +304,57 @@ Lambda::Lambda(QScriptEngine *engine, std::functionthread(), __FUNCTION__)) { + return BaseScriptEngine::unboundNullValue(); + } return operation(engine->currentContext(), engine); } +QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto engine = scopeOrCallback.engine(); + if (!engine) { + return scopeOrCallback; + } + auto scope = QScriptValue(); + auto callback = scopeOrCallback; + if (scopeOrCallback.isObject()) { + if (methodOrName.isString()) { + scope = scopeOrCallback; + callback = scope.property(methodOrName.toString()); + } else if (methodOrName.isFunction()) { + scope = scopeOrCallback; + callback = methodOrName; + } + } + auto handler = engine->newObject(); + handler.setProperty("scope", scope); + handler.setProperty("callback", callback); + return handler; +} + +QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) { + return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result })); +} + #ifdef DEBUG_JS void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } if (!header.isEmpty()) { - qCDebug(scriptengine) << header; + qCDebug(shared) << header; } if (!object.isObject()) { - qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString(); + qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString(); return; } QScriptValueIterator it(object); while (it.hasNext()) { it.next(); - qCDebug(scriptengine) << it.name() << ":" << it.value().toString(); + qCDebug(shared) << it.name() << ":" << it.value().toString(); } if (!footer.isEmpty()) { - qCDebug(scriptengine) << footer; + qCDebug(shared) << footer; } } #endif - diff --git a/libraries/shared/src/BaseScriptEngine.h b/libraries/shared/src/BaseScriptEngine.h index 27a6eff33d..138e46fafa 100644 --- a/libraries/shared/src/BaseScriptEngine.h +++ b/libraries/shared/src/BaseScriptEngine.h @@ -16,38 +16,61 @@ #include #include -#include "SettingHandle.h" - // common base class for extending QScriptEngine itself -class BaseScriptEngine : public QScriptEngine { +class BaseScriptEngine : public QScriptEngine, public QEnableSharedFromThis { Q_OBJECT public: static const QString SCRIPT_EXCEPTION_FORMAT; static const QString SCRIPT_BACKTRACE_SEP; - BaseScriptEngine() {} + // threadsafe "unbound" version of QScriptEngine::nullValue() + static const QScriptValue unboundNullValue() { return QScriptValue(0, QScriptValue::NullValue); } - Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); + BaseScriptEngine() {} Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error"); - Q_INVOKABLE QString formatException(const QScriptValue& exception); - QScriptValue cloneUncaughtException(const QString& detail = QString()); + Q_INVOKABLE QString formatException(const QScriptValue& exception, bool includeExtendedDetails); + QScriptValue cloneUncaughtException(const QString& detail = QString()); + QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); + + // if there is a pending exception and we are at the top level (non-recursive) stack frame, this emits and resets it + bool maybeEmitUncaughtException(const QString& debugHint = QString()); + + // if the currentContext() is valid then throw the passed exception; otherwise, immediately emit it. + // note: this is used in cases where C++ code might call into JS API methods directly + bool raiseException(const QScriptValue& exception); + + // helper to detect and log warnings when other code invokes QScriptEngine/BaseScriptEngine in thread-unsafe ways + static bool IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method); signals: void unhandledException(const QScriptValue& exception); protected: - void _emitUnhandledException(const QScriptValue& exception); + // like `newFunction`, but allows mapping inline C++ lambdas with captures as callable QScriptValues + // even though the context/engine parameters are redundant in most cases, the function signature matches `newFunction` + // anyway so that newLambdaFunction can be used to rapidly prototype / test utility APIs and then if becoming + // permanent more easily promoted into regular static newFunction scenarios. QScriptValue newLambdaFunction(std::function operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership); - static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; - Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; #ifdef DEBUG_JS static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString()); #endif }; +// Standardized CPS callback helpers (see: http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/) +// These two helpers allow async JS APIs that use a callback parameter to be more friendly to scripters by accepting thisObject +// context and adopting a consistent and intuitable callback signature: +// function callback(err, result) { if (err) { ... } else { /* do stuff with result */ } } +// +// To use, first pass the user-specified callback args in the same order used with optionally-scoped Qt signal connections: +// auto handler = makeScopedHandlerObject(scopeOrCallback, optionalMethodOrName); +// And then invoke the scoped handler later per CPS conventions: +// auto result = callScopedHandlerObject(handler, err, result); +QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName); +QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result); + // Lambda helps create callable QScriptValues out of std::functions: // (just meant for use from within the script engine itself) class Lambda : public QObject { From 143b67e47dafd7cd1a042ec5ed6b43dd30b665b4 Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 1 Mar 2017 09:20:53 -0500 Subject: [PATCH 07/43] Add require/module unit tests --- .../developer/libraries/jasmine/hifi-boot.js | 11 +- .../tests/unit_tests/moduleTests/cycles/a.js | 9 + .../tests/unit_tests/moduleTests/cycles/b.js | 9 + .../unit_tests/moduleTests/cycles/main.js | 13 + .../entity/entityConstructorAPIException.js | 12 + .../entity/entityConstructorModule.js | 21 ++ .../entity/entityConstructorNested.js | 13 + .../entity/entityConstructorNested2.js | 24 ++ .../entityConstructorRequireException.js | 9 + .../entity/entityPreloadAPIError.js | 12 + .../entity/entityPreloadRequire.js | 10 + .../tests/unit_tests/moduleTests/example.json | 9 + .../moduleTests/exceptions/exception.js | 3 + .../exceptions/exceptionInFunction.js | 37 ++ .../tests/unit_tests/moduleUnitTests.js | 317 ++++++++++++++++++ .../developer/tests/unit_tests/package.json | 6 + 16 files changed, 513 insertions(+), 2 deletions(-) create mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/a.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/b.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/main.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/example.json create mode 100644 scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js create mode 100644 scripts/developer/tests/unit_tests/moduleUnitTests.js create mode 100644 scripts/developer/tests/unit_tests/package.json diff --git a/scripts/developer/libraries/jasmine/hifi-boot.js b/scripts/developer/libraries/jasmine/hifi-boot.js index f490a3618f..8757550ae8 100644 --- a/scripts/developer/libraries/jasmine/hifi-boot.js +++ b/scripts/developer/libraries/jasmine/hifi-boot.js @@ -6,7 +6,7 @@ var lastSpecStartTime; function ConsoleReporter(options) { var startTime = new Date().getTime(); - var errorCount = 0; + var errorCount = 0, pending = []; this.jasmineStarted = function (obj) { print('Jasmine started with ' + obj.totalSpecsDefined + ' tests.'); }; @@ -15,11 +15,14 @@ var endTime = new Date().getTime(); print('
'); if (errorCount === 0) { - print ('All tests passed!'); + print ('All enabled tests passed!'); } else { print('Tests completed with ' + errorCount + ' ' + ERROR + '.'); } + if (pending.length) + print ('disabled:
   '+ + pending.join('
   ')+'
'); print('Tests completed in ' + (endTime - startTime) + 'ms.'); }; this.suiteStarted = function(obj) { @@ -32,6 +35,10 @@ lastSpecStartTime = new Date().getTime(); }; this.specDone = function(obj) { + if (obj.status === 'pending') { + pending.push(obj.fullName); + return print('...(pending ' + obj.fullName +')'); + } var specEndTime = new Date().getTime(); var symbol = obj.status === PASSED ? '' + CHECKMARK + '' : diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js new file mode 100644 index 0000000000..7934180da7 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js @@ -0,0 +1,9 @@ +var a = exports; +a.done = false; +var b = require('./b.js'); +a.done = true; +a.name = 'a'; +a['a.done?'] = a.done; +a['b.done?'] = b.done; + +print('from a.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js new file mode 100644 index 0000000000..285f176597 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js @@ -0,0 +1,9 @@ +var b = exports; +b.done = false; +var a = require('./a.js'); +b.done = true; +b.name = 'b'; +b['a.done?'] = a.done; +b['b.done?'] = b.done; + +print('from b.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js new file mode 100644 index 0000000000..2e9a878c82 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js @@ -0,0 +1,13 @@ +print('main.js'); +var a = require('./a.js'), + b = require('./b.js'); + +print('from main.js a.done =', a.done, 'and b.done =', b.done); + +module.exports = { + name: 'main', + a: a, + b: b, + 'a.done?': a.done, + 'b.done?': b.done, +}; diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js new file mode 100644 index 0000000000..b1bc0e33e4 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js @@ -0,0 +1,12 @@ +// test module method exception being thrown within main constructor +(function() { + var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); + print(Script.resolvePath(''), "apiMethod", apiMethod); + // this next line throws from within apiMethod + print(apiMethod()); + return { + preload: function(uuid) { + print("entityConstructorAPIException::preload -- never seen --", uuid, Script.resolvePath('')); + }, + }; +}) diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js new file mode 100644 index 0000000000..5f0e8a5938 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js @@ -0,0 +1,21 @@ +// test dual-purpose module and standalone Entity script +function MyEntity(filename) { + return { + preload: function(uuid) { + print("entityConstructorModule.js::preload"); + if (typeof module === 'object') { + print("module.filename", module.filename); + print("module.parent.filename", module.parent && module.parent.filename); + } + }, + clickDownOnEntity: function(uuid, evt) { + print("entityConstructorModule.js::clickDownOnEntity"); + }, + }; +} + +try { + module.exports = MyEntity; +} catch(e) {} +print('entityConstructorModule::MyEntity', typeof MyEntity); +(MyEntity) diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js new file mode 100644 index 0000000000..5a2b8d5974 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js @@ -0,0 +1,13 @@ +// test Entity constructor based on inherited constructor from a module +function constructor() { + print("entityConstructorNested::constructor"); + var MyEntity = Script.require('./entityConstructorModule.js'); + return new MyEntity("-- created from entityConstructorNested --"); +} + +try { + module.exports = constructor; +} catch(e) { + constructor; +} + diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js new file mode 100644 index 0000000000..85a6b977b0 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js @@ -0,0 +1,24 @@ +// test Entity constructor based on nested, inherited module constructors +function constructor() { + print("entityConstructorNested2::constructor"); + + // inherit from entityConstructorNested + var Entity = Script.require('./entityConstructorNested.js'); + function SubEntity() {} + SubEntity.prototype = new MyEntity('-- created from entityConstructorNested2 --'); + + // create new instance + var entity = new SubEntity(); + // "override" clickDownOnEntity for just this new instance + entity.clickDownOnEntity = function(uuid, evt) { + print("entityConstructorNested2::clickDownOnEntity"); + SubEntity.prototype.clickDownOnEntity.apply(this, arguments); + }; + return entity; +} + +try { + module.exports = constructor; +} catch(e) { + constructor; +} diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js new file mode 100644 index 0000000000..269ca8e7f0 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js @@ -0,0 +1,9 @@ +// test module-related exception from within "require" evaluation itself +(function() { + var mod = Script.require('../exceptions/exception.js'); + return { + preload: function(uuid) { + print("entityConstructorRequireException::preload (never happens)", uuid, Script.resolvePath('')); + }, + }; +}) diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js new file mode 100644 index 0000000000..3be0b50d43 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js @@ -0,0 +1,12 @@ +// test module method exception being thrown within preload +(function() { + var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); + print(Script.resolvePath(''), "apiMethod", apiMethod); + return { + preload: function(uuid) { + // this next line throws from within apiMethod + print(apiMethod()); + print("entityPreloadAPIException::preload -- never seen --", uuid, Script.resolvePath('')); + }, + }; +}) diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js new file mode 100644 index 0000000000..fc70838c80 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js @@ -0,0 +1,10 @@ +// test requiring a module from within preload +(function constructor() { + return { + preload: function(uuid) { + print("entityPreloadRequire::preload"); + var example = Script.require('../example.json'); + print("entityPreloadRequire::example::name", example.name); + }, + }; +}) diff --git a/scripts/developer/tests/unit_tests/moduleTests/example.json b/scripts/developer/tests/unit_tests/moduleTests/example.json new file mode 100644 index 0000000000..42d7fe07da --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/example.json @@ -0,0 +1,9 @@ +{ + "name": "Example JSON Module", + "last-modified": 1485789862, + "config": { + "title": "My Title", + "width": 800, + "height": 600 + } +} diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js new file mode 100644 index 0000000000..636ee82f79 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js @@ -0,0 +1,3 @@ +module.exports = "n/a"; +throw new Error('exception on line 2'); + diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js new file mode 100644 index 0000000000..dc2ce3c438 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js @@ -0,0 +1,37 @@ +// dummy lines to make sure exception line number is well below parent test script +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + + +function myfunc() { + throw new Error('exception on line 32 in myfunc'); + return "myfunc"; +} +module.exports = myfunc; +if (Script[module.filename] === 'throw') + myfunc(); diff --git a/scripts/developer/tests/unit_tests/moduleUnitTests.js b/scripts/developer/tests/unit_tests/moduleUnitTests.js new file mode 100644 index 0000000000..c1c20d6980 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleUnitTests.js @@ -0,0 +1,317 @@ +/* eslint-env jasmine */ + +var isNode = instrument_testrunner(); + +var NETWORK_describe = xdescribe, + INTERFACE_describe = !isNode ? describe : xdescribe, + NODE_describe = isNode ? describe : xdescribe; + +print("DESCRIBING"); +describe('require', function() { + describe('resolve', function() { + it('should resolve relative filenames', function() { + var expected = Script.resolvePath('./moduleTests/example.json'); + expect(require.resolve('./moduleTests/example.json')).toEqual(expected); + }); + }); + + describe('JSON', function() { + it('should import .json modules', function() { + var example = require('./moduleTests/example.json'); + expect(example.name).toEqual('Example JSON Module'); + }); + INTERFACE_describe('inteface', function() { + NETWORK_describe('network', function() { + //xit('should import #content-type=application/json modules', function() { + // var results = require('https://jsonip.com#content-type=application/json'); + // expect(results.ip).toMatch(/^[.0-9]+$/); + //}); + it('should import content-type: application/json modules', function() { + var scope = { 'content-type': 'application/json' }; + var results = require.call(scope, 'https://jsonip.com'); + expect(results.ip).toMatch(/^[.0-9]+$/); + }); + }); + }); + + }); + + INTERFACE_describe('system', function() { + it('require(id)', function() { + expect(require('vec3')).toEqual(jasmine.any(Function)); + }); + it('require(id).function', function() { + expect(require('vec3')().isValid).toEqual(jasmine.any(Function)); + }); + }); + + describe('exceptions', function() { + it('should reject blank "" module identifiers', function() { + expect(function() { + require.resolve(''); + }).toThrowError(/Cannot find/); + }); + it('should reject excessive identifier sizes', function() { + expect(function() { + require.resolve(new Array(8193).toString()); + }).toThrowError(/Cannot find/); + }); + it('should reject implicitly-relative filenames', function() { + expect(function() { + var mod = require.resolve('example.js'); + }).toThrowError(/Cannot find/); + }); + it('should reject non-existent filenames', function() { + expect(function() { + var mod = require.resolve('./404error.js'); + }).toThrowError(/Cannot find/); + }); + it('should reject identifiers resolving to a directory', function() { + expect(function() { + var mod = require.resolve('.'); + //console.warn('resolved(.)', mod); + }).toThrowError(/Cannot find/); + expect(function() { + var mod = require.resolve('..'); + //console.warn('resolved(..)', mod); + }).toThrowError(/Cannot find/); + expect(function() { + var mod = require.resolve('../'); + //console.warn('resolved(../)', mod); + }).toThrowError(/Cannot find/); + }); + if (typeof MODE !== 'undefined' && MODE !== 'node') { + it('should reject non-system, extensionless identifiers', function() { + expect(function() { + require.resolve('./example'); + }).toThrowError(/Cannot find/); + }); + } + }); + + describe('cache', function() { + it('should cache modules by resolved module id', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + example['.test'] = value; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + expect(example2).toBe(example); + expect(example2['.test']).toBe(example['.test']); + }); + it('should reload cached modules set to null', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + example['.test'] = value; + require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')] = null; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + expect(example2).not.toBe(example); + expect(example2['.test']).not.toBe(example['.test']); + }); + it('should reload when module property is deleted', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + example['.test'] = value; + delete require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')]; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + expect(example2).not.toBe(example); + expect(example2['.test']).not.toBe(example['.test']); + }); + }); + + describe('cyclic dependencies', function() { + describe('should allow lazy-ref cyclic module resolution', function() { + const MODULE_PATH = './moduleTests/cycles/main.js'; + var main; + beforeEach(function() { + try { this._print = print; } catch(e) {} + // for this test print is no-op'd so it doesn't disrupt the reporter output + //console = typeof console === 'object' ? console : { log: function() {} }; + print = function() {}; + Script.resetModuleCache(); + }); + afterEach(function() { + print = this._print; + }); + it('main requirable', function() { + main = require(MODULE_PATH); + expect(main).toEqual(jasmine.any(Object)); + }); + it('main with both a and b', function() { + expect(main.a['b.done?']).toBe(true); + expect(main.b['a.done?']).toBe(false); + }); + it('a.done?', function() { + expect(main['a.done?']).toBe(true); + }); + it('b.done?', function() { + expect(main['b.done?']).toBe(true); + }); + }); + }); + + describe('JS', function() { + it('should throw catchable local file errors', function() { + expect(function() { + require('file:///dev/null/non-existent-file.js'); + }).toThrowError(/path not found|Cannot find.*non-existent-file/); + }); + it('should throw catchable invalid id errors', function() { + expect(function() { + require(new Array(4096 * 2).toString()); + }).toThrowError(/invalid.*size|Cannot find.*,{30}/); + }); + it('should throw catchable unresolved id errors', function() { + expect(function() { + require('foobar:/baz.js'); + }).toThrowError(/could not resolve|Cannot find.*foobar:/); + }); + + NETWORK_describe('network', function() { + // note: with retries these tests can take up to 60 seconds each to timeout + var timeout = 75 * 1000; + it('should throw catchable host errors', function() { + expect(function() { + var mod = require('http://non.existent.highfidelity.io/moduleUnitTest.js'); + print("mod", Object.keys(mod)); + }).toThrowError(/error retrieving script .ServerUnavailable.|Cannot find.*non.existent/); + }, timeout); + it('should throw catchable network timeouts', function() { + expect(function() { + require('http://ping.highfidelity.io:1024'); + }).toThrowError(/error retrieving script .Timeout.|Cannot find.*ping.highfidelity/); + }, timeout); + }); + }); + + INTERFACE_describe('entity', function() { + var sampleScripts = [ + 'entityConstructorAPIException.js', + 'entityConstructorModule.js', + 'entityConstructorNested2.js', + 'entityConstructorNested.js', + 'entityConstructorRequireException.js', + 'entityPreloadAPIError.js', + 'entityPreloadRequire.js', + ].filter(Boolean).map(function(id) { return Script.require.resolve('./moduleTests/entity/'+id); }); + + var uuids = []; + + for(var i=0; i < sampleScripts.length; i++) { + (function(i) { + var script = sampleScripts[ i % sampleScripts.length ]; + var shortname = '['+i+'] ' + script.split('/').pop(); + var position = MyAvatar.position; + position.y -= i/2; + it(shortname, function(done) { + var uuid = Entities.addEntity({ + text: shortname, + description: Script.resolvePath('').split('/').pop(), + type: 'Text', + position: position, + rotation: MyAvatar.orientation, + script: script, + scriptTimestamp: +new Date, + lifetime: 20, + lineHeight: 1/8, + dimensions: { x: 2, y: .5, z: .01 }, + backgroundColor: { red: 0, green: 0, blue: 0 }, + color: { red: 0xff, green: 0xff, blue: 0xff }, + }, !Entities.serversExist() || !Entities.canRezTmp()); + uuids.push(uuid); + var ii = Script.setInterval(function() { + Entities.queryPropertyMetadata(uuid, "script", function(err, result) { + if (err) { + throw new Error(err); + } + if (result.success) { + clearInterval(ii); + if (/Exception/.test(script)) + expect(result.status).toMatch(/^error_(loading|running)_script$/); + else + expect(result.status).toEqual("running"); + done(); + } else { + print('!result.success', JSON.stringify(result)); + } + }); + }, 100); + Script.setTimeout(function() { + Script.clearInterval(ii); + }, 4900); + }, 5000 /* timeout */); + })(i); + } + Script.scriptEnding.connect(function() { + uuids.forEach(function(uuid) { Entities.deleteEntity(uuid); }); + }); + }); +}); + +function run() {} +function instrument_testrunner() { + var isNode = typeof process === 'object' && process.title === 'node'; + if (isNode) { + // for consistency this still uses the same local jasmine.js library + var jasmineRequire = require('../../libraries/jasmine/jasmine.js'); + var jasmine = jasmineRequire.core(jasmineRequire); + var env = jasmine.getEnv(); + var jasmineInterface = jasmineRequire.interface(jasmine, env); + for (var p in jasmineInterface) + global[p] = jasmineInterface[p]; + env.addReporter(new (require('jasmine-console-reporter'))); + // testing mocks + Script = { + resetModuleCache: function() { + module.require.cache = {}; + }, + setTimeout: setTimeout, + clearTimeout: clearTimeout, + resolvePath: function(id) { + // this attempts to accurately emulate how Script.resolvePath works + var trace = {}; Error.captureStackTrace(trace); + var base = trace.stack.split('\n')[2].replace(/^.*[(]|[)].*$/g,'').replace(/:[0-9]+:[0-9]+.*$/,''); + if (!id) + return base; + var rel = base.replace(/[^\/]+$/, id); + console.info('rel', rel); + return require.resolve(rel); + }, + require: function(mod) { + return require(Script.require.resolve(mod)); + } + }; + Script.require.cache = require.cache; + Script.require.resolve = function(mod) { + if (mod === '.' || /^\.\.($|\/)/.test(mod)) + throw new Error("Cannot find module '"+mod+"' (is dir)"); + var path = require.resolve(mod); + //console.info('node-require-reoslved', mod, path); + try { + if (require('fs').lstatSync(path).isDirectory()) { + throw new Error("Cannot find module '"+path+"' (is directory)"); + } + //console.info('!path', path); + } catch(e) { console.info(e) } + return path; + }; + print = console.info.bind(console, '[print]'); + } else { + global = this; + // Interface Test mode + Script.require('../../../system/libraries/utils.js'); + this.jasmineRequire = Script.require('../../libraries/jasmine/jasmine.js'); + Script.require('../../libraries/jasmine/hifi-boot.js') + require = Script.require; + // polyfill console + console = { + log: print, + info: print.bind(this, '[info]'), + warn: print.bind(this, '[warn]'), + error: print.bind(this, '[error]'), + debug: print.bind(this, '[debug]'), + }; + } + run = function() { global.jasmine.getEnv().execute(); }; + return isNode; +} +run(); diff --git a/scripts/developer/tests/unit_tests/package.json b/scripts/developer/tests/unit_tests/package.json new file mode 100644 index 0000000000..91d719b687 --- /dev/null +++ b/scripts/developer/tests/unit_tests/package.json @@ -0,0 +1,6 @@ +{ + "name": "unit_tests", + "devDependencies": { + "jasmine-console-reporter": "^1.2.7" + } +} From fa0d3a18455b7bb8d7a58ec96a0f049c4c0ce26c Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 1 Mar 2017 15:19:23 -0500 Subject: [PATCH 08/43] unit test cleanup pass --- .../tests/unit_tests/moduleTests/cycles/a.js | 1 + .../tests/unit_tests/moduleTests/cycles/b.js | 1 + .../unit_tests/moduleTests/cycles/main.js | 4 + .../entity/entityConstructorAPIException.js | 3 +- .../entity/entityConstructorModule.js | 6 +- .../entity/entityConstructorNested.js | 3 +- .../entity/entityConstructorNested2.js | 5 +- .../entityConstructorRequireException.js | 5 +- .../entity/entityPreloadAPIError.js | 3 +- .../entity/entityPreloadRequire.js | 3 +- .../moduleTests/exceptions/exception.js | 1 + .../exceptions/exceptionInFunction.js | 5 +- .../tests/unit_tests/moduleUnitTests.js | 309 ++++++++++-------- 13 files changed, 206 insertions(+), 143 deletions(-) diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js index 7934180da7..265cfaa2df 100644 --- a/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js @@ -1,3 +1,4 @@ +/* eslint-env node */ var a = exports; a.done = false; var b = require('./b.js'); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js index 285f176597..c46c872828 100644 --- a/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js @@ -1,3 +1,4 @@ +/* eslint-env node */ var b = exports; b.done = false; var a = require('./a.js'); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js index 2e9a878c82..0ec39cd656 100644 --- a/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js @@ -1,3 +1,7 @@ +/* eslint-env node */ +/* global print */ +/* eslint-disable comma-dangle */ + print('main.js'); var a = require('./a.js'), b = require('./b.js'); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js index b1bc0e33e4..bbe694b578 100644 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js @@ -1,3 +1,4 @@ +/* eslint-disable comma-dangle */ // test module method exception being thrown within main constructor (function() { var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); @@ -9,4 +10,4 @@ print("entityConstructorAPIException::preload -- never seen --", uuid, Script.resolvePath('')); }, }; -}) +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js index 5f0e8a5938..a4e8c17ab6 100644 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js @@ -1,3 +1,5 @@ +/* global module */ +/* eslint-disable comma-dangle */ // test dual-purpose module and standalone Entity script function MyEntity(filename) { return { @@ -16,6 +18,6 @@ function MyEntity(filename) { try { module.exports = MyEntity; -} catch(e) {} +} catch (e) {} // eslint-disable-line no-empty print('entityConstructorModule::MyEntity', typeof MyEntity); -(MyEntity) +(MyEntity); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js index 5a2b8d5974..a90d979877 100644 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js @@ -1,3 +1,4 @@ +/* global module */ // test Entity constructor based on inherited constructor from a module function constructor() { print("entityConstructorNested::constructor"); @@ -7,7 +8,7 @@ function constructor() { try { module.exports = constructor; -} catch(e) { +} catch (e) { constructor; } diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js index 85a6b977b0..29e0ed65b1 100644 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js @@ -1,9 +1,10 @@ +/* global module */ // test Entity constructor based on nested, inherited module constructors function constructor() { print("entityConstructorNested2::constructor"); // inherit from entityConstructorNested - var Entity = Script.require('./entityConstructorNested.js'); + var MyEntity = Script.require('./entityConstructorNested.js'); function SubEntity() {} SubEntity.prototype = new MyEntity('-- created from entityConstructorNested2 --'); @@ -19,6 +20,6 @@ function constructor() { try { module.exports = constructor; -} catch(e) { +} catch (e) { constructor; } diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js index 269ca8e7f0..5872bce529 100644 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js @@ -1,9 +1,10 @@ +/* eslint-disable comma-dangle */ // test module-related exception from within "require" evaluation itself (function() { var mod = Script.require('../exceptions/exception.js'); return { preload: function(uuid) { - print("entityConstructorRequireException::preload (never happens)", uuid, Script.resolvePath('')); + print("entityConstructorRequireException::preload (never happens)", uuid, Script.resolvePath(''), mod); }, }; -}) +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js index 3be0b50d43..eaee178b0a 100644 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js @@ -1,3 +1,4 @@ +/* eslint-disable comma-dangle */ // test module method exception being thrown within preload (function() { var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); @@ -9,4 +10,4 @@ print("entityPreloadAPIException::preload -- never seen --", uuid, Script.resolvePath('')); }, }; -}) +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js index fc70838c80..50dab9fa7c 100644 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js @@ -1,3 +1,4 @@ +/* eslint-disable comma-dangle */ // test requiring a module from within preload (function constructor() { return { @@ -7,4 +8,4 @@ print("entityPreloadRequire::example::name", example.name); }, }; -}) +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js index 636ee82f79..8d25d6b7a4 100644 --- a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js +++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js @@ -1,3 +1,4 @@ +/* eslint-env node */ module.exports = "n/a"; throw new Error('exception on line 2'); diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js index dc2ce3c438..69415a0741 100644 --- a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js +++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js @@ -1,3 +1,4 @@ +/* eslint-env node */ // dummy lines to make sure exception line number is well below parent test script // // @@ -30,8 +31,8 @@ function myfunc() { throw new Error('exception on line 32 in myfunc'); - return "myfunc"; } module.exports = myfunc; -if (Script[module.filename] === 'throw') +if (Script[module.filename] === 'throw') { myfunc(); +} diff --git a/scripts/developer/tests/unit_tests/moduleUnitTests.js b/scripts/developer/tests/unit_tests/moduleUnitTests.js index c1c20d6980..a9446d1d6e 100644 --- a/scripts/developer/tests/unit_tests/moduleUnitTests.js +++ b/scripts/developer/tests/unit_tests/moduleUnitTests.js @@ -1,18 +1,65 @@ -/* eslint-env jasmine */ +/* eslint-env jasmine, node */ +/* global print:true, Script:true, global:true, require:true */ +/* eslint-disable comma-dangle */ +var isNode = instrumentTestrunner(), + runInterfaceTests = !isNode, + runNetworkTests = true; -var isNode = instrument_testrunner(); +// describe wrappers (note: `xdescribe` indicates a disabled or "pending" jasmine test) +var INTERFACE = { describe: runInterfaceTests ? describe : xdescribe }, + NETWORK = { describe: runNetworkTests ? describe : xdescribe }; -var NETWORK_describe = xdescribe, - INTERFACE_describe = !isNode ? describe : xdescribe, - NODE_describe = isNode ? describe : xdescribe; - -print("DESCRIBING"); describe('require', function() { describe('resolve', function() { it('should resolve relative filenames', function() { var expected = Script.resolvePath('./moduleTests/example.json'); expect(require.resolve('./moduleTests/example.json')).toEqual(expected); }); + describe('exceptions', function() { + it('should reject blank "" module identifiers', function() { + expect(function() { + require.resolve(''); + }).toThrowError(/Cannot find/); + }); + it('should reject excessive identifier sizes', function() { + expect(function() { + require.resolve(new Array(8193).toString()); + }).toThrowError(/Cannot find/); + }); + it('should reject implicitly-relative filenames', function() { + expect(function() { + var mod = require.resolve('example.js'); + mod.exists; + }).toThrowError(/Cannot find/); + }); + it('should reject non-existent filenames', function() { + expect(function() { + require.resolve('./404error.js'); + }).toThrowError(/Cannot find/); + }); + it('should reject identifiers resolving to a directory', function() { + expect(function() { + var mod = require.resolve('.'); + mod.exists; + // console.warn('resolved(.)', mod); + }).toThrowError(/Cannot find/); + expect(function() { + var mod = require.resolve('..'); + mod.exists; + // console.warn('resolved(..)', mod); + }).toThrowError(/Cannot find/); + expect(function() { + var mod = require.resolve('../'); + mod.exists; + // console.warn('resolved(../)', mod); + }).toThrowError(/Cannot find/); + }); + (isNode ? xit : it)('should reject non-system, extensionless identifiers', function() { + expect(function() { + require.resolve('./example'); + }).toThrowError(/Cannot find/); + }); + }); }); describe('JSON', function() { @@ -20,12 +67,12 @@ describe('require', function() { var example = require('./moduleTests/example.json'); expect(example.name).toEqual('Example JSON Module'); }); - INTERFACE_describe('inteface', function() { - NETWORK_describe('network', function() { - //xit('should import #content-type=application/json modules', function() { - // var results = require('https://jsonip.com#content-type=application/json'); - // expect(results.ip).toMatch(/^[.0-9]+$/); - //}); + INTERFACE.describe('interface', function() { + NETWORK.describe('network', function() { + // xit('should import #content-type=application/json modules', function() { + // var results = require('https://jsonip.com#content-type=application/json'); + // expect(results.ip).toMatch(/^[.0-9]+$/); + // }); it('should import content-type: application/json modules', function() { var scope = { 'content-type': 'application/json' }; var results = require.call(scope, 'https://jsonip.com'); @@ -36,66 +83,32 @@ describe('require', function() { }); - INTERFACE_describe('system', function() { - it('require(id)', function() { + INTERFACE.describe('system', function() { + it('require("vec3")', function() { expect(require('vec3')).toEqual(jasmine.any(Function)); }); - it('require(id).function', function() { + it('require("vec3").method', function() { expect(require('vec3')().isValid).toEqual(jasmine.any(Function)); }); - }); - - describe('exceptions', function() { - it('should reject blank "" module identifiers', function() { - expect(function() { - require.resolve(''); - }).toThrowError(/Cannot find/); + it('require("vec3") as constructor', function() { + var vec3 = require('vec3'); + var v = vec3(1.1, 2.2, 3.3); + expect(v).toEqual(jasmine.any(Object)); + expect(v.isValid).toEqual(jasmine.any(Function)); + expect(v.isValid()).toBe(true); + expect(v.toString()).toEqual('[Vec3 (1.100,2.200,3.300)]'); }); - it('should reject excessive identifier sizes', function() { - expect(function() { - require.resolve(new Array(8193).toString()); - }).toThrowError(/Cannot find/); - }); - it('should reject implicitly-relative filenames', function() { - expect(function() { - var mod = require.resolve('example.js'); - }).toThrowError(/Cannot find/); - }); - it('should reject non-existent filenames', function() { - expect(function() { - var mod = require.resolve('./404error.js'); - }).toThrowError(/Cannot find/); - }); - it('should reject identifiers resolving to a directory', function() { - expect(function() { - var mod = require.resolve('.'); - //console.warn('resolved(.)', mod); - }).toThrowError(/Cannot find/); - expect(function() { - var mod = require.resolve('..'); - //console.warn('resolved(..)', mod); - }).toThrowError(/Cannot find/); - expect(function() { - var mod = require.resolve('../'); - //console.warn('resolved(../)', mod); - }).toThrowError(/Cannot find/); - }); - if (typeof MODE !== 'undefined' && MODE !== 'node') { - it('should reject non-system, extensionless identifiers', function() { - expect(function() { - require.resolve('./example'); - }).toThrowError(/Cannot find/); - }); - } }); describe('cache', function() { it('should cache modules by resolved module id', function() { var value = new Date; var example = require('./moduleTests/example.json'); + // earmark the module object with a unique value example['.test'] = value; var example2 = require('../../tests/unit_tests/moduleTests/example.json'); expect(example2).toBe(example); + // verify earmark is still the same after a second require() expect(example2['.test']).toBe(example['.test']); }); it('should reload cached modules set to null', function() { @@ -104,6 +117,7 @@ describe('require', function() { example['.test'] = value; require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')] = null; var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + // verify the earmark is *not* the same as before expect(example2).not.toBe(example); expect(example2['.test']).not.toBe(example['.test']); }); @@ -113,6 +127,7 @@ describe('require', function() { example['.test'] = value; delete require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')]; var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + // verify the earmark is *not* the same as before expect(example2).not.toBe(example); expect(example2['.test']).not.toBe(example['.test']); }); @@ -120,30 +135,29 @@ describe('require', function() { describe('cyclic dependencies', function() { describe('should allow lazy-ref cyclic module resolution', function() { - const MODULE_PATH = './moduleTests/cycles/main.js'; var main; beforeEach(function() { - try { this._print = print; } catch(e) {} - // for this test print is no-op'd so it doesn't disrupt the reporter output - //console = typeof console === 'object' ? console : { log: function() {} }; + // eslint-disable-next-line + try { this._print = print; } catch (e) {} + // during these tests print() is no-op'd so that it doesn't disrupt the reporter output print = function() {}; Script.resetModuleCache(); }); afterEach(function() { print = this._print; }); - it('main requirable', function() { - main = require(MODULE_PATH); + it('main is requirable', function() { + main = require('./moduleTests/cycles/main.js'); expect(main).toEqual(jasmine.any(Object)); }); - it('main with both a and b', function() { + it('transient a and b done values', function() { expect(main.a['b.done?']).toBe(true); expect(main.b['a.done?']).toBe(false); }); - it('a.done?', function() { + it('ultimate a.done?', function() { expect(main['a.done?']).toBe(true); }); - it('b.done?', function() { + it('ultimate b.done?', function() { expect(main['b.done?']).toBe(true); }); }); @@ -166,8 +180,8 @@ describe('require', function() { }).toThrowError(/could not resolve|Cannot find.*foobar:/); }); - NETWORK_describe('network', function() { - // note: with retries these tests can take up to 60 seconds each to timeout + NETWORK.describe('network', function() { + // note: depending on retries these tests can take up to 60 seconds each to timeout var timeout = 75 * 1000; it('should throw catchable host errors', function() { expect(function() { @@ -183,7 +197,7 @@ describe('require', function() { }); }); - INTERFACE_describe('entity', function() { + INTERFACE.describe('entity', function() { var sampleScripts = [ 'entityConstructorAPIException.js', 'entityConstructorModule.js', @@ -192,72 +206,98 @@ describe('require', function() { 'entityConstructorRequireException.js', 'entityPreloadAPIError.js', 'entityPreloadRequire.js', - ].filter(Boolean).map(function(id) { return Script.require.resolve('./moduleTests/entity/'+id); }); + ].filter(Boolean).map(function(id) { + return Script.require.resolve('./moduleTests/entity/'+id); + }); var uuids = []; - - for(var i=0; i < sampleScripts.length; i++) { - (function(i) { - var script = sampleScripts[ i % sampleScripts.length ]; - var shortname = '['+i+'] ' + script.split('/').pop(); - var position = MyAvatar.position; - position.y -= i/2; - it(shortname, function(done) { - var uuid = Entities.addEntity({ - text: shortname, - description: Script.resolvePath('').split('/').pop(), - type: 'Text', - position: position, - rotation: MyAvatar.orientation, - script: script, - scriptTimestamp: +new Date, - lifetime: 20, - lineHeight: 1/8, - dimensions: { x: 2, y: .5, z: .01 }, - backgroundColor: { red: 0, green: 0, blue: 0 }, - color: { red: 0xff, green: 0xff, blue: 0xff }, - }, !Entities.serversExist() || !Entities.canRezTmp()); - uuids.push(uuid); - var ii = Script.setInterval(function() { - Entities.queryPropertyMetadata(uuid, "script", function(err, result) { - if (err) { - throw new Error(err); - } - if (result.success) { - clearInterval(ii); - if (/Exception/.test(script)) - expect(result.status).toMatch(/^error_(loading|running)_script$/); - else - expect(result.status).toEqual("running"); - done(); - } else { - print('!result.success', JSON.stringify(result)); - } - }); - }, 100); - Script.setTimeout(function() { - Script.clearInterval(ii); - }, 4900); - }, 5000 /* timeout */); - })(i); + function cleanup() { + uuids.splice(0,uuids.length).forEach(function(uuid) { + Entities.deleteEntity(uuid); + }); + } + afterAll(cleanup); + // extra sanity check to avoid lingering entities + Script.scriptEnding.connect(cleanup); + + for (var i=0; i < sampleScripts.length; i++) { + maketest(i); + } + + function maketest(i) { + var script = sampleScripts[ i % sampleScripts.length ]; + var shortname = '['+i+'] ' + script.split('/').pop(); + var position = MyAvatar.position; + position.y -= i/2; + // define a unique jasmine test for the current entity script + it(shortname, function(done) { + var uuid = Entities.addEntity({ + text: shortname, + description: Script.resolvePath('').split('/').pop(), + type: 'Text', + position: position, + rotation: MyAvatar.orientation, + script: script, + scriptTimestamp: +new Date, + lifetime: 20, + lineHeight: 1/8, + dimensions: { x: 2, y: 0.5, z: 0.01 }, + backgroundColor: { red: 0, green: 0, blue: 0 }, + color: { red: 0xff, green: 0xff, blue: 0xff }, + }, !Entities.serversExist() || !Entities.canRezTmp()); + uuids.push(uuid); + function stopChecking() { + if (ii) { + Script.clearInterval(ii); + ii = 0; + } + } + var ii = Script.setInterval(function() { + Entities.queryPropertyMetadata(uuid, "script", function(err, result) { + if (err) { + stopChecking(); + throw new Error(err); + } + if (result.success) { + stopChecking(); + if (/Exception/.test(script)) { + expect(result.status).toMatch(/^error_(loading|running)_script$/); + } else { + expect(result.status).toEqual("running"); + } + Entities.deleteEntity(uuid); + done(); + } else { + print('!result.success', JSON.stringify(result)); + } + }); + }, 100); + Script.setTimeout(stopChecking, 4900); + }, 5000 /* jasmine async timeout */); } - Script.scriptEnding.connect(function() { - uuids.forEach(function(uuid) { Entities.deleteEntity(uuid); }); - }); }); }); +// support for isomorphic Node.js / Interface unit testing +// note: run `npm install` from unit_tests/ and then `node moduleUnitTests.js` function run() {} -function instrument_testrunner() { +function instrumentTestrunner() { var isNode = typeof process === 'object' && process.title === 'node'; + if (typeof describe === 'function') { + // already running within a test runner; assume jasmine is ready-to-go + return isNode; + } if (isNode) { - // for consistency this still uses the same local jasmine.js library + /* eslint-disable no-console */ + // Node.js test mode + // to keep things consistent Node.js uses the local jasmine.js library (instead of an npm version) var jasmineRequire = require('../../libraries/jasmine/jasmine.js'); var jasmine = jasmineRequire.core(jasmineRequire); var env = jasmine.getEnv(); var jasmineInterface = jasmineRequire.interface(jasmine, env); - for (var p in jasmineInterface) + for (var p in jasmineInterface) { global[p] = jasmineInterface[p]; + } env.addReporter(new (require('jasmine-console-reporter'))); // testing mocks Script = { @@ -270,39 +310,45 @@ function instrument_testrunner() { // this attempts to accurately emulate how Script.resolvePath works var trace = {}; Error.captureStackTrace(trace); var base = trace.stack.split('\n')[2].replace(/^.*[(]|[)].*$/g,'').replace(/:[0-9]+:[0-9]+.*$/,''); - if (!id) + if (!id) { return base; + } var rel = base.replace(/[^\/]+$/, id); console.info('rel', rel); return require.resolve(rel); }, require: function(mod) { return require(Script.require.resolve(mod)); - } + }, }; Script.require.cache = require.cache; Script.require.resolve = function(mod) { - if (mod === '.' || /^\.\.($|\/)/.test(mod)) + if (mod === '.' || /^\.\.($|\/)/.test(mod)) { throw new Error("Cannot find module '"+mod+"' (is dir)"); + } var path = require.resolve(mod); - //console.info('node-require-reoslved', mod, path); + // console.info('node-require-reoslved', mod, path); try { if (require('fs').lstatSync(path).isDirectory()) { throw new Error("Cannot find module '"+path+"' (is directory)"); } - //console.info('!path', path); - } catch(e) { console.info(e) } + // console.info('!path', path); + } catch (e) { + console.error(e); + } return path; }; print = console.info.bind(console, '[print]'); + /* eslint-enable no-console */ } else { + // Interface test mode global = this; - // Interface Test mode Script.require('../../../system/libraries/utils.js'); this.jasmineRequire = Script.require('../../libraries/jasmine/jasmine.js'); - Script.require('../../libraries/jasmine/hifi-boot.js') + Script.require('../../libraries/jasmine/hifi-boot.js'); require = Script.require; // polyfill console + /* global console:true */ console = { log: print, info: print.bind(this, '[info]'), @@ -311,6 +357,7 @@ function instrument_testrunner() { debug: print.bind(this, '[debug]'), }; } + // eslint-disable-next-line run = function() { global.jasmine.getEnv().execute(); }; return isNode; } From 5e5bba5aad5d5ac09732c386a35d1c17551aae8b Mon Sep 17 00:00:00 2001 From: humbletim Date: Thu, 2 Mar 2017 16:58:37 -0500 Subject: [PATCH 09/43] Separate EntityPropertyMetadataRequest for easier documentation and testing --- .../entities/src/EntityScriptingInterface.cpp | 246 +++++++++++------- .../entities/src/EntityScriptingInterface.h | 27 +- 2 files changed, 180 insertions(+), 93 deletions(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index df88194f9f..aa241023c8 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -27,7 +27,6 @@ #include "ModelEntityItem.h" #include "QVariantGLM.h" #include "SimulationOwner.h" -#include "BaseScriptEngine.h" #include "ZoneEntityItem.h" #include "WebEntityItem.h" #include @@ -684,26 +683,24 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) { return client->reloadServerScript(entityID); } -bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) { - auto name = property.toString(); - auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); - QPointer engine = dynamic_cast(handler.engine()); +#ifdef DEBUG_ENTITY_METADATA +// baseline example -- return parsed userData as a standard CPS callback +bool EntityPropertyMetadataRequest::_userData(EntityItemID entityID, QScriptValue handler) { + QScriptValue err, result; + auto engine = _engine; if (!engine) { - qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name; + qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID; return false; } - connect(engine, &QObject::destroyed, this, [=]() { - qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine"); - }); - if (!handler.property("callback").isFunction()) { - qDebug() << "!handler.callback.isFunction" << engine; - engine->raiseException(engine->makeError("callback is not a function", "TypeError")); - return false; - } - if (name == "userData") { - EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); - QScriptValue err, result; - if (entity) { + auto entityScriptingInterface = DependencyManager::get(); + auto entityTree = entityScriptingInterface ? entityScriptingInterface->getEntityTree() : nullptr; + if (!entityTree) { + err = engine->makeError("Entities Tree unavailable", "InternalError"); + } else { + EntityItemPointer entity = entityTree->findEntityByID(entityID); + if (!entity) { + err = engine->makeError("entity not found"); + } else { auto JSON = engine->globalObject().property("JSON"); auto parsed = JSON.property("parse").call(JSON, QScriptValueList({ entity->getUserData() })); if (engine->hasUncaughtException()) { @@ -712,81 +709,148 @@ bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValu } else { result = parsed; } - } else { - err = engine->makeError("entity not found"); } - QFutureWatcher *request = new QFutureWatcher; - connect(request, &QFutureWatcher::finished, engine, [=]() mutable { - if (!engine) { - qCDebug(entities) << "queryPropertyMetadata -- engine destroyed while inflight" << entityID << name; - return; - } - callScopedHandlerObject(handler, err, result); - request->deleteLater(); - }); - request->setFuture(QtConcurrent::run([]() -> QVariant { - QThread::sleep(1); - return 1; - })); - return true; - } else if (name == "script") { - using LocalScriptStatusRequest = QFutureWatcher; - LocalScriptStatusRequest *request = new LocalScriptStatusRequest; - connect(request, &LocalScriptStatusRequest::finished, engine, [=]() mutable { - if (!engine) { - qCDebug(entities) << "queryPropertyMetadata -- engine destroyed while inflight" << entityID << name; - return; - } - auto details = request->result().toMap(); - QScriptValue err, result; - if (details.contains("isError")) { - if (!details.contains("message")) { - details["message"] = details["errorInfo"]; - } - err = engine->makeError(engine->toScriptValue(details)); - } else { - details["success"] = true; - result = engine->toScriptValue(details); - } - callScopedHandlerObject(handler, err, result); - request->deleteLater(); - }); - request->setFuture(_entitiesScriptEngine->getLocalEntityScriptDetails(entityID)); - return true; - } else if (name == "serverScripts") { - auto client = DependencyManager::get(); - auto request = client->createScriptStatusRequest(entityID); - connect(request, &GetScriptStatusRequest::finished, engine, [=](GetScriptStatusRequest* request) mutable { - if (!engine) { - qCDebug(entities) << "queryPropertyMetadata -- engine destroyed while inflight" << entityID << name; - return; - } - QVariantMap details; - details["success"] = request->getResponseReceived(); - details["isRunning"] = request->getIsRunning(); - details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower(); - details["errorInfo"] = request->getErrorInfo(); - - QScriptValue err, result; - if (!details["success"].toBool()) { - if (!details.contains("message") && details.contains("errorInfo")) { - details["message"] = details["errorInfo"]; - } - if (details["message"].toString().isEmpty()) { - details["message"] = "entity server script details not found"; - } - err = engine->makeError(engine->toScriptValue(details)); - } else { - result = engine->toScriptValue(details); - } - callScopedHandlerObject(handler, err, result); - request->deleteLater(); - }); - request->start(); - return true; } - engine->raiseException(engine->makeError("property has no mapped metadata: " + name)); - return false; + // this one second delay can be used with a Client script to query metadata and immediately Script.stop() + // (testing that the signal handler never gets called once the engine is destroyed) + // note: we still might want to check engine->isStopping() as an optimization in some places + QFutureWatcher *request = new QFutureWatcher; + QObject::connect(request, &QFutureWatcher::finished, engine, [=]() mutable { + if (!engine) { + qCDebug(entities) << "queryPropertyMetadata -- engine destroyed while inflight" << entityID; + return; + } + callScopedHandlerObject(handler, err, result); + request->deleteLater(); + }); + request->setFuture(QtConcurrent::run([]() -> QVariant { + QThread::sleep(1); + return QVariant(); + })); + return true; +} +#endif + +bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) { + using LocalScriptStatusRequest = QFutureWatcher; + + LocalScriptStatusRequest *request = new LocalScriptStatusRequest; + QObject::connect(request, &LocalScriptStatusRequest::finished, _engine, [=]() mutable { + auto engine = _engine; + if (!engine) { + // this is just to address any lingering doubts -- when _engine is destroyed, this connect gets broken automatically + qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID; + return; + } + auto details = request->result().toMap(); + QScriptValue err, result; + if (details.contains("isError")) { + if (!details.contains("message")) { + details["message"] = details["errorInfo"]; + } + err = engine->makeError(engine->toScriptValue(details)); + } else { + details["success"] = true; + result = engine->toScriptValue(details); + } + callScopedHandlerObject(handler, err, result); + request->deleteLater(); + }); + auto entityScriptingInterface = DependencyManager::get(); + entityScriptingInterface->withEntitiesScriptEngine([&](EntitiesScriptEngineProvider* entitiesScriptEngine) { + if (entitiesScriptEngine) { + request->setFuture(entitiesScriptEngine->getLocalEntityScriptDetails(entityID)); + } + }); + if (!request->isStarted()) { + request->deleteLater(); + callScopedHandlerObject(handler, _engine->makeError("Entities Scripting Provider unavailable", "InternalError"), QScriptValue()); + return false; + } + return true; +} + +bool EntityPropertyMetadataRequest::serverScripts(EntityItemID entityID, QScriptValue handler) { + auto client = DependencyManager::get(); + auto request = client->createScriptStatusRequest(entityID); + QPointer engine = _engine; + QObject::connect(request, &GetScriptStatusRequest::finished, _engine, [=](GetScriptStatusRequest* request) mutable { + auto engine = _engine; + if (!engine) { + qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID; + return; + } + QVariantMap details; + details["success"] = request->getResponseReceived(); + details["isRunning"] = request->getIsRunning(); + details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower(); + details["errorInfo"] = request->getErrorInfo(); + + QScriptValue err, result; + if (!details["success"].toBool()) { + if (!details.contains("message") && details.contains("errorInfo")) { + details["message"] = details["errorInfo"]; + } + if (details["message"].toString().isEmpty()) { + details["message"] = "entity server script details not found"; + } + err = engine->makeError(engine->toScriptValue(details)); + } else { + result = engine->toScriptValue(details); + } + callScopedHandlerObject(handler, err, result); + request->deleteLater(); + }); + request->start(); + return true; +} + +bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto name = property.toString(); + auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); + QPointer engine = dynamic_cast(handler.engine()); + if (!engine) { + qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name; + return false; + } +#ifdef DEBUG_ENGINE_STATE + connect(engine, &QObject::destroyed, this, [=]() { + qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine"); + }); +#endif + if (!handler.property("callback").isFunction()) { + qDebug() << "!handler.callback.isFunction" << engine; + engine->raiseException(engine->makeError("callback is not a function", "TypeError")); + return false; + } + + // NOTE: this approach is a work-in-progress and for now just meant to work 100% correctly and provide + // some initial structure for organizing metadata adapters around. + + // The extra layer of indirection is *essential* because in real world conditions errors are often introduced + // by accident and sometimes without exact memory of "what just changed." + + // Here the scripter only needs to know an entityID and a property name -- which means all scripters can + // level this method when stuck in dead-end scenarios or to learn more about "magic" Entity properties + // like .script that work in terms of side-effects. + + // This is an async callback pattern -- so if needed C++ can easily throttle or restrict queries later. + + EntityPropertyMetadataRequest request(engine); + + if (name == "script") { + return request.script(entityID, handler); + } else if (name == "serverScripts") { + return request.serverScripts(entityID, handler); +#ifdef DEBUG_ENTITY_METADATA + } else if (name == "userData") { + return request.userData(entityID, handler); +#endif + } else { + engine->raiseException(engine->makeError("metadata for property " + name + " is not yet queryable")); + engine->maybeEmitUncaughtException(__FUNCTION__); + return false; + } } bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) { diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 2c3c654528..d6fe93b41e 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -34,8 +34,25 @@ #include "EntitiesScriptEngineProvider.h" #include "EntityItemProperties.h" +#include "BaseScriptEngine.h" + class EntityTree; +// helper factory to compose standardized, async metadata queries for "magic" Entity properties +// like .script and .serverScripts. This is used for automated testing of core scripting features +// as well as to provide early adopters a self-discoverable, consistent way to diagnose common +// problems with their own Entity scripts. +class EntityPropertyMetadataRequest { +public: + EntityPropertyMetadataRequest(BaseScriptEngine* engine) : _engine(engine) {}; + bool script(EntityItemID entityID, QScriptValue handler); + bool serverScripts(EntityItemID entityID, QScriptValue handler); + // this is used for internal testing and only available when DEBUG_ENTITY_METADATA is defined in the .cpp file + bool userData(EntityItemID entityID, QScriptValue handler); +private: + QPointer _engine; +}; + class RayToEntityIntersectionResult { public: RayToEntityIntersectionResult(); @@ -67,6 +84,7 @@ class EntityScriptingInterface : public OctreeScriptingInterface, public Depende Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier) Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity) + friend EntityPropertyMetadataRequest; public: EntityScriptingInterface(bool bidOnSimulationOwnership); @@ -213,7 +231,7 @@ public slots: Q_INVOKABLE bool reloadServerScripts(QUuid entityID); /**jsdoc - * Query for the available metadata behind one of an Entity's "magic" properties (eg: `script` and `serverScripts`). + * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`. * * @function Entities.queryPropertyMetadata * @param {EntityID} entityID The ID of the entity. @@ -221,7 +239,7 @@ public slots: * @param {ResultCallback} callback Executes callback(err, result) with the query results. */ /**jsdoc - * Query for the available metadata behind one of an Entity's "magic" properties (eg: `script` and `serverScripts`). + * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`. * * @function Entities.queryPropertyMetadata * @param {EntityID} entityID The ID of the entity. @@ -343,6 +361,11 @@ signals: void webEventReceived(const EntityItemID& entityItemID, const QVariant& message); +protected: + void withEntitiesScriptEngine(std::function function) { + std::lock_guard lock(_entitiesScriptEngineLock); + function(_entitiesScriptEngine); + }; private: bool actionWorker(const QUuid& entityID, std::function actor); bool setVoxels(QUuid entityID, std::function actor); From d93047f9e2e74c6c9a7ffe452fc3ffdcc1e55548 Mon Sep 17 00:00:00 2001 From: humbletim Date: Thu, 2 Mar 2017 17:23:24 -0500 Subject: [PATCH 10/43] Switch to READONLY_PROP_FLAGS for require.cache/require.resolve properties --- libraries/script-engine/src/ScriptEngine.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index c7364450a1..9b88e2bd24 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -580,7 +580,7 @@ void ScriptEngine::resetModuleCache(bool deleteScriptCache) { #if DEBUG_JS_MODULES cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); #endif - jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable); + jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS); } void ScriptEngine::init() { @@ -651,7 +651,7 @@ void ScriptEngine::init() { auto Script = globalObject().property("Script"); auto require = Script.property("require"); auto resolve = Script.property("_requireResolve"); - require.setProperty("resolve", resolve, QScriptValue::ReadOnly | QScriptValue::Undeletable); + require.setProperty("resolve", resolve, READONLY_PROP_FLAGS); resetModuleCache(); } From 6b927de9f1aa0e98c906096bc094e3139dfc06a7 Mon Sep 17 00:00:00 2001 From: humbletim Date: Thu, 2 Mar 2017 18:11:24 -0500 Subject: [PATCH 11/43] Add example vec3 system module --- scripts/modules/vec3.js | 69 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 scripts/modules/vec3.js diff --git a/scripts/modules/vec3.js b/scripts/modules/vec3.js new file mode 100644 index 0000000000..f164f01374 --- /dev/null +++ b/scripts/modules/vec3.js @@ -0,0 +1,69 @@ +// Example of using a "system module" to decouple Vec3's implementation details. +// +// Users would bring Vec3 support in as a module: +// var vec3 = Script.require('vec3'); +// + +// (this example is compatible with using as a Script.include and as a Script.require module) +try { + // Script.require + module.exports = vec3; +} catch(e) { + // Script.include + Script.registerValue("vec3", vec3); +} + +vec3.fromObject = function(v) { + //return new vec3(v.x, v.y, v.z); + //... this is even faster and achieves the same effect + v.__proto__ = vec3.prototype; + return v; +}; + +vec3.prototype = { + multiply: function(v2) { + // later on could support overrides like so: + // if (v2 instanceof quat) { [...] } + // which of the below is faster (C++ or JS)? + // (dunno -- but could systematically find out and go with that version) + + // pure JS option + // return new vec3(this.x * v2.x, this.y * v2.y, this.z * v2.z); + + // hybrid C++ option + return vec3.fromObject(Vec3.multiply(this, v2)); + }, + // detects any NaN and Infinity values + isValid: function() { + return isFinite(this.x) && isFinite(this.y) && isFinite(this.z); + }, + // format Vec3's, eg: + // var v = vec3(); + // print(v); // outputs [Vec3 (0.000, 0.000, 0.000)] + toString: function() { + if (this === vec3.prototype) { + return "{Vec3 prototype}"; + } + function fixed(n) { return n.toFixed(3); } + return "[Vec3 (" + [this.x, this.y, this.z].map(fixed) + ")]"; + }, +}; + +vec3.DEBUG = true; + +function vec3(x, y, z) { + if (!(this instanceof vec3)) { + // if vec3 is called as a function then re-invoke as a constructor + // (so that `value instanceof vec3` holds true for created values) + return new vec3(x, y, z); + } + + // unfold default arguments (vec3(), vec3(.5), vec3(0,1), etc.) + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : this.x; + this.z = z !== undefined ? z : this.y; + + if (vec3.DEBUG && !this.isValid()) + throw new Error('vec3() -- invalid initial values ['+[].slice.call(arguments)+']'); +}; + From 8582c7af7bfbf22f6dc9678c986ba85ef2a43e61 Mon Sep 17 00:00:00 2001 From: humbletim Date: Tue, 7 Mar 2017 14:02:20 -0500 Subject: [PATCH 12/43] Use specific debug name literal instead of __FUNCTION__ from within lambda --- libraries/script-engine/src/ScriptEngine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 9b88e2bd24..f96b733c74 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -1840,7 +1840,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); if (hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(__FUNCTION__)); + emit unhandledException(cloneUncaughtException("evaluateInclude")); clearExceptions(); } } else { From c559838dbb50c1e33dbc68c10b7b19985b320876 Mon Sep 17 00:00:00 2001 From: humbletim Date: Tue, 7 Mar 2017 14:05:29 -0500 Subject: [PATCH 13/43] Add a few more .resolvePath characterization tests --- .../tests/unit_tests/scriptUnitTests.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/developer/tests/unit_tests/scriptUnitTests.js b/scripts/developer/tests/unit_tests/scriptUnitTests.js index 63b451e97f..fa8cb44608 100644 --- a/scripts/developer/tests/unit_tests/scriptUnitTests.js +++ b/scripts/developer/tests/unit_tests/scriptUnitTests.js @@ -15,10 +15,20 @@ describe('Script', function () { // characterization tests // initially these are just to capture how the app works currently var testCases = { + // special relative resolves '': filename, '.': dirname, '..': parentdir, + + // local file "magic" tilde path expansion + '/~/defaultScripts.js': ScriptDiscoveryService.defaultScriptsPath + '/defaultScripts.js', + + // these schemes appear to always get resolved to empty URLs + 'qrc://test': '', 'about:Entities 1': '', + 'ftp://host:port/path': '', + 'data:text/html;text,foo': '', + 'Entities 1': dirname + 'Entities 1', './file.js': dirname + 'file.js', 'c:/temp/': 'file:///c:/temp/', @@ -31,6 +41,12 @@ describe('Script', function () { '/~/libraries/utils.js': 'file:///~/libraries/utils.js', '/temp/file.js': 'file:///temp/file.js', '/~/': 'file:///~/', + + // these schemes appear to always get resolved to the same URL again + 'http://highfidelity.com': 'http://highfidelity.com', + 'atp:/highfidelity': 'atp:/highfidelity', + 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f': + 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f', }; describe('resolvePath', function () { Object.keys(testCases).forEach(function(input) { @@ -42,7 +58,7 @@ describe('Script', function () { describe('include', function () { var old_cache_buster; - var cache_buster = '#' + +new Date; + var cache_buster = '#' + new Date().getTime().toString(36); beforeAll(function() { old_cache_buster = Settings.getValue('cache_buster'); Settings.setValue('cache_buster', cache_buster); From d4abdcb6c8e1df93e9286a7a62458feb2bf31ebc Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 6 Mar 2017 14:09:10 -0800 Subject: [PATCH 14/43] comments, namechange, and temporary debug code --- interface/src/Application.cpp | 4 +- interface/src/avatar/AvatarManager.cpp | 2 +- interface/src/avatar/AvatarManager.h | 2 +- libraries/entities/src/EntityItem.cpp | 21 ++++-- libraries/physics/src/EntityMotionState.cpp | 67 +++++++++++++++++-- .../physics/src/PhysicalEntitySimulation.cpp | 5 +- .../physics/src/PhysicalEntitySimulation.h | 4 +- .../physics/src/ThreadSafeDynamicsWorld.cpp | 3 + 8 files changed, 92 insertions(+), 16 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f870bd9f83..4894ae55ec 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4440,8 +4440,8 @@ void Application::update(float deltaTime) { getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); - _entitySimulation->handleOutgoingChanges(outgoingChanges); - avatarManager->handleOutgoingChanges(outgoingChanges); + _entitySimulation->handleChangedMotionStates(outgoingChanges); + avatarManager->handleChangedMotionStates(outgoingChanges); }); if (!_aboutToQuit) { diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 94ce444416..6152148887 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -424,7 +424,7 @@ void AvatarManager::getObjectsToChange(VectorOfMotionStates& result) { } } -void AvatarManager::handleOutgoingChanges(const VectorOfMotionStates& motionStates) { +void AvatarManager::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { // TODO: extract the MyAvatar results once we use a MotionState for it. } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index e1f5a3b411..b94f9e6a96 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -70,7 +70,7 @@ public: void getObjectsToRemoveFromPhysics(VectorOfMotionStates& motionStates); void getObjectsToAddToPhysics(VectorOfMotionStates& motionStates); void getObjectsToChange(VectorOfMotionStates& motionStates); - void handleOutgoingChanges(const VectorOfMotionStates& motionStates); + void handleChangedMotionStates(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); Q_INVOKABLE float getAvatarDataRate(const QUuid& sessionID, const QString& rateName = QString("")) const; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 3ef1648fae..db253b639f 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -655,13 +655,11 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // pack SimulationOwner and terse update properties near each other - // NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data // even when we would otherwise ignore the rest of the packet. bool filterRejection = false; if (propertyFlags.getHasProperty(PROP_SIMULATION_OWNER)) { - QByteArray simOwnerData; int bytes = OctreePacketData::unpackDataFromBytes(dataAt, simOwnerData); SimulationOwner newSimOwner; @@ -676,6 +674,13 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // or rejects a set of properties, it clears this. In such cases, we don't want those custom // setters to ignore what the server says. filterRejection = newSimOwner.getID().isNull(); + bool verbose = getName() == "fubar"; // adebug + if (verbose && _simulationOwner != newSimOwner) { + std::cout << (void*)(this) << " adebug ownership changed " + << _simulationOwner.getID().toString().toStdString() << "." << (int)_simulationOwner.getPriority() << "-->" + << newSimOwner.getID().toString().toStdString() << "." << (int)newSimOwner.getPriority() + << std::endl; // adebug + } if (weOwnSimulation) { if (newSimOwner.getID().isNull() && !_simulationOwner.pendingRelease(lastEditedFromBufferAdjusted)) { // entity-server is trying to clear our ownership (probably at our own request) @@ -1879,12 +1884,16 @@ void EntityItem::setSimulationOwner(const SimulationOwner& owner) { } void EntityItem::updateSimulationOwner(const SimulationOwner& owner) { + // NOTE: this method only used by EntityServer. The Interface uses special code in readEntityDataFromBuffer(). if (wantTerseEditLogging() && _simulationOwner != owner) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << owner; } if (_simulationOwner.set(owner)) { _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; + if (getName() == "fubar") { + std::cout << "debug updateSimulationOwner() " << _simulationOwner.getID().toString().toStdString() << "." << (int)(_simulationOwner.getPriority()) << std::endl; // adebug + } } } @@ -1892,10 +1901,14 @@ void EntityItem::clearSimulationOwnership() { if (wantTerseEditLogging() && !_simulationOwner.isNull()) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now null"; } + if (getName() == "fubar") { + std::cout << "debug clearSimulationOwnership()" << std::endl; // adebug + } _simulationOwner.clear(); - // don't bother setting the DIRTY_SIMULATOR_ID flag because clearSimulationOwnership() - // is only ever called on the entity-server and the flags are only used client-side + // don't bother setting the DIRTY_SIMULATOR_ID flag because: + // (a) when entity-server calls clearSimulationOwnership() the dirty-flags are meaningless (only used by interface) + // (b) the interface only calls clearSimulationOwnership() in a context that already knows best about dirty flags //_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; } diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index c175a836cc..77c5a4f697 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -27,6 +27,18 @@ #include "EntityTree.h" #endif +// adebug TODO BOOKMARK: +// The problem is that userB may deactivate and disown and object before userA deactivates +// userA will sometimes insert non-zero velocities (and position errors) into the Entity before it is deactivated locally +// +// It would be nice to prevent data export from Bullet to Entity for unowned objects except in cases where it is really needed (?) +// Maybe can recycle _serverPosition and friends to store the "before simulationStep" data to more efficiently figure out +// if data should be used for non-owned objects. +// +// If we do that, we should convert _serverPosition and friends to use Bullet data types for efficiency. +// +// adebug + const uint8_t LOOPS_FOR_SIMULATION_ORPHAN = 50; const quint64 USECS_BETWEEN_OWNERSHIP_BIDS = USECS_PER_SECOND / 5; @@ -111,6 +123,10 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION; _body->setActivationState(WANTS_DEACTIVATION); _outgoingPriority = 0; + bool verbose = _entity->getName() == "fubar"; // adebug + if (verbose) { + std::cout << (void*)(this) << " adebug flag for deactivation" << std::endl; // adebug + } } else { // disowned object is still moving --> start timer for ownership bid // TODO? put a delay in here proportional to distance from object? @@ -118,6 +134,13 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { _nextOwnershipBid = usecTimestampNow() + USECS_BETWEEN_OWNERSHIP_BIDS; } _loopsWithoutOwner = 0; + + // adebug BOOKMARK: the problem is that userB may deactivate and disown the Object + // but it may still be active for userA... who will store slightly non-zero velocities into EntityItem in the meantime + // + // It would be nice if we could ignore slight outgoing changes for unowned objects that WANT_DEACTIVATION until... + // (a) the changes exceed some threshold (--> bid for ownership) or... + // (b) they actually get deactivated (--> slam RigidBody positions to agree with EntityItem) } else if (_entity->getSimulatorID() == Physics::getSessionUUID()) { // we just inherited ownership, make sure our desired priority matches what we have upgradeOutgoingPriority(_entity->getSimulationPriority()); @@ -223,11 +246,19 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { // This callback is invoked by the physics simulation at the end of each simulation step... // iff the corresponding RigidBody is DYNAMIC and has moved. void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { - if (!_entity) { - return; - } - + assert(_entity); assert(entityTreeIsLocked()); + bool verbose = _entity->getName() == "fubar"; // adebug + // adebug BOOKMARK: the problem is that userB may deactivate and disown the Object + // but it may still be active for userA... who will store slightly non-zero velocities into EntityItem in the meantime + // so what we need to do is ignore setWorldTransform() events for unowned objects when the bullet data is not helpful + // until either the data passes some threshold (bid for it) or + // it goes inactive (at which point we should slam bullet to agree with entity) + if (_body->getActivationState() == WANTS_DEACTIVATION && !_entity->isMoving()) { + if (verbose) { + std::cout << (void*)(this) << " adebug v = " << _body->getLinearVelocity().length() << " w = " << _body->getAngularVelocity().length() << std::endl; // adebug + } + } measureBodyAcceleration(); bool positionSuccess; _entity->setPosition(bulletToGLM(worldTrans.getOrigin()) + ObjectMotionState::getWorldOffset(), positionSuccess, false); @@ -245,6 +276,12 @@ void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { "setOrientation failed.*"); qCDebug(physics) << "EntityMotionState::setWorldTransform setOrientation failed" << _entity->getID(); } + if (verbose + && (glm::length(getBodyLinearVelocity()) > 0.0f || glm::length(getBodyAngularVelocity()) > 0.0f) + && _entity->getSimulationOwner().getID().isNull()) { + std::cout << (void*)(this) << " adebug set non-zero v on unowned object AS = " << _body->getActivationState() << std::endl; // adebug + + } _entity->setVelocity(getBodyLinearVelocity()); _entity->setAngularVelocity(getBodyAngularVelocity()); _entity->setLastSimulated(usecTimestampNow()); @@ -293,6 +330,18 @@ bool EntityMotionState::isCandidateForOwnership() const { assert(_body); assert(_entity); assert(entityTreeIsLocked()); + + /* adebug + bool verbose = _entity->getName() == "fubar"; // adebug + if (verbose) { + bool isCandidate = _outgoingPriority != 0 + || Physics::getSessionUUID() == _entity->getSimulatorID() + || _entity->actionDataNeedsTransmit(); + if (!isCandidate) { + std::cout << (void*)(this) << " adebug not candidate --> erase" << std::endl; // adebug + } + } + */ return _outgoingPriority != 0 || Physics::getSessionUUID() == _entity->getSimulatorID() || _entity->actionDataNeedsTransmit(); @@ -491,6 +540,7 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_t step) { assert(_entity); assert(entityTreeIsLocked()); + bool verbose = _entity->getName() == "fubar"; // adebug if (!_body->isActive()) { // make sure all derivatives are zero @@ -576,6 +626,9 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ properties.clearSimulationOwner(); _outgoingPriority = 0; _entity->setPendingOwnershipPriority(_outgoingPriority, now); + if (verbose) { + std::cout << (void*)(this) << " adebug sendUpdate() clearOwnership numInactiveUpdates = " << (int)_numInactiveUpdates << std::endl; // adebug + } } else if (Physics::getSessionUUID() != _entity->getSimulatorID()) { // we don't own the simulation for this entity yet, but we're sending a bid for it quint8 bidPriority = glm::max(_outgoingPriority, VOLUNTEER_SIMULATION_PRIORITY); @@ -586,6 +639,9 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ // don't forget to remember that we have made a bid _entity->rememberHasSimulationOwnershipBid(); // ...then reset _outgoingPriority in preparation for the next frame + if (verbose) { + std::cout << (void*)(this) << " adebug sendUpdate() bidOwnership at " << (int)_outgoingPriority << std::endl; // adebug + } _outgoingPriority = 0; } else if (_outgoingPriority != _entity->getSimulationPriority()) { // we own the simulation but our desired priority has changed @@ -597,6 +653,9 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ properties.setSimulationOwner(Physics::getSessionUUID(), _outgoingPriority); } _entity->setPendingOwnershipPriority(_outgoingPriority, now); + if (verbose) { + std::cout << (void*)(this) << " adebug sendUpdate() changePriority to " << (int)_outgoingPriority << std::endl; // adebug + } } EntityItemID id(_entity->getID()); diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 903b160a5e..0b0bbcb6fc 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -259,13 +259,14 @@ void PhysicalEntitySimulation::getObjectsToChange(VectorOfMotionStates& result) _pendingChanges.clear(); } -void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates& motionStates) { +void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { QMutexLocker lock(&_mutex); // walk the motionStates looking for those that correspond to entities for (auto stateItr : motionStates) { ObjectMotionState* state = &(*stateItr); - if (state && state->getType() == MOTIONSTATE_TYPE_ENTITY) { + assert(state); + if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { EntityMotionState* entityState = static_cast(state); EntityItemPointer entity = entityState->getEntity(); assert(entity.get()); diff --git a/libraries/physics/src/PhysicalEntitySimulation.h b/libraries/physics/src/PhysicalEntitySimulation.h index af5def9775..9035308741 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.h +++ b/libraries/physics/src/PhysicalEntitySimulation.h @@ -56,7 +56,7 @@ public: void setObjectsToChange(const VectorOfMotionStates& objectsToChange); void getObjectsToChange(VectorOfMotionStates& result); - void handleOutgoingChanges(const VectorOfMotionStates& motionStates); + void handleChangedMotionStates(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); EntityEditPacketSender* getPacketSender() { return _entityPacketSender; } @@ -67,7 +67,7 @@ private: SetOfEntities _entitiesToAddToPhysics; SetOfEntityMotionStates _pendingChanges; // EntityMotionStates already in PhysicsEngine that need their physics changed - SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we need to send updates to entity-server + SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we may need to send updates to entity-server SetOfMotionStates _physicalObjects; // MotionStates of entities in PhysicsEngine diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp index 5fe99f137c..c9cbc6a2be 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp @@ -120,6 +120,9 @@ void ThreadSafeDynamicsWorld::synchronizeMotionState(btRigidBody* body) { void ThreadSafeDynamicsWorld::synchronizeMotionStates() { BT_PROFILE("synchronizeMotionStates"); _changedMotionStates.clear(); + + // NOTE: m_synchronizeAllMotionStates is 'false' by default for optimization. + // See PhysicsEngine::init() where we call _dynamicsWorld->setForceUpdateAllAabbs(false) if (m_synchronizeAllMotionStates) { //iterate over all collision objects for (int i=0;i Date: Thu, 9 Mar 2017 17:25:22 -0800 Subject: [PATCH 15/43] restore transform of deactivated entities --- interface/src/Application.cpp | 5 ++- libraries/entities/src/EntityItem.cpp | 2 +- libraries/physics/src/EntityMotionState.cpp | 39 +++++++++++++++---- libraries/physics/src/EntityMotionState.h | 1 + .../physics/src/PhysicalEntitySimulation.cpp | 13 +++++++ .../physics/src/PhysicalEntitySimulation.h | 1 + libraries/physics/src/PhysicsEngine.cpp | 2 +- libraries/physics/src/PhysicsEngine.h | 3 +- .../physics/src/ThreadSafeDynamicsWorld.cpp | 27 +++++++++---- .../physics/src/ThreadSafeDynamicsWorld.h | 4 ++ 10 files changed, 77 insertions(+), 20 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4894ae55ec..368541490a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4439,7 +4439,10 @@ void Application::update(float deltaTime) { getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); - const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); + const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); + _entitySimulation->handleDeactivatedMotionStates(deactivations); + + const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); _entitySimulation->handleChangedMotionStates(outgoingChanges); avatarManager->handleChangedMotionStates(outgoingChanges); }); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index db253b639f..888de44505 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -676,7 +676,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef filterRejection = newSimOwner.getID().isNull(); bool verbose = getName() == "fubar"; // adebug if (verbose && _simulationOwner != newSimOwner) { - std::cout << (void*)(this) << " adebug ownership changed " + std::cout << (void*)(this) << " " << secTimestampNow() << " adebug ownership changed " << _simulationOwner.getID().toString().toStdString() << "." << (int)_simulationOwner.getPriority() << "-->" << newSimOwner.getID().toString().toStdString() << "." << (int)newSimOwner.getPriority() << std::endl; // adebug diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 77c5a4f697..5da9d52169 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -10,6 +10,7 @@ // #include +#include // adebug #include #include @@ -28,7 +29,8 @@ #endif // adebug TODO BOOKMARK: -// The problem is that userB may deactivate and disown and object before userA deactivates +// Consider an object near deactivation owned by userB and in view of userA... +// The problem is that userB may deactivate and disown the object before userA deactivates // userA will sometimes insert non-zero velocities (and position errors) into the Entity before it is deactivated locally // // It would be nice to prevent data export from Bullet to Entity for unowned objects except in cases where it is really needed (?) @@ -109,6 +111,24 @@ void EntityMotionState::updateServerPhysicsVariables() { _serverActionData = _entity->getActionData(); } +void EntityMotionState::handleDeactivation() { + // adebug + glm::vec3 pos = _entity->getPosition(); + float dx = glm::distance(pos, _serverPosition); + glm::vec3 v = _entity->getVelocity(); + float dv = glm::distance(v, _serverVelocity); + std::cout << "adebug deactivate '" << _entity->getName().toStdString() + << "' dx = " << dx << " dv = " << dv << std::endl; // adebug + // adebug + + // copy _server data to entity + bool success; + _entity->setPosition(_serverPosition, success, false); + _entity->setOrientation(_serverRotation, success, false); + _entity->setVelocity(ENTITY_ITEM_ZERO_VEC3); + _entity->setAngularVelocity(ENTITY_ITEM_ZERO_VEC3); +} + // virtual void EntityMotionState::handleEasyChanges(uint32_t& flags) { assert(entityTreeIsLocked()); @@ -125,7 +145,7 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { _outgoingPriority = 0; bool verbose = _entity->getName() == "fubar"; // adebug if (verbose) { - std::cout << (void*)(this) << " adebug flag for deactivation" << std::endl; // adebug + std::cout << (void*)(this) << " " << secTimestampNow() << " adebug flag for deactivation" << std::endl; // adebug } } else { // disowned object is still moving --> start timer for ownership bid @@ -244,7 +264,7 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { } // This callback is invoked by the physics simulation at the end of each simulation step... -// iff the corresponding RigidBody is DYNAMIC and has moved. +// iff the corresponding RigidBody is DYNAMIC and ACTIVE. void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { assert(_entity); assert(entityTreeIsLocked()); @@ -256,7 +276,10 @@ void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { // it goes inactive (at which point we should slam bullet to agree with entity) if (_body->getActivationState() == WANTS_DEACTIVATION && !_entity->isMoving()) { if (verbose) { - std::cout << (void*)(this) << " adebug v = " << _body->getLinearVelocity().length() << " w = " << _body->getAngularVelocity().length() << std::endl; // adebug + std::cout << (void*)(this) << " " << secTimestampNow() << " adebug entity at rest but physics is not?" + << " v = " << _body->getLinearVelocity().length() + << " w = " << _body->getAngularVelocity().length() + << std::endl; // adebug } } measureBodyAcceleration(); @@ -279,7 +302,7 @@ void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { if (verbose && (glm::length(getBodyLinearVelocity()) > 0.0f || glm::length(getBodyAngularVelocity()) > 0.0f) && _entity->getSimulationOwner().getID().isNull()) { - std::cout << (void*)(this) << " adebug set non-zero v on unowned object AS = " << _body->getActivationState() << std::endl; // adebug + std::cout << (void*)(this) << " " << secTimestampNow() << " adebug set non-zero v on unowned object AS = " << _body->getActivationState() << std::endl; // adebug } _entity->setVelocity(getBodyLinearVelocity()); @@ -627,7 +650,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ _outgoingPriority = 0; _entity->setPendingOwnershipPriority(_outgoingPriority, now); if (verbose) { - std::cout << (void*)(this) << " adebug sendUpdate() clearOwnership numInactiveUpdates = " << (int)_numInactiveUpdates << std::endl; // adebug + std::cout << (void*)(this) << " " << secTimestampNow() << " adebug sendUpdate() clearOwnership numInactiveUpdates = " << (int)_numInactiveUpdates << std::endl; // adebug } } else if (Physics::getSessionUUID() != _entity->getSimulatorID()) { // we don't own the simulation for this entity yet, but we're sending a bid for it @@ -640,7 +663,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ _entity->rememberHasSimulationOwnershipBid(); // ...then reset _outgoingPriority in preparation for the next frame if (verbose) { - std::cout << (void*)(this) << " adebug sendUpdate() bidOwnership at " << (int)_outgoingPriority << std::endl; // adebug + std::cout << (void*)(this) << " " << secTimestampNow() << " adebug sendUpdate() bidOwnership at " << (int)_outgoingPriority << std::endl; // adebug } _outgoingPriority = 0; } else if (_outgoingPriority != _entity->getSimulationPriority()) { @@ -654,7 +677,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ } _entity->setPendingOwnershipPriority(_outgoingPriority, now); if (verbose) { - std::cout << (void*)(this) << " adebug sendUpdate() changePriority to " << (int)_outgoingPriority << std::endl; // adebug + std::cout << (void*)(this) << " " << secTimestampNow() << " adebug sendUpdate() changePriority to " << (int)_outgoingPriority << std::endl; // adebug } } diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index feac47d8ec..380edf3927 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -29,6 +29,7 @@ public: virtual ~EntityMotionState(); void updateServerPhysicsVariables(); + void handleDeactivation(); virtual void handleEasyChanges(uint32_t& flags) override; virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) override; diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 0b0bbcb6fc..bd76b2d70f 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -259,6 +259,19 @@ void PhysicalEntitySimulation::getObjectsToChange(VectorOfMotionStates& result) _pendingChanges.clear(); } +void PhysicalEntitySimulation::handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates) { + for (auto stateItr : motionStates) { + ObjectMotionState* state = &(*stateItr); + assert(state); + if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { + EntityMotionState* entityState = static_cast(state); + entityState->handleDeactivation(); + EntityItemPointer entity = entityState->getEntity(); + _entitiesToSort.insert(entity); + } + } +} + void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { QMutexLocker lock(&_mutex); diff --git a/libraries/physics/src/PhysicalEntitySimulation.h b/libraries/physics/src/PhysicalEntitySimulation.h index 9035308741..5f6185add3 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.h +++ b/libraries/physics/src/PhysicalEntitySimulation.h @@ -56,6 +56,7 @@ public: void setObjectsToChange(const VectorOfMotionStates& objectsToChange); void getObjectsToChange(VectorOfMotionStates& result); + void handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates); void handleChangedMotionStates(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index f57be4eab3..7d97d25135 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -449,7 +449,7 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { return _collisionEvents; } -const VectorOfMotionStates& PhysicsEngine::getOutgoingChanges() { +const VectorOfMotionStates& PhysicsEngine::getChangedMotionStates() { BT_PROFILE("copyOutgoingChanges"); // Bullet will not deactivate static objects (it doesn't expect them to be active) // so we must deactivate them ourselves diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index bbafbb06b6..b2ebe58f08 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -65,7 +65,8 @@ public: bool hasOutgoingChanges() const { return _hasOutgoingChanges; } /// \return reference to list of changed MotionStates. The list is only valid until beginning of next simulation loop. - const VectorOfMotionStates& getOutgoingChanges(); + const VectorOfMotionStates& getChangedMotionStates(); + const VectorOfMotionStates& getDeactivatedMotionStates() const { return _dynamicsWorld->getDeactivatedMotionStates(); } /// \return reference to list of Collision events. The list is only valid until beginning of next simulation loop. const CollisionEvents& getCollisionEvents(); diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp index c9cbc6a2be..3c05257fc5 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp @@ -128,25 +128,36 @@ void ThreadSafeDynamicsWorld::synchronizeMotionStates() { for (int i=0;igetMotionState()) { - synchronizeMotionState(body); - _changedMotionStates.push_back(static_cast(body->getMotionState())); - } + if (body && body->getMotionState()) { + synchronizeMotionState(body); + _changedMotionStates.push_back(static_cast(body->getMotionState())); } } } else { //iterate over all active rigid bodies + // TODO? if this becomes a performance bottleneck we could derive our own SimulationIslandManager + // that remembers a list of objects it recently deactivated + _activeStates.clear(); + _deactivatedStates.clear(); for (int i=0;iisActive()) { - if (body->getMotionState()) { + ObjectMotionState* motionState = static_cast(body->getMotionState()); + if (motionState) { + if (body->isActive()) { synchronizeMotionState(body); - _changedMotionStates.push_back(static_cast(body->getMotionState())); + _changedMotionStates.push_back(motionState); + _activeStates.insert(motionState); + } else if (_lastActiveStates.find(motionState) != _lastActiveStates.end()) { + // this object was active last frame but is no longer + _deactivatedStates.push_back(motionState); } } } } + if (_deactivatedStates.size() > 0) { + std::cout << secTimestampNow() << " adebug num deactivated = " << _deactivatedStates.size() << std::endl; // adebug + } + _activeStates.swap(_lastActiveStates); } void ThreadSafeDynamicsWorld::saveKinematicState(btScalar timeStep) { diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.h b/libraries/physics/src/ThreadSafeDynamicsWorld.h index 68062d8d29..b4fcca8cdb 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.h +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.h @@ -49,12 +49,16 @@ public: float getLocalTimeAccumulation() const { return m_localTime; } const VectorOfMotionStates& getChangedMotionStates() const { return _changedMotionStates; } + const VectorOfMotionStates& getDeactivatedMotionStates() const { return _deactivatedStates; } private: // call this instead of non-virtual btDiscreteDynamicsWorld::synchronizeSingleMotionState() void synchronizeMotionState(btRigidBody* body); VectorOfMotionStates _changedMotionStates; + VectorOfMotionStates _deactivatedStates; + SetOfMotionStates _activeStates; + SetOfMotionStates _lastActiveStates; }; #endif // hifi_ThreadSafeDynamicsWorld_h From be3012181fee72dcdefaa39c189f5447b82b3058 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 9 Mar 2017 17:53:17 -0800 Subject: [PATCH 16/43] force local deactivation on remote deactivation --- libraries/physics/src/EntityMotionState.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 5da9d52169..03bcc43362 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -143,6 +143,8 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION; _body->setActivationState(WANTS_DEACTIVATION); _outgoingPriority = 0; + const float ACTIVATION_EXPIRY = 3.0f; // something larger than the 2.0 hard coded in Bullet + _body->setDeactivationTime(ACTIVATION_EXPIRY); bool verbose = _entity->getName() == "fubar"; // adebug if (verbose) { std::cout << (void*)(this) << " " << secTimestampNow() << " adebug flag for deactivation" << std::endl; // adebug From a16760278e5f84844604a8eda1e76dc379b19cfc Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 9 Mar 2017 17:58:53 -0800 Subject: [PATCH 17/43] remove debug code --- libraries/entities/src/EntityItem.cpp | 13 ---- libraries/physics/src/EntityMotionState.cpp | 76 ------------------- .../physics/src/ThreadSafeDynamicsWorld.cpp | 5 +- 3 files changed, 1 insertion(+), 93 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 888de44505..0bb085459e 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -674,13 +674,6 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // or rejects a set of properties, it clears this. In such cases, we don't want those custom // setters to ignore what the server says. filterRejection = newSimOwner.getID().isNull(); - bool verbose = getName() == "fubar"; // adebug - if (verbose && _simulationOwner != newSimOwner) { - std::cout << (void*)(this) << " " << secTimestampNow() << " adebug ownership changed " - << _simulationOwner.getID().toString().toStdString() << "." << (int)_simulationOwner.getPriority() << "-->" - << newSimOwner.getID().toString().toStdString() << "." << (int)newSimOwner.getPriority() - << std::endl; // adebug - } if (weOwnSimulation) { if (newSimOwner.getID().isNull() && !_simulationOwner.pendingRelease(lastEditedFromBufferAdjusted)) { // entity-server is trying to clear our ownership (probably at our own request) @@ -1891,9 +1884,6 @@ void EntityItem::updateSimulationOwner(const SimulationOwner& owner) { if (_simulationOwner.set(owner)) { _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; - if (getName() == "fubar") { - std::cout << "debug updateSimulationOwner() " << _simulationOwner.getID().toString().toStdString() << "." << (int)(_simulationOwner.getPriority()) << std::endl; // adebug - } } } @@ -1901,9 +1891,6 @@ void EntityItem::clearSimulationOwnership() { if (wantTerseEditLogging() && !_simulationOwner.isNull()) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now null"; } - if (getName() == "fubar") { - std::cout << "debug clearSimulationOwnership()" << std::endl; // adebug - } _simulationOwner.clear(); // don't bother setting the DIRTY_SIMULATOR_ID flag because: diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 03bcc43362..d80615e2c6 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -10,7 +10,6 @@ // #include -#include // adebug #include #include @@ -28,19 +27,6 @@ #include "EntityTree.h" #endif -// adebug TODO BOOKMARK: -// Consider an object near deactivation owned by userB and in view of userA... -// The problem is that userB may deactivate and disown the object before userA deactivates -// userA will sometimes insert non-zero velocities (and position errors) into the Entity before it is deactivated locally -// -// It would be nice to prevent data export from Bullet to Entity for unowned objects except in cases where it is really needed (?) -// Maybe can recycle _serverPosition and friends to store the "before simulationStep" data to more efficiently figure out -// if data should be used for non-owned objects. -// -// If we do that, we should convert _serverPosition and friends to use Bullet data types for efficiency. -// -// adebug - const uint8_t LOOPS_FOR_SIMULATION_ORPHAN = 50; const quint64 USECS_BETWEEN_OWNERSHIP_BIDS = USECS_PER_SECOND / 5; @@ -112,15 +98,6 @@ void EntityMotionState::updateServerPhysicsVariables() { } void EntityMotionState::handleDeactivation() { - // adebug - glm::vec3 pos = _entity->getPosition(); - float dx = glm::distance(pos, _serverPosition); - glm::vec3 v = _entity->getVelocity(); - float dv = glm::distance(v, _serverVelocity); - std::cout << "adebug deactivate '" << _entity->getName().toStdString() - << "' dx = " << dx << " dv = " << dv << std::endl; // adebug - // adebug - // copy _server data to entity bool success; _entity->setPosition(_serverPosition, success, false); @@ -145,10 +122,6 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { _outgoingPriority = 0; const float ACTIVATION_EXPIRY = 3.0f; // something larger than the 2.0 hard coded in Bullet _body->setDeactivationTime(ACTIVATION_EXPIRY); - bool verbose = _entity->getName() == "fubar"; // adebug - if (verbose) { - std::cout << (void*)(this) << " " << secTimestampNow() << " adebug flag for deactivation" << std::endl; // adebug - } } else { // disowned object is still moving --> start timer for ownership bid // TODO? put a delay in here proportional to distance from object? @@ -156,13 +129,6 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { _nextOwnershipBid = usecTimestampNow() + USECS_BETWEEN_OWNERSHIP_BIDS; } _loopsWithoutOwner = 0; - - // adebug BOOKMARK: the problem is that userB may deactivate and disown the Object - // but it may still be active for userA... who will store slightly non-zero velocities into EntityItem in the meantime - // - // It would be nice if we could ignore slight outgoing changes for unowned objects that WANT_DEACTIVATION until... - // (a) the changes exceed some threshold (--> bid for ownership) or... - // (b) they actually get deactivated (--> slam RigidBody positions to agree with EntityItem) } else if (_entity->getSimulatorID() == Physics::getSessionUUID()) { // we just inherited ownership, make sure our desired priority matches what we have upgradeOutgoingPriority(_entity->getSimulationPriority()); @@ -270,20 +236,6 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { assert(_entity); assert(entityTreeIsLocked()); - bool verbose = _entity->getName() == "fubar"; // adebug - // adebug BOOKMARK: the problem is that userB may deactivate and disown the Object - // but it may still be active for userA... who will store slightly non-zero velocities into EntityItem in the meantime - // so what we need to do is ignore setWorldTransform() events for unowned objects when the bullet data is not helpful - // until either the data passes some threshold (bid for it) or - // it goes inactive (at which point we should slam bullet to agree with entity) - if (_body->getActivationState() == WANTS_DEACTIVATION && !_entity->isMoving()) { - if (verbose) { - std::cout << (void*)(this) << " " << secTimestampNow() << " adebug entity at rest but physics is not?" - << " v = " << _body->getLinearVelocity().length() - << " w = " << _body->getAngularVelocity().length() - << std::endl; // adebug - } - } measureBodyAcceleration(); bool positionSuccess; _entity->setPosition(bulletToGLM(worldTrans.getOrigin()) + ObjectMotionState::getWorldOffset(), positionSuccess, false); @@ -301,12 +253,6 @@ void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { "setOrientation failed.*"); qCDebug(physics) << "EntityMotionState::setWorldTransform setOrientation failed" << _entity->getID(); } - if (verbose - && (glm::length(getBodyLinearVelocity()) > 0.0f || glm::length(getBodyAngularVelocity()) > 0.0f) - && _entity->getSimulationOwner().getID().isNull()) { - std::cout << (void*)(this) << " " << secTimestampNow() << " adebug set non-zero v on unowned object AS = " << _body->getActivationState() << std::endl; // adebug - - } _entity->setVelocity(getBodyLinearVelocity()); _entity->setAngularVelocity(getBodyAngularVelocity()); _entity->setLastSimulated(usecTimestampNow()); @@ -355,18 +301,6 @@ bool EntityMotionState::isCandidateForOwnership() const { assert(_body); assert(_entity); assert(entityTreeIsLocked()); - - /* adebug - bool verbose = _entity->getName() == "fubar"; // adebug - if (verbose) { - bool isCandidate = _outgoingPriority != 0 - || Physics::getSessionUUID() == _entity->getSimulatorID() - || _entity->actionDataNeedsTransmit(); - if (!isCandidate) { - std::cout << (void*)(this) << " adebug not candidate --> erase" << std::endl; // adebug - } - } - */ return _outgoingPriority != 0 || Physics::getSessionUUID() == _entity->getSimulatorID() || _entity->actionDataNeedsTransmit(); @@ -565,7 +499,6 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_t step) { assert(_entity); assert(entityTreeIsLocked()); - bool verbose = _entity->getName() == "fubar"; // adebug if (!_body->isActive()) { // make sure all derivatives are zero @@ -651,9 +584,6 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ properties.clearSimulationOwner(); _outgoingPriority = 0; _entity->setPendingOwnershipPriority(_outgoingPriority, now); - if (verbose) { - std::cout << (void*)(this) << " " << secTimestampNow() << " adebug sendUpdate() clearOwnership numInactiveUpdates = " << (int)_numInactiveUpdates << std::endl; // adebug - } } else if (Physics::getSessionUUID() != _entity->getSimulatorID()) { // we don't own the simulation for this entity yet, but we're sending a bid for it quint8 bidPriority = glm::max(_outgoingPriority, VOLUNTEER_SIMULATION_PRIORITY); @@ -664,9 +594,6 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ // don't forget to remember that we have made a bid _entity->rememberHasSimulationOwnershipBid(); // ...then reset _outgoingPriority in preparation for the next frame - if (verbose) { - std::cout << (void*)(this) << " " << secTimestampNow() << " adebug sendUpdate() bidOwnership at " << (int)_outgoingPriority << std::endl; // adebug - } _outgoingPriority = 0; } else if (_outgoingPriority != _entity->getSimulationPriority()) { // we own the simulation but our desired priority has changed @@ -678,9 +605,6 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ properties.setSimulationOwner(Physics::getSessionUUID(), _outgoingPriority); } _entity->setPendingOwnershipPriority(_outgoingPriority, now); - if (verbose) { - std::cout << (void*)(this) << " " << secTimestampNow() << " adebug sendUpdate() changePriority to " << (int)_outgoingPriority << std::endl; // adebug - } } EntityItemID id(_entity->getID()); diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp index 3c05257fc5..24cfbc2609 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp @@ -136,7 +136,7 @@ void ThreadSafeDynamicsWorld::synchronizeMotionStates() { } else { //iterate over all active rigid bodies // TODO? if this becomes a performance bottleneck we could derive our own SimulationIslandManager - // that remembers a list of objects it recently deactivated + // that remembers a list of objects deactivated last step _activeStates.clear(); _deactivatedStates.clear(); for (int i=0;i 0) { - std::cout << secTimestampNow() << " adebug num deactivated = " << _deactivatedStates.size() << std::endl; // adebug - } _activeStates.swap(_lastActiveStates); } From 9c9eb879255d3136b321622d0b221196a27e96d9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 10 Mar 2017 12:55:16 -0800 Subject: [PATCH 18/43] sync RigidBody on deactivation --- libraries/physics/src/EntityMotionState.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index d80615e2c6..bfa15537fc 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -104,6 +104,11 @@ void EntityMotionState::handleDeactivation() { _entity->setOrientation(_serverRotation, success, false); _entity->setVelocity(ENTITY_ITEM_ZERO_VEC3); _entity->setAngularVelocity(ENTITY_ITEM_ZERO_VEC3); + // and also to RigidBody + btTransform worldTrans; + worldTrans.setOrigin(glmToBullet(_serverPosition)); + worldTrans.setRotation(glmToBullet(_serverRotation)); + // no need to update velocities... should already be zero } // virtual From bde2222dfd758292f8ac0ca6c3e26ec3dd6c492b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 10 Mar 2017 18:30:56 -0800 Subject: [PATCH 19/43] actually use worldTrans --- libraries/physics/src/EntityMotionState.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index bfa15537fc..d383f4c199 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -108,6 +108,7 @@ void EntityMotionState::handleDeactivation() { btTransform worldTrans; worldTrans.setOrigin(glmToBullet(_serverPosition)); worldTrans.setRotation(glmToBullet(_serverRotation)); + _body->setWorldTransform(worldTrans); // no need to update velocities... should already be zero } From 075574b428d098cb6c989bb035ff81671e8f983c Mon Sep 17 00:00:00 2001 From: humbletim Date: Mon, 13 Mar 2017 16:33:42 -0400 Subject: [PATCH 20/43] log cleanup per CR; add more specific hints (instead of relying on __FUNCTION__) --- libraries/script-engine/src/ScriptEngine.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index f96b733c74..615c385a52 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -564,7 +564,6 @@ void ScriptEngine::resetModuleCache(bool deleteScriptCache) { if (it.flags() & QScriptValue::SkipInEnumeration) { continue; } - //scriptCache->deleteScript(it.name()); qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; cacheMeta.setProperty(it.name(), true); } @@ -594,8 +593,8 @@ void ScriptEngine::init() { entityScriptingInterface->init(); connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { if (_entityScripts.contains(entityID)) { - if (isEntityScriptRunning(entityID)) { - qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID; + if (isEntityScriptRunning(entityID) && !isStopping()) { + qCWarning(scriptengine) << "deletingEntity while entity script is still running" << entityID; } _entityScripts.remove(entityID); emit entityScriptDetailsUpdated(); @@ -954,7 +953,7 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi syntaxError.setProperty("detail", "evaluate"); } raiseException(syntaxError); - maybeEmitUncaughtException(__FUNCTION__); + maybeEmitUncaughtException("lint"); return syntaxError; } QScriptProgram program { sourceCode, fileName, lineNumber }; @@ -962,14 +961,14 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi // can this happen? auto err = makeError("could not create QScriptProgram for " + fileName); raiseException(err); - maybeEmitUncaughtException(__FUNCTION__); + maybeEmitUncaughtException("compile"); return err; } QScriptValue result; { result = BaseScriptEngine::evaluate(program); - maybeEmitUncaughtException(__FUNCTION__); + maybeEmitUncaughtException("evaluate"); } return result; } @@ -1644,11 +1643,11 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { auto exports = module.property("exports"); if (!invalidateCache && exports.isObject()) { - // we have found a cacheed module -- just need to possibly register it with current parent + // we have found a cached module -- just need to possibly register it with current parent qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)") .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString()); registerModuleWithParent(module, parent); - maybeEmitUncaughtException(__FUNCTION__); + maybeEmitUncaughtException("cached module"); return exports; } @@ -2380,9 +2379,12 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { const EntityScriptDetails &oldDetails = _entityScripts[entityID]; if (isEntityScriptRunning(entityID)) { callEntityScriptMethod(entityID, "unload"); - } else { + } +#ifdef DEBUG_ENTITY_STATES + else { qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; } +#endif if (oldDetails.status != EntityScriptStatus::UNLOADED) { EntityScriptDetails newDetails; newDetails.status = EntityScriptStatus::UNLOADED; From 0926b2df2a1b1c880d44f33a3d0ce0004d2697f1 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 13 Mar 2017 15:14:20 -0700 Subject: [PATCH 21/43] add LX snap turn to standard mapping --- interface/resources/controllers/standard.json | 32 ++++++++++++------- ...oggleAdvancedMovementForHandControllers.js | 1 - 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 04a3f560b6..b0654daaa7 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -2,6 +2,18 @@ "name": "Standard to Action", "channels": [ { "from": "Standard.LY", "to": "Actions.TranslateZ" }, + + { "from": "Standard.LX", + "when": [ "Application.InHMD", "Application.SnapTurn" ], + "to": "Actions.StepYaw", + "filters": + [ + { "type": "deadZone", "min": 0.15 }, + "constrainToInteger", + { "type": "pulse", "interval": 0.25 }, + { "type": "scale", "scale": 22.5 } + ] + }, { "from": "Standard.LX", "to": "Actions.TranslateX" }, { "from": "Standard.RX", @@ -15,29 +27,27 @@ { "type": "scale", "scale": 22.5 } ] }, - { "from": "Standard.RX", "to": "Actions.Yaw" }, - { "from": "Standard.RY", - "when": "Application.Grounded", - "to": "Actions.Up", - "filters": + + { "from": "Standard.RY", + "when": "Application.Grounded", + "to": "Actions.Up", + "filters": [ { "type": "deadZone", "min": 0.6 }, "invert" ] - }, + }, - { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"}, + { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"}, { "from": "Standard.Back", "to": "Actions.CycleCamera" }, { "from": "Standard.Start", "to": "Actions.ContextMenu" }, - { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, + { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, { "from": "Standard.RT", "to": "Actions.RightHandClick" }, - { "from": "Standard.LeftHand", "to": "Actions.LeftHand" }, + { "from": "Standard.LeftHand", "to": "Actions.LeftHand" }, { "from": "Standard.RightHand", "to": "Actions.RightHand" } ] } - - diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js index 46464dc2e1..61d2df5d9e 100644 --- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js +++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -72,7 +72,6 @@ function registerBasicMapping() { } return; }); - basicMapping.from(Controller.Standard.LX).to(Controller.Standard.RX); basicMapping.from(Controller.Standard.RY).to(function(value) { if (isDisabled) { return; From db73c80ba19f21416eac33e66db1cb929004d366 Mon Sep 17 00:00:00 2001 From: humbletim Date: Mon, 13 Mar 2017 19:16:00 -0400 Subject: [PATCH 22/43] remove unused meta property adapter --- .../entities/src/EntityScriptingInterface.cpp | 52 ------------------- .../entities/src/EntityScriptingInterface.h | 2 - 2 files changed, 54 deletions(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index aa241023c8..54efa3d89f 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -683,54 +683,6 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) { return client->reloadServerScript(entityID); } -#ifdef DEBUG_ENTITY_METADATA -// baseline example -- return parsed userData as a standard CPS callback -bool EntityPropertyMetadataRequest::_userData(EntityItemID entityID, QScriptValue handler) { - QScriptValue err, result; - auto engine = _engine; - if (!engine) { - qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID; - return false; - } - auto entityScriptingInterface = DependencyManager::get(); - auto entityTree = entityScriptingInterface ? entityScriptingInterface->getEntityTree() : nullptr; - if (!entityTree) { - err = engine->makeError("Entities Tree unavailable", "InternalError"); - } else { - EntityItemPointer entity = entityTree->findEntityByID(entityID); - if (!entity) { - err = engine->makeError("entity not found"); - } else { - auto JSON = engine->globalObject().property("JSON"); - auto parsed = JSON.property("parse").call(JSON, QScriptValueList({ entity->getUserData() })); - if (engine->hasUncaughtException()) { - err = engine->cloneUncaughtException(__FUNCTION__); - engine->clearExceptions(); - } else { - result = parsed; - } - } - } - // this one second delay can be used with a Client script to query metadata and immediately Script.stop() - // (testing that the signal handler never gets called once the engine is destroyed) - // note: we still might want to check engine->isStopping() as an optimization in some places - QFutureWatcher *request = new QFutureWatcher; - QObject::connect(request, &QFutureWatcher::finished, engine, [=]() mutable { - if (!engine) { - qCDebug(entities) << "queryPropertyMetadata -- engine destroyed while inflight" << entityID; - return; - } - callScopedHandlerObject(handler, err, result); - request->deleteLater(); - }); - request->setFuture(QtConcurrent::run([]() -> QVariant { - QThread::sleep(1); - return QVariant(); - })); - return true; -} -#endif - bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) { using LocalScriptStatusRequest = QFutureWatcher; @@ -842,10 +794,6 @@ bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValu return request.script(entityID, handler); } else if (name == "serverScripts") { return request.serverScripts(entityID, handler); -#ifdef DEBUG_ENTITY_METADATA - } else if (name == "userData") { - return request.userData(entityID, handler); -#endif } else { engine->raiseException(engine->makeError("metadata for property " + name + " is not yet queryable")); engine->maybeEmitUncaughtException(__FUNCTION__); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index d6fe93b41e..7631541b3e 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -47,8 +47,6 @@ public: EntityPropertyMetadataRequest(BaseScriptEngine* engine) : _engine(engine) {}; bool script(EntityItemID entityID, QScriptValue handler); bool serverScripts(EntityItemID entityID, QScriptValue handler); - // this is used for internal testing and only available when DEBUG_ENTITY_METADATA is defined in the .cpp file - bool userData(EntityItemID entityID, QScriptValue handler); private: QPointer _engine; }; From 1fe02477b0457945d4d8da808e666e3166f391a5 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 14 Mar 2017 14:21:21 -0700 Subject: [PATCH 23/43] fix LX behaviour in snap/advanced/basic --- interface/resources/controllers/standard.json | 18 ++++++++++++++---- interface/src/Application.cpp | 7 ++++++- interface/src/avatar/MyAvatar.cpp | 1 + interface/src/avatar/MyAvatar.h | 3 +++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index b0654daaa7..9e3b2f4d13 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -4,7 +4,10 @@ { "from": "Standard.LY", "to": "Actions.TranslateZ" }, { "from": "Standard.LX", - "when": [ "Application.InHMD", "Application.SnapTurn" ], + "when": [ + "Application.InHMD", "!Application.AdvancedMovement", + "Application.SnapTurn", "!Standard.RX" + ], "to": "Actions.StepYaw", "filters": [ @@ -14,7 +17,12 @@ { "type": "scale", "scale": 22.5 } ] }, - { "from": "Standard.LX", "to": "Actions.TranslateX" }, + { "from": "Standard.LX", "to": "Actions.TranslateX", + "when": [ "Application.AdvancedMovement" ] + }, + { "from": "Standard.LX", "to": "Actions.Yaw", + "when": [ "!Application.AdvancedMovement", "!Application.SnapTurn" ] + }, { "from": "Standard.RX", "when": [ "Application.InHMD", "Application.SnapTurn" ], @@ -27,8 +35,10 @@ { "type": "scale", "scale": 22.5 } ] }, - { "from": "Standard.RX", "to": "Actions.Yaw" }, - + { "from": "Standard.RX", "to": "Actions.Yaw", + "when": [ "!Application.SnapTurn" ] + }, + { "from": "Standard.RY", "when": "Application.Grounded", "to": "Actions.Up", diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4bebc80bd1..706fd424fd 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -423,6 +423,7 @@ static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; static const QString STATE_CAMERA_ENTITY = "CameraEntity"; static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; static const QString STATE_SNAP_TURN = "SnapTurn"; +static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement"; static const QString STATE_GROUNDED = "Grounded"; static const QString STATE_NAV_FOCUSED = "NavigationFocused"; @@ -513,7 +514,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, - STATE_SNAP_TURN, STATE_GROUNDED, STATE_NAV_FOCUSED } }); + STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED } }); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -1127,6 +1128,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; }); + _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float { + return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; }); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 969268c549..b045d2c005 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -104,6 +104,7 @@ MyAvatar::MyAvatar(RigPointer rig) : _eyeContactTarget(LEFT_EYE), _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), + _useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false), _hmdSensorMatrix(), _hmdSensorOrientation(), _hmdSensorPosition(), diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 3cc665b533..1a5883edca 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -171,6 +171,8 @@ public: Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } + bool useAdvancedMovementControls() const { return _useAdvancedMovementControls.get(); } + // get/set avatar data void saveData(); void loadData(); @@ -423,6 +425,7 @@ private: glm::vec3 _trackedHeadPosition; Setting::Handle _realWorldFieldOfView; + Setting::Handle _useAdvancedMovementControls; // private methods void updateOrientation(float deltaTime); From 490cb834899c80edd94ad143b0bfc6df0acd606d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 14 Mar 2017 15:03:01 -0700 Subject: [PATCH 24/43] move advanced movement control changes to MyAvatar --- interface/src/avatar/MyAvatar.h | 3 +++ .../toggleAdvancedMovementForHandControllers.js | 14 ++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 1a5883edca..f892a8d8b7 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -86,6 +86,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) + Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls) public: explicit MyAvatar(RigPointer rig); @@ -172,6 +173,8 @@ public: Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } bool useAdvancedMovementControls() const { return _useAdvancedMovementControls.get(); } + void setUseAdvancedMovementControls(bool useAdvancedMovementControls) + { _useAdvancedMovementControls.set(useAdvancedMovementControls); } // get/set avatar data void saveData(); diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js index 61d2df5d9e..a453e50d1b 100644 --- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js +++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -17,15 +17,14 @@ var mappingName, basicMapping, isChecked; var TURN_RATE = 1000; var MENU_ITEM_NAME = "Advanced Movement For Hand Controllers"; -var SETTINGS_KEY = 'advancedMovementForHandControllersIsChecked'; var isDisabled = false; -var previousSetting = Settings.getValue(SETTINGS_KEY); -if (previousSetting === '' || previousSetting === false || previousSetting === 'false') { +var previousSetting = MyAvatar.useAdvancedMovementControls; +if (previousSetting === false) { previousSetting = false; isChecked = false; } -if (previousSetting === true || previousSetting === 'true') { +if (previousSetting === true) { previousSetting = true; isChecked = true; } @@ -37,7 +36,6 @@ function addAdvancedMovementItemToSettingsMenu() { isCheckable: true, isChecked: previousSetting }); - } function rotate180() { @@ -111,10 +109,10 @@ function menuItemEvent(menuItem) { if (menuItem == MENU_ITEM_NAME) { isChecked = Menu.isOptionChecked(MENU_ITEM_NAME); if (isChecked === true) { - Settings.setValue(SETTINGS_KEY, true); + MyAvatar.setUseAdvancedMovementControls(true); disableMappings(); - } else if (isChecked === false) { - Settings.setValue(SETTINGS_KEY, false); + } else if (isChecked === false) + MyAvatar.setUseAdvancedMovementControls(true); enableMappings(); } } From 188530590a18ebe8efc02b94b1f5e60a1304aafc Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 14 Mar 2017 17:16:49 -0700 Subject: [PATCH 25/43] add missing curly brace to script --- .../controllers/toggleAdvancedMovementForHandControllers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js index a453e50d1b..8371259915 100644 --- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js +++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -111,7 +111,7 @@ function menuItemEvent(menuItem) { if (isChecked === true) { MyAvatar.setUseAdvancedMovementControls(true); disableMappings(); - } else if (isChecked === false) + } else if (isChecked === false) { MyAvatar.setUseAdvancedMovementControls(true); enableMappings(); } From 7ddb5ff7707318083f69ef94faff43524b8f801d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 14 Mar 2017 18:09:17 -0700 Subject: [PATCH 26/43] use correct boolean when changing AMC --- .../controllers/toggleAdvancedMovementForHandControllers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js index 8371259915..5cf27a0ec5 100644 --- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js +++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -112,7 +112,7 @@ function menuItemEvent(menuItem) { MyAvatar.setUseAdvancedMovementControls(true); disableMappings(); } else if (isChecked === false) { - MyAvatar.setUseAdvancedMovementControls(true); + MyAvatar.setUseAdvancedMovementControls(false); enableMappings(); } } From bfc51d8222f1bd85a5014c557f9bfdcf9322b07e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 14 Mar 2017 18:10:44 -0700 Subject: [PATCH 27/43] use property not function for advanced movement controls --- .../controllers/toggleAdvancedMovementForHandControllers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js index 5cf27a0ec5..8f8f5c2d3e 100644 --- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js +++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -109,10 +109,10 @@ function menuItemEvent(menuItem) { if (menuItem == MENU_ITEM_NAME) { isChecked = Menu.isOptionChecked(MENU_ITEM_NAME); if (isChecked === true) { - MyAvatar.setUseAdvancedMovementControls(true); + MyAvatar.advancedMovementControls = true; disableMappings(); } else if (isChecked === false) { - MyAvatar.setUseAdvancedMovementControls(false); + MyAvatar.advancedMovementControls = false; enableMappings(); } } From d13752a7c6971e15bc2f4c377192a6adf1e1acd9 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 15 Mar 2017 10:34:43 -0700 Subject: [PATCH 28/43] use the correct property name for useAdvancedMovementControls --- .../controllers/toggleAdvancedMovementForHandControllers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js index 8f8f5c2d3e..e6c9b0aee0 100644 --- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js +++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -109,10 +109,10 @@ function menuItemEvent(menuItem) { if (menuItem == MENU_ITEM_NAME) { isChecked = Menu.isOptionChecked(MENU_ITEM_NAME); if (isChecked === true) { - MyAvatar.advancedMovementControls = true; + MyAvatar.useAdvancedMovementControls = true; disableMappings(); } else if (isChecked === false) { - MyAvatar.advancedMovementControls = false; + MyAvatar.useAdvancedMovementControls = false; enableMappings(); } } From 52a571558cc974eedae95cc3f2f4908433c40c0e Mon Sep 17 00:00:00 2001 From: humbletim Date: Thu, 16 Mar 2017 01:23:23 -0400 Subject: [PATCH 29/43] * changes per CR feedback * revert JSON content-type workaround * add specific error check / advice for unanchored module ids * update unit tests --- .../entities/src/EntityScriptingInterface.cpp | 12 +- libraries/script-engine/src/ScriptEngine.cpp | 132 +++++------------- libraries/script-engine/src/ScriptEngine.h | 7 +- .../developer/libraries/jasmine/hifi-boot.js | 2 +- .../tests/unit_tests/moduleUnitTests.js | 40 ++++-- 5 files changed, 65 insertions(+), 128 deletions(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 54efa3d89f..08216b015a 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -686,24 +686,18 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) { bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) { using LocalScriptStatusRequest = QFutureWatcher; - LocalScriptStatusRequest *request = new LocalScriptStatusRequest; + LocalScriptStatusRequest* request = new LocalScriptStatusRequest; QObject::connect(request, &LocalScriptStatusRequest::finished, _engine, [=]() mutable { - auto engine = _engine; - if (!engine) { - // this is just to address any lingering doubts -- when _engine is destroyed, this connect gets broken automatically - qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID; - return; - } auto details = request->result().toMap(); QScriptValue err, result; if (details.contains("isError")) { if (!details.contains("message")) { details["message"] = details["errorInfo"]; } - err = engine->makeError(engine->toScriptValue(details)); + err = _engine->makeError(_engine->toScriptValue(details)); } else { details["success"] = true; - result = engine->toScriptValue(details); + result = _engine->toScriptValue(details); } callScopedHandlerObject(handler, err, result); request->deleteLater(); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 615c385a52..d956deed2f 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -73,15 +73,11 @@ #include "MIDIEvent.h" -const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT { - "com.highfidelity.experimental.enableExtendedModuleCompatbility" -}; - const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { "com.highfidelity.experimental.enableExtendedJSExceptions" }; -static const int MAX_MODULE_ID_LENTGH { 4096 }; +static const int MAX_MODULE_ID_LENGTH { 4096 }; static const int MAX_DEBUG_VALUE_LENGTH { 80 }; static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = @@ -96,7 +92,7 @@ int functionSignatureMetaID = qRegisterMetaTypeargumentCount(); i++) { if (i > 0) { @@ -345,11 +341,7 @@ void ScriptEngine::runInThread() { // The thread interface cannot live on itself, and we want to move this into the thread, so // the thread cannot have this as a parent. QThread* workerThread = new QThread(); -#ifdef Q_OS_LINUX - workerThread->setObjectName(QString("js:") + getFilename()); -#else - workerThread->setObjectName(QString("Script Thread:") + getFilename()); -#endif + workerThread->setObjectName(QString("js:") + getFilename().replace("about:","")); moveToThread(workerThread); // NOTE: If you connect any essential signals for proper shutdown or cleanup of @@ -550,7 +542,7 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) { void ScriptEngine::resetModuleCache(bool deleteScriptCache) { if (QThread::currentThread() != thread()) { - executeOnScriptThread([=](){ resetModuleCache(deleteScriptCache); }); + executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); }); return; } auto jsRequire = globalObject().property("Script").property("require"); @@ -1379,14 +1371,14 @@ QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& re auto throwResolveError = [&](const QScriptValue& error) -> QString { raiseException(error); maybeEmitUncaughtException("require.resolve"); - return nullptr; + return QString(); }; // de-fuzz the input a little by restricting to rational sizes auto idLength = url.toString().length(); - if (idLength < 1 || idLength > MAX_MODULE_ID_LENTGH) { + if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) { auto details = QString("rejecting invalid module id size (%1 chars [1,%2])") - .arg(idLength).arg(MAX_MODULE_ID_LENTGH); + .arg(idLength).arg(MAX_MODULE_ID_LENGTH); return throwResolveError(makeError(message.arg(details), "RangeError")); } @@ -1402,11 +1394,21 @@ QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& re url = resolvePath(moduleId); } else { // check if the moduleId refers to a "system" module - QString defaultsPath = defaultScriptsLoc.path(); - QString systemModulePath = QString("%1/modules/%2.js").arg(defaultsPath).arg(moduleId); + QString systemPath = defaultScriptsLoc.path(); + QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId); url = defaultScriptsLoc; url.setPath(systemModulePath); if (!QFileInfo(url.toLocalFile()).isFile()) { + if (!moduleId.contains("./")) { + // the user might be trying to refer to a relative file without anchoring it + // let's do them a favor and test for that case -- offering specific advice if detected + auto unanchoredUrl = resolvePath("./" + moduleId); + if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) { + auto msg = QString("relative module ids must be anchored; use './%1' instead") + .arg(moduleId); + return throwResolveError(makeError(message.arg(msg))); + } + } return throwResolveError(makeError(message.arg("system module not found"))); } } @@ -1469,7 +1471,7 @@ bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QS if (children.isArray()) { auto key = module.property("id"); auto length = children.property("length").toInt32(); - for (int i=0; i < length; i++) { + for (int i = 0; i < length; i++) { if (children.property(i).property("id").strictlyEquals(key)) { qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; children.setProperty(i, module); @@ -1516,10 +1518,10 @@ QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptVal } // synchronously fetch a module's source code using BatchLoader -QScriptValue ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { +QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { using UrlMap = QMap; auto scriptCache = DependencyManager::get(); - QScriptValue req = newObject(); + QVariantMap req; qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread(); auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) { @@ -1528,13 +1530,13 @@ QScriptValue ScriptEngine::fetchModuleSource(const QString& modulePath, const bo auto contents = data[url]; qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread(); if (isStopping()) { - req.setProperty("status", "Stopped"); - req.setProperty("success", false); + req["status"] = "Stopped"; + req["success"] = false; } else { - req.setProperty("url", url); - req.setProperty("status", status); - req.setProperty("success", ScriptCache::isSuccessStatus(status)); - req.setProperty("contents", contents, READONLY_HIDDEN_PROP_FLAGS); + req["url"] = url; + req["status"] = status; + req["success"] = ScriptCache::isSuccessStatus(status); + req["contents"] = contents; } }; @@ -1661,17 +1663,17 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { // download the module source auto req = fetchModuleSource(modulePath, invalidateCache); - if (!req.property("success").toBool()) { - auto error = QString("error retrieving script (%1)").arg(req.property("status").toString()); + if (!req.contains("success") || !req["success"].toBool()) { + auto error = QString("error retrieving script (%1)").arg(req["status"].toString()); return throwModuleError(modulePath, error); } #if DEBUG_JS_MODULES qCDebug(scriptengine_module) << "require.loaded: " << - QUrl(req.property("url").toString()).fileName() << req.property("status").toString(); + QUrl(req["url"].toString()).fileName() << req["status"].toString(); #endif - auto sourceCode = req.property("contents").toString(); + auto sourceCode = req["contents"].toString(); if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) { module.setProperty("content-type", "application/json"); @@ -1679,28 +1681,6 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { module.setProperty("content-type", "application/javascript"); } - { - // It seems that many JSON sources don't actually put .json in the URL... - // so for now as a workaround users wanting to indicate JSON parsing mode can - // do so by calling with a custom this context, eg: - // - // var ITEMS_URL = 'https://highfidelity.io/api/v1/marketplace/items'; - // var thisObject = { 'content-type': 'application/json' }; - // var items = Script.require.call(thisObject, ITEMS_URL + '?category=everything&sort=recent'); - - auto thisObject = currentContext()->thisObject(); - bool calledWithCustomThis = thisObject.isObject() && - !thisObject.strictlyEquals(globalObject()) && - !thisObject.toQObject(); - - if (calledWithCustomThis) { -#ifdef DEBUG_JS - _debugDump("this", thisObject); -#endif - applyUserOptions(module, thisObject); - } - } - // evaluate the module auto result = instantiateModule(module, sourceCode); @@ -1721,54 +1701,6 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { return module.property("exports"); } -// User-configurable override options -void ScriptEngine::applyUserOptions(QScriptValue& module, QScriptValue& options) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return; - } - if (!options.isValid()) { - return; - } - // options['content-type'] === 'application/json' - // -- allows JSON modules to be used from URLs not ending in .json - if (options.property("content-type").isString()) { - module.setProperty("content-type", options.property("content-type")); - qCDebug(scriptengine_module) << "module['content-type'] =" << module.property("content-type").toString(); - } - - if (ScriptEngine::_enableExtendedModuleCompatbility.get()) { - auto closure = module.property("__closure__"); - - auto maybeSetToExports = [&](const QString& key) { - if (options.property(key).toString() == "exports") { - closure.setProperty(key, module.property("exports")); - qCDebug(scriptengine_module) << "module.closure[" << key << "] = exports"; - } - }; - - // options[{key}] = 'exports' - // several "agnostic" modules in the wild are just one step away from being compatible -- - // they just didn't know not to look specifically for this, self or global for attaching - // things onto. - maybeSetToExports("global"); - maybeSetToExports("self"); - maybeSetToExports("this"); - - // when options is an Object it will get used as the value of "this" during module evaluation - // (which is what one might expect when calling require.call(thisObject, ...)) - if (options.isObject()) { - closure.setProperty("this", options); - } - - // when options.global is an Object it'll get used as the global object (during evaluation only) - if (options.property("global").isObject()) { - closure.setProperty("global", options.property("global")); - qCDebug(scriptengine_module) << "module.closure['global'] = options.global"; - } - } - maybeEmitUncaughtException(__FUNCTION__); -} - // 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 diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index ef6f3b6896..a3f709eff1 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -167,7 +167,7 @@ public: QScriptValue currentModule(); bool registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent); QScriptValue newModule(const QString& modulePath, const QScriptValue& parent = QScriptValue()); - QScriptValue fetchModuleSource(const QString& modulePath, const bool forceDownload = false); + QVariantMap fetchModuleSource(const QString& modulePath, const bool forceDownload = false); QScriptValue instantiateModule(const QScriptValue& module, const QString& sourceCode); Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS); @@ -308,7 +308,7 @@ protected: AssetScriptingInterface _assetScriptingInterface{ this }; - std::function _emitScriptUpdates{ [](){ return true; } }; + std::function _emitScriptUpdates{ []() { return true; } }; std::recursive_mutex _lock; @@ -317,10 +317,7 @@ protected: static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT; static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; - Setting::Handle _enableExtendedModuleCompatbility { _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT, false }; Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; - - void applyUserOptions(QScriptValue& module, QScriptValue& options); }; #endif // hifi_ScriptEngine_h diff --git a/scripts/developer/libraries/jasmine/hifi-boot.js b/scripts/developer/libraries/jasmine/hifi-boot.js index 8757550ae8..772dd8c17e 100644 --- a/scripts/developer/libraries/jasmine/hifi-boot.js +++ b/scripts/developer/libraries/jasmine/hifi-boot.js @@ -62,7 +62,7 @@ clearTimeout = Script.clearTimeout; clearInterval = Script.clearInterval; - var jasmine = jasmineRequire.core(jasmineRequire); + var jasmine = this.jasmine = jasmineRequire.core(jasmineRequire); var env = jasmine.getEnv(); diff --git a/scripts/developer/tests/unit_tests/moduleUnitTests.js b/scripts/developer/tests/unit_tests/moduleUnitTests.js index a9446d1d6e..6810dd8b6d 100644 --- a/scripts/developer/tests/unit_tests/moduleUnitTests.js +++ b/scripts/developer/tests/unit_tests/moduleUnitTests.js @@ -32,6 +32,18 @@ describe('require', function() { mod.exists; }).toThrowError(/Cannot find/); }); + it('should reject unanchored, existing filenames with advice', function() { + expect(function() { + var mod = require.resolve('moduleTests/example.json'); + mod.exists; + }).toThrowError(/use '.\/moduleTests\/example\.json'/); + }); + it('should reject unanchored, non-existing filenames', function() { + expect(function() { + var mod = require.resolve('asdfssdf/example.json'); + mod.exists; + }).toThrowError(/Cannot find.*system module not found/); + }); it('should reject non-existent filenames', function() { expect(function() { require.resolve('./404error.js'); @@ -67,19 +79,21 @@ describe('require', function() { var example = require('./moduleTests/example.json'); expect(example.name).toEqual('Example JSON Module'); }); - INTERFACE.describe('interface', function() { - NETWORK.describe('network', function() { - // xit('should import #content-type=application/json modules', function() { - // var results = require('https://jsonip.com#content-type=application/json'); - // expect(results.ip).toMatch(/^[.0-9]+$/); - // }); - it('should import content-type: application/json modules', function() { - var scope = { 'content-type': 'application/json' }; - var results = require.call(scope, 'https://jsonip.com'); - expect(results.ip).toMatch(/^[.0-9]+$/); - }); - }); - }); + // noet: support for loading JSON via content type workarounds reverted + // (leaving these tests intact in case ever revisited later) + // INTERFACE.describe('interface', function() { + // NETWORK.describe('network', function() { + // xit('should import #content-type=application/json modules', function() { + // var results = require('https://jsonip.com#content-type=application/json'); + // expect(results.ip).toMatch(/^[.0-9]+$/); + // }); + // xit('should import content-type: application/json modules', function() { + // var scope = { 'content-type': 'application/json' }; + // var results = require.call(scope, 'https://jsonip.com'); + // expect(results.ip).toMatch(/^[.0-9]+$/); + // }); + // }); + // }); }); From 366c90ef6acfb46e5f85998cc433ba2fce1a7639 Mon Sep 17 00:00:00 2001 From: humbletim Date: Thu, 16 Mar 2017 01:28:50 -0400 Subject: [PATCH 30/43] add Q_ASSERT to IS_THREADSAFE_INVOCATION --- libraries/shared/src/BaseScriptEngine.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/shared/src/BaseScriptEngine.cpp b/libraries/shared/src/BaseScriptEngine.cpp index d803e85ed6..c92d629b75 100644 --- a/libraries/shared/src/BaseScriptEngine.cpp +++ b/libraries/shared/src/BaseScriptEngine.cpp @@ -31,6 +31,7 @@ bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QSt qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3") .arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName()); qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)"; + Q_ASSERT(false); return false; } From e265a0a1f88709bb201f0029bcb4c91e44edf422 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 16 Mar 2017 13:12:30 -0700 Subject: [PATCH 31/43] Fix possible crash in Menu::removeSeparator --- libraries/ui/src/ui/Menu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index f68fff0204..a793942056 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -470,8 +470,8 @@ void Menu::removeSeparator(const QString& menuName, const QString& separatorName if (menu) { int textAt = findPositionOfMenuItem(menu, separatorName); QList menuActions = menu->actions(); - QAction* separatorText = menuActions[textAt]; if (textAt > 0 && textAt < menuActions.size()) { + QAction* separatorText = menuActions[textAt]; QAction* separatorLine = menuActions[textAt - 1]; if (separatorLine) { if (separatorLine->isSeparator()) { From e21d7d9edf7c39693988bb4418cae3edfa85daf0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 17 Mar 2017 13:34:11 +1300 Subject: [PATCH 32/43] Add Developer > Network > Clear Disk Cache menu action --- interface/src/Menu.cpp | 2 ++ interface/src/Menu.h | 1 + 2 files changed, 3 insertions(+) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index beacbaccab..765d0f4a09 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -554,6 +554,8 @@ Menu::Menu() { "NetworkingPreferencesDialog"); }); addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); + addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCache, 0, + DependencyManager::get().data(), SLOT(clearCache())); addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableActivityLogger, 0, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index c806ffa9ee..2755e11dd4 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -57,6 +57,7 @@ namespace MenuOption { const QString CameraEntityMode = "Entity Mode"; const QString CenterPlayerInView = "Center Player In View"; const QString Chat = "Chat..."; + const QString ClearDiskCache = "Clear Disk Cache"; const QString Collisions = "Collisions"; const QString Connexion = "Activate 3D Connexion Devices"; const QString Console = "Console..."; From 939b4f16129c9d81f5b2ae145933cbf145fbb8f9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 17 Mar 2017 13:49:09 +1300 Subject: [PATCH 33/43] Remove Developer > Network > Disk Cache Editor menu item and dialog --- interface/src/Menu.cpp | 2 - interface/src/Menu.h | 1 - interface/src/ui/DialogsManager.cpp | 6 -- interface/src/ui/DialogsManager.h | 3 - interface/src/ui/DiskCacheEditor.cpp | 146 --------------------------- interface/src/ui/DiskCacheEditor.h | 49 --------- 6 files changed, 207 deletions(-) delete mode 100644 interface/src/ui/DiskCacheEditor.cpp delete mode 100644 interface/src/ui/DiskCacheEditor.h diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 765d0f4a09..9afd2d6472 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -564,8 +564,6 @@ Menu::Menu() { SLOT(disable(bool))); addActionToQMenuAndActionHash(networkMenu, MenuOption::CachesSize, 0, dialogsManager.data(), SLOT(cachesSizeDialog())); - addActionToQMenuAndActionHash(networkMenu, MenuOption::DiskCacheEditor, 0, - dialogsManager.data(), SLOT(toggleDiskCacheEditor())); addActionToQMenuAndActionHash(networkMenu, MenuOption::ShowDSConnectTable, 0, dialogsManager.data(), SLOT(showDomainConnectionDialog())); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 2755e11dd4..367abe935a 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -84,7 +84,6 @@ namespace MenuOption { const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment"; const QString DisableLightEntities = "Disable Light Entities"; - const QString DiskCacheEditor = "Disk Cache Editor"; const QString DisplayCrashOptions = "Display Crash Options"; const QString DisplayHandTargets = "Show Hand Targets"; const QString DisplayModelBounds = "Display Model Bounds"; diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 3252fef4f0..ac8b943aa8 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -21,7 +21,6 @@ #include "AddressBarDialog.h" #include "CachesSizeDialog.h" #include "ConnectionFailureDialog.h" -#include "DiskCacheEditor.h" #include "DomainConnectionDialog.h" #include "HMDToolsDialog.h" #include "LodToolsDialog.h" @@ -67,11 +66,6 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { } } -void DialogsManager::toggleDiskCacheEditor() { - maybeCreateDialog(_diskCacheEditor); - _diskCacheEditor->toggle(); -} - void DialogsManager::toggleLoginDialog() { LoginDialog::toggleAction(); } diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index 54aef38984..bb144ba99b 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -22,7 +22,6 @@ class AnimationsDialog; class AttachmentsDialog; class CachesSizeDialog; -class DiskCacheEditor; class LodToolsDialog; class OctreeStatsDialog; class ScriptEditorWindow; @@ -46,7 +45,6 @@ public slots: void showAddressBar(); void showFeed(); void setDomainConnectionFailureVisibility(bool visible); - void toggleDiskCacheEditor(); void toggleLoginDialog(); void showLoginDialog(); void octreeStatsDetails(); @@ -77,7 +75,6 @@ private: QPointer _animationsDialog; QPointer _attachmentsDialog; QPointer _cachesSizeDialog; - QPointer _diskCacheEditor; QPointer _ircInfoBox; QPointer _hmdToolsDialog; QPointer _lodToolsDialog; diff --git a/interface/src/ui/DiskCacheEditor.cpp b/interface/src/ui/DiskCacheEditor.cpp deleted file mode 100644 index 1a7be8642b..0000000000 --- a/interface/src/ui/DiskCacheEditor.cpp +++ /dev/null @@ -1,146 +0,0 @@ -// -// DiskCacheEditor.cpp -// -// -// Created by Clement on 3/4/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "DiskCacheEditor.h" - -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "OffscreenUi.h" - -DiskCacheEditor::DiskCacheEditor(QWidget* parent) : QObject(parent) { -} - -QWindow* DiskCacheEditor::windowHandle() { - return (_dialog) ? _dialog->windowHandle() : nullptr; -} - -void DiskCacheEditor::toggle() { - if (!_dialog) { - makeDialog(); - } - - if (!_dialog->isActiveWindow()) { - _dialog->show(); - _dialog->raise(); - _dialog->activateWindow(); - } else { - _dialog->close(); - } -} - -void DiskCacheEditor::makeDialog() { - _dialog = new QDialog(static_cast(parent())); - Q_CHECK_PTR(_dialog); - _dialog->setAttribute(Qt::WA_DeleteOnClose); - _dialog->setWindowTitle("Disk Cache Editor"); - - QGridLayout* layout = new QGridLayout(_dialog); - Q_CHECK_PTR(layout); - _dialog->setLayout(layout); - - - QLabel* path = new QLabel("Path : ", _dialog); - Q_CHECK_PTR(path); - path->setAlignment(Qt::AlignRight); - layout->addWidget(path, 0, 0); - - QLabel* size = new QLabel("Current Size : ", _dialog); - Q_CHECK_PTR(size); - size->setAlignment(Qt::AlignRight); - layout->addWidget(size, 1, 0); - - QLabel* maxSize = new QLabel("Max Size : ", _dialog); - Q_CHECK_PTR(maxSize); - maxSize->setAlignment(Qt::AlignRight); - layout->addWidget(maxSize, 2, 0); - - - _path = new QLabel(_dialog); - Q_CHECK_PTR(_path); - _path->setAlignment(Qt::AlignLeft); - layout->addWidget(_path, 0, 1, 1, 3); - - _size = new QLabel(_dialog); - Q_CHECK_PTR(_size); - _size->setAlignment(Qt::AlignLeft); - layout->addWidget(_size, 1, 1, 1, 3); - - _maxSize = new QLabel(_dialog); - Q_CHECK_PTR(_maxSize); - _maxSize->setAlignment(Qt::AlignLeft); - layout->addWidget(_maxSize, 2, 1, 1, 3); - - refresh(); - - - static const int REFRESH_INTERVAL = 100; // msec - _refreshTimer = new QTimer(_dialog); - _refreshTimer->setInterval(REFRESH_INTERVAL); // Qt::CoarseTimer acceptable, no need for real time accuracy - _refreshTimer->setSingleShot(false); - QObject::connect(_refreshTimer.data(), &QTimer::timeout, this, &DiskCacheEditor::refresh); - _refreshTimer->start(); - - QPushButton* clearCacheButton = new QPushButton(_dialog); - Q_CHECK_PTR(clearCacheButton); - clearCacheButton->setText("Clear"); - clearCacheButton->setToolTip("Erases the entire content of the disk cache."); - connect(clearCacheButton, SIGNAL(clicked()), SLOT(clear())); - layout->addWidget(clearCacheButton, 3, 3); -} - -void DiskCacheEditor::refresh() { - DependencyManager::get()->cacheInfoRequest(this, "cacheInfoCallback"); -} - -void DiskCacheEditor::cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize) { - static const auto stringify = [](qint64 number) { - static const QStringList UNITS = QStringList() << "B" << "KB" << "MB" << "GB"; - static const qint64 CHUNK = 1024; - QString unit; - int i = 0; - for (i = 0; i < 4; ++i) { - if (number / CHUNK > 0) { - number /= CHUNK; - } else { - break; - } - } - return QString("%0 %1").arg(number).arg(UNITS[i]); - }; - - if (_path) { - _path->setText(cacheDirectory); - } - if (_size) { - _size->setText(stringify(cacheSize)); - } - if (_maxSize) { - _maxSize->setText(stringify(maximumCacheSize)); - } -} - -void DiskCacheEditor::clear() { - auto buttonClicked = OffscreenUi::question(_dialog, "Clearing disk cache", - "You are about to erase all the content of the disk cache, " - "are you sure you want to do that?", - QMessageBox::Ok | QMessageBox::Cancel); - if (buttonClicked == QMessageBox::Ok) { - DependencyManager::get()->clearCache(); - } -} diff --git a/interface/src/ui/DiskCacheEditor.h b/interface/src/ui/DiskCacheEditor.h deleted file mode 100644 index 3f8fa1a883..0000000000 --- a/interface/src/ui/DiskCacheEditor.h +++ /dev/null @@ -1,49 +0,0 @@ -// -// DiskCacheEditor.h -// -// -// Created by Clement on 3/4/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_DiskCacheEditor_h -#define hifi_DiskCacheEditor_h - -#include -#include - -class QDialog; -class QLabel; -class QWindow; -class QTimer; - -class DiskCacheEditor : public QObject { - Q_OBJECT - -public: - DiskCacheEditor(QWidget* parent = nullptr); - - QWindow* windowHandle(); - -public slots: - void toggle(); - -private slots: - void refresh(); - void cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize); - void clear(); - -private: - void makeDialog(); - - QPointer _dialog; - QPointer _path; - QPointer _size; - QPointer _maxSize; - QPointer _refreshTimer; -}; - -#endif // hifi_DiskCacheEditor_h \ No newline at end of file From eba6c8de5e64721c95083d15a0245d0074ef8147 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Fri, 17 Mar 2017 17:37:19 +0000 Subject: [PATCH 34/43] remove About Interface from File menu --- interface/resources/html/img/devices.png | Bin 7492 -> 0 bytes interface/resources/html/img/models.png | Bin 8664 -> 0 bytes interface/resources/html/img/move.png | Bin 6121 -> 0 bytes interface/resources/html/img/run-script.png | Bin 4873 -> 0 bytes interface/resources/html/img/talk.png | Bin 2611 -> 0 bytes interface/resources/html/img/write-script.png | Bin 2006 -> 0 bytes .../resources/html/interface-welcome.html | 187 ------------------ interface/src/Application.cpp | 5 - interface/src/Application.h | 1 - interface/src/Menu.cpp | 3 - interface/src/Menu.h | 1 - 11 files changed, 197 deletions(-) delete mode 100644 interface/resources/html/img/devices.png delete mode 100644 interface/resources/html/img/models.png delete mode 100644 interface/resources/html/img/move.png delete mode 100644 interface/resources/html/img/run-script.png delete mode 100644 interface/resources/html/img/talk.png delete mode 100644 interface/resources/html/img/write-script.png delete mode 100644 interface/resources/html/interface-welcome.html diff --git a/interface/resources/html/img/devices.png b/interface/resources/html/img/devices.png deleted file mode 100644 index fc4231e96e25732a0659c911e7c15ded5b54911b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7492 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 diff --git a/interface/resources/html/img/models.png b/interface/resources/html/img/models.png deleted file mode 100644 index b09c36011d540759da5326ed70bda97592e9636b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8664 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 diff --git a/interface/resources/html/img/run-script.png b/interface/resources/html/img/run-script.png deleted file mode 100644 index 941b8ee9f13664fc2b2ec51a75273ba3e553d3a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4873 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 diff --git a/interface/resources/html/img/write-script.png b/interface/resources/html/img/write-script.png deleted file mode 100644 index dae97e59b14bae50013b583cd9cb3b5bdf6ad464..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2006 zcmb`HX*Ao38pi)h(Ww^WC~;|$Yo=Oy6}h!H%2>+{#aNnJBh9EKC6-vKrnL4lqlR8f z+CxLd)*4blsO1QWsHU;S(vqMgmZY&S_c%TCW#-;9=gheuo-gltK0NP--!H=jVRz~) zwXXmGIOSk(?Fs;Z5CDKBS!n`!Um!t4q*Wpv@1Fw zC@!KOZ4Ll3cn51scl-o@sspQp0s1~_21*qu6P+5Qx8)(@64A${yN;nQG-&a!vK46|YxiCC+UCW# z?78c1pDn_!(s`qA{2QK}Osh^O6w)&nq|Y#8K%!6GZ2QSKTb`%$ z3fZ>{+WbzhbKE8?t5!s}?(f#PZ^l`Cy89ua>7VGH@y;Zlu%JU%-FpS2``0zP)S?M~ zYMcE+3MG7Uj=OMBeYTuBMD$bP8*mLO*t0C(S41!!jdI!=H4CS}wHew%@uOnH0Z!6q zos_8@nng~FU`KAV#e8;03_6sDa8IZg`}c(Z%?KG9_pr}Qda$=!#5pMW2uHg4cCb2Rtjhd*rqmR@Ph0qaA_#**$5H#z#5Q?MJ=)Ym$FZY9?% zS|;<*8MQ}Nr2o58>dbfJE24Op%Qi~X%CMlTv`)|WljBXHaM;DiJZ4^&B916Q8tH`B z-RQPdyR4W#97+6;nfFACb8g?Ma&$S41a8QYlLaXlK!T2Pm6n`mNlVU?fJ$0PA0<^z z7IYK^1HETh;ov5s#Q3_pMbh3JFLrfW89PH2b8Kmi@!LWkWApCXhc|bgSl5s0FiXum zOH6P?>2-|Y#)3HGd^xH%B$d%Dc>UAX!Tv6x z))ph)9&g0hbj*MckS!%+&ZVZ86Ki7DK>eB|4uq?38_etv!)W7hRZ-DI{NAdSd0U7JdT(r6szh|#Qp-YgZ1M|+?autgHT9)N;V z*=+WZE+#tuUAJQfygZURO})$U1FYaQ#g_IR^PIiC=Z=LB^;7cX?RSkMaK*8%=h7iu z$&u;eD}s+&3PSZCR;TJP(Ld&?*l!q;$yn^<>-AoQo`O)_u}9EtJ04EOEg&L9^MlNm z5eV1hasGwKu@{1_2Pb>;wVWSXVm4G{EX(7_nALToi@caH&RYn`95W@l$;-i zzxg}V%;`a%N@yzc?8*kM#E35mz2wCZcn2UTS7|4!+U+>gA*tq?>zzglk-I){VpB1! zv@IRN)o`N7;Ek}O`C$`Ii$&tzr`v{YKBS2?pR^Js(v*Jz`me?trK~fp%{Z8;)?)BvYwS>KaG-Vg!;)==52-4{=z4A@{ucqO zr$jL4JyU49Nq10&i%vjed#)3d+T8q2FnBT)9piD=py#!GLdYRlBx)gCVJAACE-2@9 z_|1WtF~PR8^M)a_FACc|?wxgSmv$j-rr)dWLl4yybP%YO0g=3!ZBa!thC}mtHPA*# zi(Yc}ks^)!S2T~f{lcyUH02Assd8V~RWSGh_g^XeM*g3w;SU8j - - - - - Welcome to Interface - - - - - -
-
-

Move around

- Move around -

- Move around with WASD & fly
- up or down with E & C.
- Cmnd/Ctrl+G will send you
- home. Hitting Enter will let you
- teleport to a user or location. -

-
-
-

Listen & talk

- Talk -

- Use your best headphones
- and microphone for high
- fidelity audio. -

-
-
-

Connect devices

- Connect devices -

- Have an Oculus Rift, a Razer
- Hydra, or a PrimeSense 3D
- camera? We support them all. -

-
-
-

Run a script

- Run a script -

- Cmnd/Cntrl+J will launch a
- Running Scripts dialog to help
- manage your scripts and search
- for new ones to run. -

-
-
-

Script something

- Write a script -

- Write a script; we're always
- adding new features.
- Cmnd/Cntrl+J will launch a
- Running Scripts dialog to help
- manage your scripts. -

-
-
-

Import models

- Import models -

- Use the edit.js script to
- add FBX models in-world. You
- can use grids and fine tune
- placement-related parameters
- with ease. -

-
-
-
-

Read the docs

-

- We are writing documentation on
- just about everything. Please,
- devour all we've written and make
- suggestions where necessary.
- Documentation is always at
- docs.highfidelity.com -

-
-
-
- - - - - diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1bb4c64884..6d779c81bc 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -224,7 +224,6 @@ static const float MIRROR_FIELD_OF_VIEW = 30.0f; static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; -static const QString INFO_WELCOME_PATH = "html/interface-welcome.html"; static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; static const QString INFO_HELP_PATH = "html/help.html"; @@ -2381,10 +2380,6 @@ void Application::setSettingConstrainToolbarPosition(bool setting) { DependencyManager::get()->setConstrainToolbarToCenterX(setting); } -void Application::aboutApp() { - InfoView::show(INFO_WELCOME_PATH); -} - void Application::showHelp() { static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; diff --git a/interface/src/Application.h b/interface/src/Application.h index 98080783a6..73b2c399e6 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -368,7 +368,6 @@ public slots: void calibrateEyeTracker5Points(); #endif - void aboutApp(); static void showHelp(); void cycleCamera(); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8e124d27c7..0e22c05787 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -74,9 +74,6 @@ Menu::Menu() { // File > Help addActionToQMenuAndActionHash(fileMenu, MenuOption::Help, 0, qApp, SLOT(showHelp())); - // File > About - addActionToQMenuAndActionHash(fileMenu, MenuOption::AboutApp, 0, qApp, SLOT(aboutApp()), QAction::AboutRole); - // File > Quit addActionToQMenuAndActionHash(fileMenu, MenuOption::Quit, Qt::CTRL | Qt::Key_Q, qApp, SLOT(quit()), QAction::QuitRole); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index e0ac340edc..a51419aa73 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -26,7 +26,6 @@ public: }; namespace MenuOption { - const QString AboutApp = "About Interface"; const QString AddRemoveFriends = "Add/Remove Friends..."; const QString AddressBar = "Show Address Bar"; const QString Animations = "Animations..."; From 38bb48e72c41ad30bd825f2aa48f6ff89110aa74 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 18 Mar 2017 12:13:13 +1300 Subject: [PATCH 35/43] Disable Developer > Assets > ATP Asset Migration menu item --- interface/src/Menu.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8e124d27c7..02b8f5de78 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -417,6 +417,8 @@ Menu::Menu() { } // Developer > Assets >>> + // Menu item is not currently needed but code should be kept in case it proves useful again at some stage. + /* MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets"); auto& atpMigrator = ATPAssetMigrator::getInstance(); atpMigrator.setDialogParent(this); @@ -424,6 +426,7 @@ Menu::Menu() { addActionToQMenuAndActionHash(assetDeveloperMenu, MenuOption::AssetMigration, 0, &atpMigrator, SLOT(loadEntityServerFile())); + */ // Developer > Avatar >>> MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar"); From ab7d82edb6ba5f9a32770afd8cc941f639f6230c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 18 Mar 2017 12:56:34 +1300 Subject: [PATCH 36/43] Use #ifdef instead of comment --- interface/src/Menu.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 02b8f5de78..b1d97e6467 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -418,7 +418,8 @@ Menu::Menu() { // Developer > Assets >>> // Menu item is not currently needed but code should be kept in case it proves useful again at some stage. - /* +//#define WANT_ASSET_MIGRATION +#ifdef WANT_ASSET_MIGRATION MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets"); auto& atpMigrator = ATPAssetMigrator::getInstance(); atpMigrator.setDialogParent(this); @@ -426,7 +427,7 @@ Menu::Menu() { addActionToQMenuAndActionHash(assetDeveloperMenu, MenuOption::AssetMigration, 0, &atpMigrator, SLOT(loadEntityServerFile())); - */ +#endif // Developer > Avatar >>> MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar"); From a9b1a3866587ecb01a33577faa1a1d9ad92e0485 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Sat, 18 Mar 2017 00:53:13 +0000 Subject: [PATCH 37/43] remove Mini Mirror from View menu Avatar inputs only contains the audio meter now. --- interface/resources/qml/AvatarInputs.qml | 57 +------ interface/src/Application.cpp | 153 +++++------------- interface/src/Application.h | 4 - interface/src/Menu.cpp | 3 - interface/src/Menu.h | 1 - interface/src/ui/ApplicationOverlay.cpp | 43 ----- interface/src/ui/ApplicationOverlay.h | 3 - interface/src/ui/AvatarInputs.cpp | 20 --- interface/src/ui/AvatarInputs.h | 6 - .../render-utils/src/FramebufferCache.cpp | 12 -- libraries/render-utils/src/FramebufferCache.h | 5 - 11 files changed, 41 insertions(+), 266 deletions(-) diff --git a/interface/resources/qml/AvatarInputs.qml b/interface/resources/qml/AvatarInputs.qml index 384504aaa0..28f3c0c7b9 100644 --- a/interface/resources/qml/AvatarInputs.qml +++ b/interface/resources/qml/AvatarInputs.qml @@ -15,12 +15,11 @@ import Qt.labs.settings 1.0 Hifi.AvatarInputs { id: root objectName: "AvatarInputs" - width: mirrorWidth - height: controls.height + mirror.height + width: rootWidth + height: controls.height x: 10; y: 5 - readonly property int mirrorHeight: 215 - readonly property int mirrorWidth: 265 + readonly property int rootWidth: 265 readonly property int iconSize: 24 readonly property int iconPadding: 5 @@ -39,61 +38,15 @@ Hifi.AvatarInputs { anchors.fill: parent } - Item { - id: mirror - width: root.mirrorWidth - height: root.mirrorVisible ? root.mirrorHeight : 0 - visible: root.mirrorVisible - anchors.left: parent.left - clip: true - - Image { - id: closeMirror - visible: hover.containsMouse - width: root.iconSize - height: root.iconSize - anchors.top: parent.top - anchors.topMargin: root.iconPadding - anchors.left: parent.left - anchors.leftMargin: root.iconPadding - source: "../images/close.svg" - MouseArea { - anchors.fill: parent - onClicked: { - root.closeMirror(); - } - } - } - - Image { - id: zoomIn - visible: hover.containsMouse - width: root.iconSize - height: root.iconSize - anchors.bottom: parent.bottom - anchors.bottomMargin: root.iconPadding - anchors.left: parent.left - anchors.leftMargin: root.iconPadding - source: root.mirrorZoomed ? "../images/minus.svg" : "../images/plus.svg" - MouseArea { - anchors.fill: parent - onClicked: { - root.toggleZoom(); - } - } - } - } - Item { id: controls - width: root.mirrorWidth + width: root.rootWidth height: 44 visible: root.showAudioTools - anchors.top: mirror.bottom Rectangle { anchors.fill: parent - color: root.mirrorVisible ? (root.audioClipping ? "red" : "#696969") : "#00000000" + color: "#00000000" Item { id: audioMeter diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8cd5a9564a..d6516db8f0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -213,14 +213,7 @@ static const QString FBX_EXTENSION = ".fbx"; static const QString OBJ_EXTENSION = ".obj"; static const QString AVA_JSON_EXTENSION = ".ava.json"; -static const int MIRROR_VIEW_TOP_PADDING = 5; -static const int MIRROR_VIEW_LEFT_PADDING = 10; -static const int MIRROR_VIEW_WIDTH = 265; -static const int MIRROR_VIEW_HEIGHT = 215; static const float MIRROR_FULLSCREEN_DISTANCE = 0.389f; -static const float MIRROR_REARVIEW_DISTANCE = 0.722f; -static const float MIRROR_REARVIEW_BODY_DISTANCE = 2.56f; -static const float MIRROR_FIELD_OF_VIEW = 30.0f; static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; @@ -565,7 +558,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _entityClipboardRenderer(false, this, this), _entityClipboard(new EntityTree()), _lastQueriedTime(usecTimestampNow()), - _mirrorViewRect(QRect(MIRROR_VIEW_LEFT_PADDING, MIRROR_VIEW_TOP_PADDING, MIRROR_VIEW_WIDTH, MIRROR_VIEW_HEIGHT)), _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), @@ -2119,21 +2111,6 @@ void Application::paintGL() { batch.resetStages(); }); - auto inputs = AvatarInputs::getInstance(); - if (inputs->mirrorVisible()) { - PerformanceTimer perfTimer("Mirror"); - - renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; - renderArgs._blitFramebuffer = DependencyManager::get()->getSelfieFramebuffer(); - - _mirrorViewRect.moveTo(inputs->x(), inputs->y()); - - renderRearViewMirror(&renderArgs, _mirrorViewRect, inputs->mirrorZoomed()); - - renderArgs._blitFramebuffer.reset(); - renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - } - { PerformanceTimer perfTimer("renderOverlay"); // NOTE: There is no batch associated with this renderArgs @@ -2887,49 +2864,45 @@ void Application::keyPressEvent(QKeyEvent* event) { #endif case Qt::Key_H: - if (isShifted) { - Menu::getInstance()->triggerOption(MenuOption::MiniMirror); - } else { - // whenever switching to/from full screen mirror from the keyboard, remember - // the state you were in before full screen mirror, and return to that. - auto previousMode = _myCamera.getMode(); - if (previousMode != CAMERA_MODE_MIRROR) { - switch (previousMode) { - case CAMERA_MODE_FIRST_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; - break; - case CAMERA_MODE_THIRD_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; - - // FIXME - it's not clear that these modes make sense to return to... - case CAMERA_MODE_INDEPENDENT: - _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; - break; - case CAMERA_MODE_ENTITY: - _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; - break; - - default: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; - } - } - - bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); - Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); - if (isMirrorChecked) { - - // if we got here without coming in from a non-Full Screen mirror case, then our - // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old - // behavior of returning to ThirdPerson - if (_returnFromFullScreenMirrorTo.isEmpty()) { + // whenever switching to/from full screen mirror from the keyboard, remember + // the state you were in before full screen mirror, and return to that. + auto previousMode = _myCamera.getMode(); + if (previousMode != CAMERA_MODE_MIRROR) { + switch (previousMode) { + case CAMERA_MODE_FIRST_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; + break; + case CAMERA_MODE_THIRD_PERSON: _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - } - Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); + break; + + // FIXME - it's not clear that these modes make sense to return to... + case CAMERA_MODE_INDEPENDENT: + _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; + break; + case CAMERA_MODE_ENTITY: + _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; + break; + + default: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; } - cameraMenuChanged(); } + + bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); + Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); + if (isMirrorChecked) { + + // if we got here without coming in from a non-Full Screen mirror case, then our + // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old + // behavior of returning to ThirdPerson + if (_returnFromFullScreenMirrorTo.isEmpty()) { + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + } + Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); + } + cameraMenuChanged(); break; case Qt::Key_P: { if (!(isShifted || isMeta || isOption)) { @@ -3845,8 +3818,6 @@ void Application::init() { DependencyManager::get()->init(); _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); - _mirrorCamera.setMode(CAMERA_MODE_MIRROR); - _timerStart.start(); _lastTimeUpdated.start(); @@ -5122,58 +5093,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se activeRenderingThread = nullptr; } -void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed) { - auto originalViewport = renderArgs->_viewport; - // Grab current viewport to reset it at the end - - float aspect = (float)region.width() / region.height(); - float fov = MIRROR_FIELD_OF_VIEW; - - auto myAvatar = getMyAvatar(); - - // bool eyeRelativeCamera = false; - if (!isZoomed) { - _mirrorCamera.setPosition(myAvatar->getChestPosition() + - myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_BODY_DISTANCE * myAvatar->getScale()); - - } else { // HEAD zoom level - // FIXME note that the positioning of the camera relative to the avatar can suffer limited - // precision as the user's position moves further away from the origin. Thus at - // /1e7,1e7,1e7 (well outside the buildable volume) the mirror camera veers and sways - // wildly as you rotate your avatar because the floating point values are becoming - // larger, squeezing out the available digits of precision you have available at the - // human scale for camera positioning. - - // Previously there was a hack to correct this using the mechanism of repositioning - // the avatar at the origin of the world for the purposes of rendering the mirror, - // but it resulted in failing to render the avatar's head model in the mirror view - // when in first person mode. Presumably this was because of some missed culling logic - // that was not accounted for in the hack. - - // This was removed in commit 71e59cfa88c6563749594e25494102fe01db38e9 but could be further - // investigated in order to adapt the technique while fixing the head rendering issue, - // but the complexity of the hack suggests that a better approach - _mirrorCamera.setPosition(myAvatar->getDefaultEyePosition() + - myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_DISTANCE * myAvatar->getScale()); - } - _mirrorCamera.setProjection(glm::perspective(glm::radians(fov), aspect, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); - _mirrorCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI, 0.0f))); - - - // set the bounds of rear mirror view - // the region is in device independent coordinates; must convert to device - float ratio = (float)QApplication::desktop()->windowHandle()->devicePixelRatio() * getRenderResolutionScale(); - int width = region.width() * ratio; - int height = region.height() * ratio; - gpu::Vec4i viewport = gpu::Vec4i(0, 0, width, height); - renderArgs->_viewport = viewport; - - // render rear mirror view - displaySide(renderArgs, _mirrorCamera, true); - - renderArgs->_viewport = originalViewport; -} - void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); DependencyManager::get()->reset(); diff --git a/interface/src/Application.h b/interface/src/Application.h index 98080783a6..da815db4cd 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -276,8 +276,6 @@ public: virtual void pushPostUpdateLambda(void* key, std::function func) override; - const QRect& getMirrorViewRect() const { return _mirrorViewRect; } - void updateMyAvatarLookAtPosition(); float getAvatarSimrate() const { return _avatarSimCounter.rate(); } @@ -557,8 +555,6 @@ private: int _avatarSimsPerSecondReport {0}; quint64 _lastAvatarSimsPerSecondUpdate {0}; Camera _myCamera; // My view onto the world - Camera _mirrorCamera; // Camera for mirror view - QRect _mirrorViewRect; Setting::Handle _previousScriptLocation; Setting::Handle _fieldOfView; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8e124d27c7..893b23839d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -249,9 +249,6 @@ Menu::Menu() { viewMenu->addSeparator(); - // View > Mini Mirror - addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::MiniMirror, 0, false); - // View > Center Player In View addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::CenterPlayerInView, 0, true, qApp, SLOT(rotationModeChanged()), diff --git a/interface/src/Menu.h b/interface/src/Menu.h index e0ac340edc..4a990254ad 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -123,7 +123,6 @@ namespace MenuOption { const QString LogExtraTimings = "Log Extra Timing Details"; const QString LowVelocityFilter = "Low Velocity Filter"; const QString MeshVisible = "Draw Mesh"; - const QString MiniMirror = "Mini Mirror"; const QString MuteAudio = "Mute Microphone"; const QString MuteEnvironment = "Mute Environment"; const QString MuteFaceTracking = "Mute Face Tracking"; diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 364dff52a3..b9d7fadc97 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -13,7 +13,6 @@ #include #include -#include #include #include #include @@ -42,7 +41,6 @@ ApplicationOverlay::ApplicationOverlay() _domainStatusBorder = geometryCache->allocateID(); _magnifierBorder = geometryCache->allocateID(); _qmlGeometryId = geometryCache->allocateID(); - _rearViewGeometryId = geometryCache->allocateID(); } ApplicationOverlay::~ApplicationOverlay() { @@ -51,7 +49,6 @@ ApplicationOverlay::~ApplicationOverlay() { geometryCache->releaseID(_domainStatusBorder); geometryCache->releaseID(_magnifierBorder); geometryCache->releaseID(_qmlGeometryId); - geometryCache->releaseID(_rearViewGeometryId); } } @@ -86,7 +83,6 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) { // Now render the overlay components together into a single texture renderDomainConnectionStatusBorder(renderArgs); // renders the connected domain line renderAudioScope(renderArgs); // audio scope in the very back - NOTE: this is the debug audio scope, not the VU meter - renderRearView(renderArgs); // renders the mirror view selfie renderOverlays(renderArgs); // renders Scripts Overlay and AudioScope renderQmlUi(renderArgs); // renders a unit quad with the QML UI texture, and the text overlays from scripts renderStatsAndLogs(renderArgs); // currently renders nothing @@ -163,45 +159,6 @@ void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) { qApp->getOverlays().renderHUD(renderArgs); } -void ApplicationOverlay::renderRearViewToFbo(RenderArgs* renderArgs) { -} - -void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { - if (!qApp->isHMDMode() && Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && - !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { - gpu::Batch& batch = *renderArgs->_batch; - - auto geometryCache = DependencyManager::get(); - - auto framebuffer = DependencyManager::get(); - auto selfieTexture = framebuffer->getSelfieFramebuffer()->getRenderBuffer(0); - - int width = renderArgs->_viewport.z; - int height = renderArgs->_viewport.w; - mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); - batch.setProjectionTransform(legacyProjection); - batch.setModelTransform(Transform()); - batch.resetViewTransform(); - - float screenRatio = ((float)qApp->getDevicePixelRatio()); - float renderRatio = ((float)qApp->getRenderResolutionScale()); - - auto viewport = qApp->getMirrorViewRect(); - glm::vec2 bottomLeft(viewport.left(), viewport.top() + viewport.height()); - glm::vec2 topRight(viewport.left() + viewport.width(), viewport.top()); - bottomLeft *= screenRatio; - topRight *= screenRatio; - glm::vec2 texCoordMinCorner(0.0f, 0.0f); - glm::vec2 texCoordMaxCorner(viewport.width() * renderRatio / float(selfieTexture->getWidth()), viewport.height() * renderRatio / float(selfieTexture->getHeight())); - - batch.setResourceTexture(0, selfieTexture); - float alpha = DependencyManager::get()->getDesktop()->property("unpinnedAlpha").toFloat(); - geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, alpha), _rearViewGeometryId); - - batch.setResourceTexture(0, renderArgs->_whiteTexture); - } -} - void ApplicationOverlay::renderStatsAndLogs(RenderArgs* renderArgs) { // Display stats and log text onscreen diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index 7ace5ee885..af4d8779d4 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -31,8 +31,6 @@ public: private: void renderStatsAndLogs(RenderArgs* renderArgs); void renderDomainConnectionStatusBorder(RenderArgs* renderArgs); - void renderRearViewToFbo(RenderArgs* renderArgs); - void renderRearView(RenderArgs* renderArgs); void renderQmlUi(RenderArgs* renderArgs); void renderAudioScope(RenderArgs* renderArgs); void renderOverlays(RenderArgs* renderArgs); @@ -51,7 +49,6 @@ private: gpu::TexturePointer _overlayColorTexture; gpu::FramebufferPointer _overlayFramebuffer; int _qmlGeometryId { 0 }; - int _rearViewGeometryId { 0 }; }; #endif // hifi_ApplicationOverlay_h diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index b09289c78a..944be4bf9e 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -20,10 +20,6 @@ HIFI_QML_DEF(AvatarInputs) static AvatarInputs* INSTANCE{ nullptr }; -static const char SETTINGS_GROUP_NAME[] = "Rear View Tools"; -static const char ZOOM_LEVEL_SETTINGS[] = "ZoomLevel"; - -static Setting::Handle rearViewZoomLevel(QStringList() << SETTINGS_GROUP_NAME << ZOOM_LEVEL_SETTINGS, 0); AvatarInputs* AvatarInputs::getInstance() { if (!INSTANCE) { @@ -36,8 +32,6 @@ AvatarInputs* AvatarInputs::getInstance() { AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) { INSTANCE = this; - int zoomSetting = rearViewZoomLevel.get(); - _mirrorZoomed = zoomSetting == 0; } #define AI_UPDATE(name, src) \ @@ -62,8 +56,6 @@ void AvatarInputs::update() { if (!Menu::getInstance()) { return; } - AI_UPDATE(mirrorVisible, Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && !qApp->isHMDMode() - && !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)); AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking)); AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking)); AI_UPDATE(isHMD, qApp->isHMDMode()); @@ -122,15 +114,3 @@ void AvatarInputs::toggleAudioMute() { void AvatarInputs::resetSensors() { qApp->resetSensors(); } - -void AvatarInputs::toggleZoom() { - _mirrorZoomed = !_mirrorZoomed; - rearViewZoomLevel.set(_mirrorZoomed ? 0 : 1); - emit mirrorZoomedChanged(); -} - -void AvatarInputs::closeMirror() { - if (Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror)) { - Menu::getInstance()->triggerOption(MenuOption::MiniMirror); - } -} diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 85570ecd3c..5535469445 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -28,8 +28,6 @@ class AvatarInputs : public QQuickItem { AI_PROPERTY(bool, audioMuted, false) AI_PROPERTY(bool, audioClipping, false) AI_PROPERTY(float, audioLevel, 0) - AI_PROPERTY(bool, mirrorVisible, false) - AI_PROPERTY(bool, mirrorZoomed, true) AI_PROPERTY(bool, isHMD, false) AI_PROPERTY(bool, showAudioTools, true) @@ -44,8 +42,6 @@ signals: void audioMutedChanged(); void audioClippingChanged(); void audioLevelChanged(); - void mirrorVisibleChanged(); - void mirrorZoomedChanged(); void isHMDChanged(); void showAudioToolsChanged(); @@ -53,8 +49,6 @@ protected: Q_INVOKABLE void resetSensors(); Q_INVOKABLE void toggleCameraMute(); Q_INVOKABLE void toggleAudioMute(); - Q_INVOKABLE void toggleZoom(); - Q_INVOKABLE void closeMirror(); private: float _trailingAudioLoudness{ 0 }; diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 27429595b4..31b345fa9f 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -21,7 +21,6 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { //If the size changed, we need to delete our FBOs if (_frameBufferSize != frameBufferSize) { _frameBufferSize = frameBufferSize; - _selfieFramebuffer.reset(); { std::unique_lock lock(_mutex); _cachedFramebuffers.clear(); @@ -36,10 +35,6 @@ void FramebufferCache::createPrimaryFramebuffer() { auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _selfieFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("selfie")); - auto tex = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width * 0.5, height * 0.5, defaultSampler)); - _selfieFramebuffer->setRenderBuffer(0, tex); - auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); } @@ -60,10 +55,3 @@ void FramebufferCache::releaseFramebuffer(const gpu::FramebufferPointer& framebu _cachedFramebuffers.push_back(framebuffer); } } - -gpu::FramebufferPointer FramebufferCache::getSelfieFramebuffer() { - if (!_selfieFramebuffer) { - createPrimaryFramebuffer(); - } - return _selfieFramebuffer; -} diff --git a/libraries/render-utils/src/FramebufferCache.h b/libraries/render-utils/src/FramebufferCache.h index f74d224a61..8065357615 100644 --- a/libraries/render-utils/src/FramebufferCache.h +++ b/libraries/render-utils/src/FramebufferCache.h @@ -27,9 +27,6 @@ public: void setFrameBufferSize(QSize frameBufferSize); const QSize& getFrameBufferSize() const { return _frameBufferSize; } - /// Returns the framebuffer object used to render selfie maps; - gpu::FramebufferPointer getSelfieFramebuffer(); - /// Returns a free framebuffer with a single color attachment for temp or intra-frame operations gpu::FramebufferPointer getFramebuffer(); @@ -42,8 +39,6 @@ private: gpu::FramebufferPointer _shadowFramebuffer; - gpu::FramebufferPointer _selfieFramebuffer; - QSize _frameBufferSize{ 100, 100 }; std::mutex _mutex; From 8e086a8fc8b20c78f185b0065e8446ea8dfffcd7 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Sat, 18 Mar 2017 03:51:55 +0000 Subject: [PATCH 38/43] fix build error --- interface/src/Application.cpp | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d6516db8f0..4a8ffe702f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2863,30 +2863,30 @@ void Application::keyPressEvent(QKeyEvent* event) { break; #endif - case Qt::Key_H: + case Qt::Key_H: { // whenever switching to/from full screen mirror from the keyboard, remember // the state you were in before full screen mirror, and return to that. auto previousMode = _myCamera.getMode(); if (previousMode != CAMERA_MODE_MIRROR) { switch (previousMode) { - case CAMERA_MODE_FIRST_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; - break; - case CAMERA_MODE_THIRD_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; + case CAMERA_MODE_FIRST_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; + break; + case CAMERA_MODE_THIRD_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; // FIXME - it's not clear that these modes make sense to return to... - case CAMERA_MODE_INDEPENDENT: - _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; - break; - case CAMERA_MODE_ENTITY: - _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; - break; + case CAMERA_MODE_INDEPENDENT: + _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; + break; + case CAMERA_MODE_ENTITY: + _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; + break; - default: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; + default: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; } } @@ -2904,6 +2904,8 @@ void Application::keyPressEvent(QKeyEvent* event) { } cameraMenuChanged(); break; + } + case Qt::Key_P: { if (!(isShifted || isMeta || isOption)) { bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); From 8b845658e2418cf147bb43f88a338bc696ff467a Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Sat, 18 Mar 2017 05:00:05 +0000 Subject: [PATCH 39/43] remove redundant declarations --- libraries/render-utils/src/FramebufferCache.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 31b345fa9f..72b3c2ceb4 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -29,10 +29,6 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { } void FramebufferCache::createPrimaryFramebuffer() { - auto colorFormat = gpu::Element::COLOR_SRGBA_32; - auto width = _frameBufferSize.width(); - auto height = _frameBufferSize.height(); - auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); From b945d9c4cb92b80a78846108444556c35d57bb4f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 18 Mar 2017 14:34:19 -0700 Subject: [PATCH 40/43] fix a bug that caused obj models with no material to randomly use the materials of other things in the view --- libraries/fbx/src/OBJReader.cpp | 5 +++-- libraries/fbx/src/OBJReader.h | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 73cf7a520e..0cb932b375 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -546,6 +546,7 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, QString queryPart = _url.query(); bool suppressMaterialsHack = queryPart.contains("hifiusemat"); // If this appears in query string, don't fetch mtl even if used. OBJMaterial& preDefinedMaterial = materials[SMART_DEFAULT_MATERIAL_NAME]; + preDefinedMaterial.used = true; if (suppressMaterialsHack) { needsMaterialLibrary = preDefinedMaterial.userSpecifiesUV = false; // I said it was a hack... } @@ -594,8 +595,8 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, } foreach (QString materialID, materials.keys()) { - OBJMaterial& objMaterial = materials[materialID]; - if (!objMaterial.used) { + OBJMaterial& objMaterial = materials[materialID]; + if (!objMaterial.used) { continue; } geometry.materials[materialID] = FBXMaterial(objMaterial.diffuseColor, diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 200f11548d..b4a48c570e 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -58,7 +58,7 @@ public: QByteArray specularTextureFilename; bool used { false }; bool userSpecifiesUV { false }; - OBJMaterial() : shininess(96.0f), opacity(1.0f), diffuseColor(1.0f), specularColor(1.0f) {} + OBJMaterial() : shininess(0.0f), opacity(1.0f), diffuseColor(0.9f), specularColor(0.9f) {} }; class OBJReader: public QObject { // QObject so we can make network requests. From e6446b69136dda58a174f11a3776768eec020bde Mon Sep 17 00:00:00 2001 From: anshuman64 Date: Sat, 18 Mar 2017 19:59:23 -0700 Subject: [PATCH 41/43] New simplified, stand-alone build guide for Windows Still throws this error after trying to build in Visual Studio: Error 9 error MSB3073: The command "setlocal "C:\Program Files\CMake\bin\cmake.exe" "-DBUNDLE_EXECUTABLE=C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/tests/render-perf/Release/render-perf-test.exe" "-DBUNDLE_PLUGIN_DIR=C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/tests/render-perf/Release/plugins" -P "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/tests/render-perf/FixupBundlePostBuild.cmake" if %errorlevel% neq 0 goto :cmEnd :cmEnd endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone :cmErrorLevel exit /b %1 :cmDone if %errorlevel% neq 0 goto :VCEnd setlocal CMD /C "SET PATH=%PATH%;C:/Qt/Qt5.6.1/5.6/msvc2013_64/bin && C:/Qt/Qt5.6.1/5.6/msvc2013_64/bin/windeployqt.exe --release C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/tests/render-perf/Release/render-perf-test.exe" if %errorlevel% neq 0 goto :cmEnd :cmEnd endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone :cmErrorLevel exit /b %1 :cmDone if %errorlevel% neq 0 goto :VCEnd setlocal if exist "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/tests/render-perf/Release/audio/qtaudio_windows.dll" ( "C:/Program Files/CMake/bin/cmake.exe" -E remove "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/tests/render-perf/Release/audio/qtaudio_windows.dll" && "C:/Program Files/CMake/bin/cmake.exe" -E copy "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/ext/vc12/wasapi/project/src/wasapi/qtaudio_wasapi.dll" "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/tests/render-perf/Release/audio" && "C:/Program Files/CMake/bin/cmake.exe" -E copy "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/ext/vc12/wasapi/project/src/wasapi/qtaudio_wasapi.pdb" "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/tests/render-perf/Release/audio" ) if %errorlevel% neq 0 goto :cmEnd if exist "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/tests/render-perf/Release/audio/qtaudio_windowsd.dll" ( "C:/Program Files/CMake/bin/cmake.exe" -E remove "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/tests/render-perf/Release/audio/qtaudio_windowsd.dll" && "C:/Program Files/CMake/bin/cmake.exe" -E copy "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/ext/vc12/wasapi/project/src/wasapi/qtaudio_wasapid.dll" "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/tests/render-perf/Release/audio" && "C:/Program Files/CMake/bin/cmake.exe" -E copy "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/ext/vc12/wasapi/project/src/wasapi/qtaudio_wasapid.pdb" "C:/Users/Chris i5 AMD/Documents/GitHub/hifi/build/tests/render-perf/Release/audio" ) if %errorlevel% neq 0 goto :cmEnd :cmEnd endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone :cmErrorLevel exit /b %1 :cmDone if %errorlevel% neq 0 goto :VCEnd :VCEnd" exited with code 1. C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\V120\Microsoft.CppCommon.targets 132 5 render-perf-test --- BUILD_WIN.md | 144 +++++++++++++++++++++------------------------------ 1 file changed, 60 insertions(+), 84 deletions(-) diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 45373d3093..5d7812342a 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -1,104 +1,80 @@ -Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Windows specific instructions are found in this file. +This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit. -Interface can be built as 32 or 64 bit. +###Step 1. Installing Visual Studio 2013 -###Visual Studio 2013 +If you don't already have the Community or Professional edition of Visual Studio 2013, download and install [Visual Studio Community 2013](https://www.visualstudio.com/en-us/news/releasenotes/vs2013-community-vs). You do not need to install any of the optional components when going through the installer. -You can use the Community or Professional editions of Visual Studio 2013. +Note: Newer versions of Visual Studio are not yet compatible. -You can start a Visual Studio 2013 command prompt using the shortcut provided in the Visual Studio Tools folder installed as part of Visual Studio 2013. +###Step 2. Installing CMake -Or you can start a regular command prompt and then run: +Download and install the "win64-x64 Installer" from the [CMake Website](https://cmake.org/download/). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. - "%VS120COMNTOOLS%\vsvars32.bat" +###Step 3. Installing Qt -####Windows SDK 8.1 +Download and install the [Qt 5.6.1 Installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe) -If using Visual Studio 2013 and building as a Visual Studio 2013 project you need the Windows 8 SDK which you should already have as part of installing Visual Studio 2013. You should be able to see it at `C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86`. +Make sure to select all components when going through the installer. -####nmake +###Step 4. Setting Qt Environment Variable -Some of the external projects may require nmake to compile and install. If it is not installed at the location listed below, please ensure that it is in your PATH so CMake can find it when required. +Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." +* Set "Variable name": QT_CMAKE_PREFIX_PATH +* Set "Variable value": `C:\Qt\Qt5.6.1\5.6\msvc2013_64\lib\cmake` -We expect nmake.exe to be located at the following path. +###Step 5. Installing OpenSSL - C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin +Download and install the "Win64 OpenSSL v1.0.xk" Installer from [this website](https://slproweb.com/products/Win32OpenSSL.html), where "x" is the number of the latest release (ex. Win64 OpenSSL v1.0.2k). -###Qt -You can use the online installer or the offline installer. If you use the offline installer, be sure to select the "OpenGL" version. - -* [Download the online installer](http://www.qt.io/download-open-source/#section-2) - * When it asks you to select components, ONLY select one of the following, 32- or 64-bit to match your build preference: - * Qt > Qt 5.6.1 > **msvc2013 32-bit** - * Qt > Qt 5.6.1 > **msvc2013 64-bit** - -* Download the offline installer, 32- or 64-bit to match your build preference: - * [32-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013-5.6.1-1.exe) - * [64-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe) - -Once Qt is installed, you need to manually configure the following: -* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt\5.6.1\msvc2013\lib\cmake` or `Qt\5.6.1\msvc2013_64\lib\cmake` directory. - * You can set an environment variable from Control Panel > System > Advanced System Settings > Environment Variables > New - -###External Libraries - -All libraries should be 32- or 64-bit to match your build preference. - -CMake will need to know where the headers and libraries for required external dependencies are. - -We use CMake's `fixup_bundle` to find the DLLs all of our executable targets require, and then copy them beside the executable in a post-build step. If `fixup_bundle` is having problems finding a DLL, you can fix it manually on your end by adding the folder containing that DLL to your path. Let us know which DLL CMake had trouble finding, as it is possible a tweak to our CMake files is required. - -The recommended route for CMake to find the external dependencies is to place all of the dependencies in one folder and set one ENV variable - HIFI_LIB_DIR. That ENV variable should point to a directory with the following structure: - - root_lib_dir - -> openssl - -> bin - -> include - -> lib - -For many of the external libraries where precompiled binaries are readily available you should be able to simply copy the extracted folder that you get from the download links provided at the top of the guide. Otherwise you may need to build from source and install the built product to this directory. The `root_lib_dir` in the above example can be wherever you choose on your system - as long as the environment variable HIFI_LIB_DIR is set to it. From here on, whenever you see %HIFI_LIB_DIR% you should substitute the directory that you chose. - -####OpenSSL - -Qt will use OpenSSL if it's available, but it doesn't install it, so you must install it separately. - -Your system may already have several versions of the OpenSSL DLL's (ssleay32.dll, libeay32.dll) lying around, but they may be the wrong version. If these DLL's are in the PATH then QT will try to use them, and if they're the wrong version then you will see the following errors in the console: - - QSslSocket: cannot resolve TLSv1_1_client_method - QSslSocket: cannot resolve TLSv1_2_client_method - QSslSocket: cannot resolve TLSv1_1_server_method - QSslSocket: cannot resolve TLSv1_2_server_method - QSslSocket: cannot resolve SSL_select_next_proto - QSslSocket: cannot resolve SSL_CTX_set_next_proto_select_cb - QSslSocket: cannot resolve SSL_get0_next_proto_negotiated - -To prevent these problems, install OpenSSL yourself. Download one of the following binary packages [from this website](https://slproweb.com/products/Win32OpenSSL.html): -* Win32 OpenSSL v1.0.1q -* Win64 OpenSSL v1.0.1q - -Install OpenSSL into the Windows system directory, to make sure that Qt uses the version that you've just installed, and not some other version. - -###Build High Fidelity using Visual Studio -Follow the same build steps from the CMake section of [BUILD.md](BUILD.md), but pass a different generator to CMake. - -For 32-bit builds: - - cmake .. -G "Visual Studio 12" - -For 64-bit builds: +###Step 6. Running CMake to Generate Build Files +Run Command Prompt from Start and run the following commands: + cd "%HIFI_DIR%" + mkdir build + cd build cmake .. -G "Visual Studio 12 Win64" + +Where %HIFI_DIR% is the directory for the highfidelity repository. -Open %HIFI_DIR%\build\hifi.sln and compile. +###Step 7. Making a Build -###Running Interface -If you need to debug Interface, you can run interface from within Visual Studio (see the section below). You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Debug\interface.exe +Open '%HIFI_DIR%\build\hifi.sln' using Visual Studio. -###Debugging Interface -* In the Solution Explorer, right click interface and click Set as StartUp Project -* Set the "Working Directory" for the Interface debugging sessions to the Debug output directory so that your application can load resources. Do this: right click interface and click Properties, choose Debugging from Configuration Properties, set Working Directory to .\Debug -* Now you can run and debug interface through Visual Studio +Change the Solution Configuration (next to the green play button) from "Debug" to "Release" for best performance. -For better performance when running debug builds, set the environment variable ```_NO_DEBUG_HEAP``` to ```1``` +Run Build > Build Solution. -http://preshing.com/20110717/the-windows-heap-is-slow-when-launched-from-the-debugger/ +###Step 8. Testing the Interface + +Create another environment variable (see Step #4) +* Set "Variable name": _NO_DEBUG_HEAP +* Set "Variable value": 1 + +In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run Debug > Start Debugging. + +Now, you should have a full build of High Fidelity and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow. + +Note: You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Debug\interface.exe + +###Troubleshooting + +For any problems after Step #6, first try this: +* Delete the highfidelity repository +* Restart your computer +* Redownload the [repository](https://github.com/highfidelity/hifi) +* Restart directions from Step #6 + +####CMake gives you the same error message repeatedly after the build fails + +Remove `CMakeCache.txt` found in the '%HIFI_DIR%\build' directory + +####nmake cannot be found + +Make sure nmake.exe is located at the following path: + C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin + +If not, add the directory where nmake is located to the PATH environment variable. + +####Qt is throwing an error + +Make sure you have the current version (5.6.1-1) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. From c430e0c9d6d6fd9b6596ab6dc5eee10760f73ff8 Mon Sep 17 00:00:00 2001 From: anshuman64 Date: Sun, 19 Mar 2017 12:07:45 -0700 Subject: [PATCH 42/43] Small changes based on Zach's recommendations --- BUILD_WIN.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 5d7812342a..e37bf27503 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -8,23 +8,23 @@ Note: Newer versions of Visual Studio are not yet compatible. ###Step 2. Installing CMake -Download and install the "win64-x64 Installer" from the [CMake Website](https://cmake.org/download/). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. +Download and install the CMake 3.8.0-rc2 "win64-x64 Installer" from the [CMake Website](https://cmake.org/download/). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. ###Step 3. Installing Qt -Download and install the [Qt 5.6.1 Installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe) +Download and install the [Qt 5.6.1 Installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe). Please note that the download file is large (850MB) and may take some time. Make sure to select all components when going through the installer. ###Step 4. Setting Qt Environment Variable -Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." +Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." (or search “Environment Variables” in Start Search). * Set "Variable name": QT_CMAKE_PREFIX_PATH * Set "Variable value": `C:\Qt\Qt5.6.1\5.6\msvc2013_64\lib\cmake` ###Step 5. Installing OpenSSL -Download and install the "Win64 OpenSSL v1.0.xk" Installer from [this website](https://slproweb.com/products/Win32OpenSSL.html), where "x" is the number of the latest release (ex. Win64 OpenSSL v1.0.2k). +Download and install the "Win64 OpenSSL v1.0.2k" Installer from [this website](https://slproweb.com/products/Win32OpenSSL.html). ###Step 6. Running CMake to Generate Build Files @@ -44,7 +44,7 @@ Change the Solution Configuration (next to the green play button) from "Debug" t Run Build > Build Solution. -###Step 8. Testing the Interface +###Step 8. Testing Interface Create another environment variable (see Step #4) * Set "Variable name": _NO_DEBUG_HEAP @@ -54,12 +54,12 @@ In Visual Studio, right+click "interface" under the Apps folder in Solution Expl Now, you should have a full build of High Fidelity and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow. -Note: You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Debug\interface.exe +Note: You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Release\interface.exe ###Troubleshooting For any problems after Step #6, first try this: -* Delete the highfidelity repository +* Delete your locally cloned copy of the highfidelity repository * Restart your computer * Redownload the [repository](https://github.com/highfidelity/hifi) * Restart directions from Step #6 @@ -77,4 +77,5 @@ If not, add the directory where nmake is located to the PATH environment variabl ####Qt is throwing an error -Make sure you have the current version (5.6.1-1) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. +Make sure you have the correct version (5.6.1-1) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. + From 3a08611c26d96e01f5c0a32e2869dea595d3d364 Mon Sep 17 00:00:00 2001 From: Sam Cake Date: Sun, 19 Mar 2017 17:33:28 -0700 Subject: [PATCH 43/43] Fix the typo in the assert for isWireframe --- libraries/render-utils/src/RenderPipelines.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index 4fbac4170e..414bcf0d63 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -307,7 +307,7 @@ void initForwardPipelines(render::ShapePlumber& plumber) { void addPlumberPipeline(ShapePlumber& plumber, const ShapeKey& key, const gpu::ShaderPointer& vertex, const gpu::ShaderPointer& pixel) { // These key-values' pipelines are added by this functor in addition to the key passed - assert(!key.isWireFrame()); + assert(!key.isWireframe()); assert(!key.isDepthBiased()); assert(key.isCullFace());