diff --git a/interface/src/scripting/MenuScriptingInterface.cpp b/interface/src/scripting/MenuScriptingInterface.cpp index 7eb80b5946..2fa7470561 100644 --- a/interface/src/scripting/MenuScriptingInterface.cpp +++ b/interface/src/scripting/MenuScriptingInterface.cpp @@ -125,3 +125,8 @@ void MenuScriptingInterface::setIsOptionChecked(const QString& menuOption, bool Q_ARG(const QString&, menuOption), Q_ARG(bool, isChecked)); } + +void MenuScriptingInterface::triggerOption(const QString& menuOption) { + QMetaObject::invokeMethod(Menu::getInstance(), "triggerOption", Q_ARG(const QString&, menuOption)); +} + diff --git a/interface/src/scripting/MenuScriptingInterface.h b/interface/src/scripting/MenuScriptingInterface.h index 03ff4b512a..5b8a437529 100644 --- a/interface/src/scripting/MenuScriptingInterface.h +++ b/interface/src/scripting/MenuScriptingInterface.h @@ -48,6 +48,8 @@ public slots: bool isOptionChecked(const QString& menuOption); void setIsOptionChecked(const QString& menuOption, bool isChecked); + + void triggerOption(const QString& menuOption); signals: void menuItemEvent(const QString& menuItem); diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index c528c26b99..0f9dd698fd 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -206,3 +206,7 @@ void WindowScriptingInterface::takeSnapshot(bool notify, float aspectRatio) { void WindowScriptingInterface::shareSnapshot(const QString& path) { qApp->shareSnapshot(path); } + +bool WindowScriptingInterface::isPhysicsEnabled() { + return qApp->isPhysicsEnabled(); +} diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 9303636a1f..f4a89ae221 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -54,6 +54,7 @@ public slots: void copyToClipboard(const QString& text); void takeSnapshot(bool notify = true, float aspectRatio = 0.0f); void shareSnapshot(const QString& path); + bool isPhysicsEnabled(); signals: void domainChanged(const QString& domainHostname); diff --git a/libraries/ui/src/ui/Menu.h b/libraries/ui/src/ui/Menu.h index 895e40fe68..ee60a031c3 100644 --- a/libraries/ui/src/ui/Menu.h +++ b/libraries/ui/src/ui/Menu.h @@ -62,7 +62,6 @@ public: MenuWrapper* getMenu(const QString& menuName); MenuWrapper* getSubMenuFromName(const QString& menuName, MenuWrapper* menu); - void triggerOption(const QString& menuOption); QAction* getActionForOption(const QString& menuOption); QAction* addActionToQMenuAndActionHash(MenuWrapper* destinationMenu, @@ -112,6 +111,8 @@ public slots: void toggleDeveloperMenus(); void toggleAdvancedMenus(); + + void triggerOption(const QString& menuOption); static bool isSomeSubmenuShown() { return _isSomeSubmenuShown; } diff --git a/scripts/developer/tests/performance/crowd-agent.js b/scripts/developer/tests/performance/crowd-agent.js new file mode 100644 index 0000000000..5df576cf99 --- /dev/null +++ b/scripts/developer/tests/performance/crowd-agent.js @@ -0,0 +1,95 @@ +"use strict"; +/*jslint vars: true, plusplus: true*/ +/*global Agent, Avatar, Script, Entities, Vec3, Quat, print*/ +// +// crowd-agent.js +// scripts/developer/tests/performance/ +// +// Created by Howard Stearns on 9/29/16. +// Copyright 2016 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 +// +// Add this to domain-settings scripts url with n instances. It will lie dormant until +// a script like summon.js calls up to n avatars to be around you. + +var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; + +print('crowd-agent version 1'); + +/* Observations: +- File urls for AC scripts silently fail. Use a local server (e.g., python SimpleHTTPServer) for development. +- URLs are cached regardless of server headers. Must use cache-defeating query parameters. +- JSON.stringify(Avatar) silently fails (even when Agent.isAvatar) +*/ + +function startAgent(parameters) { // Can also be used to update. + print('crowd-agent starting params', JSON.stringify(parameters), JSON.stringify(Agent)); + Agent.isAvatar = true; + if (parameters.position) { + Avatar.position = parameters.position; + } + if (parameters.orientation) { + Avatar.orientation = parameters.orientation; + } + if (parameters.skeletonModelURL) { + Avatar.skeletonModelURL = parameters.skeletonModelURL; + } + if (parameters.animationData) { + data = parameters.animationData; + Avatar.startAnimation(data.url, data.fps || 30, 1.0, (data.loopFlag === undefined) ? true : data.loopFlag, false, data.startFrame || 0, data.endFrame); + } + print('crowd-agent avatars started'); +} +function stopAgent(parameters) { + Agent.isAvatar = false; + print('crowd-agent stopped', JSON.stringify(parameters), JSON.stringify(Agent)); +} + +function messageSend(message) { + Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); +} +function messageHandler(channel, messageString, senderID) { + if (channel !== MESSAGE_CHANNEL) { + return; + } + print('crowd-agent message', channel, messageString, senderID); + if (Agent.sessionUUID === senderID) { // ignore my own + return; + } + var message = {}; + try { + message = JSON.parse(messageString); + } catch (e) { + print(e); + } + switch (message.key) { + case "HELO": + messageSend({key: 'hello'}); // Allow the coordinator to count responses and make assignments. + break; + case 'hello': // ignore responses (e.g., from other agents) + break; + case "SUMMON": + if (message.rcpt === Agent.sessionUUID) { + startAgent(message); + } + break; + case "STOP": + if (message.rcpt === Agent.sessionUUID) { + stopAgent(message); + } + break; + default: + print("crowd-agent received unrecognized message:", channel, messageString, senderID); + } +} +Messages.subscribe(MESSAGE_CHANNEL); +Messages.messageReceived.connect(messageHandler); + +Script.scriptEnding.connect(function () { + print('crowd-agent shutting down'); + Messages.messageReceived.disconnect(messageHandler); + Messages.unsubscribe(MESSAGE_CHANNEL); + print('crowd-agent unsubscribed'); +}); diff --git a/scripts/developer/tests/performance/domain-check.js b/scripts/developer/tests/performance/domain-check.js index eceffa278b..f085c3f685 100644 --- a/scripts/developer/tests/performance/domain-check.js +++ b/scripts/developer/tests/performance/domain-check.js @@ -20,15 +20,18 @@ var EXPECTED_HMD_FRAMERATE = 90; var MAXIMUM_LOAD_TIME = 60; // seconds var MINIMUM_AVATARS = 25; // FIXME: not implemented yet. Requires agent scripts. Idea is to have them organize themselves to the right number. -var version = 1; +var version = 2; function debug() { print.apply(null, [].concat.apply(['hrs fixme', version], [].map.call(arguments, JSON.stringify))); } -var emptyishPlace = 'empty'; -var cachePlaces = ['localhost', 'Welcome']; -var isInCachePlace = cachePlaces.indexOf(location.hostname) >= 0; -var defaultPlace = isInCachePlace ? 'Playa' : location.hostname; +function isNowIn(place) { // true if currently in specified place + return location.hostname.toLowerCase() === place.toLowerCase(); +} + +var cachePlaces = ['dev-Welcome', 'localhost']; // For now, list the lighter weight one first. +var isInCachePlace = cachePlaces.some(isNowIn); +var defaultPlace = isInCachePlace ? 'dev-Playa' : location.hostname; var prompt = "domain-check.js version " + version + "\n\nWhat place should we enter?"; debug(cachePlaces, isInCachePlace, defaultPlace, prompt); var entryPlace = Window.prompt(prompt, defaultPlace); @@ -73,10 +76,17 @@ function startTwirl(targetRotation, degreesPerUpdate, interval, strafeDistance, function doLoad(place, continuationWithLoadTime) { // Go to place and call continuationWithLoadTime(loadTimeInSeconds) var start = Date.now(), timeout, onDownloadUpdate, finishedTwirl = false, loadTime; + // There are two ways to learn of changes: connect to change signals, or poll. + // Until we get reliable results, we'll poll. + var POLL_INTERVAL = 500, poll; + function setHandlers() { + //Stats.downloadsPendingChanged.connect(onDownloadUpdate); downloadsChanged, and physics... + poll = Script.setInterval(onDownloadUpdate, POLL_INTERVAL); + } function clearHandlers() { debug('clearHandlers'); - Stats.downloadsPendingChanged.disconnect(onDownloadUpdate); - Stats.downloadsChanged.disconnect(onDownloadUpdate); + //Stats.downloadsPendingChanged.disconnect(onDownloadUpdate); downloadsChanged, and physics.. + Script.clearInterval(poll); } function waitForLoad(flag) { debug('entry', place, 'initial downloads/pending', Stats.downloads, Stats.downloadsPending); @@ -93,13 +103,11 @@ function doLoad(place, continuationWithLoadTime) { // Go to place and call conti continuationWithLoadTime(loadTime); } }); - Stats.downloadsPendingChanged.connect(onDownloadUpdate); - Stats.downloadsChanged.connect(onDownloadUpdate); + setHandlers(); } function isLoading() { - // FIXME: This tells us when download are completed, but it doesn't tell us when the objects are parsed and loaded. - // We really want something like _physicsEnabled, but that isn't signalled. - return Stats.downloads || Stats.downloadsPending; + // FIXME: We should also confirm that textures have loaded. + return Stats.downloads || Stats.downloadsPending || !Window.isPhysicsEnabled(); } onDownloadUpdate = function onDownloadUpdate() { debug('update downloads/pending', Stats.downloads, Stats.downloadsPending); @@ -114,17 +122,9 @@ function doLoad(place, continuationWithLoadTime) { // Go to place and call conti } }; - function doit() { - debug('go', place); - location.hostChanged.connect(waitForLoad); - location.handleLookupString(place); - } - if (location.placename.toLowerCase() === place.toLowerCase()) { - location.handleLookupString(emptyishPlace); - Script.setTimeout(doit, 1000); - } else { - doit(); - } + debug('go', place); + location.hostChanged.connect(waitForLoad); + location.handleLookupString(place); } var config = Render.getConfig("Stats"); @@ -144,6 +144,7 @@ function doRender(continuation) { }); } +var TELEPORT_PAUSE = 500; function maybePrepareCache(continuation) { var prepareCache = Window.confirm("Prepare cache?\n\n\ Should we start with all and only those items cached that are encountered when visiting:\n" + cachePlaces.join(', ') + "\n\ @@ -151,8 +152,6 @@ If 'yes', cache will be cleared and we will visit these two, with a turn in each You would want to say 'no' (and make other preparations) if you were testing these places."); if (prepareCache) { - location.handleLookupString(emptyishPlace); - Window.alert("Please do menu Edit->Reload Content (Clears all caches) and THEN press 'ok'."); function loadNext() { var place = cachePlaces.shift(); doLoad(place, function (prepTime) { @@ -164,16 +163,19 @@ You would want to say 'no' (and make other preparations) if you were testing the } }); } - loadNext(); + location.handleLookupString(cachePlaces[cachePlaces.length - 1]); + Menu.triggerOption("Reload Content (Clears all caches)"); + Script.setTimeout(loadNext, TELEPORT_PAUSE); } else { - continuation(); + location.handleLookupString(isNowIn(cachePlaces[0]) ? cachePlaces[1] : cachePlaces[0]); + Script.setTimeout(continuation, TELEPORT_PAUSE); } } function maybeRunTribbles(continuation) { if (Window.confirm("Run tribbles?\n\n\ At most, only one participant should say yes.")) { - Script.load('http://howard-stearns.github.io/models/scripts/tests/performance/tribbles.js'); // FIXME: replace with AWS + Script.load('http://cdn.highfidelity.com/davidkelly/production/scripts/tests/performance/tribbles.js'); Script.setTimeout(continuation, 3000); } else { continuation(); diff --git a/scripts/developer/tests/performance/summon.js b/scripts/developer/tests/performance/summon.js new file mode 100644 index 0000000000..2eb1fbe301 --- /dev/null +++ b/scripts/developer/tests/performance/summon.js @@ -0,0 +1,103 @@ +"use strict"; +/*jslint vars: true, plusplus: true*/ +/*global Agent, Avatar, Script, Entities, Vec3, Quat, print*/ +// +// crowd-agent.js +// scripts/developer/tests/performance/ +// +// Created by Howard Stearns on 9/29/16. +// Copyright 2016 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 +// +// See crowd-agent.js + +var version = 1; +var label = "summon"; +function debug() { + print.apply(null, [].concat.apply([label, version], [].map.call(arguments, JSON.stringify))); +} +var MINIMUM_AVATARS = 25; +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 spread = Math.sqrt(MINIMUM_AVATARS * DENSITY); // meters +var turnSpread = 90; // How many degrees should turn from front range over. + +function coord() { return (Math.random() * spread) - (spread / 2); } // randomly distribute a coordinate zero += spread/2. + + +var summonedAgents = []; +var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; +function messageSend(message) { + Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); +} +function messageHandler(channel, messageString, senderID) { + if (channel !== MESSAGE_CHANNEL) { + return; + } + debug('message', channel, messageString, senderID); + if (MyAvatar.sessionUUID === senderID) { // ignore my own + return; + } + var message = {}, avatarIdentifiers; + try { + message = JSON.parse(messageString); + } catch (e) { + print(e); + } + switch (message.key) { + case "hello": + // There can be avatars we've summoned that do not yet appear in the AvatarList. + avatarIdentifiers = AvatarList.getAvatarIdentifiers().filter(function (id) { return summonedAgents.indexOf(id) === -1; }); + debug('present', avatarIdentifiers, summonedAgents); + if ((summonedAgents.length + avatarIdentifiers.length) < MINIMUM_AVATARS ) { + 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)/*, + // No need to specify skeletonModelURL + //skeletonModelURL: "file:///c:/Program Files/High Fidelity Release/resources/meshes/being_of_light/being_of_light.fbx", + //skeletonModelURL: "file:///c:/Program Files/High Fidelity Release/resources/meshes/defaultAvatar_full.fst"/, + animationData: { // T-pose until we get animations working again. + "url": "file:///C:/Program Files/High Fidelity Release/resources/avatar/animations/idle.fbx", + //"url": "file:///c:/Program Files/High Fidelity Release/resources/avatar/animations/walk_fwd.fbx", + "startFrame": 0.0, + "endFrame": 300.0, + "timeScale": 1.0, + "loopFlag": true + }*/ + }); + } + break; + case "HELO": + Window.alert("Someone else is summoning avatars."); + break; + default: + print("crowd-agent 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}); }); + debug('agents stopped'); + Script.setTimeout(function () { + Messages.messageReceived.disconnect(messageHandler); + Messages.unsubscribe(MESSAGE_CHANNEL); + debug('unsubscribed'); + }, 500); +}); + +messageSend({key: 'HELO'}); // Ask agents to report in now. +Script.setTimeout(function () { + 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\ +on your domain server."); + } else if (summonedAgents.length < MINIMUM_AVATARS) { + Window.alert("Only " + summonedAgents.length + " of the expected " + MINIMUM_AVATARS + " agents reported in."); + } +}, 5000); diff --git a/scripts/developer/tests/performance/tribbles.js b/scripts/developer/tests/performance/tribbles.js index c9ae347a82..4c04f8b5b7 100644 --- a/scripts/developer/tests/performance/tribbles.js +++ b/scripts/developer/tests/performance/tribbles.js @@ -54,42 +54,46 @@ function randomVector(range) { }; } -Script.setInterval(function () { - if (!Entities.serversExist() || !Entities.canRez()) { - return; - } - if (totalCreated >= NUMBER_TO_CREATE) { - print("Created " + totalCreated + " tribbles."); - Script.stop(); - } +if (!Entities.canRezTmp()) { + Window.alert("Cannot create temp objects here."); + Script.stop(); +} else { + Script.setInterval(function () { + if (!Entities.serversExist()) { + return; + } + if (totalCreated >= NUMBER_TO_CREATE) { + print("Created " + totalCreated + " tribbles."); + Script.stop(); + } - var i, numToCreate = RATE_PER_SECOND * (SCRIPT_INTERVAL / 1000.0); - var parameters = JSON.stringify({ - moveTimeout: MOVE_TIMEOUT, - moveRate: MOVE_RATE, - editTimeout: EDIT_TIMEOUT, - editRate: EDIT_RATE, - debug: {flow: false, send: false, receive: false} - }); - for (i = 0; (i < numToCreate) && (totalCreated < NUMBER_TO_CREATE); i++) { - Entities.addEntity({ - userData: parameters, - type: TYPE, - name: "tribble-" + totalCreated, - position: Vec3.sum(center, randomVector({ x: RANGE, y: RANGE, z: RANGE })), - dimensions: {x: SIZE, y: SIZE, z: SIZE}, - color: {red: Math.random() * 255, green: Math.random() * 255, blue: Math.random() * 255}, - velocity: VELOCITY, - angularVelocity: Vec3.multiply(Math.random(), ANGULAR_VELOCITY), - damping: DAMPING, - angularDamping: ANGULAR_DAMPING, - gravity: GRAVITY, - collisionsWillMove: true, - lifetime: LIFETIME, - script: Script.resolvePath("tribbleEntity.js") + var i, numToCreate = RATE_PER_SECOND * (SCRIPT_INTERVAL / 1000.0); + var parameters = JSON.stringify({ + moveTimeout: MOVE_TIMEOUT, + moveRate: MOVE_RATE, + editTimeout: EDIT_TIMEOUT, + editRate: EDIT_RATE, + debug: {flow: false, send: false, receive: false} }); + for (i = 0; (i < numToCreate) && (totalCreated < NUMBER_TO_CREATE); i++) { + Entities.addEntity({ + userData: parameters, + type: TYPE, + name: "tribble-" + totalCreated, + position: Vec3.sum(center, randomVector({ x: RANGE, y: RANGE, z: RANGE })), + dimensions: {x: SIZE, y: SIZE, z: SIZE}, + color: {red: Math.random() * 255, green: Math.random() * 255, blue: Math.random() * 255}, + velocity: VELOCITY, + angularVelocity: Vec3.multiply(Math.random(), ANGULAR_VELOCITY), + damping: DAMPING, + angularDamping: ANGULAR_DAMPING, + gravity: GRAVITY, + collisionsWillMove: true, + lifetime: LIFETIME, + script: Script.resolvePath("tribbleEntity.js") + }); - totalCreated++; - } -}, SCRIPT_INTERVAL); - + totalCreated++; + } + }, SCRIPT_INTERVAL); +}