From a25584c90f480e1f4f9557939832eab9e22c80ff Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 26 Apr 2017 18:51:55 +0200 Subject: [PATCH] ESLINT, style and spelling fixes --- scripts/system/makeUserConnection.js | 1592 +++++++++++++------------- 1 file changed, 806 insertions(+), 786 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 674da2d677..42f8eceed9 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -1,6 +1,6 @@ "use strict"; // -// makeUserConnetion.js +// makeUserConnection.js // scripts/system // // Created by David Kelly on 3/7/2017. @@ -11,854 +11,874 @@ // (function() { // BEGIN LOCAL_SCOPE -const label = "makeUserConnection"; -const MAX_AVATAR_DISTANCE = 0.2; // m -const GRIP_MIN = 0.05; // goes from 0-1, so 5% pressed is pressed -const MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; -const STATES = { - inactive : 0, - waiting: 1, - connecting: 2, - makingConnection: 3 -}; -const STATE_STRINGS = ["inactive", "waiting", "connecting", "makingConnection"]; -const WAITING_INTERVAL = 100; // ms -const CONNECTING_INTERVAL = 100; // ms -const MAKING_CONNECTION_TIMEOUT = 800; // ms -const CONNECTING_TIME = 1600; // ms -const PARTICLE_RADIUS = 0.15; // m -const PARTICLE_ANGLE_INCREMENT = 360/45; // 1hz -const HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/4beat_sweep.wav"; -const SUCCESSFUL_HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/3rdbeat_success_bell.wav"; -const HAPTIC_DATA = { - initial: { duration: 20, strength: 0.6}, // duration is in ms - background: { duration: 100, strength: 0.3 }, // duration is in ms - success: { duration: 60, strength: 1.0} // duration is in ms -}; -const PARTICLE_EFFECT_PROPS = { - "alpha": 0.8, - "azimuthFinish": Math.PI, - "azimuthStart": -1*Math.PI, - "emitRate": 500, - "emitSpeed": 0.0, - "emitterShouldTrail": 1, - "isEmitting": 1, - "lifespan": 3, - "maxParticles": 1000, - "particleRadius": 0.003, - "polarStart": 1, - "polarFinish": 1, - "radiusFinish": 0.008, - "radiusStart": 0.0025, - "speedSpread": 0.025, - "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", - "color": {"red": 255, "green": 255, "blue": 255}, - "colorFinish": {"red": 0, "green": 164, "blue": 255}, - "colorStart": {"red": 255, "green": 255, "blue": 255}, - "emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71}, - "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, - "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, - "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, - "type": "ParticleEffect" -}; -const MAKING_CONNECTION_PARTICLE_PROPS = { - "alpha": 0.07, - "alphaStart":0.011, - "alphaSpread": 0, - "alphaFinish": 0, - "azimuthFinish": Math.PI, - "azimuthStart": -1*Math.PI, - "emitRate": 2000, - "emitSpeed": 0.0, - "emitterShouldTrail": 1, - "isEmitting": 1, - "lifespan": 3.6, - "maxParticles": 4000, - "particleRadius": 0.048, - "polarStart": 0, - "polarFinish": 1, - "radiusFinish": 0.3, - "radiusStart": 0.04, - "speedSpread": 0.01, - "radiusSpread": 0.9, - "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", - "color": {"red": 200, "green": 170, "blue": 255}, - "colorFinish": {"red": 0, "green": 134, "blue": 255}, - "colorStart": {"red": 185, "green": 222, "blue": 255}, - "emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71}, - "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, - "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, - "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, - "type": "ParticleEffect" -}; + var LABEL = "makeUserConnection"; + var MAX_AVATAR_DISTANCE = 0.2; // m + var GRIP_MIN = 0.05; // goes from 0-1, so 5% pressed is pressed + var MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; + var STATES = { + INACTIVE: 0, + WAITING: 1, + CONNECTING: 2, + MAKING_CONNECTION: 3 + }; + var STATE_STRINGS = ["inactive", "waiting", "connecting", "makingConnection"]; + var WAITING_INTERVAL = 100; // ms + var CONNECTING_INTERVAL = 100; // ms + var MAKING_CONNECTION_TIMEOUT = 800; // ms + var CONNECTING_TIME = 1600; // ms + var PARTICLE_RADIUS = 0.15; // m + var PARTICLE_ANGLE_INCREMENT = 360/45; // 1hz + var HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/4beat_sweep.wav"; + var SUCCESSFUL_HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/3rdbeat_success_bell.wav"; + var HAPTIC_DATA = { + initial: { duration: 20, strength: 0.6 }, // duration is in ms + background: { duration: 100, strength: 0.3 }, // duration is in ms + success: { duration: 60, strength: 1.0 } // duration is in ms + }; + var PARTICLE_EFFECT_PROPS = { + "alpha": 0.8, + "azimuthFinish": Math.PI, + "azimuthStart": -1*Math.PI, + "emitRate": 500, + "emitSpeed": 0.0, + "emitterShouldTrail": 1, + "isEmitting": 1, + "lifespan": 3, + "maxParticles": 1000, + "particleRadius": 0.003, + "polarStart": 1, + "polarFinish": 1, + "radiusFinish": 0.008, + "radiusStart": 0.0025, + "speedSpread": 0.025, + "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", + "color": {"red": 255, "green": 255, "blue": 255}, + "colorFinish": {"red": 0, "green": 164, "blue": 255}, + "colorStart": {"red": 255, "green": 255, "blue": 255}, + "emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71}, + "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, + "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, + "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, + "type": "ParticleEffect" + }; + var MAKING_CONNECTION_PARTICLE_PROPS = { + "alpha": 0.07, + "alphaStart": 0.011, + "alphaSpread": 0, + "alphaFinish": 0, + "azimuthFinish": Math.PI, + "azimuthStart": -1*Math.PI, + "emitRate": 2000, + "emitSpeed": 0.0, + "emitterShouldTrail": 1, + "isEmitting": 1, + "lifespan": 3.6, + "maxParticles": 4000, + "particleRadius": 0.048, + "polarStart": 0, + "polarFinish": 1, + "radiusFinish": 0.3, + "radiusStart": 0.04, + "speedSpread": 0.01, + "radiusSpread": 0.9, + "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", + "color": {"red": 200, "green": 170, "blue": 255}, + "colorFinish": {"red": 0, "green": 134, "blue": 255}, + "colorStart": {"red": 185, "green": 222, "blue": 255}, + "emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71}, + "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, + "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, + "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, + "type": "ParticleEffect" + }; -var currentHand; -var state = STATES.inactive; -var connectingInterval; -var waitingInterval; -var makingConnectionTimeout; -var animHandlerId; -var connectingId; -var connectingHand; -var waitingList = {}; -var particleEffect; -var waitingBallScale; -var particleRotationAngle = 0.0; -var makingConnectionParticleEffect; -var makingConnectionEmitRate = 2000; -var particleEmitRate = 500; -var handshakeInjector; -var successfulHandshakeInjector; -var handshakeSound; -var successfulHandshakeSound; + var currentHand; + var state = STATES.INACTIVE; + var connectingInterval; + var waitingInterval; + var makingConnectionTimeout; + var animHandlerId; + var connectingId; + var connectingHand; + var waitingList = {}; + var particleEffect; + var particleRotationAngle = 0.0; + var makingConnectionParticleEffect; + var makingConnectionEmitRate = 2000; + var particleEmitRate = 500; + var handshakeInjector; + var successfulHandshakeInjector; + var handshakeSound; + var successfulHandshakeSound; -function debug() { - var stateString = "<" + STATE_STRINGS[state] + ">"; - var connecting = "[" + connectingId + "/" + connectingHand + "]"; - print.apply(null, [].concat.apply([label, stateString, JSON.stringify(waitingList), connecting], [].map.call(arguments, JSON.stringify))); -} + function debug() { + var stateString = "<" + STATE_STRINGS[state] + ">"; + var connecting = "[" + connectingId + "/" + connectingHand + "]"; + print.apply(null, [].concat.apply([LABEL, stateString, JSON.stringify(waitingList), connecting], + [].map.call(arguments, JSON.stringify))); + } -function cleanId(guidWithCurlyBraces) { - return guidWithCurlyBraces.slice(1, -1); -} -function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. - var httpRequest = new XMLHttpRequest(), key; - // QT bug: apparently doesn't handle onload. Workaround using readyState. - httpRequest.onreadystatechange = function () { - var READY_STATE_DONE = 4; - var HTTP_OK = 200; - if (httpRequest.readyState >= READY_STATE_DONE) { - var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, - response = !error && httpRequest.responseText, - contentType = !error && httpRequest.getResponseHeader('content-type'); - if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc. - try { - response = JSON.parse(response); - } catch (e) { - error = e; + function cleanId(guidWithCurlyBraces) { + return guidWithCurlyBraces.slice(1, -1); + } + function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. + var httpRequest = new XMLHttpRequest(), key; + // QT bug: apparently doesn't handle onload. Workaround using readyState. + httpRequest.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (httpRequest.readyState >= READY_STATE_DONE) { + var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, + response = !error && httpRequest.responseText, + contentType = !error && httpRequest.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc. + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + if (error) { + response = {statusCode: httpRequest.status}; + } + callback(error, response); + } + }; + if (typeof options === 'string') { + options = {uri: options}; + } + if (options.url) { + options.uri = options.url; + } + if (!options.method) { + options.method = 'GET'; + } + if (options.body && (options.method === 'GET')) { // add query parameters + var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&'; + for (key in options.body) { + if (options.body.hasOwnProperty(key)) { + params.push(key + '=' + options.body[key]); } } - if (error) { - response = {statusCode: httpRequest.status}; + options.uri += appender + params.join('&'); + delete options.body; + } + if (options.json) { + options.headers = options.headers || {}; + options.headers["Content-type"] = "application/json"; + options.body = JSON.stringify(options.body); + } + for (key in options.headers || {}) { + if (options.headers.hasOwnProperty(key)) { + httpRequest.setRequestHeader(key, options.headers[key]); } - callback(error, response); } - }; - if (typeof options === 'string') { - options = {uri: options}; + httpRequest.open(options.method, options.uri, true); + httpRequest.send(options.body); } - if (options.url) { - options.uri = options.url; - } - if (!options.method) { - options.method = 'GET'; - } - if (options.body && (options.method === 'GET')) { // add query parameters - var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&'; - for (key in options.body) { - params.push(key + '=' + options.body[key]); + + function handToString(hand) { + if (hand === Controller.Standard.RightHand) { + return "RightHand"; + } else if (hand === Controller.Standard.LeftHand) { + return "LeftHand"; } - options.uri += appender + params.join('&'); - delete options.body; + debug("handToString called without valid hand!"); + return ""; } - if (options.json) { - options.headers = options.headers || {}; - options.headers["Content-type"] = "application/json"; - options.body = JSON.stringify(options.body); - } - for (key in options.headers || {}) { - httpRequest.setRequestHeader(key, options.headers[key]); - } - httpRequest.open(options.method, options.uri, true); - httpRequest.send(options.body); -} -function handToString(hand) { - if (hand === Controller.Standard.RightHand) { - return "RightHand"; - } else if (hand === Controller.Standard.LeftHand) { - return "LeftHand"; - } - debug("handToString called without valid hand!"); - return ""; -} - -function stringToHand(hand) { - if (hand == "RightHand") { - return Controller.Standard.RightHand; - } else if (hand == "LeftHand") { - return Controller.Standard.LeftHand; - } - debug("stringToHand called with bad hand string:", hand); - return 0; -} - -function handToHaptic(hand) { - if (hand === Controller.Standard.RightHand) { - return 1; - } else if (hand === Controller.Standard.LeftHand) { + function stringToHand(hand) { + if (hand === "RightHand") { + return Controller.Standard.RightHand; + } else if (hand === "LeftHand") { + return Controller.Standard.LeftHand; + } + debug("stringToHand called with bad hand string:", hand); return 0; } - debug("handToHaptic called without a valid hand!"); - return -1; -} -function stopWaiting() { - if (waitingInterval) { - waitingInterval = Script.clearInterval(waitingInterval); + function handToHaptic(hand) { + if (hand === Controller.Standard.RightHand) { + return 1; + } else if (hand === Controller.Standard.LeftHand) { + return 0; + } + debug("handToHaptic called without a valid hand!"); + return -1; } -} -function stopConnecting() { - if (connectingInterval) { - connectingInterval = Script.clearInterval(connectingInterval); + function stopWaiting() { + if (waitingInterval) { + waitingInterval = Script.clearInterval(waitingInterval); + } } -} -function stopMakingConnection() { - if (makingConnectionTimeout) { - makingConnectionTimeout = Script.clearTimeout(makingConnectionTimeout); + function stopConnecting() { + if (connectingInterval) { + connectingInterval = Script.clearInterval(connectingInterval); + } + } + + function stopMakingConnection() { + if (makingConnectionTimeout) { + makingConnectionTimeout = Script.clearTimeout(makingConnectionTimeout); + } } -} // This returns the position of the palm, really. Which relies on the avatar // having the expected middle1 joint. TODO: fallback for when this isn't part // of the avatar? -function getHandPosition(avatar, hand) { - if (!hand) { - debug("calling getHandPosition with no hand! (returning avatar position but this is a BUG)"); - debug(new Error().stack); - return avatar.position; - } - var jointName = handToString(hand) + "Middle1"; - return avatar.getJointPosition(avatar.getJointIndex(jointName)); -} - -function shakeHandsAnimation(animationProperties) { - // all we are doing here is moving the right hand to a spot - // that is in front of and a bit above the hips. Basing how - // far in front as scaling with the avatar's height (say hips - // to head distance) - var headIndex = MyAvatar.getJointIndex("Head"); - var offset = 0.5; // default distance of hand in front of you - var result = {}; - if (headIndex) { - offset = 0.8 * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; - } - var handPos = Vec3.multiply(offset, {x: -0.25, y: 0.8, z: 1.3}); - result.rightHandPosition = handPos; - result.rightHandRotation = Quat.fromPitchYawRollDegrees(90, 0, 90); - return result; -} - -function positionFractionallyTowards(posA, posB, frac) { - return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA))); -} - -function deleteParticleEffect() { - if (particleEffect) { - particleEffect = Entities.deleteEntity(particleEffect); - } -} - -function deleteMakeConnectionParticleEffect() { - if (makingConnectionParticleEffect) { - makingConnectionParticleEffect = Entities.deleteEntity(makingConnectionParticleEffect); - } -} - -function stopHandshakeSound() { - if (handshakeInjector) { - handshakeInjector.stop(); - handshakeInjector = null; - } -} - -function calcParticlePos(myHand, otherHand, otherOrientation, reset) { - if (reset) { - particleRotationAngle = 0.0; - } - var position = positionFractionallyTowards(myHand, otherHand, 0.5); - particleRotationAngle += PARTICLE_ANGLE_INCREMENT; // about 0.5 hz - var radius = Math.min(PARTICLE_RADIUS, PARTICLE_RADIUS * particleRotationAngle / 360); - var axis = Vec3.mix(Quat.getFront(MyAvatar.orientation), Quat.inverse(Quat.getFront(otherOrientation)), 0.5); - return Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, axis), {x: 0, y: radius, z: 0})); -} - -// this is called frequently, but usually does nothing -function updateVisualization() { - if (state == STATES.inactive) { - deleteParticleEffect(); - deleteMakeConnectionParticleEffect(); - // this should always be true if inactive, but just in case: - currentHand = undefined; - return; + function getHandPosition(avatar, hand) { + if (!hand) { + debug("calling getHandPosition with no hand! (returning avatar position but this is a BUG)"); + debug(new Error().stack); + return avatar.position; + } + var jointName = handToString(hand) + "Middle1"; + return avatar.getJointPosition(avatar.getJointIndex(jointName)); } - var myHandPosition = getHandPosition(MyAvatar, currentHand); - var otherHand; - var otherOrientation; - if (connectingId) { - var other = AvatarList.getAvatar(connectingId); - if (other) { - otherOrientation = other.orientation; - otherHand = getHandPosition(other, stringToHand(connectingHand)); + function shakeHandsAnimation(animationProperties) { + // all we are doing here is moving the right hand to a spot + // that is in front of and a bit above the hips. Basing how + // far in front as scaling with the avatar's height (say hips + // to head distance) + var headIndex = MyAvatar.getJointIndex("Head"); + var offset = 0.5; // default distance of hand in front of you + var result = {}; + if (headIndex) { + offset = 0.8 * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; + } + result.rightHandPosition = Vec3.multiply(offset, {x: -0.25, y: 0.8, z: 1.3}); + result.rightHandRotation = Quat.fromPitchYawRollDegrees(90, 0, 90); + return result; + } + + function positionFractionallyTowards(posA, posB, frac) { + return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA))); + } + + function deleteParticleEffect() { + if (particleEffect) { + particleEffect = Entities.deleteEntity(particleEffect); } } - var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); - var d = Math.min(MAX_AVATAR_DISTANCE, Vec3.distance(wrist, myHandPosition)); - switch (state) { - case STATES.waiting: - // no visualization while waiting + function deleteMakeConnectionParticleEffect() { + if (makingConnectionParticleEffect) { + makingConnectionParticleEffect = Entities.deleteEntity(makingConnectionParticleEffect); + } + } + + function stopHandshakeSound() { + if (handshakeInjector) { + handshakeInjector.stop(); + handshakeInjector = null; + } + } + + function calcParticlePos(myHand, otherHand, otherOrientation, reset) { + if (reset) { + particleRotationAngle = 0.0; + } + var position = positionFractionallyTowards(myHand, otherHand, 0.5); + particleRotationAngle += PARTICLE_ANGLE_INCREMENT; // about 0.5 hz + var radius = Math.min(PARTICLE_RADIUS, PARTICLE_RADIUS * particleRotationAngle / 360); + var axis = Vec3.mix(Quat.getFront(MyAvatar.orientation), Quat.inverse(Quat.getFront(otherOrientation)), 0.5); + return Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, axis), {x: 0, y: radius, z: 0})); + } + + // this is called frequently, but usually does nothing + function updateVisualization() { + if (state === STATES.INACTIVE) { deleteParticleEffect(); deleteMakeConnectionParticleEffect(); - stopHandshakeSound(); - break; - case STATES.connecting: - var particleProps = {}; - // put the position between the 2 hands, if we have a connectingId. This - // helps define the plane in which the particles move. - positionFractionallyTowards(myHandPosition, otherHand, 0.5); - // now manage the rest of the entity - if (!particleEffect) { - particleRotationAngle = 0.0; - particleEmitRate = 500; - particleProps = PARTICLE_EFFECT_PROPS; - particleProps.isEmitting = 0; - particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); - particleProps.parentID = MyAvatar.sessionUUID; - particleEffect = Entities.addEntity(particleProps, true); - } else { - particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); - particleProps.isEmitting = 1; - Entities.editEntity(particleEffect, particleProps); - } - if (!makingConnectionParticleEffect) { - var props = MAKING_CONNECTION_PARTICLE_PROPS; - props.parentID = MyAvatar.sessionUUID; - makingConnectionEmitRate = 2000; - props.emitRate = makingConnectionEmitRate; - props.position = myHandPosition; - makingConnectionParticleEffect = Entities.addEntity(props, true); - } else { - makingConnectionEmitRate *= 0.5; - Entities.editEntity(makingConnectionParticleEffect, {emitRate: makingConnectionEmitRate, position: myHandPosition, isEmitting: 1}); - } - break; - case STATES.makingConnection: - particleEmitRate = Math.max(50, particleEmitRate * 0.5); - Entities.editEntity(makingConnectionParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition}); - Entities.editEntity(particleEffect, {position: calcParticlePos(myHandPosition, otherHand, otherOrientation), emitRate: particleEmitRate}); - break; - default: - debug("unexpected state", state); - break; - } -} - -function isNearby(id, hand) { - if (currentHand) { - var handPos = getHandPosition(MyAvatar, currentHand); - var avatar = AvatarList.getAvatar(id); - if (avatar) { - var otherHand = stringToHand(hand); - var distance = Vec3.distance(getHandPosition(avatar, otherHand), handPos); - return (distance < MAX_AVATAR_DISTANCE); - } - } - return false; -} - -function findNearestWaitingAvatar() { - var handPos = getHandPosition(MyAvatar, currentHand); - var minDistance = MAX_AVATAR_DISTANCE; - var nearestAvatar = {}; - Object.keys(waitingList).forEach(function (identifier) { - var avatar = AvatarList.getAvatar(identifier); - if (avatar) { - var hand = stringToHand(waitingList[identifier]); - var distance = Vec3.distance(getHandPosition(avatar, hand), handPos); - if (distance < minDistance) { - minDistance = distance; - nearestAvatar = {avatar: identifier, hand: hand}; - } - } - }); - return nearestAvatar; -} - - -// As currently implemented, we select the closest waiting avatar (if close enough) and send -// them a connectionRequest. If nobody is close enough we send a waiting message, and wait for a -// connectionRequest. If the 2 people who want to connect are both somewhat out of range when they -// initiate the shake, they will race to see who sends the connectionRequest after noticing the -// waiting message. Either way, they will start connecting eachother at that point. -function startHandshake(fromKeyboard) { - if (fromKeyboard) { - debug("adding animation"); - // just in case order of press/unpress is broken - if (animHandlerId) { - animHandlerId = MyAvatar.removeAnimationStateHandler(animHandlerId); - } - animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []); - } - debug("starting handshake for", currentHand); - pollCount = 0; - state = STATES.waiting; - connectingId = undefined; - connectingHand = undefined; - // just in case - stopWaiting(); - stopConnecting(); - stopMakingConnection(); - - var nearestAvatar = findNearestWaitingAvatar(); - if (nearestAvatar.avatar) { - connectingId = nearestAvatar.avatar; - connectingHand = handToString(nearestAvatar.hand); - debug("sending connectionRequest to", connectingId); - messageSend({ - key: "connectionRequest", - id: connectingId, - hand: handToString(currentHand) - }); - } else { - // send waiting message - debug("sending waiting message"); - messageSend({ - key: "waiting", - hand: handToString(currentHand) - }); - lookForWaitingAvatar(); - } -} - -function endHandshake() { - debug("ending handshake for", currentHand); - - deleteParticleEffect(); - deleteMakeConnectionParticleEffect(); - currentHand = undefined; - // note that setting the state to inactive should really - // only be done here, unless we change how the triggering works, - // as we ignore the key release event when inactive. See updateTriggers - // below. - state = STATES.inactive; - connectingId = undefined; - connectingHand = undefined; - stopWaiting(); - stopConnecting(); - stopMakingConnection(); - stopHandshakeSound(); - // send done to let connection know you are not making connections now - messageSend({ - key: "done" - }); - - if (animHandlerId) { - debug("removing animation"); - MyAvatar.removeAnimationStateHandler(animHandlerId); - } - // No-op if we were successful, but this way we ensure that failures and abandoned handshakes don't leave us in a weird state. - request({uri: requestUrl, method: 'DELETE'}, debug); -} - -function updateTriggers(value, fromKeyboard, hand) { - if (currentHand && hand !== currentHand) { - debug("currentHand", currentHand, "ignoring messages from", hand); - return; - } - if (!currentHand) { - currentHand = hand; - } - // ok now, we are either initiating or quitting... - var isGripping = value > GRIP_MIN; - if (isGripping) { - debug("updateTriggers called - gripping", handToString(hand)); - if (state != STATES.inactive) { - return; - } else { - startHandshake(fromKeyboard); - } - } else { - // TODO: should we end handshake even when inactive? Ponder - debug("updateTriggers called -- no longer gripping", handToString(hand)); - if (state != STATES.inactive) { - endHandshake(); - } else { + // this should always be true if inactive, but just in case: + currentHand = undefined; return; } - } -} -function messageSend(message) { - Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); -} + var myHandPosition = getHandPosition(MyAvatar, currentHand); + var otherHand; + var otherOrientation; + if (connectingId) { + var other = AvatarList.getAvatar(connectingId); + if (other) { + otherOrientation = other.orientation; + otherHand = getHandPosition(other, stringToHand(connectingHand)); + } + } -function lookForWaitingAvatar() { - // we started with nobody close enough, but maybe I've moved - // or they did. Note that 2 people doing this race, so stop - // as soon as you have a connectingId (which means you got their - // message before noticing they were in range in this loop) - - // just in case we reenter before stopping - stopWaiting(); - debug("started looking for waiting avatars"); - waitingInterval = Script.setInterval(function () { - if (state == STATES.waiting && !connectingId) { - // find the closest in-range avatar, and send connection request - var nearestAvatar = findNearestWaitingAvatar(); - if (nearestAvatar.avatar) { - connectingId = nearestAvatar.avatar; - connectingHand = handToString(nearestAvatar.hand); - debug("sending connectionRequest to", connectingId); - messageSend({ - key: "connectionRequest", - id: connectingId, - hand: handToString(currentHand) + switch (state) { + case STATES.WAITING: + // no visualization while waiting + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); + stopHandshakeSound(); + break; + case STATES.CONNECTING: + var particleProps = {}; + // put the position between the 2 hands, if we have a connectingId. This + // helps define the plane in which the particles move. + positionFractionallyTowards(myHandPosition, otherHand, 0.5); + // now manage the rest of the entity + if (!particleEffect) { + particleRotationAngle = 0.0; + particleEmitRate = 500; + particleProps = PARTICLE_EFFECT_PROPS; + particleProps.isEmitting = 0; + particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); + particleProps.parentID = MyAvatar.sessionUUID; + particleEffect = Entities.addEntity(particleProps, true); + } else { + particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); + particleProps.isEmitting = 1; + Entities.editEntity(particleEffect, particleProps); + } + if (!makingConnectionParticleEffect) { + var props = MAKING_CONNECTION_PARTICLE_PROPS; + props.parentID = MyAvatar.sessionUUID; + makingConnectionEmitRate = 2000; + props.emitRate = makingConnectionEmitRate; + props.position = myHandPosition; + makingConnectionParticleEffect = Entities.addEntity(props, true); + } else { + makingConnectionEmitRate *= 0.5; + Entities.editEntity(makingConnectionParticleEffect, { + emitRate: makingConnectionEmitRate, + position: myHandPosition, + isEmitting: true + }); + } + break; + case STATES.MAKING_CONNECTION: + particleEmitRate = Math.max(50, particleEmitRate * 0.5); + Entities.editEntity(makingConnectionParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition}); + Entities.editEntity(particleEffect, { + position: calcParticlePos(myHandPosition, otherHand, otherOrientation), + emitRate: particleEmitRate }); + break; + default: + debug("unexpected state", state); + break; + } + } + + function isNearby(id, hand) { + if (currentHand) { + var handPos = getHandPosition(MyAvatar, currentHand); + var avatar = AvatarList.getAvatar(id); + if (avatar) { + var otherHand = stringToHand(hand); + var distance = Vec3.distance(getHandPosition(avatar, otherHand), handPos); + return (distance < MAX_AVATAR_DISTANCE); } - } else { - // something happened, stop looking for avatars to connect - stopWaiting(); - debug("stopped looking for waiting avatars"); } - }, WAITING_INTERVAL); -} - -/* There is a mini-state machine after entering STATES.makingConnection. - We make a request (which might immediately succeed, fail, or neither. - If we immediately fail, we tell the user. - Otherwise, we wait MAKING_CONNECTION_TIMEOUT. At that time, we poll until success or fail. - */ -var result, requestBody, pollCount = 0, requestUrl = location.metaverseServerUrl + '/api/v1/user/connection_request'; -function connectionRequestCompleted() { // Final result is in. Do effects. - if (result.status === 'success') { // set earlier - if (!successfulHandshakeInjector) { - successfulHandshakeInjector = Audio.playSound(successfulHandshakeSound, {position: getHandPosition(MyAvatar, currentHand), volume: 0.5, localOnly: true}); - } else { - successfulHandshakeInjector.restart(); - } - Controller.triggerHapticPulse(HAPTIC_DATA.success.strength, HAPTIC_DATA.success.duration, handToHaptic(currentHand)); - // don't change state (so animation continues while gripped) - // but do send a notification, by calling the slot that emits the signal for it - Window.makeConnection(true, result.connection.new_connection ? "You and " + result.connection.username + " are now connected!" : result.connection.username); - UserActivityLogger.makeUserConnection(connectingId, true, result.connection.new_connection ? "new connection" : "already connected"); - return; - } // failed - endHandshake(); - debug("failing with result data", result); - // IWBNI we also did some fail sound/visual effect. - Window.makeConnection(false, result.connection); - UserActivityLogger.makeUserConnection(connectingId, false, result.connection); -} -var POLL_INTERVAL_MS = 200, POLL_LIMIT = 5; -function handleConnectionResponseAndMaybeRepeat(error, response) { - // If response is 'pending', set a short timeout to try again. - // If we fail other than pending, set result and immediately call connectionRequestCompleted. - // If we succceed, set result and call connectionRequestCompleted immediately (if we've been polling), and otherwise on a timeout. - if (response && (response.connection === 'pending')) { - debug(response, 'pollCount', pollCount); - if (pollCount++ >= POLL_LIMIT) { // server will expire, but let's not wait that long. - debug('POLL LIMIT REACHED; TIMEOUT: expired message generated by CLIENT'); - result = {status: 'error', connection: 'expired'}; - connectionRequestCompleted(); - } else { // poll - Script.setTimeout(function () { - request({ - uri: requestUrl, - // N.B.: server gives bad request if we specify json content type, so don't do that. - body: requestBody - }, handleConnectionResponseAndMaybeRepeat); - }, POLL_INTERVAL_MS); - } - } else if (error || (response.status !== 'success')) { - debug('server fail', error, response.status); - if (response && (response.statusCode === 401)) { - error = "All participants must be logged in to connect."; - } - result = error ? {status: 'error', connection: error} : response; - UserActivityLogger.makeUserConnection(connectingId, false, error || response); - connectionRequestCompleted(); - } else { - debug('server success', result); - result = response; - if (pollCount++) { - connectionRequestCompleted(); - } else { // Wait for other guy, so that final succcess is at roughly the same time. - Script.setTimeout(connectionRequestCompleted, MAKING_CONNECTION_TIMEOUT); - } - } -} - -// this should be where we make the appropriate connection call. For now just make the -// visualization change. -function makeConnection(id) { - // send done to let the connection know you have made connection. - messageSend({ - key: "done", - connectionId: id - }); - - state = STATES.makingConnection; - - // continue the haptic background until the timeout fires. When we make calls, we will have an interval - // probably, in which we do this. - Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, MAKING_CONNECTION_TIMEOUT, handToHaptic(currentHand)); - requestBody = {node_id: cleanId(MyAvatar.sessionUUID), proposed_node_id: cleanId(id)}; // for use when repeating - - // It would be "simpler" to skip this and just look at the response, but: - // 1. We don't want to bother the metaverse with request that we know will fail. - // 2. We don't want our code here to be dependent on precisely how the metaverse responds (400, 401, etc.) - if (!Account.isLoggedIn()) { - handleConnectionResponseAndMaybeRepeat("401:Unauthorized", {statusCode: 401}); - return; + return false; } - // This will immediately set response if successfull (e.g., the other guy got his request in first), or immediate failure, - // and will otherwise poll (using the requestBody we just set). - request({ // - uri: requestUrl, - method: 'POST', - json: true, - body: {user_connection_request: requestBody} - }, handleConnectionResponseAndMaybeRepeat); -} - -// we change states, start the connectionInterval where we check -// to be sure the hand is still close enough. If not, we terminate -// the interval, go back to the waiting state. If we make it -// the entire CONNECTING_TIME, we make the connection. -function startConnecting(id, hand) { - var count = 0; - debug("connecting", id, "hand", hand); - // do we need to do this? - connectingId = id; - connectingHand = hand; - state = STATES.connecting; - - // play sound - if (!handshakeInjector) { - handshakeInjector = Audio.playSound(handshakeSound, {position: getHandPosition(MyAvatar, currentHand), volume: 0.5, localOnly: true}); - } else { - handshakeInjector.restart(); + function findNearestWaitingAvatar() { + var handPos = getHandPosition(MyAvatar, currentHand); + var minDistance = MAX_AVATAR_DISTANCE; + var nearestAvatar = {}; + Object.keys(waitingList).forEach(function (identifier) { + var avatar = AvatarList.getAvatar(identifier); + if (avatar) { + var hand = stringToHand(waitingList[identifier]); + var distance = Vec3.distance(getHandPosition(avatar, hand), handPos); + if (distance < minDistance) { + minDistance = distance; + nearestAvatar = {avatar: identifier, hand: hand}; + } + } + }); + return nearestAvatar; } - // send message that we are connecting with them - messageSend({ - key: "connecting", - id: id, - hand: handToString(currentHand) - }); - Controller.triggerHapticPulse(HAPTIC_DATA.initial.strength, HAPTIC_DATA.initial.duration, handToHaptic(currentHand)); - connectingInterval = Script.setInterval(function () { - count += 1; - Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, HAPTIC_DATA.background.duration, handToHaptic(currentHand)); - if (state != STATES.connecting) { - debug("stopping connecting interval, state changed"); - stopConnecting(); - } else if (!isNearby(id, hand)) { - // gotta go back to waiting - debug(id, "moved, back to waiting"); - stopConnecting(); + // As currently implemented, we select the closest waiting avatar (if close enough) and send + // them a connectionRequest. If nobody is close enough we send a waiting message, and wait for a + // connectionRequest. If the 2 people who want to connect are both somewhat out of range when they + // initiate the shake, they will race to see who sends the connectionRequest after noticing the + // waiting message. Either way, they will start connecting each other at that point. + function startHandshake(fromKeyboard) { + if (fromKeyboard) { + debug("adding animation"); + // just in case order of press/release is broken + if (animHandlerId) { + animHandlerId = MyAvatar.removeAnimationStateHandler(animHandlerId); + } + animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []); + } + debug("starting handshake for", currentHand); + pollCount = 0; + state = STATES.WAITING; + connectingId = undefined; + connectingHand = undefined; + // just in case + stopWaiting(); + stopConnecting(); + stopMakingConnection(); + + var nearestAvatar = findNearestWaitingAvatar(); + if (nearestAvatar.avatar) { + connectingId = nearestAvatar.avatar; + connectingHand = handToString(nearestAvatar.hand); + debug("sending connectionRequest to", connectingId); messageSend({ - key: "done" - }); - startHandshake(); - } else if (count > CONNECTING_TIME/CONNECTING_INTERVAL) { - debug("made connection with " + id); - makeConnection(id); - stopConnecting(); - } - }, CONNECTING_INTERVAL); -} -/* -A simple sequence diagram: NOTE that the ConnectionAck is somewhat -vestigial, and probably should be removed shortly. - - Avatar A Avatar B - | | - | <-----(waiting) ----- startHandshake -startHandshake - (connectionRequest) -> | - | | - | <----(connectionAck) -------- | - | <-----(connecting) -- startConnecting - startConnecting ---(connecting) ----> | - | | - | connected - connected | - | <--------- (done) ---------- | - | ---------- (done) ---------> | -*/ -function messageHandler(channel, messageString, senderID) { - if (channel !== MESSAGE_CHANNEL) { - return; - } - if (MyAvatar.sessionUUID === senderID) { // ignore my own - return; - } - var message = {}; - try { - message = JSON.parse(messageString); - } catch (e) { - debug(e); - } - switch (message.key) { - case "waiting": - // add this guy to waiting object. Any other message from this person will - // remove it from the list - waitingList[senderID] = message.hand; - break; - case "connectionRequest": - delete waitingList[senderID]; - if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!connectingId || connectingId == senderID)) { - // you were waiting for a connection request, so send the ack. Or, you and the other - // guy raced and both send connectionRequests. Handle that too - connectingId = senderID; - connectingHand = message.hand; - messageSend({ - key: "connectionAck", - id: senderID, + key: "connectionRequest", + id: connectingId, hand: handToString(currentHand) }); } else { - if (state == STATES.waiting && connectingId == senderID) { - // the person you are trying to connect sent a request to someone else. See the - // if statement above. So, don't cry, just start the handshake over again - startHandshake(); - } + // send waiting message + debug("sending waiting message"); + messageSend({ + key: "waiting", + hand: handToString(currentHand) + }); + lookForWaitingAvatar(); } - break; - case "connectionAck": - delete waitingList[senderID]; - if (state == STATES.waiting && (!connectingId || connectingId == senderID)) { - if (message.id == MyAvatar.sessionUUID) { - // start connecting... - connectingId = senderID; - connectingHand = message.hand; - stopWaiting(); - startConnecting(senderID, message.hand); + } + + function endHandshake() { + debug("ending handshake for", currentHand); + + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); + currentHand = undefined; + // note that setting the state to inactive should really + // only be done here, unless we change how the triggering works, + // as we ignore the key release event when inactive. See updateTriggers + // below. + state = STATES.INACTIVE; + connectingId = undefined; + connectingHand = undefined; + stopWaiting(); + stopConnecting(); + stopMakingConnection(); + stopHandshakeSound(); + // send done to let connection know you are not making connections now + messageSend({ + key: "done" + }); + + if (animHandlerId) { + debug("removing animation"); + MyAvatar.removeAnimationStateHandler(animHandlerId); + } + // No-op if we were successful, but this way we ensure that failures and abandoned handshakes don't leave us + // in a weird state. + request({uri: requestUrl, method: 'DELETE'}, debug); + } + + function updateTriggers(value, fromKeyboard, hand) { + if (currentHand && hand !== currentHand) { + debug("currentHand", currentHand, "ignoring messages from", hand); + return; + } + if (!currentHand) { + currentHand = hand; + } + // ok now, we are either initiating or quitting... + var isGripping = value > GRIP_MIN; + if (isGripping) { + debug("updateTriggers called - gripping", handToString(hand)); + if (state !== STATES.INACTIVE) { + return; } else { - if (connectingId) { - // this is for someone else (we lost race in connectionRequest), - // so lets start over - startHandshake(); - } - } - } - // TODO: check to see if we are waiting for this but the person we are connecting sent it to - // someone else, and try again - break; - case "connecting": - delete waitingList[senderID]; - if (state == STATES.waiting && senderID == connectingId) { - // temporary logging - if (connectingHand != message.hand) { - debug("connecting hand", connectingHand, "not same as connecting hand in message", message.hand); - } - connectingHand = message.hand; - if (message.id != MyAvatar.sessionUUID) { - // the person we were trying to connect is connecting to someone else - // so try again - startHandshake(); - break; - } - startConnecting(senderID, message.hand); - } - break; - case "done": - delete waitingList[senderID]; - if (state == STATES.connecting && connectingId == senderID) { - // if they are done, and didn't connect us, terminate our - // connecting - if (message.connectionId !== MyAvatar.sessionUUID) { - stopConnecting(); - // now just call startHandshake. Should be ok to do so without a - // value for isKeyboard, as we should not change the animation - // state anyways (if any) - startHandshake(); + startHandshake(fromKeyboard); } } else { - // if waiting or inactive, lets clear the connecting id. If in makingConnection, - // do nothing - if (state != STATES.makingConnection && connectingId == senderID) { - connectingId = undefined; - connectingHand = undefined; - if (state != STATES.inactive) { - startHandshake(); - } + // TODO: should we end handshake even when inactive? Ponder + debug("updateTriggers called -- no longer gripping", handToString(hand)); + if (state !== STATES.INACTIVE) { + endHandshake(); + } else { + return; } } - break; - default: - debug("unknown message", message); - break; } -} -Messages.subscribe(MESSAGE_CHANNEL); -Messages.messageReceived.connect(messageHandler); - - -function makeGripHandler(hand, animate) { - // determine if we are gripping or un-gripping - if (animate) { - return function(value) { - updateTriggers(value, true, hand); - }; - - } else { - return function (value) { - updateTriggers(value, false, hand); - }; + function messageSend(message) { + Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); } -} -function keyPressEvent(event) { - if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { - updateTriggers(1.0, true, Controller.Standard.RightHand); + function lookForWaitingAvatar() { + // we started with nobody close enough, but maybe I've moved + // or they did. Note that 2 people doing this race, so stop + // as soon as you have a connectingId (which means you got their + // message before noticing they were in range in this loop) + + // just in case we re-enter before stopping + stopWaiting(); + debug("started looking for waiting avatars"); + waitingInterval = Script.setInterval(function () { + if (state === STATES.WAITING && !connectingId) { + // find the closest in-range avatar, and send connection request + var nearestAvatar = findNearestWaitingAvatar(); + if (nearestAvatar.avatar) { + connectingId = nearestAvatar.avatar; + connectingHand = handToString(nearestAvatar.hand); + debug("sending connectionRequest to", connectingId); + messageSend({ + key: "connectionRequest", + id: connectingId, + hand: handToString(currentHand) + }); + } + } else { + // something happened, stop looking for avatars to connect + stopWaiting(); + debug("stopped looking for waiting avatars"); + } + }, WAITING_INTERVAL); } -} -function keyReleaseEvent(event) { - if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { - updateTriggers(0.0, true, Controller.Standard.RightHand); + + /* There is a mini-state machine after entering STATES.makingConnection. + We make a request (which might immediately succeed, fail, or neither. + If we immediately fail, we tell the user. + Otherwise, we wait MAKING_CONNECTION_TIMEOUT. At that time, we poll until success or fail. + */ + var result, requestBody, pollCount = 0, requestUrl = location.metaverseServerUrl + '/api/v1/user/connection_request'; + function connectionRequestCompleted() { // Final result is in. Do effects. + if (result.status === 'success') { // set earlier + if (!successfulHandshakeInjector) { + successfulHandshakeInjector = Audio.playSound(successfulHandshakeSound, { + position: getHandPosition(MyAvatar, currentHand), + volume: 0.5, + localOnly: true + }); + } else { + successfulHandshakeInjector.restart(); + } + Controller.triggerHapticPulse(HAPTIC_DATA.success.strength, HAPTIC_DATA.success.duration, + handToHaptic(currentHand)); + // don't change state (so animation continues while gripped) + // but do send a notification, by calling the slot that emits the signal for it + Window.makeConnection(true, result.connection.new_connection ? + "You and " + result.connection.username + " are now connected!" : result.connection.username); + UserActivityLogger.makeUserConnection(connectingId, true, result.connection.new_connection ? + "new connection" : "already connected"); + return; + } // failed + endHandshake(); + debug("failing with result data", result); + // IWBNI we also did some fail sound/visual effect. + Window.makeConnection(false, result.connection); + UserActivityLogger.makeUserConnection(connectingId, false, result.connection); + } + var POLL_INTERVAL_MS = 200, POLL_LIMIT = 5; + function handleConnectionResponseAndMaybeRepeat(error, response) { + // If response is 'pending', set a short timeout to try again. + // If we fail other than pending, set result and immediately call connectionRequestCompleted. + // If we succeed, set result and call connectionRequestCompleted immediately (if we've been polling), + // and otherwise on a timeout. + if (response && (response.connection === 'pending')) { + debug(response, 'pollCount', pollCount); + if (pollCount++ >= POLL_LIMIT) { // server will expire, but let's not wait that long. + debug('POLL LIMIT REACHED; TIMEOUT: expired message generated by CLIENT'); + result = {status: 'error', connection: 'expired'}; + connectionRequestCompleted(); + } else { // poll + Script.setTimeout(function () { + request({ + uri: requestUrl, + // N.B.: server gives bad request if we specify json content type, so don't do that. + body: requestBody + }, handleConnectionResponseAndMaybeRepeat); + }, POLL_INTERVAL_MS); + } + } else if (error || (response.status !== 'success')) { + debug('server fail', error, response.status); + if (response && (response.statusCode === 401)) { + error = "All participants must be logged in to connect."; + } + result = error ? {status: 'error', connection: error} : response; + UserActivityLogger.makeUserConnection(connectingId, false, error || response); + connectionRequestCompleted(); + } else { + debug('server success', result); + result = response; + if (pollCount++) { + connectionRequestCompleted(); + } else { // Wait for other guy, so that final success is at roughly the same time. + Script.setTimeout(connectionRequestCompleted, MAKING_CONNECTION_TIMEOUT); + } + } } -} -// map controller actions -var connectionMapping = Controller.newMapping(Script.resolvePath('') + '-grip'); -connectionMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripHandler(Controller.Standard.LeftHand)); -connectionMapping.from(Controller.Standard.RightGrip).peek().to(makeGripHandler(Controller.Standard.RightHand)); -// setup keyboard initiation -Controller.keyPressEvent.connect(keyPressEvent); -Controller.keyReleaseEvent.connect(keyReleaseEvent); + // this should be where we make the appropriate connection call. For now just make the + // visualization change. + function makeConnection(id) { + // send done to let the connection know you have made connection. + messageSend({ + key: "done", + connectionId: id + }); -// xbox controller cuz that's important -connectionMapping.from(Controller.Standard.RB).peek().to(makeGripHandler(Controller.Standard.RightHand, true)); + state = STATES.MAKING_CONNECTION; -// it is easy to forget this and waste a lot of time for nothing -connectionMapping.enable(); + // continue the haptic background until the timeout fires. When we make calls, we will have an interval + // probably, in which we do this. + Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, MAKING_CONNECTION_TIMEOUT, handToHaptic(currentHand)); + requestBody = {'node_id': cleanId(MyAvatar.sessionUUID), 'proposed_node_id': cleanId(id)}; // for use when repeating -// connect updateVisualization to update frequently -Script.update.connect(updateVisualization); + // It would be "simpler" to skip this and just look at the response, but: + // 1. We don't want to bother the metaverse with request that we know will fail. + // 2. We don't want our code here to be dependent on precisely how the metaverse responds (400, 401, etc.) + if (!Account.isLoggedIn()) { + handleConnectionResponseAndMaybeRepeat("401:Unauthorized", {statusCode: 401}); + return; + } -// load the sounds when the script loads -handshakeSound = SoundCache.getSound(HANDSHAKE_SOUND_URL); -successfulHandshakeSound = SoundCache.getSound(SUCCESSFUL_HANDSHAKE_SOUND_URL); + // This will immediately set response if successful (e.g., the other guy got his request in first), + // or immediate failure, and will otherwise poll (using the requestBody we just set). + request({ // + uri: requestUrl, + method: 'POST', + json: true, + body: {'user_connection_request': requestBody} + }, handleConnectionResponseAndMaybeRepeat); + } -Script.scriptEnding.connect(function () { - debug("removing controller mappings"); - connectionMapping.disable(); - debug("removing key mappings"); - Controller.keyPressEvent.disconnect(keyPressEvent); - Controller.keyReleaseEvent.disconnect(keyReleaseEvent); - debug("disconnecting updateVisualization"); - Script.update.disconnect(updateVisualization); - deleteParticleEffect(); - deleteMakeConnectionParticleEffect(); -}); + // we change states, start the connectionInterval where we check + // to be sure the hand is still close enough. If not, we terminate + // the interval, go back to the waiting state. If we make it + // the entire CONNECTING_TIME, we make the connection. + function startConnecting(id, hand) { + var count = 0; + debug("connecting", id, "hand", hand); + // do we need to do this? + connectingId = id; + connectingHand = hand; + state = STATES.CONNECTING; + + // play sound + if (!handshakeInjector) { + handshakeInjector = Audio.playSound(handshakeSound, { + position: getHandPosition(MyAvatar, currentHand), + volume: 0.5, + localOnly: true + }); + } else { + handshakeInjector.restart(); + } + + // send message that we are connecting with them + messageSend({ + key: "connecting", + id: id, + hand: handToString(currentHand) + }); + Controller.triggerHapticPulse(HAPTIC_DATA.initial.strength, HAPTIC_DATA.initial.duration, handToHaptic(currentHand)); + + connectingInterval = Script.setInterval(function () { + count += 1; + Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, HAPTIC_DATA.background.duration, + handToHaptic(currentHand)); + if (state !== STATES.CONNECTING) { + debug("stopping connecting interval, state changed"); + stopConnecting(); + } else if (!isNearby(id, hand)) { + // gotta go back to waiting + debug(id, "moved, back to waiting"); + stopConnecting(); + messageSend({ + key: "done" + }); + startHandshake(); + } else if (count > CONNECTING_TIME/CONNECTING_INTERVAL) { + debug("made connection with " + id); + makeConnection(id); + stopConnecting(); + } + }, CONNECTING_INTERVAL); + } + /* + A simple sequence diagram: NOTE that the ConnectionAck is somewhat + vestigial, and probably should be removed shortly. + + Avatar A Avatar B + | | + | <-----(waiting) ----- startHandshake + startHandshake - (connectionRequest) -> | + | | + | <----(connectionAck) -------- | + | <-----(connecting) -- startConnecting + startConnecting ---(connecting) ----> | + | | + | connected + connected | + | <--------- (done) ---------- | + | ---------- (done) ---------> | + */ + function messageHandler(channel, messageString, senderID) { + if (channel !== MESSAGE_CHANNEL) { + return; + } + if (MyAvatar.sessionUUID === senderID) { // ignore my own + return; + } + var message = {}; + try { + message = JSON.parse(messageString); + } catch (e) { + debug(e); + } + switch (message.key) { + case "waiting": + // add this guy to waiting object. Any other message from this person will + // remove it from the list + waitingList[senderID] = message.hand; + break; + case "connectionRequest": + delete waitingList[senderID]; + if (state === STATES.WAITING && message.id === MyAvatar.sessionUUID && + (!connectingId || connectingId === senderID)) { + // you were waiting for a connection request, so send the ack. Or, you and the other + // guy raced and both send connectionRequests. Handle that too + connectingId = senderID; + connectingHand = message.hand; + messageSend({ + key: "connectionAck", + id: senderID, + hand: handToString(currentHand) + }); + } else if (state === STATES.WAITING && connectingId === senderID) { + // the person you are trying to connect sent a request to someone else. See the + // if statement above. So, don't cry, just start the handshake over again + startHandshake(); + } + break; + case "connectionAck": + delete waitingList[senderID]; + if (state === STATES.WAITING && (!connectingId || connectingId === senderID)) { + if (message.id === MyAvatar.sessionUUID) { + // start connecting... + connectingId = senderID; + connectingHand = message.hand; + stopWaiting(); + startConnecting(senderID, message.hand); + } else if (connectingId) { + // this is for someone else (we lost race in connectionRequest), + // so lets start over + startHandshake(); + } + } + // TODO: check to see if we are waiting for this but the person we are connecting sent it to + // someone else, and try again + break; + case "connecting": + delete waitingList[senderID]; + if (state === STATES.WAITING && senderID === connectingId) { + // temporary logging + if (connectingHand !== message.hand) { + debug("connecting hand", connectingHand, "not same as connecting hand in message", message.hand); + } + connectingHand = message.hand; + if (message.id !== MyAvatar.sessionUUID) { + // the person we were trying to connect is connecting to someone else + // so try again + startHandshake(); + break; + } + startConnecting(senderID, message.hand); + } + break; + case "done": + delete waitingList[senderID]; + if (state === STATES.CONNECTING && connectingId === senderID) { + // if they are done, and didn't connect us, terminate our + // connecting + if (message.connectionId !== MyAvatar.sessionUUID) { + stopConnecting(); + // now just call startHandshake. Should be ok to do so without a + // value for isKeyboard, as we should not change the animation + // state anyways (if any) + startHandshake(); + } + } else { + // if waiting or inactive, lets clear the connecting id. If in makingConnection, + // do nothing + if (state !== STATES.MAKING_CONNECTION && connectingId === senderID) { + connectingId = undefined; + connectingHand = undefined; + if (state !== STATES.INACTIVE) { + startHandshake(); + } + } + } + break; + default: + debug("unknown message", message); + break; + } + } + + Messages.subscribe(MESSAGE_CHANNEL); + Messages.messageReceived.connect(messageHandler); + + + function makeGripHandler(hand, animate) { + // determine if we are gripping or un-gripping + if (animate) { + return function(value) { + updateTriggers(value, true, hand); + }; + + } else { + return function (value) { + updateTriggers(value, false, hand); + }; + } + } + + function keyPressEvent(event) { + if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && + !event.isAlt) { + updateTriggers(1.0, true, Controller.Standard.RightHand); + } + } + function keyReleaseEvent(event) { + if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && + !event.isAlt) { + updateTriggers(0.0, true, Controller.Standard.RightHand); + } + } + // map controller actions + var connectionMapping = Controller.newMapping(Script.resolvePath('') + '-grip'); + connectionMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripHandler(Controller.Standard.LeftHand)); + connectionMapping.from(Controller.Standard.RightGrip).peek().to(makeGripHandler(Controller.Standard.RightHand)); + + // setup keyboard initiation + Controller.keyPressEvent.connect(keyPressEvent); + Controller.keyReleaseEvent.connect(keyReleaseEvent); + + // Xbox controller because that is important + connectionMapping.from(Controller.Standard.RB).peek().to(makeGripHandler(Controller.Standard.RightHand, true)); + + // it is easy to forget this and waste a lot of time for nothing + connectionMapping.enable(); + + // connect updateVisualization to update frequently + Script.update.connect(updateVisualization); + + // load the sounds when the script loads + handshakeSound = SoundCache.getSound(HANDSHAKE_SOUND_URL); + successfulHandshakeSound = SoundCache.getSound(SUCCESSFUL_HANDSHAKE_SOUND_URL); + + Script.scriptEnding.connect(function () { + debug("removing controller mappings"); + connectionMapping.disable(); + debug("removing key mappings"); + Controller.keyPressEvent.disconnect(keyPressEvent); + Controller.keyReleaseEvent.disconnect(keyReleaseEvent); + debug("disconnecting updateVisualization"); + Script.update.disconnect(updateVisualization); + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); + }); }()); // END LOCAL_SCOPE -