diff --git a/interface/resources/images/steam-min-spec-failed.png b/interface/resources/images/steam-min-spec-failed.png index 2d3c85bda0..99abac9e1c 100644 Binary files a/interface/resources/images/steam-min-spec-failed.png and b/interface/resources/images/steam-min-spec-failed.png differ diff --git a/interface/resources/meshes/being_of_light/being_of_light.fbx b/interface/resources/meshes/being_of_light/being_of_light.fbx index 57505ca80d..20e71abd6d 100644 Binary files a/interface/resources/meshes/being_of_light/being_of_light.fbx and b/interface/resources/meshes/being_of_light/being_of_light.fbx differ diff --git a/interface/src/Application.h b/interface/src/Application.h index afdeb57f6c..79c9ae735c 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -229,7 +229,7 @@ public: qint64 getCurrentSessionRuntime() const { return _sessionRunTimer.elapsed(); } - bool isAboutToQuit() const { return _aboutToQuit; } + bool isAboutToQuit() const override { return _aboutToQuit; } bool isPhysicsEnabled() const { return _physicsEnabled; } // the isHMDMode is true whenever we use the interface from an HMD and not a standard flat display diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index ded0107314..f426f4a816 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -95,7 +95,13 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer auto deleter = [](OffscreenQmlSurface* webSurface) { AbstractViewStateInterface::instance()->postLambdaEvent([webSurface] { - webSurface->deleteLater(); + if (AbstractViewStateInterface::instance()->isAboutToQuit()) { + // WebEngineView may run other threads (wasapi), so they must be deleted for a clean shutdown + // if the application has already stopped its event loop, delete must be explicit + delete webSurface; + } else { + webSurface->deleteLater(); + } }); }; _webSurface = QSharedPointer(new OffscreenQmlSurface(), deleter); @@ -333,6 +339,7 @@ void RenderableWebEntityItem::destroyWebSurface() { if (rootItem) { QObject* obj = rootItem->findChild("webEngineView"); if (obj) { + // stop loading QMetaObject::invokeMethod(obj, "stop"); } } diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index 248811b86a..fe5a42bb85 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -24,8 +24,11 @@ void UserActivityLoggerScriptingInterface::toggledAway(bool isAway) { logAction("toggled_away", { { "is_away", isAway } }); } -void UserActivityLoggerScriptingInterface::tutorialProgress(QString stepName, int stepNumber, float secondsToComplete, float tutorialElapsedTime) { +void UserActivityLoggerScriptingInterface::tutorialProgress( QString stepName, int stepNumber, float secondsToComplete, + float tutorialElapsedTime, QString tutorialRunID, int tutorialVersion) { logAction("tutorial_progress", { + { "tutorial_run_id", tutorialRunID }, + { "tutorial_version", tutorialVersion }, { "step", stepName }, { "step_number", stepNumber }, { "seconds_to_complete", secondsToComplete }, diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index bf3e20a2d7..52101e3e53 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -23,7 +23,8 @@ public: Q_INVOKABLE void enabledEdit(); Q_INVOKABLE void openedMarketplace(); Q_INVOKABLE void toggledAway(bool isAway); - Q_INVOKABLE void tutorialProgress(QString stepName, int stepNumber, float secondsToComplete, float tutorialElapsedTime); + Q_INVOKABLE void tutorialProgress(QString stepName, int stepNumber, float secondsToComplete, + float tutorialElapsedTime, QString tutorialRunID = "", int tutorialVersion = 0); private: void logAction(QString action, QJsonObject details = {}); diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index fbf2a1c86a..0e7a5e102b 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -49,7 +49,7 @@ Socket::Socket(QObject* parent, bool shouldChangeSocketOptions) : connect(&_udpSocket, &QAbstractSocket::stateChanged, this, &Socket::handleStateChanged); // in order to help track down the zombie server bug, add a timer to check if we missed a readyRead - const int READY_READ_BACKUP_CHECK_MSECS = 10 * 1000; + const int READY_READ_BACKUP_CHECK_MSECS = 2 * 1000; connect(_readyReadBackupTimer, &QTimer::timeout, this, &Socket::checkForReadyReadBackup); _readyReadBackupTimer->start(READY_READ_BACKUP_CHECK_MSECS); } diff --git a/libraries/render-utils/src/AbstractViewStateInterface.h b/libraries/render-utils/src/AbstractViewStateInterface.h index 362c0cc1bf..4570ead9e1 100644 --- a/libraries/render-utils/src/AbstractViewStateInterface.h +++ b/libraries/render-utils/src/AbstractViewStateInterface.h @@ -40,7 +40,9 @@ public: virtual glm::vec3 getAvatarPosition() const = 0; + virtual bool isAboutToQuit() const = 0; virtual void postLambdaEvent(std::function f) = 0; + virtual qreal getDevicePixelRatio() = 0; virtual render::ScenePointer getMain3DScene() = 0; diff --git a/libraries/script-engine/src/BatchLoader.cpp b/libraries/script-engine/src/BatchLoader.cpp index 65c1bc5a2a..964807894e 100644 --- a/libraries/script-engine/src/BatchLoader.cpp +++ b/libraries/script-engine/src/BatchLoader.cpp @@ -56,10 +56,6 @@ void BatchLoader::start() { // If BatchLoader is deleted before the callback is called, the subsequent "emit" call will not do // anything. ScriptCacheSignalProxy* proxy = new ScriptCacheSignalProxy(scriptCache.data()); - scriptCache->getScriptContents(url.toString(), [proxy](const QString& url, const QString& contents, bool isURL, bool success) { - proxy->receivedContent(url, contents, isURL, success); - proxy->deleteLater(); - }, false); connect(proxy, &ScriptCacheSignalProxy::contentAvailable, this, [this](const QString& url, const QString& contents, bool isURL, bool success) { if (isURL && success) { @@ -75,6 +71,11 @@ void BatchLoader::start() { emit finished(_data); } }); + + scriptCache->getScriptContents(url.toString(), [proxy](const QString& url, const QString& contents, bool isURL, bool success) { + proxy->receivedContent(url, contents, isURL, success); + proxy->deleteLater(); + }, false); } } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 9fc3de1c9e..01088660ff 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -146,6 +146,8 @@ public: Q_INVOKABLE void requestGarbageCollection() { collectGarbage(); } + Q_INVOKABLE QUuid generateUUID() { return QUuid::createUuid(); } + bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget bool isRunning() const { return _isRunning; } // used by ScriptWidget diff --git a/scripts/developer/tests/performance/crowd-agent.js b/scripts/developer/tests/performance/crowd-agent.js index 6768b41318..b87d418643 100644 --- a/scripts/developer/tests/performance/crowd-agent.js +++ b/scripts/developer/tests/performance/crowd-agent.js @@ -16,7 +16,7 @@ var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; -print('crowd-agent version 3'); +print('crowd-agent version 4'); /* Observations: - File urls for AC scripts silently fail. Use a local server (e.g., python SimpleHTTPServer) for development. @@ -34,19 +34,56 @@ function getSound(data, callback) { // callback(sound) when downloaded (which ma if (sound.downloaded) { return callback(sound); } - sound.ready.connect(function () { callback(sound); }); + function onDownloaded() { + sound.ready.disconnect(onDownloaded); + callback(sound); + } + sound.ready.connect(onDownloaded); } function onFinishedPlaying() { messageSend({key: 'finishedSound'}); } var attachment; +var stopper; +function clearStopper() { + if (!stopper) { + return; + } + Script.clearTimeout(stopper); + stopper = null; +} +function stopAgent(parameters) { + function stop() { + clearStopper(); + if (attachment) { + Avatar.detachOne(attachment.modelURL, attachment.jointName); + attachment = undefined; + } + Agent.isListeningToAudioStream = false; + Agent.isAvatar = false; + print('crowd-agent stopped', JSON.stringify(parameters), JSON.stringify(Agent)); + } + // Shutting down lots of agents at once can be hard on other parts of the system. (See fogbugz 2095.) + // For now, accept a parameter to delay for the given number of milliseconds before stopping. + // (We cannot count on summoning scripts to spread out the STOP messages, because they might be doing so + // on scriptEnding, in which case they are not allowed to create new delays.) + if (parameters.delay) { + if (!stopper) { // Let the first stopper do the deed. + stopper = Script.setTimeout(stop, parameters.delay); + } + } else { + stop(); + } +} + var MILLISECONDS_IN_SECOND = 1000; function startAgent(parameters) { // Can also be used to update. print('crowd-agent starting params', JSON.stringify(parameters), JSON.stringify(Agent)); + clearStopper(); + var wasOff = !Agent.isAvatar; Agent.isAvatar = true; - Agent.isListeningToAudioStream = true; // Send silence when not chattering. if (parameters.position) { Avatar.position = parameters.position; } @@ -56,6 +93,11 @@ function startAgent(parameters) { // Can also be used to update. if (parameters.skeletonModelURL) { Avatar.skeletonModelURL = parameters.skeletonModelURL; } + if (parameters.listen != undefined) { + Agent.isListeningToAudioStream = parameters.listen; // Send silence when not chattering. + } else if (wasOff) { + Agent.isListeningToAudioStream = true; + } if (parameters.soundData) { getSound(parameters.soundData, function (sound) { Script.setTimeout(onFinishedPlaying, sound.duration * MILLISECONDS_IN_SECOND); @@ -74,14 +116,6 @@ function startAgent(parameters) { // Can also be used to update. } print('crowd-agent avatars started'); } -function stopAgent(parameters) { - if (attachment) { - Avatar.detachOne(attachment.modelURL, attachment.jointName); - attachment = undefined; - } - Agent.isAvatar = false; - print('crowd-agent stopped', JSON.stringify(parameters), JSON.stringify(Agent)); -} function messageHandler(channel, messageString, senderID) { if (channel !== MESSAGE_CHANNEL) { diff --git a/scripts/developer/tests/performance/domain-check.js b/scripts/developer/tests/performance/domain-check.js index 806b58dbf6..398bc4fd0a 100644 --- a/scripts/developer/tests/performance/domain-check.js +++ b/scripts/developer/tests/performance/domain-check.js @@ -21,6 +21,10 @@ var NOMINAL_LOAD_TIME = 30; // seconds var MAXIMUM_LOAD_TIME = NOMINAL_LOAD_TIME * 2; var MINIMUM_AVATARS = 25; // changeable by prompt +// If we add or remove things too quickly, we get problems (e.g., audio, fogbugz 2095). +// For now, spread them out this timing apart. +var SPREAD_TIME_MS = 500; + var DENSITY = 0.3; // square meters per person. Some say 10 sq ft is arm's length (0.9m^2), 4.5 is crowd (0.4m^2), 2.5 is mosh pit (0.2m^2). var SOUND_DATA = {url: "http://hifi-content.s3.amazonaws.com/howard/sounds/piano1.wav"}; var AVATARS_CHATTERING_AT_ONCE = 4; // How many of the agents should we request to play SOUND at once. @@ -34,7 +38,7 @@ var ANIMATION_DATA = { "loopFlag": true }; -var version = 3; +var version = 4; function debug() { print.apply(null, [].concat.apply(['hrs fixme', version], [].map.call(arguments, JSON.stringify))); } @@ -86,6 +90,7 @@ function nextAfter(array, id) { // Wrapping next element in array after id. var summonedAgents = []; var chattering = []; +var accumulatedDelay = 0; var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; function messageSend(message) { Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); @@ -106,25 +111,29 @@ function messageHandler(channel, messageString, senderID) { } switch (message.key) { case "hello": - // There can be avatars we've summoned that do not yet appear in the AvatarList. - avatarIdentifiers = without(AvatarList.getAvatarIdentifiers(), summonedAgents); - debug('present', avatarIdentifiers, summonedAgents); - if ((summonedAgents.length + avatarIdentifiers.length) < MINIMUM_AVATARS) { - var chatter = chattering.length < AVATARS_CHATTERING_AT_ONCE; - if (chatter) { - chattering.push(senderID); + Script.setTimeout(function () { + // There can be avatars we've summoned that do not yet appear in the AvatarList. + avatarIdentifiers = without(AvatarList.getAvatarIdentifiers(), summonedAgents); + debug('present', avatarIdentifiers, summonedAgents); + if ((summonedAgents.length + avatarIdentifiers.length) < MINIMUM_AVATARS) { + var chatter = chattering.length < AVATARS_CHATTERING_AT_ONCE; + if (chatter) { + chattering.push(senderID); + } + summonedAgents.push(senderID); + messageSend({ + key: 'SUMMON', + rcpt: senderID, + position: Vec3.sum(MyAvatar.position, {x: coord(), y: 0, z: coord()}), + orientation: Quat.fromPitchYawRollDegrees(0, Quat.safeEulerAngles(MyAvatar.orientation).y + (turnSpread * (Math.random() - 0.5)), 0), + soundData: chatter && SOUND_DATA, + listen: true, + skeletonModelURL: "http://hifi-content.s3.amazonaws.com/howard/resources/meshes/defaultAvatar_full.fst", + animationData: ANIMATION_DATA + }); } - summonedAgents.push(senderID); - messageSend({ - key: 'SUMMON', - rcpt: senderID, - position: Vec3.sum(MyAvatar.position, {x: coord(), y: 0, z: coord()}), - orientation: Quat.fromPitchYawRollDegrees(0, Quat.safeEulerAngles(MyAvatar.orientation).y + (turnSpread * (Math.random() - 0.5)), 0), - soundData: chatter && SOUND_DATA, - skeletonModelURL: "http://hifi-content.s3.amazonaws.com/howard/resources/meshes/defaultAvatar_full.fst", - animationData: ANIMATION_DATA - }); - } + }, accumulatedDelay); + accumulatedDelay += SPREAD_TIME_MS; // assume we'll get all the hello respsponses more or less together. break; case "finishedSound": // Give someone else a chance. chattering = without(chattering, [senderID]); @@ -147,13 +156,15 @@ Messages.subscribe(MESSAGE_CHANNEL); Messages.messageReceived.connect(messageHandler); Script.scriptEnding.connect(function () { debug('stopping agents', summonedAgents); - summonedAgents.forEach(function (id) { messageSend({key: 'STOP', rcpt: id}); }); + Messages.messageReceived.disconnect(messageHandler); // don't respond to any messages during shutdown + accumulatedDelay = 0; + summonedAgents.forEach(function (id) { + messageSend({key: 'STOP', rcpt: id, delay: accumulatedDelay}); + accumulatedDelay += SPREAD_TIME_MS; + }); debug('agents stopped'); - Script.setTimeout(function () { - Messages.messageReceived.disconnect(messageHandler); - Messages.unsubscribe(MESSAGE_CHANNEL); - debug('unsubscribed'); - }, 500); + Messages.unsubscribe(MESSAGE_CHANNEL); + debug('unsubscribed'); }); var fail = false, results = ""; @@ -257,7 +268,7 @@ function doRender(continuation) { } config.newStats.connect(onNewStats); - startTwirl(720, 1, 15, 0.08, function () { + startTwirl(720, 1, 20, 0.08, function () { var end = Date.now(); config.newStats.disconnect(onNewStats); addResult('frame rate', 1000 * frames / (end - start), @@ -270,7 +281,7 @@ function doRender(continuation) { http://hifi-content.s3.amazonaws.com/howard/scripts/tests/performance/crowd-agent.js?v=3\n\ on your domain server."; } else if (total < MINIMUM_AVATARS) { - fail = "FAIL: Only " + summonedAgents.length + " avatars reported. Missing " + (MINIMUM_AVATARS - total) + "."; + fail = "FAIL: Only " + summonedAgents.length + " agents reported. Now missing " + (MINIMUM_AVATARS - total) + " avatars, total."; } } continuation(); diff --git a/scripts/developer/tests/performance/summon.js b/scripts/developer/tests/performance/summon.js index 8b67859b3a..69bf0860ae 100644 --- a/scripts/developer/tests/performance/summon.js +++ b/scripts/developer/tests/performance/summon.js @@ -13,19 +13,26 @@ // // See crowd-agent.js -var version = 1; +var version = 2; var label = "summon"; function debug() { print.apply(null, [].concat.apply([label, version], [].map.call(arguments, JSON.stringify))); } + var MINIMUM_AVATARS = 25; // We will summon agents to produce this many total. (Of course, there might not be enough agents.) +var N_LISTENING = MINIMUM_AVATARS - 1; +var AVATARS_CHATTERING_AT_ONCE = 4; // How many of the agents should we request to play SOUND_DATA at once. + +// If we add or remove things too quickly, we get problems (e.g., audio, fogbugz 2095). +// For now, spread them out this timing apart. +var SPREAD_TIME_MS = 500; + var DENSITY = 0.3; // square meters per person. Some say 10 sq ft is arm's length (0.9m^2), 4.5 is crowd (0.4m^2), 2.5 is mosh pit (0.2m^2). -var SOUND_DATA = {url: "http://howard-stearns.github.io/models/sounds/piano1.wav"}; -var AVATARS_CHATTERING_AT_ONCE = 4; // How many of the agents should we request to play SOUND at once. +var SOUND_DATA = {url: "http://hifi-content.s3.amazonaws.com/howard/sounds/piano1.wav"}; var NEXT_SOUND_SPREAD = 500; // millisecond range of how long to wait after one sound finishes, before playing the next var ANIMATION_DATA = { - "url": "http://howard-stearns.github.io/models/resources/avatar/animations/idle.fbx", - // "url": "http://howard-stearns.github.io/models/resources/avatar/animations/walk_fwd.fbx", // alternative example + "url": "http://hifi-content.s3.amazonaws.com/howard/resources/avatar/animations/idle.fbx", + // "url": "http://hifi-content.s3.amazonaws.com/howard/resources/avatar/animations/walk_fwd.fbx", // alternative example "startFrame": 0.0, "endFrame": 300.0, "timeScale": 1.0, @@ -45,6 +52,8 @@ function nextAfter(array, id) { // Wrapping next element in array after id. var summonedAgents = []; var chattering = []; +var nListening = 0; +var accumulatedDelay = 0; var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; function messageSend(message) { Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); @@ -65,25 +74,33 @@ function messageHandler(channel, messageString, senderID) { } switch (message.key) { case "hello": - // There can be avatars we've summoned that do not yet appear in the AvatarList. - avatarIdentifiers = without(AvatarList.getAvatarIdentifiers(), summonedAgents); - debug('present', avatarIdentifiers, summonedAgents); - if ((summonedAgents.length + avatarIdentifiers.length) < MINIMUM_AVATARS ) { - var chatter = chattering.length < AVATARS_CHATTERING_AT_ONCE; - if (chatter) { - chattering.push(senderID); + Script.setTimeout(function () { + // There can be avatars we've summoned that do not yet appear in the AvatarList. + avatarIdentifiers = without(AvatarList.getAvatarIdentifiers(), summonedAgents); + debug('present', avatarIdentifiers, summonedAgents); + if ((summonedAgents.length + avatarIdentifiers.length) < MINIMUM_AVATARS ) { + var chatter = chattering.length < AVATARS_CHATTERING_AT_ONCE; + var listen = nListening < N_LISTENING; + if (chatter) { + chattering.push(senderID); + } + if (listen) { + nListening++; + } + summonedAgents.push(senderID); + messageSend({ + key: 'SUMMON', + rcpt: senderID, + position: Vec3.sum(MyAvatar.position, {x: coord(), y: 0, z: coord()}), + orientation: Quat.fromPitchYawRollDegrees(0, Quat.safeEulerAngles(MyAvatar.orientation).y + (turnSpread * (Math.random() - 0.5)), 0), + soundData: chatter && SOUND_DATA, + listen: listen, + skeletonModelURL: "http://hifi-content.s3.amazonaws.com/howard/resources/meshes/defaultAvatar_full.fst", + animationData: ANIMATION_DATA + }); } - summonedAgents.push(senderID); - messageSend({ - key: 'SUMMON', - rcpt: senderID, - position: Vec3.sum(MyAvatar.position, {x: coord(), y: 0, z: coord()}), - orientation: Quat.fromPitchYawRollDegrees(0, Quat.safeEulerAngles(MyAvatar.orientation).y + (turnSpread * (Math.random() - 0.5)), 0), - soundData: chatter && SOUND_DATA, - skeletonModelURL: "http://howard-stearns.github.io/models/resources/meshes/defaultAvatar_full.fst", - animationData: ANIMATION_DATA - }); - } + }, accumulatedDelay); + accumulatedDelay += SPREAD_TIME_MS; // assume we'll get all the hello respsponses more or less together. break; case "finishedSound": // Give someone else a chance. chattering = without(chattering, [senderID]); @@ -99,20 +116,22 @@ function messageHandler(channel, messageString, senderID) { Window.alert("Someone else is summoning avatars."); break; default: - print("crowd-agent received unrecognized message:", messageString); + print("crowd summon.js received unrecognized message:", messageString); } } Messages.subscribe(MESSAGE_CHANNEL); Messages.messageReceived.connect(messageHandler); Script.scriptEnding.connect(function () { debug('stopping agents', summonedAgents); - summonedAgents.forEach(function (id) { messageSend({key: 'STOP', rcpt: id}); }); + Messages.messageReceived.disconnect(messageHandler); // don't respond to any messages during shutdown + accumulatedDelay = 0; + summonedAgents.forEach(function (id) { + messageSend({key: 'STOP', rcpt: id, delay: accumulatedDelay}); + accumulatedDelay += SPREAD_TIME_MS; + }); debug('agents stopped'); - Script.setTimeout(function () { - Messages.messageReceived.disconnect(messageHandler); - Messages.unsubscribe(MESSAGE_CHANNEL); - debug('unsubscribed'); - }, 500); + Messages.unsubscribe(MESSAGE_CHANNEL); + debug('unsubscribed'); }); messageSend({key: 'HELO'}); // Ask agents to report in now. @@ -120,9 +139,9 @@ Script.setTimeout(function () { var total = AvatarList.getAvatarIdentifiers().length; if (0 === summonedAgents.length) { Window.alert("No agents reported.\n\Please run " + MINIMUM_AVATARS + " instances of\n\ -http://cdn.highfidelity.com/davidkelly/production/scripts/tests/performance/crowd-agent.js\n\ +http://hifi-content.s3.amazonaws.com/howard/scripts/tests/performance/crowd-agent.js\n\ on your domain server."); } else if (total < MINIMUM_AVATARS) { - Window.alert("Only " + summonedAgents.length + " of the expected " + (MINIMUM_AVATARS - total) + " agents reported in."); + Window.alert("Only " + summonedAgents.length + " agents reported. Now missing " + (MINIMUM_AVATARS - total) + " avatars, total."); } -}, 5000); +}, MINIMUM_AVATARS * SPREAD_TIME_MS ) diff --git a/scripts/system/progress.js b/scripts/system/progress.js index c0b7143a11..92853c9ada 100644 --- a/scripts/system/progress.js +++ b/scripts/system/progress.js @@ -36,7 +36,7 @@ BAR_DESKTOP_2K_HEIGHT = 3, // Display height of SVG BAR_DESKTOP_2K_URL = Script.resolvePath("assets/images/progress-bar-2k.svg"), - BAR_DESKTOP_4K_WIDTH = 4480, // Width of SVG image in pixels. Sized for 1920 x 1080 display with 6 visible repeats. + BAR_DESKTOP_4K_WIDTH = 4480, // Width of SVG image in pixels. Sized for 4096 x 1920 display with 6 visible repeats. BAR_DESKTOP_4K_REPEAT = 640, // Length of repeat in bar = 2240 / 7. BAR_DESKTOP_4K_HEIGHT = 6, // Display height of SVG BAR_DESKTOP_4K_URL = Script.resolvePath("assets/images/progress-bar-4k.svg"), diff --git a/server-console/src/main.js b/server-console/src/main.js index 577c56d9e2..d8a6f30ac1 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -163,17 +163,19 @@ function shutdownCallback(idx) { if (homeServer.state == ProcessGroupStates.STOPPED) { // if the home server is already down, take down the server console now log.debug("Quitting."); - app.quit(); + app.exit(0); } else { // if the home server is still running, wait until we get a state change or timeout // before quitting the app log.debug("Server still shutting down. Waiting"); - var timeoutID = setTimeout(app.quit, 5000); + var timeoutID = setTimeout(function() { + app.exit(0); + }, 5000); homeServer.on('state-update', function(processGroup) { if (processGroup.state == ProcessGroupStates.STOPPED) { clearTimeout(timeoutID); log.debug("Quitting."); - app.quit(); + app.exit(0); } }); } @@ -240,7 +242,7 @@ var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory) if (shouldQuit) { log.warn("Another instance of the Sandbox is already running - this instance will quit."); - app.quit(); + app.exit(0); return; } @@ -288,12 +290,12 @@ function binaryMissingMessage(displayName, executableName, required) { if (!dsPath) { dialog.showErrorBox("Domain Server Not Found", binaryMissingMessage("domain-server", "domain-server", true)); - app.quit(); + app.exit(0); } if (!acPath) { dialog.showErrorBox("Assignment Client Not Found", binaryMissingMessage("assignment-client", "assignment-client", true)); - app.quit(); + app.exit(0); } function openFileBrowser(path) { diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 7cc84463bd..d4a8322f8a 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -425,6 +425,7 @@ protected: return vec3(); } + bool isAboutToQuit() const override { return false; } void postLambdaEvent(std::function f) override {} qreal getDevicePixelRatio() override { diff --git a/tutorial/tutorial.js b/tutorial/tutorial.js index 0fb86d7247..4908465779 100644 --- a/tutorial/tutorial.js +++ b/tutorial/tutorial.js @@ -391,61 +391,6 @@ stepOrient.prototype = { } }; -/////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////// -// // -// STEP: Raise hands above head // -// // -/////////////////////////////////////////////////////////////////////////////// -var stepRaiseAboveHead = function(name) { - this.tag = name; - this.tempTag = name + "-temporary"; -} -stepRaiseAboveHead.prototype = { - start: function(onFinish) { - var tag = this.tag; - - var STATE_START = 0; - var STATE_HANDS_DOWN = 1; - var STATE_HANDS_UP = 2; - this.state = STATE_START; - - editEntitiesWithTag(this.tag, { visible: true }); - - // Wait 2 seconds before starting to check for hands - this.checkIntervalID = null; - function checkForHandsAboveHead() { - debug("RaiseAboveHead | Checking hands"); - if (this.state == STATE_START) { - if (MyAvatar.getLeftPalmPosition().y < (MyAvatar.getHeadPosition().y - 0.1)) { - this.state = STATE_HANDS_DOWN; - } - } else if (this.state == STATE_HANDS_DOWN) { - if (MyAvatar.getLeftPalmPosition().y > (MyAvatar.getHeadPosition().y + 0.1)) { - this.state = STATE_HANDS_UP; - Script.clearInterval(this.checkIntervalID); - this.checkIntervalID = null; - playSuccessSound(); - onFinish(); - } - } - } - this.checkIntervalID = Script.setInterval(checkForHandsAboveHead.bind(this), 500); - }, - cleanup: function() { - debug("RaiseAboveHead | Cleanup"); - if (this.checkIntervalID) { - Script.clearInterval(this.checkIntervalID); - this.checkIntervalID = null - } - if (this.waitTimeoutID) { - Script.clearTimeout(this.waitTimeoutID); - this.waitTimeoutID = null; - } - editEntitiesWithTag(this.tag, { visible: false, collisionless: 1 }); - deleteEntitiesWithTag(this.tempTag); - } -}; /////////////////////////////////////////////////////////////////////////////// @@ -1027,16 +972,23 @@ TutorialManager = function() { var startedTutorialAt = 0; var startedLastStepAt = 0; + var wentToEntryStepNum; + var VERSION = 1; + var tutorialID; + var self = this; this.startTutorial = function() { currentStepNum = -1; currentStep = null; startedTutorialAt = Date.now(); + + // Old versions of interface do not have the Script.generateUUID function. + // If Script.generateUUID is not available, default to an empty string. + tutorialID = Script.generateUUID ? Script.generateUUID() : ""; STEPS = [ new stepStart("start"), new stepOrient("orient"), - //new stepRaiseAboveHead("raiseHands"), new stepNearGrab("nearGrab"), new stepFarGrab("farGrab"), new stepEquip("equip"), @@ -1045,6 +997,7 @@ TutorialManager = function() { new stepFinish("finish"), new stepEnableControllers("enableControllers"), ]; + wentToEntryStepNum = STEPS.length; for (var i = 0; i < STEPS.length; ++i) { STEPS[i].cleanup(); } @@ -1055,10 +1008,7 @@ TutorialManager = function() { this.onFinish = function() { debug("onFinish", currentStepNum); if (currentStep && currentStep.shouldLog !== false) { - var timeToFinishStep = (Date.now() - startedLastStepAt) / 1000; - var tutorialTimeElapsed = (Date.now() - startedTutorialAt) / 1000; - UserActivityLogger.tutorialProgress( - currentStep.tag, currentStepNum, timeToFinishStep, tutorialTimeElapsed); + self.trackStep(currentStep.tag, currentStepNum); } self.startNextStep(); @@ -1071,6 +1021,12 @@ TutorialManager = function() { ++currentStepNum; + // This always needs to be set because we use this value when + // tracking that the user has gone through the entry portal. When the + // tutorial finishes, there is a last "pseudo" step that the user + // finishes when stepping into the portal. + startedLastStepAt = Date.now(); + if (currentStepNum >= STEPS.length) { // Done info("DONE WITH TUTORIAL"); @@ -1080,7 +1036,6 @@ TutorialManager = function() { } else { info("Starting step", currentStepNum); currentStep = STEPS[currentStepNum]; - startedLastStepAt = Date.now(); currentStep.start(this.onFinish); return true; } @@ -1102,6 +1057,21 @@ TutorialManager = function() { currentStepNum = -1; currentStep = null; } + + this.trackStep = function(name, stepNum) { + var timeToFinishStep = (Date.now() - startedLastStepAt) / 1000; + var tutorialTimeElapsed = (Date.now() - startedTutorialAt) / 1000; + UserActivityLogger.tutorialProgress( + name, stepNum, timeToFinishStep, tutorialTimeElapsed, + tutorialID, VERSION); + } + + // This is a message sent from the "entry" portal in the courtyard, + // after the tutorial has finished. + this.enteredEntryPortal = function() { + info("Got enteredEntryPortal, tracking"); + this.trackStep("wentToEntry", wentToEntryStepNum); + } } // To run the tutorial: diff --git a/tutorial/tutorialZone.js b/tutorial/tutorialZone.js index 40028c9cd7..01e2aa4c52 100644 --- a/tutorial/tutorialZone.js +++ b/tutorial/tutorialZone.js @@ -113,6 +113,14 @@ if (!Function.prototype.bind) { } }, + onEnteredEntryPortal: function() { + print("TutorialZone | Got onEnteredEntryPortal"); + if (this.tutorialManager) { + print("TutorialZone | Calling enteredEntryPortal"); + this.tutorialManager.enteredEntryPortal(); + } + }, + enterEntity: function() { print("TutorialZone | ENTERED THE TUTORIAL AREA"); }, @@ -125,7 +133,7 @@ if (!Function.prototype.bind) { } if (this.tutorialManager) { this.tutorialManager.stopTutorial(); - this.tutorialManager = null; + //this.tutorialManager = null; } } };