From 7d956198cda66c95dded909d51438b0f3a180fca Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 24 Sep 2019 16:00:09 -0700 Subject: [PATCH 01/73] re-enable prefer finger over stylus --- interface/src/Application.h | 4 +--- interface/src/ui/PreferencesDialog.cpp | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/interface/src/Application.h b/interface/src/Application.h index 198f5ef7cf..684ff6bdaa 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -223,9 +223,7 @@ public: bool getPreferStylusOverLaser() { return _preferStylusOverLaserSetting.get(); } void setPreferStylusOverLaser(bool value); - // FIXME: Remove setting completely or make available through JavaScript API? - //bool getPreferAvatarFingerOverStylus() { return _preferAvatarFingerOverStylusSetting.get(); } - bool getPreferAvatarFingerOverStylus() { return false; } + bool getPreferAvatarFingerOverStylus() { return _preferAvatarFingerOverStylusSetting.get(); } void setPreferAvatarFingerOverStylus(bool value); bool getMiniTabletEnabled() { return _miniTabletEnabledSetting.get(); } diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 7e11406808..34cacb733e 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -223,13 +223,11 @@ void setupPreferences() { preferences->addPreference(preference); } - /* - // FIXME: Remove setting completely or make available through JavaScript API? { auto getter = []()->bool { return qApp->getPreferAvatarFingerOverStylus(); }; auto setter = [](bool value) { qApp->setPreferAvatarFingerOverStylus(value); }; preferences->addPreference(new CheckPreference(UI_CATEGORY, "Prefer Avatar Finger Over Stylus", getter, setter)); - }*/ + } // Snapshots static const QString SNAPSHOTS { "Snapshots" }; From 3d4de67c49d65144559630cbb61c76e5c816d903 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 26 Sep 2019 12:40:03 -0700 Subject: [PATCH 02/73] allow thumb+index finger pinch to mean same as trigger click --- .../controllers/controllerDispatcher.js | 38 ++++++++++++++++++- .../controllerModules/nearGrabEntity.js | 2 +- .../controllerModules/stylusInput.js | 2 +- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index f0d3ec0c03..d73e5de7e7 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -35,7 +35,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); var DEBUG = false; var SHOW_GRAB_SPHERE = false; - if (typeof Test !== "undefined") { PROFILE = true; } @@ -97,11 +96,15 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }; this.runningPluginNames = {}; + this.leftTriggerValue = 0; this.leftTriggerClicked = 0; + this.leftTrackerClicked = false; // is leftTriggerClicked == 1 because a hand tracker set it? + this.leftSecondaryValue = 0; + this.rightTriggerValue = 0; this.rightTriggerClicked = 0; - this.leftSecondaryValue = 0; + this.rightTrackerClicked = false; // is rightTriggerClicked == 1 because a hand tracker set it? this.rightSecondaryValue = 0; this.leftTriggerPress = function (value) { @@ -162,6 +165,34 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } }; + this.checkForHandTrackingClick = function() { + + var pinchOnBelowDistance = 0.016; + var pinchOffAboveDistance = 0.04; + + var leftIndexPose = Controller.getPoseValue(Controller.Standard.LeftHandIndex4); + var leftThumbPose = Controller.getPoseValue(Controller.Standard.LeftHandThumb4); + var leftThumbToIndexDistance = Vec3.distance(leftIndexPose.translation, leftThumbPose.translation); + if (leftIndexPose.valid && leftThumbPose.valid && leftThumbToIndexDistance < pinchOnBelowDistance) { + _this.leftTriggerClicked = 1; + _this.leftTrackerClicked = true; + } else if (_this.leftTrackerClicked && leftThumbToIndexDistance > pinchOffAboveDistance) { + _this.leftTriggerClicked = 0; + _this.leftTrackerClicked = false; + } + + var rightIndexPose = Controller.getPoseValue(Controller.Standard.RightHandIndex4); + var rightThumbPose = Controller.getPoseValue(Controller.Standard.RightHandThumb4); + var rightThumbToIndexDistance = Vec3.distance(rightIndexPose.translation, rightThumbPose.translation); + if (rightIndexPose.valid && rightThumbPose.valid && rightThumbToIndexDistance < pinchOnBelowDistance) { + _this.rightTriggerClicked = 1; + _this.rightTrackerClicked = true; + } else if (_this.rightTrackerClicked && rightThumbToIndexDistance > pinchOffAboveDistance) { + _this.rightTriggerClicked = 0; + _this.rightTrackerClicked = false; + } + }; + this.update = function () { try { _this.updateInternal(); @@ -369,6 +400,9 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } } + // check for hand-tracking "click" + _this.checkForHandTrackingClick(); + // bundle up all the data about the current situation var controllerData = { triggerValues: [_this.leftTriggerValue, _this.rightTriggerValue], diff --git a/scripts/system/controllers/controllerModules/nearGrabEntity.js b/scripts/system/controllers/controllerModules/nearGrabEntity.js index 763c1a1ce0..45d518bb39 100644 --- a/scripts/system/controllers/controllerModules/nearGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearGrabEntity.js @@ -151,7 +151,7 @@ Script.include("/~/system/libraries/controllers.js"); this.run = function (controllerData, deltaTime) { if (this.grabbing) { - if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE && + if (!controllerData.triggerClicks[this.hand] && controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { this.endNearGrabEntity(); return makeRunningValues(false, [], []); diff --git a/scripts/system/controllers/controllerModules/stylusInput.js b/scripts/system/controllers/controllerModules/stylusInput.js index c4aa9efd50..544fbb9277 100644 --- a/scripts/system/controllers/controllerModules/stylusInput.js +++ b/scripts/system/controllers/controllerModules/stylusInput.js @@ -67,7 +67,7 @@ Script.include("/~/system/libraries/controllers.js"); var nearTabletHighlightModuleReady = nearTabletHighlightModule ? nearTabletHighlightModule.isReady(controllerData) : makeRunningValues(false, [], []); return grabOverlayModuleReady.active || farGrabModuleReady.active || grabEntityModuleReady.active - || nearTabletHighlightModuleReady.active; + /* || nearTabletHighlightModuleReady.active */ ; }; this.overlayLaserActive = function(controllerData) { From 2c535fa204ab97ca7bfb4f772d978dfd64c3c205 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 26 Sep 2019 12:41:14 -0700 Subject: [PATCH 03/73] touching tips of index-fingers together means walk forward --- scripts/defaultScripts.js | 3 +- scripts/system/hand-track-walk.js | 69 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 scripts/system/hand-track-walk.js diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 1eac2ae0aa..a19bb9c41a 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -35,7 +35,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/miniTablet.js", "system/audioMuteOverlay.js", "system/inspect.js", - "system/keyboardShortcuts/keyboardShortcuts.js" + "system/keyboardShortcuts/keyboardShortcuts.js", + "system/hand-track-walk.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/system/hand-track-walk.js b/scripts/system/hand-track-walk.js new file mode 100644 index 0000000000..cb9b700ae5 --- /dev/null +++ b/scripts/system/hand-track-walk.js @@ -0,0 +1,69 @@ + +/* global Script, Controller, Vec3 */ +/* jshint loopfunc:true */ + +(function() { + + var mappingName = 'hand-track-walk-' + Math.random(); + var inputMapping = Controller.newMapping(mappingName); + + var leftIndexPos = null; + var rightIndexPos = null; + + var pinchOnBelowDistance = 0.016; + var pinchOffAboveDistance = 0.04; + + var walking = false; + + function updateWalking() { + if (leftIndexPos && rightIndexPos) { + var tipDistance = Vec3.distance(leftIndexPos, rightIndexPos); + if (tipDistance < pinchOnBelowDistance) { + print("qqqq walking"); + walking = true; + } else if (walking && tipDistance > pinchOffAboveDistance) { + print("qqqq stopping"); + walking = false; + } + } + } + + function leftIndexChanged(pose) { + if (pose.valid) { + leftIndexPos = pose.translation; + } else { + leftIndexPos = null; + } + updateWalking(); + } + + function rightIndexChanged(pose) { + if (pose.valid) { + rightIndexPos = pose.translation; + } else { + rightIndexPos = null; + } + updateWalking(); + } + + function cleanUp() { + inputMapping.disable(); + } + + Script.scriptEnding.connect(function () { + cleanUp(); + }); + + inputMapping.from(Controller.Standard.LeftHandIndex4).peek().to(leftIndexChanged); + inputMapping.from(Controller.Standard.RightHandIndex4).peek().to(rightIndexChanged); + + inputMapping.from(function() { + if (walking) { + return -1; + } else { + return Controller.getActionValue(Controller.Standard.TranslateZ); + } + }).to(Controller.Actions.TranslateZ); + + Controller.enableMapping(mappingName); +})(); From 296617977ce4121fa5426d8c565841117bb160ab Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 1 Oct 2019 09:14:55 -0700 Subject: [PATCH 04/73] get near-grab working with camera-tracked hands --- scripts/system/controllers/controllerDispatcher.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index d73e5de7e7..24d0e2703d 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -168,16 +168,18 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.checkForHandTrackingClick = function() { var pinchOnBelowDistance = 0.016; - var pinchOffAboveDistance = 0.04; + var pinchOffAboveDistance = 0.035; var leftIndexPose = Controller.getPoseValue(Controller.Standard.LeftHandIndex4); var leftThumbPose = Controller.getPoseValue(Controller.Standard.LeftHandThumb4); var leftThumbToIndexDistance = Vec3.distance(leftIndexPose.translation, leftThumbPose.translation); if (leftIndexPose.valid && leftThumbPose.valid && leftThumbToIndexDistance < pinchOnBelowDistance) { _this.leftTriggerClicked = 1; + _this.leftTriggerValue = 1; _this.leftTrackerClicked = true; } else if (_this.leftTrackerClicked && leftThumbToIndexDistance > pinchOffAboveDistance) { _this.leftTriggerClicked = 0; + _this.leftTriggerValue = 0; _this.leftTrackerClicked = false; } @@ -186,9 +188,11 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); var rightThumbToIndexDistance = Vec3.distance(rightIndexPose.translation, rightThumbPose.translation); if (rightIndexPose.valid && rightThumbPose.valid && rightThumbToIndexDistance < pinchOnBelowDistance) { _this.rightTriggerClicked = 1; + _this.rightTriggerValue = 1; _this.rightTrackerClicked = true; } else if (_this.rightTrackerClicked && rightThumbToIndexDistance > pinchOffAboveDistance) { _this.rightTriggerClicked = 0; + _this.rightTriggerValue = 0; _this.rightTrackerClicked = false; } }; From 1c926db2db6e350baeaad2c68aded09a12e41b6a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 1 Oct 2019 11:45:33 -0700 Subject: [PATCH 05/73] if hands are tracked, make mini-tablet be just a big open-tablet button (remove mute and goto buttons) --- scripts/system/html/css/miniHandTablet.css | 56 ++++++++++++++++++++++ scripts/system/html/js/miniTablet.js | 34 +++++++++---- scripts/system/html/miniHandsTablet.html | 26 ++++++++++ scripts/system/miniTablet.js | 47 ++++++++++-------- 4 files changed, 133 insertions(+), 30 deletions(-) create mode 100644 scripts/system/html/css/miniHandTablet.css create mode 100644 scripts/system/html/miniHandsTablet.html diff --git a/scripts/system/html/css/miniHandTablet.css b/scripts/system/html/css/miniHandTablet.css new file mode 100644 index 0000000000..ef2c27ff14 --- /dev/null +++ b/scripts/system/html/css/miniHandTablet.css @@ -0,0 +1,56 @@ +/* +miniTablet.css + +Copyright 2019 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 +*/ + +* { + box-sizing: border-box; + padding: 0; + margin: 0; + user-select: none; +} + +html { + background-color: #404040; +} + +body { + height: 100%; +} + +section { + background-color: #404040; + position: relative; + padding: 32px 0px; +} + +.button { + text-align: center; +} + +img { + width: 149px; + height: 149px; +} + +#expand { + width: 149px; + height: 149px; + background-size: 100% 100%; + background-image: url("./img/mt-expand-normal.svg"); +} + +#expand:hover { + background-image: url("./img/mt-expand-hover.svg"); +} + +#expand:hover.unhover { + background-image: url("./img/mt-expand-normal.svg"); +} + +#expand img { +} diff --git a/scripts/system/html/js/miniTablet.js b/scripts/system/html/js/miniTablet.js index c48201cef5..b02c6ae213 100644 --- a/scripts/system/html/js/miniTablet.js +++ b/scripts/system/html/js/miniTablet.js @@ -35,7 +35,9 @@ function setUnhover() { if (!isUnhover) { - gotoButton.classList.add("unhover"); + if (gotoButton) { + gotoButton.classList.add("unhover"); + } expandButton.classList.add("unhover"); isUnhover = true; } @@ -43,7 +45,9 @@ function clearUnhover() { if (isUnhover) { - gotoButton.classList.remove("unhover"); + if (gotoButton) { + gotoButton.classList.remove("unhover"); + } expandButton.classList.remove("unhover"); isUnhover = false; } @@ -62,10 +66,14 @@ switch (message.type) { case MUTE_MESSAGE: - muteImage.src = message.icon; + if (muteImage) { + muteImage.src = message.icon; + } break; case GOTO_MESSAGE: - gotoImage.src = message.icon; + if (gotoImage) { + gotoImage.src = message.icon; + } break; } } @@ -130,9 +138,7 @@ function onLoad() { muteButton = document.getElementById("mute"); - muteImage = document.getElementById("mute-img"); gotoButton = document.getElementById("goto"); - gotoImage = document.getElementById("goto-img"); expandButton = document.getElementById("expand"); connectEventBridge(); @@ -140,11 +146,19 @@ document.body.addEventListener("mouseenter", onBodyHover, false); document.body.addEventListener("mouseleave", onBodyUnhover, false); - muteButton.addEventListener("mouseenter", onButtonHover, false); - gotoButton.addEventListener("mouseenter", onButtonHover, false); + if (muteButton) { + muteImage = document.getElementById("mute-img"); + muteButton.addEventListener("mouseenter", onButtonHover, false); + muteButton.addEventListener("click", onMuteButtonClick, true); + } + + if (gotoButton) { + gotoImage = document.getElementById("goto-img"); + gotoButton.addEventListener("mouseenter", onButtonHover, false); + gotoButton.addEventListener("click", onGotoButtonClick, true); + } + expandButton.addEventListener("mouseenter", onButtonHover, false); - muteButton.addEventListener("click", onMuteButtonClick, true); - gotoButton.addEventListener("click", onGotoButtonClick, true); expandButton.addEventListener("click", onExpandButtonClick, true); document.body.onunload = function () { diff --git a/scripts/system/html/miniHandsTablet.html b/scripts/system/html/miniHandsTablet.html new file mode 100644 index 0000000000..1d140797f4 --- /dev/null +++ b/scripts/system/html/miniHandsTablet.html @@ -0,0 +1,26 @@ + + + + + + + + + + +
+
+ +
+
+ + + diff --git a/scripts/system/miniTablet.js b/scripts/system/miniTablet.js index f5b5ecf0a1..bc9bcfe36d 100644 --- a/scripts/system/miniTablet.js +++ b/scripts/system/miniTablet.js @@ -8,7 +8,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global getTabletWidthFromSettings, TRIGGER_OFF_VALUE */ +/* global getTabletWidthFromSettings, TRIGGER_OFF_VALUE, Controller, Script, Camera, Tablet, MyAvatar, + Quat, SoundCache, HMD, Overlays, Vec3, Uuid, Messages */ (function () { @@ -80,6 +81,10 @@ return hand === LEFT_HAND ? RIGHT_HAND : LEFT_HAND; } + function handsAreTracked() { + return Controller.getPoseValue(Controller.Standard.LeftHandIndex3).valid || + Controller.getPoseValue(Controller.Standard.RightHandIndex3).valid; + } UI = function () { @@ -114,6 +119,7 @@ uiHand = LEFT_HAND, miniUIOverlay = null, MINI_UI_HTML = Script.resolvePath("./html/miniTablet.html"), + MINI_HAND_UI_HTML = Script.resolvePath("./html/miniHandsTablet.html"), MINI_UI_DIMENSIONS = { x: 0.059, y: 0.0865, z: 0.001 }, MINI_UI_WIDTH_PIXELS = 150, METERS_TO_INCHES = 39.3701, @@ -291,6 +297,7 @@ visible: true }); Overlays.editOverlay(miniUIOverlay, { + url: handsAreTracked() ? MINI_HAND_UI_HTML : MINI_UI_HTML, localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_LOCAL_POSITION), localRotation: MINI_UI_LOCAL_ROTATION, dimensions: Vec3.multiply(initialScale, MINI_UI_DIMENSIONS), @@ -353,8 +360,8 @@ localRotation, localPosition; - tabletScaleFactor = MyAvatar.sensorToWorldScale - * (1 + scaleFactor * (miniTargetWidth - miniInitialWidth) / miniInitialWidth); + tabletScaleFactor = MyAvatar.sensorToWorldScale * + (1 + scaleFactor * (miniTargetWidth - miniInitialWidth) / miniInitialWidth); dimensions = Vec3.multiply(tabletScaleFactor, MINI_DIMENSIONS); localRotation = Quat.mix(miniExpandLocalRotation, miniTargetLocalRotation, scaleFactor); localPosition = @@ -469,11 +476,11 @@ solid: true, grabbable: true, showKeyboardFocusHighlight: false, - drawInFront: true, + drawInFront: false, visible: false }); miniUIOverlay = Overlays.addOverlay("web3d", { - url: MINI_UI_HTML, + url: handsAreTracked() ? MINI_HAND_UI_HTML : MINI_UI_HTML, parentID: miniOverlay, localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_LOCAL_POSITION), localRotation: MINI_UI_LOCAL_ROTATION, @@ -482,7 +489,7 @@ alpha: 0, // Hide overlay while its content is being created. grabbable: false, showKeyboardFocusHighlight: false, - drawInFront: true, + drawInFront: false, visible: false }); @@ -642,8 +649,8 @@ // is grabbing something) or the other hand's trigger is pressed unless it is pointing at the mini tablet. Allow // the triggers to be pressed briefly to allow for the grabbing process. if (show) { - isLeftTriggerOff = Controller.getValue(Controller.Standard.LT) < TRIGGER_OFF_VALUE - && Controller.getValue(Controller.Standard.LeftGrip) < TRIGGER_OFF_VALUE; + isLeftTriggerOff = Controller.getValue(Controller.Standard.LT) < TRIGGER_OFF_VALUE && + Controller.getValue(Controller.Standard.LeftGrip) < TRIGGER_OFF_VALUE; if (!isLeftTriggerOff) { if (leftTriggerOn === 0) { leftTriggerOn = Date.now(); @@ -653,8 +660,8 @@ } else { leftTriggerOn = 0; } - isRightTriggerOff = Controller.getValue(Controller.Standard.RT) < TRIGGER_OFF_VALUE - && Controller.getValue(Controller.Standard.RightGrip) < TRIGGER_OFF_VALUE; + isRightTriggerOff = Controller.getValue(Controller.Standard.RT) < TRIGGER_OFF_VALUE && + Controller.getValue(Controller.Standard.RightGrip) < TRIGGER_OFF_VALUE; if (!isRightTriggerOff) { if (rightTriggerOn === 0) { rightTriggerOn = Date.now(); @@ -665,8 +672,8 @@ rightTriggerOn = 0; } - show = (hand === LEFT_HAND ? wasLeftTriggerOff : wasRightTriggerOff) - && ((hand === LEFT_HAND ? wasRightTriggerOff : wasLeftTriggerOff) || ui.isLaserPointingAt()); + show = (hand === LEFT_HAND ? wasLeftTriggerOff : wasRightTriggerOff) && + ((hand === LEFT_HAND ? wasRightTriggerOff : wasLeftTriggerOff) || ui.isLaserPointingAt()); } // Should show mini tablet if it would be oriented toward the camera. @@ -691,10 +698,10 @@ normalDot = Vec3.dot(normalHandVector, miniToCameraDirection); medialAngle = Math.atan2(medialDot, normalDot); lateralAngle = Math.atan2(lateralDot, normalDot); - show = -MAX_MEDIAL_WRIST_CAMERA_ANGLE_RAD <= medialAngle - && medialAngle <= MAX_MEDIAL_FINGER_CAMERA_ANGLE_RAD - && -MAX_LATERAL_THUMB_CAMERA_ANGLE_RAD <= lateralAngle - && lateralAngle <= MAX_LATERAL_PINKY_CAMERA_ANGLE_RAD; + show = -MAX_MEDIAL_WRIST_CAMERA_ANGLE_RAD <= medialAngle && + medialAngle <= MAX_MEDIAL_FINGER_CAMERA_ANGLE_RAD && + -MAX_LATERAL_THUMB_CAMERA_ANGLE_RAD <= lateralAngle && + lateralAngle <= MAX_LATERAL_PINKY_CAMERA_ANGLE_RAD; // Camera looking at mini tablet? cameraToMini = -Vec3.dot(miniToCameraDirection, Quat.getForward(Camera.orientation)); @@ -972,8 +979,8 @@ function setState(state, data) { if (state !== miniState) { - debug("State transition from " + STATE_STRINGS[miniState] + " to " + STATE_STRINGS[state] - + ( data ? " " + JSON.stringify(data) : "")); + debug("State transition from " + STATE_STRINGS[miniState] + " to " + STATE_STRINGS[state] + + ( data ? " " + JSON.stringify(data) : "")); if (STATE_MACHINE[STATE_STRINGS[miniState]].exit) { STATE_MACHINE[STATE_STRINGS[miniState]].exit(data); } @@ -1061,8 +1068,8 @@ return; } - if (miniState.getState() === miniState.MINI_DISABLED - || (message.grabbedEntity !== HMD.tabletID && message.grabbedEntity !== ui.getMiniTabletID())) { + if (miniState.getState() === miniState.MINI_DISABLED || + (message.grabbedEntity !== HMD.tabletID && message.grabbedEntity !== ui.getMiniTabletID())) { return; } From fa4d055ab16614b99da59f0354053898614deb01 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 1 Oct 2019 14:52:33 -0700 Subject: [PATCH 06/73] disable far-trigger and grab if using hand tracker --- .../controllerModules/farActionGrabEntity.js | 5 ++++- .../controllers/controllerModules/farGrabEntity.js | 8 +++++--- .../system/controllers/controllerModules/farTrigger.js | 5 ++++- scripts/system/libraries/controllerDispatcherUtils.js | 10 ++++++++-- scripts/system/miniTablet.js | 7 +------ 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 1eaed44ce2..8f18be9c27 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -14,7 +14,7 @@ TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic, getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, makeLaserParams, AddressManager, getEntityParents, Selection, DISPATCHER_HOVERING_LIST, - worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES, Uuid, Picks + worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES, Uuid, Picks, handsAreTracked, Messages */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -374,6 +374,9 @@ Script.include("/~/system/libraries/controllers.js"); this.isReady = function (controllerData) { if (HMD.active) { + if (handsAreTracked()) { + return makeRunningValues(false, [], []); + } if (this.notPointingAtEntity(controllerData)) { return makeRunningValues(false, [], []); } diff --git a/scripts/system/controllers/controllerModules/farGrabEntity.js b/scripts/system/controllers/controllerModules/farGrabEntity.js index ecafa3cb26..c486d46c33 100644 --- a/scripts/system/controllers/controllerModules/farGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farGrabEntity.js @@ -12,7 +12,7 @@ HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, makeLaserParams, AddressManager, getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, findGrabbableGroupParent, - worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES + worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES, handsAreTracked */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -63,7 +63,6 @@ Script.include("/~/system/libraries/controllers.js"); this.endedGrab = 0; this.MIN_HAPTIC_PULSE_INTERVAL = 500; // ms this.disabled = false; - var _this = this; this.initialControllerRotation = Quat.IDENTITY; this.currentControllerRotation = Quat.IDENTITY; this.manipulating = false; @@ -99,7 +98,7 @@ Script.include("/~/system/libraries/controllers.js"); this.getOffhand = function () { return (this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND); - } + }; // Activation criteria for rotating a fargrabbed entity. If we're changing the mapping, this is where to do it. this.shouldManipulateTarget = function (controllerData) { @@ -406,6 +405,9 @@ Script.include("/~/system/libraries/controllers.js"); this.isReady = function (controllerData) { if (HMD.active) { + if (handsAreTracked()) { + return makeRunningValues(false, [], []); + } if (this.notPointingAtEntity(controllerData)) { return makeRunningValues(false, [], []); } diff --git a/scripts/system/controllers/controllerModules/farTrigger.js b/scripts/system/controllers/controllerModules/farTrigger.js index c9c9d3deee..2a8a4d7246 100644 --- a/scripts/system/controllers/controllerModules/farTrigger.js +++ b/scripts/system/controllers/controllerModules/farTrigger.js @@ -8,7 +8,7 @@ /* global Script, RIGHT_HAND, LEFT_HAND, MyAvatar, makeRunningValues, Entities, enableDispatcherModule, disableDispatcherModule, makeDispatcherModuleParameters, - getGrabbableData, makeLaserParams, DISPATCHER_PROPERTIES + getGrabbableData, makeLaserParams, DISPATCHER_PROPERTIES, RayPick, handsAreTracked */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -63,6 +63,9 @@ Script.include("/~/system/libraries/controllers.js"); this.isReady = function (controllerData) { this.targetEntityID = null; + if (handsAreTracked()) { + return makeRunningValues(false, [], []); + } if (controllerData.triggerClicks[this.hand] === 0) { return makeRunningValues(false, [], []); } diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 3b81e17473..fc2306fe28 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -63,7 +63,8 @@ clearHighlightedEntities:true, unhighlightTargetEntity:true, distanceBetweenEntityLocalPositionAndBoundingBox: true, - worldPositionToRegistrationFrameMatrix: true + worldPositionToRegistrationFrameMatrix: true, + handsAreTracked: true */ MSECS_PER_SEC = 1000.0; @@ -600,6 +601,10 @@ worldPositionToRegistrationFrameMatrix = function(wptrProps, pos) { return offsetMat; }; +handsAreTracked = function () { + return Controller.getPoseValue(Controller.Standard.LeftHandIndex3).valid || + Controller.getPoseValue(Controller.Standard.RightHandIndex3).valid; +} if (typeof module !== 'undefined') { module.exports = { @@ -624,6 +629,7 @@ if (typeof module !== 'undefined') { TRIGGER_OFF_VALUE: TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE: TRIGGER_ON_VALUE, DISPATCHER_HOVERING_LIST: DISPATCHER_HOVERING_LIST, - worldPositionToRegistrationFrameMatrix: worldPositionToRegistrationFrameMatrix + worldPositionToRegistrationFrameMatrix: worldPositionToRegistrationFrameMatrix, + handsAreTracked: handsAreTracked }; } diff --git a/scripts/system/miniTablet.js b/scripts/system/miniTablet.js index bc9bcfe36d..1650cb60f4 100644 --- a/scripts/system/miniTablet.js +++ b/scripts/system/miniTablet.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global getTabletWidthFromSettings, TRIGGER_OFF_VALUE, Controller, Script, Camera, Tablet, MyAvatar, +/* global getTabletWidthFromSettings, handsAreTracked, TRIGGER_OFF_VALUE, Controller, Script, Camera, Tablet, MyAvatar, Quat, SoundCache, HMD, Overlays, Vec3, Uuid, Messages */ (function () { @@ -81,11 +81,6 @@ return hand === LEFT_HAND ? RIGHT_HAND : LEFT_HAND; } - function handsAreTracked() { - return Controller.getPoseValue(Controller.Standard.LeftHandIndex3).valid || - Controller.getPoseValue(Controller.Standard.RightHandIndex3).valid; - } - UI = function () { if (!(this instanceof UI)) { From 5511b18432b69942d408ea84defca9d1bde5eb17 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 1 Oct 2019 14:56:48 -0700 Subject: [PATCH 07/73] don't allow the hand with the mini-tablet to trigger opening the tablet --- scripts/system/controllers/controllerModules/stylusInput.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/stylusInput.js b/scripts/system/controllers/controllerModules/stylusInput.js index 544fbb9277..c04edbe085 100644 --- a/scripts/system/controllers/controllerModules/stylusInput.js +++ b/scripts/system/controllers/controllerModules/stylusInput.js @@ -121,7 +121,8 @@ Script.include("/~/system/libraries/controllers.js"); } // Add the mini tablet. - if (HMD.miniTabletScreenID && Overlays.getProperty(HMD.miniTabletScreenID, "visible")) { + if (HMD.miniTabletScreenID && Overlays.getProperty(HMD.miniTabletScreenID, "visible") && + this.hand != HMD.miniTabletHand) { stylusTarget = getOverlayDistance(controllerPosition, HMD.miniTabletScreenID); if (stylusTarget) { stylusTargets.push(stylusTarget); From 17ceda0d3e331e5b8a8d2ca25649179199be6701 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 1 Oct 2019 15:25:03 -0700 Subject: [PATCH 08/73] quiet jshint --- .../system/controllers/controllerModules/stylusInput.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/system/controllers/controllerModules/stylusInput.js b/scripts/system/controllers/controllerModules/stylusInput.js index c04edbe085..f19b023545 100644 --- a/scripts/system/controllers/controllerModules/stylusInput.js +++ b/scripts/system/controllers/controllerModules/stylusInput.js @@ -7,7 +7,7 @@ /* global Script, MyAvatar, Controller, Uuid, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, makeRunningValues, Vec3, makeDispatcherModuleParameters, Overlays, HMD, Settings, getEnabledModuleByName, Pointers, - Picks, PickType + Picks, PickType, Keyboard */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -64,8 +64,8 @@ Script.include("/~/system/libraries/controllers.js"); var nearTabletHighlightModuleName = this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight"; var nearTabletHighlightModule = getEnabledModuleByName(nearTabletHighlightModuleName); - var nearTabletHighlightModuleReady = nearTabletHighlightModule - ? nearTabletHighlightModule.isReady(controllerData) : makeRunningValues(false, [], []); + var nearTabletHighlightModuleReady = nearTabletHighlightModule ? + nearTabletHighlightModule.isReady(controllerData) : makeRunningValues(false, [], []); return grabOverlayModuleReady.active || farGrabModuleReady.active || grabEntityModuleReady.active /* || nearTabletHighlightModuleReady.active */ ; }; @@ -129,7 +129,7 @@ Script.include("/~/system/libraries/controllers.js"); } } - const WEB_DISPLAY_STYLUS_DISTANCE = (Keyboard.raised && Keyboard.preferMalletsOverLasers) ? 0.2 : 0.5; + var WEB_DISPLAY_STYLUS_DISTANCE = (Keyboard.raised && Keyboard.preferMalletsOverLasers) ? 0.2 : 0.5; var nearStylusTarget = isNearStylusTarget(stylusTargets, WEB_DISPLAY_STYLUS_DISTANCE * sensorScaleFactor); if (nearStylusTarget.length !== 0) { From 63dcf0e1c92b6187273d6ef0e11ea2caebd647d3 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 2 Oct 2019 11:24:05 -0700 Subject: [PATCH 09/73] use same near-grab module to grab entities and overlays --- .../controllers/controllerDispatcher.js | 56 +++++++++++++++++-- .../controllerModules/nearGrabEntity.js | 2 +- .../controllerModules/stylusInput.js | 16 ++++-- .../system/controllers/controllerScripts.js | 2 +- scripts/system/libraries/WebTablet.js | 1 + .../libraries/controllerDispatcherUtils.js | 5 +- 6 files changed, 66 insertions(+), 16 deletions(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 24d0e2703d..de583b8f0c 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -53,6 +53,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.pointerManager = new PointerManager(); this.grabSphereOverlays = [null, null]; this.targetIDs = {}; + this.debugPanelID = null; + this.debugLines = []; // a module can occupy one or more "activity" slots while it's running. If all the required slots for a module are // not set to false (not in use), a module cannot start. When a module is using a slot, that module's name @@ -206,6 +208,18 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.setTimeout(_this.update, BASIC_TIMER_INTERVAL_MS); }; + this.addDebugLine = function(line) { + if (this.debugLines.length > 8) { + this.debugLines.shift(); + } + this.debugLines.push(line); + var debugPanelText = ""; + this.debugLines.forEach(function(debugLine) { + debugPanelText += debugLine + "\n"; + }); + Entities.editEntity(this.debugPanelID, { text: debugPanelText }); + }; + this.updateInternal = function () { if (PROFILE) { Script.beginProfileRange("dispatch.pre"); @@ -309,6 +323,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } var nearbyEntityIDs = Entities.findEntities(controllerPosition, findRadius); + nearbyEntityIDs = nearbyEntityIDs.concat(nearbyOverlayIDs[h]); // overlays are now entities + for (var j = 0; j < nearbyEntityIDs.length; j++) { var entityID = nearbyEntityIDs[j]; var props = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES); @@ -444,7 +460,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); _this.markSlots(candidatePlugin, orderedPluginName); _this.pointerManager.makePointerVisible(candidatePlugin.parameters.handLaser); if (DEBUG) { - print("controllerDispatcher running " + orderedPluginName); + _this.addDebugLine("running " + orderedPluginName); } } if (PROFILE) { @@ -476,8 +492,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); if (DEBUG) { if (JSON.stringify(_this.targetIDs[runningPluginName]) != JSON.stringify(runningness.targets)) { - print("controllerDispatcher targetIDs[" + runningPluginName + "] = " + - JSON.stringify(runningness.targets)); + _this.addDebugLine("targetIDs[" + runningPluginName + "] = " + + JSON.stringify(runningness.targets)); } } @@ -488,12 +504,12 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); delete _this.runningPluginNames[runningPluginName]; delete _this.targetIDs[runningPluginName]; if (DEBUG) { - print("controllerDispatcher deleted targetIDs[" + runningPluginName + "]"); + _this.addDebugLine("deleted targetIDs[" + runningPluginName + "]"); } _this.markSlots(plugin, false); _this.pointerManager.makePointerInvisible(plugin.parameters.handLaser); if (DEBUG) { - print("controllerDispatcher stopping " + runningPluginName); + _this.addDebugLine("stopping " + runningPluginName); } } _this.pointerManager.lockPointerEnd(plugin.parameters.handLaser, runningness.laserLockInfo); @@ -637,7 +653,33 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Overlays.mousePressOnOverlay.disconnect(mousePress); Entities.mousePressOnEntity.disconnect(mousePress); Messages.messageReceived.disconnect(controllerDispatcher.handleMessage); + if (_this.debugPanelID) { + Entities.deleteEntity(_this.debugPanelID); + _this.debugPanelID = null; + } }; + + if (DEBUG) { + this.debugPanelID = Entities.addEntity({ + name: "controllerDispatcher debug panel", + type: "Text", + dimensions: { x: 1.0, y: 0.3, z: 0.01 }, + parentID: MyAvatar.sessionUUID, + // parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), + parentJointIndex: -1, + localPosition: { x: -0.25, y: 0.8, z: -1.2 }, + textColor: { red: 255, green: 255, blue: 255}, + backgroundColor: { red: 0, green: 0, blue: 0}, + text: "", + lineHeight: 0.03, + leftMargin: 0.015, + topMargin: 0.01, + backgroundAlpha: 0.7, + textAlpha: 1.0, + unlit: true, + ignorePickIntersection: true + }, "local"); + } } function mouseReleaseOnOverlay(overlayID, event) { @@ -667,6 +709,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.messageReceived.connect(controllerDispatcher.handleMessage); - Script.scriptEnding.connect(controllerDispatcher.cleanup); + Script.scriptEnding.connect(function () { + controllerDispatcher.cleanup(); + }); Script.setTimeout(controllerDispatcher.update, BASIC_TIMER_INTERVAL_MS); }()); diff --git a/scripts/system/controllers/controllerModules/nearGrabEntity.js b/scripts/system/controllers/controllerModules/nearGrabEntity.js index 45d518bb39..381197badf 100644 --- a/scripts/system/controllers/controllerModules/nearGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearGrabEntity.js @@ -28,7 +28,7 @@ Script.include("/~/system/libraries/controllers.js"); this.grabID = null; this.parameters = makeDispatcherModuleParameters( - 500, + 90, this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], [], 100); diff --git a/scripts/system/controllers/controllerModules/stylusInput.js b/scripts/system/controllers/controllerModules/stylusInput.js index f19b023545..fabfb91f02 100644 --- a/scripts/system/controllers/controllerModules/stylusInput.js +++ b/scripts/system/controllers/controllerModules/stylusInput.js @@ -52,21 +52,25 @@ Script.include("/~/system/libraries/controllers.js"); this.disable = false; this.otherModuleNeedsToRun = function(controllerData) { - var grabOverlayModuleName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"; - var grabOverlayModule = getEnabledModuleByName(grabOverlayModuleName); - var grabEntityModuleName = this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity"; + // var grabOverlayModuleName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"; + // var grabOverlayModule = getEnabledModuleByName(grabOverlayModuleName); + // var grabOverlayModuleReady = grabOverlayModule ? grabOverlayModule.isReady(controllerData) : makeRunningValues(false, [], []); + + var grabEntityModuleName = this.hand === RIGHT_HAND ? "RightNearGrabEntity" : "LeftNearGrabEntity"; var grabEntityModule = getEnabledModuleByName(grabEntityModuleName); - var grabOverlayModuleReady = grabOverlayModule ? grabOverlayModule.isReady(controllerData) : makeRunningValues(false, [], []); var grabEntityModuleReady = grabEntityModule ? grabEntityModule.isReady(controllerData) : makeRunningValues(false, [], []); - var farGrabModuleName = this.hand === RIGHT_HAND ? "RightFarActionGrabEntity" : "LeftFarActionGrabEntity"; + + var farGrabModuleName = this.hand === RIGHT_HAND ? "RightFarGrabEntity" : "LeftFarGrabEntity"; var farGrabModule = getEnabledModuleByName(farGrabModuleName); var farGrabModuleReady = farGrabModule ? farGrabModule.isReady(controllerData) : makeRunningValues(false, [], []); + var nearTabletHighlightModuleName = this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight"; var nearTabletHighlightModule = getEnabledModuleByName(nearTabletHighlightModuleName); var nearTabletHighlightModuleReady = nearTabletHighlightModule ? nearTabletHighlightModule.isReady(controllerData) : makeRunningValues(false, [], []); - return grabOverlayModuleReady.active || farGrabModuleReady.active || grabEntityModuleReady.active + + return /* grabOverlayModuleReady.active || */ farGrabModuleReady.active || grabEntityModuleReady.active /* || nearTabletHighlightModuleReady.active */ ; }; diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index c9cb61b5f5..fdc81e0780 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -18,7 +18,7 @@ var CONTOLLER_SCRIPTS = [ //"toggleAdvancedMovementForHandControllers.js", "handTouch.js", "controllerDispatcher.js", - "controllerModules/nearParentGrabOverlay.js", + // "controllerModules/nearParentGrabOverlay.js", "controllerModules/stylusInput.js", "controllerModules/equipEntity.js", "controllerModules/nearTrigger.js", diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index b7593656a3..9f2142504c 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -164,6 +164,7 @@ WebTablet = function (url, width, dpi, hand, location, visible) { parentID: this.tabletEntityID, parentJointIndex: -1, showKeyboardFocusHighlight: false, + grabbable: false, visible: visible }); diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index fc2306fe28..5cfd899da0 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -6,7 +6,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html /* global module, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, Xform, Mat4, - Selection, Uuid, + Selection, Uuid, Controller, MSECS_PER_SEC:true , LEFT_HAND:true, RIGHT_HAND:true, FORBIDDEN_GRAB_TYPES:true, HAPTIC_PULSE_STRENGTH:true, HAPTIC_PULSE_DURATION:true, ZERO_VEC:true, ONE_VEC:true, DEFAULT_REGISTRATION_POINT:true, INCHES_TO_METERS:true, @@ -56,6 +56,7 @@ TEAR_AWAY_DISTANCE:true, TEAR_AWAY_COUNT:true, TEAR_AWAY_CHECK_TIME:true, + TELEPORT_DEADZONE: true, NEAR_GRAB_DISTANCE: true, distanceBetweenPointAndEntityBoundingBox:true, entityIsEquipped:true, @@ -604,7 +605,7 @@ worldPositionToRegistrationFrameMatrix = function(wptrProps, pos) { handsAreTracked = function () { return Controller.getPoseValue(Controller.Standard.LeftHandIndex3).valid || Controller.getPoseValue(Controller.Standard.RightHandIndex3).valid; -} +}; if (typeof module !== 'undefined') { module.exports = { From 4d3da24c33eb9ec340f4e9902e41d5a500a67dd2 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 2 Oct 2019 16:19:08 -0700 Subject: [PATCH 10/73] fix angularVelocity reported by leap-motion plugin --- .../hifiLeapMotion/src/LeapMotionPlugin.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp b/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp index 5c5b975676..3f0cad02b4 100644 --- a/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp +++ b/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp @@ -281,6 +281,7 @@ void LeapMotionPlugin::InputDevice::update(float deltaTime, const controller::In glm::vec3 pos; glm::quat rot; + glm::quat prevRot; if (_isLeapOnHMD) { auto jointPosition = joints[i].position; const glm::vec3 HMD_EYE_TO_LEAP_OFFSET = glm::vec3(0.0f, 0.0f, -0.09f); // Eyes to surface of Leap Motion. @@ -291,17 +292,33 @@ void LeapMotionPlugin::InputDevice::update(float deltaTime, const controller::In glm::quat jointOrientation = joints[i].orientation; jointOrientation = glm::quat(jointOrientation.w, -jointOrientation.x, -jointOrientation.z, -jointOrientation.y); rot = controllerToAvatarRotation * hmdSensorOrientation * jointOrientation; + + glm::quat prevJointOrientation = prevJoints[i].orientation; + prevJointOrientation = + glm::quat(prevJointOrientation.w, -prevJointOrientation.x, -prevJointOrientation.z, -prevJointOrientation.y); + prevRot = controllerToAvatarRotation * hmdSensorOrientation * prevJointOrientation; + } else { pos = controllerToAvatarRotation * (joints[i].position - leapMotionOffset); const glm::quat ZERO_HAND_ORIENTATION = glm::quat(glm::vec3(PI_OVER_TWO, PI, 0.0f)); rot = controllerToAvatarRotation * joints[i].orientation * ZERO_HAND_ORIENTATION; + prevRot = controllerToAvatarRotation * prevJoints[i].orientation * ZERO_HAND_ORIENTATION; } + // glm::vec3 linearVelocity, angularVelocity; + // if (i < prevJoints.size()) { + // linearVelocity = (pos - (prevJoints[i].position * METERS_PER_CENTIMETER)) / deltaTime; // m/s + // glm::quat dQ = rot * glm::inverse(prevRot); + // float angle = glm::angle(dQ); + // glm::vec3 axis = glm::axis(dQ); + // angularVelocity = (angle / deltaTime) * axis; + // } + glm::vec3 linearVelocity, angularVelocity; if (i < prevJoints.size()) { linearVelocity = (pos - (prevJoints[i].position * METERS_PER_CENTIMETER)) / deltaTime; // m/s // quat log imaginary part points along the axis of rotation, with length of one half the angle of rotation. - glm::quat d = glm::log(rot * glm::inverse(prevJoints[i].orientation)); + glm::quat d = glm::log(rot * glm::inverse(prevRot)); angularVelocity = glm::vec3(d.x, d.y, d.z) / (0.5f * deltaTime); // radians/s } From 865584e7e35f963c533c6e7279de9584652865c1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 2 Oct 2019 17:25:17 -0700 Subject: [PATCH 11/73] put tracked hand walk into a controller-dispatcher module --- scripts/defaultScripts.js | 1 - .../controllerModules/trackedHandWalk.js | 106 ++++++++++++++++++ .../system/controllers/controllerScripts.js | 4 +- scripts/system/hand-track-walk.js | 69 ------------ 4 files changed, 108 insertions(+), 72 deletions(-) create mode 100644 scripts/system/controllers/controllerModules/trackedHandWalk.js delete mode 100644 scripts/system/hand-track-walk.js diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index a19bb9c41a..0efabd7773 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -34,7 +34,6 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/emote.js", "system/miniTablet.js", "system/audioMuteOverlay.js", - "system/inspect.js", "system/keyboardShortcuts/keyboardShortcuts.js", "system/hand-track-walk.js" ]; diff --git a/scripts/system/controllers/controllerModules/trackedHandWalk.js b/scripts/system/controllers/controllerModules/trackedHandWalk.js new file mode 100644 index 0000000000..ab1e3534b2 --- /dev/null +++ b/scripts/system/controllers/controllerModules/trackedHandWalk.js @@ -0,0 +1,106 @@ +"use strict"; + +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, RIGHT_HAND, LEFT_HAND, makeRunningValues, enableDispatcherModule, disableDispatcherModule, + makeDispatcherModuleParameters, handsAreTracked, Controller, Vec3 +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + + function TrackedHandWalk(hand) { + this.mappingName = 'hand-track-walk-' + Math.random(); + this.inputMapping = Controller.newMapping(this.mappingName); + this.leftIndexPos = null; + this.rightIndexPos = null; + this.pinchOnBelowDistance = 0.016; + this.pinchOffAboveDistance = 0.04; + this.walking = false; + + this.parameters = makeDispatcherModuleParameters( + 80, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.updateWalking = function () { + if (this.leftIndexPos && this.rightIndexPos) { + var tipDistance = Vec3.distance(this.leftIndexPos, this.rightIndexPos); + if (tipDistance < this.pinchOnBelowDistance) { + this.walking = true; + } else if (this.walking && tipDistance > this.pinchOffAboveDistance) { + this.walking = false; + } + } + }; + + this.leftIndexChanged = function (pose) { + if (pose.valid) { + this.leftIndexPos = pose.translation; + } else { + this.leftIndexPos = null; + } + this.updateWalking(); + }; + + this.rightIndexChanged = function (pose) { + if (pose.valid) { + this.rightIndexPos = pose.translation; + } else { + this.rightIndexPos = null; + } + this.updateWalking(); + }; + + this.isReady = function (controllerData) { + if (!handsAreTracked()) { + return makeRunningValues(false, [], []); + } + if (this.walking) { + return makeRunningValues(true, [], []); + } + }; + + this.run = function (controllerData) { + return this.isReady(controllerData); + }; + + this.setup = function () { + var _this = this; + this.inputMapping.from(Controller.Standard.LeftHandIndex4).peek().to(function (pose) { + _this.leftIndexChanged(pose); + }); + this.inputMapping.from(Controller.Standard.RightHandIndex4).peek().to(function (pose) { + _this.rightIndexChanged(pose); + }); + + this.inputMapping.from(function() { + if (_this.walking) { + return -1; + } else { + return Controller.getActionValue(Controller.Standard.TranslateZ); + } + }).to(Controller.Actions.TranslateZ); + + Controller.enableMapping(this.mappingName); + }; + + this.cleanUp = function () { + this.inputMapping.disable(); + }; + } + + var trackedHandWalk = new TrackedHandWalk(LEFT_HAND); + trackedHandWalk.setup(); + enableDispatcherModule("TrackedHandWalk", trackedHandWalk); + + function cleanup() { + trackedHandWalk.cleanUp(); + disableDispatcherModule("TrackedHandWalk"); + } + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index fdc81e0780..a75ba164b5 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -18,7 +18,6 @@ var CONTOLLER_SCRIPTS = [ //"toggleAdvancedMovementForHandControllers.js", "handTouch.js", "controllerDispatcher.js", - // "controllerModules/nearParentGrabOverlay.js", "controllerModules/stylusInput.js", "controllerModules/equipEntity.js", "controllerModules/nearTrigger.js", @@ -34,7 +33,8 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/nearTabletHighlight.js", "controllerModules/nearGrabEntity.js", "controllerModules/farGrabEntity.js", - "controllerModules/pushToTalk.js" + "controllerModules/pushToTalk.js", + "controllerModules/trackedHandWalk.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; diff --git a/scripts/system/hand-track-walk.js b/scripts/system/hand-track-walk.js deleted file mode 100644 index cb9b700ae5..0000000000 --- a/scripts/system/hand-track-walk.js +++ /dev/null @@ -1,69 +0,0 @@ - -/* global Script, Controller, Vec3 */ -/* jshint loopfunc:true */ - -(function() { - - var mappingName = 'hand-track-walk-' + Math.random(); - var inputMapping = Controller.newMapping(mappingName); - - var leftIndexPos = null; - var rightIndexPos = null; - - var pinchOnBelowDistance = 0.016; - var pinchOffAboveDistance = 0.04; - - var walking = false; - - function updateWalking() { - if (leftIndexPos && rightIndexPos) { - var tipDistance = Vec3.distance(leftIndexPos, rightIndexPos); - if (tipDistance < pinchOnBelowDistance) { - print("qqqq walking"); - walking = true; - } else if (walking && tipDistance > pinchOffAboveDistance) { - print("qqqq stopping"); - walking = false; - } - } - } - - function leftIndexChanged(pose) { - if (pose.valid) { - leftIndexPos = pose.translation; - } else { - leftIndexPos = null; - } - updateWalking(); - } - - function rightIndexChanged(pose) { - if (pose.valid) { - rightIndexPos = pose.translation; - } else { - rightIndexPos = null; - } - updateWalking(); - } - - function cleanUp() { - inputMapping.disable(); - } - - Script.scriptEnding.connect(function () { - cleanUp(); - }); - - inputMapping.from(Controller.Standard.LeftHandIndex4).peek().to(leftIndexChanged); - inputMapping.from(Controller.Standard.RightHandIndex4).peek().to(rightIndexChanged); - - inputMapping.from(function() { - if (walking) { - return -1; - } else { - return Controller.getActionValue(Controller.Standard.TranslateZ); - } - }).to(Controller.Actions.TranslateZ); - - Controller.enableMapping(mappingName); -})(); From 600c2c3947fa557021ffcba0897b371e843bd4bb Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 3 Oct 2019 10:37:18 -0700 Subject: [PATCH 12/73] double OK to open/close tablet. touch thumb-tips to walk backwards --- .../controllerModules/trackedHandTablet.js | 142 ++++++++++++++++++ .../controllerModules/trackedHandWalk.js | 90 +++++++++-- .../system/controllers/controllerScripts.js | 3 +- 3 files changed, 218 insertions(+), 17 deletions(-) create mode 100644 scripts/system/controllers/controllerModules/trackedHandTablet.js diff --git a/scripts/system/controllers/controllerModules/trackedHandTablet.js b/scripts/system/controllers/controllerModules/trackedHandTablet.js new file mode 100644 index 0000000000..d0a4ac8af1 --- /dev/null +++ b/scripts/system/controllers/controllerModules/trackedHandTablet.js @@ -0,0 +1,142 @@ +"use strict"; + +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, makeRunningValues, enableDispatcherModule, disableDispatcherModule, + makeDispatcherModuleParameters, handsAreTracked, Controller, Vec3, Tablet, HMD, MyAvatar +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + + function TrackedHandTablet() { + this.mappingName = 'hand-track-tablet-' + Math.random(); + this.inputMapping = Controller.newMapping(this.mappingName); + this.leftIndexPos = null; + this.leftThumbPos = null; + this.rightIndexPos = null; + this.rightThumbPos = null; + this.touchOnBelowDistance = 0.016; + this.touchOffAboveDistance = 0.045; + + this.gestureCompleted = false; + this.previousGestureCompleted = false; + + this.parameters = makeDispatcherModuleParameters( + 70, + ["rightHand", "leftHand"], + [], + 100); + + this.checkForGesture = function () { + if (this.leftThumbPos && this.leftIndexPos && this.rightThumbPos && this.rightIndexPos) { + var leftTipDistance = Vec3.distance(this.leftThumbPos, this.leftIndexPos); + var rightTipDistance = Vec3.distance(this.rightThumbPos, this.rightIndexPos); + if (leftTipDistance < this.touchOnBelowDistance && rightTipDistance < this.touchOnBelowDistance) { + this.gestureCompleted = true; + } else if (leftTipDistance > this.touchOffAboveDistance || rightTipDistance > this.touchOffAboveDistance) { + this.gestureCompleted = false; + } + } else { + this.gestureCompleted = false; + } + + if (this.gestureCompleted && !this.previousGestureCompleted) { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + if (HMD.showTablet) { + HMD.closeTablet(false); + } else if (!HMD.showTablet && !tablet.toolbarMode && !MyAvatar.isAway) { + tablet.gotoHomeScreen(); + HMD.openTablet(false); + } + } + + this.previousGestureCompleted = this.gestureCompleted; + }; + + this.leftIndexChanged = function (pose) { + if (pose.valid) { + this.leftIndexPos = pose.translation; + } else { + this.leftIndexPos = null; + } + this.checkForGesture(); + }; + + this.leftThumbChanged = function (pose) { + if (pose.valid) { + this.leftThumbPos = pose.translation; + } else { + this.leftThumbPos = null; + } + this.checkForGesture(); + }; + + this.rightIndexChanged = function (pose) { + if (pose.valid) { + this.rightIndexPos = pose.translation; + } else { + this.rightIndexPos = null; + } + this.checkForGesture(); + }; + + this.rightThumbChanged = function (pose) { + if (pose.valid) { + this.rightThumbPos = pose.translation; + } else { + this.rightThumbPos = null; + } + this.checkForGesture(); + }; + + this.isReady = function (controllerData) { + if (!handsAreTracked()) { + return makeRunningValues(false, [], []); + } else if (this.gestureCompleted) { + return makeRunningValues(true, [], []); + } else { + return makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData) { + return this.isReady(controllerData); + }; + + this.setup = function () { + var _this = this; + this.inputMapping.from(Controller.Standard.LeftHandIndex4).peek().to(function (pose) { + _this.leftIndexChanged(pose); + }); + this.inputMapping.from(Controller.Standard.LeftHandThumb4).peek().to(function (pose) { + _this.leftThumbChanged(pose); + }); + this.inputMapping.from(Controller.Standard.RightHandIndex4).peek().to(function (pose) { + _this.rightIndexChanged(pose); + }); + this.inputMapping.from(Controller.Standard.RightHandThumb4).peek().to(function (pose) { + _this.rightThumbChanged(pose); + }); + + Controller.enableMapping(this.mappingName); + }; + + this.cleanUp = function () { + this.inputMapping.disable(); + }; + } + + var trackedHandWalk = new TrackedHandTablet(); + trackedHandWalk.setup(); + enableDispatcherModule("TrackedHandTablet", trackedHandWalk); + + function cleanup() { + trackedHandWalk.cleanUp(); + disableDispatcherModule("TrackedHandTablet"); + } + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/trackedHandWalk.js b/scripts/system/controllers/controllerModules/trackedHandWalk.js index ab1e3534b2..b721797d34 100644 --- a/scripts/system/controllers/controllerModules/trackedHandWalk.js +++ b/scripts/system/controllers/controllerModules/trackedHandWalk.js @@ -3,7 +3,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global Script, RIGHT_HAND, LEFT_HAND, makeRunningValues, enableDispatcherModule, disableDispatcherModule, +/* global Script, makeRunningValues, enableDispatcherModule, disableDispatcherModule, makeDispatcherModuleParameters, handsAreTracked, Controller, Vec3 */ @@ -12,28 +12,46 @@ Script.include("/~/system/libraries/controllers.js"); (function() { - function TrackedHandWalk(hand) { + function TrackedHandWalk() { this.mappingName = 'hand-track-walk-' + Math.random(); this.inputMapping = Controller.newMapping(this.mappingName); this.leftIndexPos = null; + this.leftThumbPos = null; this.rightIndexPos = null; - this.pinchOnBelowDistance = 0.016; - this.pinchOffAboveDistance = 0.04; - this.walking = false; + this.rightThumbPos = null; + this.touchOnBelowDistance = 0.016; + this.touchOffAboveDistance = 0.045; + this.walkingForward = false; + this.walkingBackward = false; this.parameters = makeDispatcherModuleParameters( 80, - this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + ["rightHand", "leftHand"], [], 100); + this.getControlPoint = function () { + return Vec3.multiply(Vec3.sum(this.leftIndexPos, this.rightIndexPos), 0.5); + }; + this.updateWalking = function () { if (this.leftIndexPos && this.rightIndexPos) { - var tipDistance = Vec3.distance(this.leftIndexPos, this.rightIndexPos); - if (tipDistance < this.pinchOnBelowDistance) { - this.walking = true; - } else if (this.walking && tipDistance > this.pinchOffAboveDistance) { - this.walking = false; + var indexTipDistance = Vec3.distance(this.leftIndexPos, this.rightIndexPos); + if (indexTipDistance < this.touchOnBelowDistance) { + this.walkingForward = true; + this.controlPoint = this.getControlPoint(); + } else if (this.walkingForward && indexTipDistance > this.touchOffAboveDistance) { + this.walkingForward = false; + } + } + + if (this.leftThumbPos && this.rightThumbPos) { + var thumbTipDistance = Vec3.distance(this.leftThumbPos, this.rightThumbPos); + if (thumbTipDistance < this.touchOnBelowDistance) { + this.walkingBackward = true; + this.controlPoint = this.getControlPoint(); + } else if (this.walkingBackward && thumbTipDistance > this.touchOffAboveDistance) { + this.walkingBackward = false; } } }; @@ -47,6 +65,15 @@ Script.include("/~/system/libraries/controllers.js"); this.updateWalking(); }; + this.leftThumbChanged = function (pose) { + if (pose.valid) { + this.leftThumbPos = pose.translation; + } else { + this.leftThumbPos = null; + } + this.updateWalking(); + }; + this.rightIndexChanged = function (pose) { if (pose.valid) { this.rightIndexPos = pose.translation; @@ -56,12 +83,22 @@ Script.include("/~/system/libraries/controllers.js"); this.updateWalking(); }; + this.rightThumbChanged = function (pose) { + if (pose.valid) { + this.rightThumbPos = pose.translation; + } else { + this.rightThumbPos = null; + } + this.updateWalking(); + }; + this.isReady = function (controllerData) { if (!handsAreTracked()) { return makeRunningValues(false, [], []); - } - if (this.walking) { + } else if (this.walkingForward || this.walkingBackward) { return makeRunningValues(true, [], []); + } else { + return makeRunningValues(false, [], []); } }; @@ -74,18 +111,39 @@ Script.include("/~/system/libraries/controllers.js"); this.inputMapping.from(Controller.Standard.LeftHandIndex4).peek().to(function (pose) { _this.leftIndexChanged(pose); }); + this.inputMapping.from(Controller.Standard.LeftHandThumb4).peek().to(function (pose) { + _this.leftThumbChanged(pose); + }); this.inputMapping.from(Controller.Standard.RightHandIndex4).peek().to(function (pose) { _this.rightIndexChanged(pose); }); + this.inputMapping.from(Controller.Standard.RightHandThumb4).peek().to(function (pose) { + _this.rightThumbChanged(pose); + }); this.inputMapping.from(function() { - if (_this.walking) { - return -1; + if (_this.walkingForward) { + // var currentPoint = _this.getControlPoint(); + // return currentPoint.z - _this.controlPoint.z; + return -0.5; + } else if (_this.walkingBackward) { + // var currentPoint = _this.getControlPoint(); + // return currentPoint.z - _this.controlPoint.z; + return 0.5; } else { return Controller.getActionValue(Controller.Standard.TranslateZ); } }).to(Controller.Actions.TranslateZ); + // this.inputMapping.from(function() { + // if (_this.walkingForward) { + // var currentPoint = _this.getControlPoint(); + // return currentPoint.x - _this.controlPoint.x; + // } else { + // return Controller.getActionValue(Controller.Standard.Yaw); + // } + // }).to(Controller.Actions.Yaw); + Controller.enableMapping(this.mappingName); }; @@ -94,7 +152,7 @@ Script.include("/~/system/libraries/controllers.js"); }; } - var trackedHandWalk = new TrackedHandWalk(LEFT_HAND); + var trackedHandWalk = new TrackedHandWalk(); trackedHandWalk.setup(); enableDispatcherModule("TrackedHandWalk", trackedHandWalk); diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index a75ba164b5..f41dcbd445 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -34,7 +34,8 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/nearGrabEntity.js", "controllerModules/farGrabEntity.js", "controllerModules/pushToTalk.js", - "controllerModules/trackedHandWalk.js" + "controllerModules/trackedHandWalk.js", + "controllerModules/trackedHandTablet.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; From a799d305ee495f43e155f029ef5a3804b164ada1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 8 Oct 2019 11:13:34 -0700 Subject: [PATCH 13/73] don't mess with TranslateZ unless module is active --- .../system/controllers/controllerModules/trackedHandWalk.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/trackedHandWalk.js b/scripts/system/controllers/controllerModules/trackedHandWalk.js index b721797d34..62ce3fdfbd 100644 --- a/scripts/system/controllers/controllerModules/trackedHandWalk.js +++ b/scripts/system/controllers/controllerModules/trackedHandWalk.js @@ -131,7 +131,8 @@ Script.include("/~/system/libraries/controllers.js"); // return currentPoint.z - _this.controlPoint.z; return 0.5; } else { - return Controller.getActionValue(Controller.Standard.TranslateZ); + // return Controller.getActionValue(Controller.Standard.TranslateZ); + return null; } }).to(Controller.Actions.TranslateZ); From 4f7252a0d8a6f474c0dac71ab45c7a8bf2f52b2e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 15 Oct 2019 10:06:53 -0700 Subject: [PATCH 14/73] enable and disable TranslateZ mapping, as needed --- .../controllerModules/trackedHandWalk.js | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/scripts/system/controllers/controllerModules/trackedHandWalk.js b/scripts/system/controllers/controllerModules/trackedHandWalk.js index 62ce3fdfbd..2d589f8747 100644 --- a/scripts/system/controllers/controllerModules/trackedHandWalk.js +++ b/scripts/system/controllers/controllerModules/trackedHandWalk.js @@ -13,6 +13,8 @@ Script.include("/~/system/libraries/controllers.js"); (function() { function TrackedHandWalk() { + this.gestureMappingName = 'hand-track-walk-gesture-' + Math.random(); + this.inputGestureMapping = Controller.newMapping(this.gestureMappingName); this.mappingName = 'hand-track-walk-' + Math.random(); this.inputMapping = Controller.newMapping(this.mappingName); this.leftIndexPos = null; @@ -24,6 +26,8 @@ Script.include("/~/system/libraries/controllers.js"); this.walkingForward = false; this.walkingBackward = false; + this.mappingEnabled = false; + this.parameters = makeDispatcherModuleParameters( 80, ["rightHand", "leftHand"], @@ -54,6 +58,14 @@ Script.include("/~/system/libraries/controllers.js"); this.walkingBackward = false; } } + + if ((this.walkingForward || this.walkingBackward) && !this.mappingEnabled) { + Controller.enableMapping(this.mappingName); + this.mappingEnabled = true; + } else if (!(this.walkingForward || this.walkingBackward) && this.mappingEnabled) { + this.inputMapping.disable(); + this.mappingEnabled = false; + } }; this.leftIndexChanged = function (pose) { @@ -108,16 +120,16 @@ Script.include("/~/system/libraries/controllers.js"); this.setup = function () { var _this = this; - this.inputMapping.from(Controller.Standard.LeftHandIndex4).peek().to(function (pose) { + this.inputGestureMapping.from(Controller.Standard.LeftHandIndex4).peek().to(function (pose) { _this.leftIndexChanged(pose); }); - this.inputMapping.from(Controller.Standard.LeftHandThumb4).peek().to(function (pose) { + this.inputGestureMapping.from(Controller.Standard.LeftHandThumb4).peek().to(function (pose) { _this.leftThumbChanged(pose); }); - this.inputMapping.from(Controller.Standard.RightHandIndex4).peek().to(function (pose) { + this.inputGestureMapping.from(Controller.Standard.RightHandIndex4).peek().to(function (pose) { _this.rightIndexChanged(pose); }); - this.inputMapping.from(Controller.Standard.RightHandThumb4).peek().to(function (pose) { + this.inputGestureMapping.from(Controller.Standard.RightHandThumb4).peek().to(function (pose) { _this.rightThumbChanged(pose); }); @@ -132,7 +144,7 @@ Script.include("/~/system/libraries/controllers.js"); return 0.5; } else { // return Controller.getActionValue(Controller.Standard.TranslateZ); - return null; + return 0.0; } }).to(Controller.Actions.TranslateZ); @@ -145,10 +157,11 @@ Script.include("/~/system/libraries/controllers.js"); // } // }).to(Controller.Actions.Yaw); - Controller.enableMapping(this.mappingName); + Controller.enableMapping(this.gestureMappingName); }; this.cleanUp = function () { + this.inputGestureMapping.disable(); this.inputMapping.disable(); }; } From 07034721c77a01b1188fb95b590174fb0e6a11bd Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 4 Nov 2019 10:18:09 -0800 Subject: [PATCH 15/73] adjust when stylus input is active --- .../system/controllers/controllerModules/stylusInput.js | 7 +------ .../controllers/controllerModules/trackedHandTablet.js | 2 +- .../controllers/controllerModules/trackedHandWalk.js | 6 +++--- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/scripts/system/controllers/controllerModules/stylusInput.js b/scripts/system/controllers/controllerModules/stylusInput.js index fabfb91f02..cbef45050e 100644 --- a/scripts/system/controllers/controllerModules/stylusInput.js +++ b/scripts/system/controllers/controllerModules/stylusInput.js @@ -52,10 +52,6 @@ Script.include("/~/system/libraries/controllers.js"); this.disable = false; this.otherModuleNeedsToRun = function(controllerData) { - // var grabOverlayModuleName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"; - // var grabOverlayModule = getEnabledModuleByName(grabOverlayModuleName); - // var grabOverlayModuleReady = grabOverlayModule ? grabOverlayModule.isReady(controllerData) : makeRunningValues(false, [], []); - var grabEntityModuleName = this.hand === RIGHT_HAND ? "RightNearGrabEntity" : "LeftNearGrabEntity"; var grabEntityModule = getEnabledModuleByName(grabEntityModuleName); var grabEntityModuleReady = grabEntityModule ? grabEntityModule.isReady(controllerData) : makeRunningValues(false, [], []); @@ -70,8 +66,7 @@ Script.include("/~/system/libraries/controllers.js"); var nearTabletHighlightModuleReady = nearTabletHighlightModule ? nearTabletHighlightModule.isReady(controllerData) : makeRunningValues(false, [], []); - return /* grabOverlayModuleReady.active || */ farGrabModuleReady.active || grabEntityModuleReady.active - /* || nearTabletHighlightModuleReady.active */ ; + return farGrabModuleReady.active || grabEntityModuleReady.active; }; this.overlayLaserActive = function(controllerData) { diff --git a/scripts/system/controllers/controllerModules/trackedHandTablet.js b/scripts/system/controllers/controllerModules/trackedHandTablet.js index d0a4ac8af1..6bb9d67ef8 100644 --- a/scripts/system/controllers/controllerModules/trackedHandTablet.js +++ b/scripts/system/controllers/controllerModules/trackedHandTablet.js @@ -39,7 +39,7 @@ Script.include("/~/system/libraries/controllers.js"); this.gestureCompleted = true; } else if (leftTipDistance > this.touchOffAboveDistance || rightTipDistance > this.touchOffAboveDistance) { this.gestureCompleted = false; - } + } // else don't change gestureCompleted } else { this.gestureCompleted = false; } diff --git a/scripts/system/controllers/controllerModules/trackedHandWalk.js b/scripts/system/controllers/controllerModules/trackedHandWalk.js index 2d589f8747..92549eaa81 100644 --- a/scripts/system/controllers/controllerModules/trackedHandWalk.js +++ b/scripts/system/controllers/controllerModules/trackedHandWalk.js @@ -46,7 +46,7 @@ Script.include("/~/system/libraries/controllers.js"); this.controlPoint = this.getControlPoint(); } else if (this.walkingForward && indexTipDistance > this.touchOffAboveDistance) { this.walkingForward = false; - } + } // else don't change walkingForward } if (this.leftThumbPos && this.rightThumbPos) { @@ -56,7 +56,7 @@ Script.include("/~/system/libraries/controllers.js"); this.controlPoint = this.getControlPoint(); } else if (this.walkingBackward && thumbTipDistance > this.touchOffAboveDistance) { this.walkingBackward = false; - } + } // else don't change this.walkingBackward } if ((this.walkingForward || this.walkingBackward) && !this.mappingEnabled) { @@ -65,7 +65,7 @@ Script.include("/~/system/libraries/controllers.js"); } else if (!(this.walkingForward || this.walkingBackward) && this.mappingEnabled) { this.inputMapping.disable(); this.mappingEnabled = false; - } + } // else don't change mappingEnabled }; this.leftIndexChanged = function (pose) { From a0031c6f1046f303bcf9b8e484422d086094e761 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 9 Aug 2019 09:28:58 -0700 Subject: [PATCH 16/73] eye and hand tracking for htc vive pro --- cmake/ports/hifi-client-deps/CONTROL | 2 +- plugins/openvr/CMakeLists.txt | 2 + plugins/openvr/src/ViveControllerManager.cpp | 530 ++++++++++++++++++- plugins/openvr/src/ViveControllerManager.h | 41 ++ 4 files changed, 570 insertions(+), 5 deletions(-) diff --git a/cmake/ports/hifi-client-deps/CONTROL b/cmake/ports/hifi-client-deps/CONTROL index 7070cb6fb9..5b9b0bcce0 100644 --- a/cmake/ports/hifi-client-deps/CONTROL +++ b/cmake/ports/hifi-client-deps/CONTROL @@ -1,4 +1,4 @@ Source: hifi-client-deps Version: 0.1 Description: Collected dependencies for High Fidelity applications -Build-Depends: hifi-deps, aristo (windows), glslang, liblo (windows), nlohmann-json, openvr (windows), quazip (!android), sdl2 (!android), spirv-cross (!android), spirv-tools (!android), sranipal (windows), vulkanmemoryallocator +Build-Depends: hifi-deps, glslang, nlohmann-json, openvr (windows), sdl2 (!android), spirv-cross (!android), spirv-tools (!android), vulkanmemoryallocator, aristo (windows), sranipal (windows) diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt index dcb2e39e1b..e80b2215bf 100644 --- a/plugins/openvr/CMakeLists.txt +++ b/plugins/openvr/CMakeLists.txt @@ -15,5 +15,7 @@ if (WIN32 AND (NOT USE_GLES)) include_hifi_library_headers(octree) target_openvr() + target_sranipal() + target_aristo() target_link_libraries(${TARGET_NAME} Winmm.lib) endif() diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 8aa7311de4..b1d3b791d6 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -13,6 +13,21 @@ #include #include +#ifdef _WIN32 +#pragma warning( push ) +#pragma warning( disable : 4091 ) +#pragma warning( disable : 4334 ) +#endif + +#include +#include +#include +#include + +#ifdef _WIN32 +#pragma warning( pop ) +#endif + #include #include #include @@ -37,6 +52,8 @@ #include #include +#include "OpenVrDisplayPlugin.h" + extern PoseData _nextSimPoseData; vr::IVRSystem* acquireOpenVrSystem(); @@ -130,6 +147,51 @@ static glm::mat4 calculateResetMat() { return glm::mat4(); } +class ViveProEyeReadThread : public QThread { +public: + ViveProEyeReadThread() { + setObjectName("OpenVR ViveProEye Read Thread"); + } + void run() override { + while (!quit) { + ViveSR::anipal::Eye::EyeData eyeData; + int result = ViveSR::anipal::Eye::GetEyeData(&eyeData); + { + QMutexLocker locker(&eyeDataMutex); + eyeDataBuffer.getEyeDataResult = result; + if (result == ViveSR::Error::WORK) { + uint64_t leftValids = eyeData.verbose_data.left.eye_data_validata_bit_mask; + uint64_t rightValids = eyeData.verbose_data.right.eye_data_validata_bit_mask; + + eyeDataBuffer.leftDirectionValid = + (leftValids & (uint64_t)ViveSR::anipal::Eye::SINGLE_EYE_DATA_GAZE_DIRECTION_VALIDITY) > (uint64_t)0; + eyeDataBuffer.rightDirectionValid = + (rightValids & (uint64_t)ViveSR::anipal::Eye::SINGLE_EYE_DATA_GAZE_DIRECTION_VALIDITY) > (uint64_t)0; + eyeDataBuffer.leftOpennessValid = + (leftValids & (uint64_t)ViveSR::anipal::Eye::SINGLE_EYE_DATA_EYE_OPENNESS_VALIDITY) > (uint64_t)0; + eyeDataBuffer.rightOpennessValid = + (rightValids & (uint64_t)ViveSR::anipal::Eye::SINGLE_EYE_DATA_EYE_OPENNESS_VALIDITY) > (uint64_t)0; + + float *leftGaze = eyeData.verbose_data.left.gaze_direction_normalized.elem_; + float *rightGaze = eyeData.verbose_data.right.gaze_direction_normalized.elem_; + eyeDataBuffer.leftEyeGaze = glm::vec3(leftGaze[0], leftGaze[1], leftGaze[2]); + eyeDataBuffer.rightEyeGaze = glm::vec3(rightGaze[0], rightGaze[1], rightGaze[2]); + + eyeDataBuffer.leftEyeOpenness = eyeData.verbose_data.left.eye_openness; + eyeDataBuffer.rightEyeOpenness = eyeData.verbose_data.right.eye_openness; + } + } + } + } + + bool quit { false }; + + // mutex and buffer for moving data from this thread to the other one + QMutex eyeDataMutex; + EyeDataBuffer eyeDataBuffer; +}; + + static QString outOfRangeDataStrategyToString(ViveControllerManager::OutOfRangeDataStrategy strategy) { switch (strategy) { default: @@ -211,6 +273,81 @@ QString ViveControllerManager::configurationLayout() { return OPENVR_LAYOUT; } +bool isDeviceIndexActive(vr::IVRSystem*& system, uint32_t deviceIndex) { + if (!system) { + return false; + } + if (deviceIndex != vr::k_unTrackedDeviceIndexInvalid && + system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_Controller && + system->IsTrackedDeviceConnected(deviceIndex)) { + vr::EDeviceActivityLevel activityLevel = system->GetTrackedDeviceActivityLevel(deviceIndex); + if (activityLevel == vr::k_EDeviceActivityLevel_UserInteraction) { + return true; + } + } + return false; +} + +bool isHandControllerActive(vr::IVRSystem*& system, vr::ETrackedControllerRole deviceRole) { + if (!system) { + return false; + } + auto deviceIndex = system->GetTrackedDeviceIndexForControllerRole(deviceRole); + return isDeviceIndexActive(system, deviceIndex); +} + +bool areBothHandControllersActive(vr::IVRSystem*& system) { + return + isHandControllerActive(system, vr::TrackedControllerRole_LeftHand) && + isHandControllerActive(system, vr::TrackedControllerRole_RightHand); +} + + +void ViveControllerManager::enableGestureDetection() { + if (_viveCameraHandTracker) { + return; + } + if (!ViveSR::anipal::Eye::IsViveProEye()) { + return; + } + +// #define HAND_TRACKER_USE_EXTERNAL_TRANSFORM 1 + +#ifdef HAND_TRACKER_USE_EXTERNAL_TRANSFORM + UseExternalTransform(true); // camera hand tracker results are in HMD frame +#else + UseExternalTransform(false); // camera hand tracker results are in sensor frame +#endif + GestureOption options; // defaults are GestureBackendAuto and GestureModeSkeleton + GestureFailure gestureFailure = StartGestureDetection(&options); + switch (gestureFailure) { + case GestureFailureNone: + qDebug() << "StartGestureDetection success"; + _viveCameraHandTracker = true; + break; + case GestureFailureOpenCL: + qDebug() << "StartGestureDetection (Only on Windows) OpenCL is not supported on the machine"; + break; + case GestureFailureCamera: + qDebug() << "StartGestureDetection Start camera failed"; + break; + case GestureFailureInternal: + qDebug() << "StartGestureDetection Internal errors"; + break; + case GestureFailureCPUOnPC: + qDebug() << "StartGestureDetection CPU backend is not supported on Windows"; + break; + } +} + +void ViveControllerManager::disableGestureDetection() { + if (!_viveCameraHandTracker) { + return; + } + StopGestureDetection(); + _viveCameraHandTracker = false; +} + bool ViveControllerManager::activate() { InputPlugin::activate(); @@ -230,6 +367,28 @@ bool ViveControllerManager::activate() { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; + + if (ViveSR::anipal::Eye::IsViveProEye()) { + qDebug() << "Vive Pro eye-tracking detected"; + + int error = ViveSR::anipal::Initial(ViveSR::anipal::Eye::ANIPAL_TYPE_EYE, NULL); + if (error == ViveSR::Error::WORK) { + _viveProEye = true; + qDebug() << "Successfully initialize Eye engine."; + } else if (error == ViveSR::Error::RUNTIME_NOT_FOUND) { + _viveProEye = false; + qDebug() << "please follows SRanipal SDK guide to install SR_Runtime first"; + } else { + _viveProEye = false; + qDebug() << "Failed to initialize Eye engine. please refer to ViveSR error code:" << error; + } + + if (_viveProEye) { + _viveProEyeReadThread = std::make_shared(); + _viveProEyeReadThread->start(QThread::HighPriority); + } + } + return true; } @@ -251,6 +410,13 @@ void ViveControllerManager::deactivate() { userInputMapper->removeDevice(_inputDevice->_deviceID); _registeredWithInputMapper = false; + if (_viveProEyeReadThread) { + _viveProEyeReadThread->quit = true; + _viveProEyeReadThread->wait(); + _viveProEyeReadThread = nullptr; + ViveSR::anipal::Release(ViveSR::anipal::Eye::ANIPAL_TYPE_EYE); + } + saveSettings(); } @@ -262,6 +428,311 @@ bool ViveControllerManager::isHeadControllerMounted() const { return activityLevel == vr::k_EDeviceActivityLevel_UserInteraction; } +void ViveControllerManager::invalidateEyeInputs() { + _inputDevice->_poseStateMap[controller::LEFT_EYE].valid = false; + _inputDevice->_poseStateMap[controller::RIGHT_EYE].valid = false; + _inputDevice->_axisStateMap[controller::LEFT_EYE_BLINK].valid = false; + _inputDevice->_axisStateMap[controller::RIGHT_EYE_BLINK].valid = false; +} + + +void ViveControllerManager::updateEyeTracker(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + if (!isHeadControllerMounted()) { + invalidateEyeInputs(); + return; + } + + EyeDataBuffer eyeDataBuffer; + { + // GetEyeData takes around 4ms to finish, so we run it on a thread. + QMutexLocker locker(&_viveProEyeReadThread->eyeDataMutex); + memcpy(&eyeDataBuffer, &_viveProEyeReadThread->eyeDataBuffer, sizeof(eyeDataBuffer)); + } + + if (eyeDataBuffer.getEyeDataResult != ViveSR::Error::WORK) { + invalidateEyeInputs(); + return; + } + + // only update from buffer values if the new data is "valid" + if (!eyeDataBuffer.leftDirectionValid) { + eyeDataBuffer.leftEyeGaze = _prevEyeData.leftEyeGaze; + eyeDataBuffer.leftDirectionValid = _prevEyeData.leftDirectionValid; + } + if (!eyeDataBuffer.rightDirectionValid) { + eyeDataBuffer.rightEyeGaze = _prevEyeData.rightEyeGaze; + eyeDataBuffer.rightDirectionValid = _prevEyeData.rightDirectionValid; + } + if (!eyeDataBuffer.leftOpennessValid) { + eyeDataBuffer.leftEyeOpenness = _prevEyeData.leftEyeOpenness; + eyeDataBuffer.leftOpennessValid = _prevEyeData.leftOpennessValid; + } + if (!eyeDataBuffer.rightOpennessValid) { + eyeDataBuffer.rightEyeOpenness = _prevEyeData.rightEyeOpenness; + eyeDataBuffer.rightOpennessValid = _prevEyeData.rightOpennessValid; + } + _prevEyeData = eyeDataBuffer; + + // transform data into what the controller system expects. + + // in the data from sranipal, left=+x, up=+y, forward=+z + mat4 localLeftEyeMat = glm::lookAt(vec3(0.0f, 0.0f, 0.0f), + glm::vec3(-eyeDataBuffer.leftEyeGaze[0], + eyeDataBuffer.leftEyeGaze[1], + eyeDataBuffer.leftEyeGaze[2]), + vec3(0.0f, 1.0f, 0.0f)); + quat localLeftEyeRot = glm::quat_cast(localLeftEyeMat); + quat avatarLeftEyeRot = _inputDevice->_poseStateMap[controller::HEAD].rotation * localLeftEyeRot; + + mat4 localRightEyeMat = glm::lookAt(vec3(0.0f, 0.0f, 0.0f), + glm::vec3(-eyeDataBuffer.rightEyeGaze[0], + eyeDataBuffer.rightEyeGaze[1], + eyeDataBuffer.rightEyeGaze[2]), + vec3(0.0f, 1.0f, 0.0f)); + quat localRightEyeRot = glm::quat_cast(localRightEyeMat); + quat avatarRightEyeRot = _inputDevice->_poseStateMap[controller::HEAD].rotation * localRightEyeRot; + + // TODO -- figure out translations for eyes + if (eyeDataBuffer.leftDirectionValid) { + _inputDevice->_poseStateMap[controller::LEFT_EYE] = controller::Pose(glm::vec3(), avatarLeftEyeRot); + _inputDevice->_poseStateMap[controller::LEFT_EYE].valid = true; + } else { + _inputDevice->_poseStateMap[controller::LEFT_EYE].valid = false; + } + if (eyeDataBuffer.rightDirectionValid) { + _inputDevice->_poseStateMap[controller::RIGHT_EYE] = controller::Pose(glm::vec3(), avatarRightEyeRot); + _inputDevice->_poseStateMap[controller::RIGHT_EYE].valid = true; + } else { + _inputDevice->_poseStateMap[controller::RIGHT_EYE].valid = false; + } + + quint64 now = usecTimestampNow(); + + // in hifi, 0 is open 1 is closed. in SRanipal 1 is open, 0 is closed. + if (eyeDataBuffer.leftOpennessValid) { + _inputDevice->_axisStateMap[controller::LEFT_EYE_BLINK] = + controller::AxisValue(1.0f - eyeDataBuffer.leftEyeOpenness, now); + } else { + _inputDevice->_poseStateMap[controller::LEFT_EYE_BLINK].valid = false; + } + if (eyeDataBuffer.rightOpennessValid) { + _inputDevice->_axisStateMap[controller::RIGHT_EYE_BLINK] = + controller::AxisValue(1.0f - eyeDataBuffer.rightEyeOpenness, now); + } else { + _inputDevice->_poseStateMap[controller::RIGHT_EYE_BLINK].valid = false; + } +} + +glm::vec3 ViveControllerManager::getRollingAverageHandPoint(int handIndex, int pointIndex) const { +#if 0 + return _handPoints[0][handIndex][pointIndex]; +#else + glm::vec3 result; + for (int s = 0; s < NUMBER_OF_HAND_TRACKER_SMOOTHING_FRAMES; s++) { + result += _handPoints[s][handIndex][pointIndex]; + } + return result / NUMBER_OF_HAND_TRACKER_SMOOTHING_FRAMES; +#endif +} + + +controller::Pose ViveControllerManager::trackedHandDataToPose(int hand, const glm::vec3& palmFacing, + int nearHandPositionIndex, int farHandPositionIndex) { + glm::vec3 nearPoint = getRollingAverageHandPoint(hand, nearHandPositionIndex); + + glm::quat poseRot; + if (nearHandPositionIndex != farHandPositionIndex) { + glm::vec3 farPoint = getRollingAverageHandPoint(hand, farHandPositionIndex); + + glm::vec3 pointingDir = farPoint - nearPoint; // y axis + glm::vec3 otherAxis = glm::cross(pointingDir, palmFacing); + + glm::mat4 rotMat; + rotMat = glm::mat4(glm::vec4(otherAxis, 0.0f), + glm::vec4(pointingDir, 0.0f), + glm::vec4(palmFacing * (hand == 0 ? 1.0f : -1.0f), 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + poseRot = glm::normalize(glmExtractRotation(rotMat)); + } + + if (!isNaN(poseRot)) { + controller::Pose pose(nearPoint, poseRot); + return pose; + } else { + controller::Pose pose; + pose.valid = false; + return pose; + } +} + + +void ViveControllerManager::trackFinger(int hand, int jointIndex1, int jointIndex2, int jointIndex3, int jointIndex4, + controller::StandardPoseChannel joint1, controller::StandardPoseChannel joint2, + controller::StandardPoseChannel joint3, controller::StandardPoseChannel joint4) { + + glm::vec3 point1 = getRollingAverageHandPoint(hand, jointIndex1); + glm::vec3 point2 = getRollingAverageHandPoint(hand, jointIndex2); + glm::vec3 point3 = getRollingAverageHandPoint(hand, jointIndex3); + glm::vec3 point4 = getRollingAverageHandPoint(hand, jointIndex4); + + glm::vec3 wristPos = getRollingAverageHandPoint(hand, 0); + glm::vec3 thumb2 = getRollingAverageHandPoint(hand, 2); + glm::vec3 pinkie1 = getRollingAverageHandPoint(hand, 17); + + // 1st + glm::vec3 palmFacing = glm::normalize(glm::cross(pinkie1 - wristPos, thumb2 - wristPos)); + glm::vec3 handForward = glm::normalize(point1 - wristPos); + glm::vec3 x = glm::normalize(glm::cross(palmFacing, handForward)); + glm::vec3 y = glm::normalize(point2 - point1); + glm::vec3 z = (hand == 0) ? glm::cross(y, x) : glm::cross(x, y); + glm::mat4 rotMat1 = glm::mat4(glm::vec4(x, 0.0f), + glm::vec4(y, 0.0f), + glm::vec4(z, 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + glm::quat rot1 = glm::normalize(glmExtractRotation(rotMat1)); + if (!isNaN(rot1)) { + _inputDevice->_poseStateMap[joint1] = controller::Pose(point1, rot1); + } + + + // 2nd + glm::vec3 x2 = x; // glm::normalize(glm::cross(point3 - point2, point2 - point1)); + glm::vec3 y2 = glm::normalize(point3 - point2); + glm::vec3 z2 = (hand == 0) ? glm::cross(y2, x2) : glm::cross(x2, y2); + + glm::mat4 rotMat2 = glm::mat4(glm::vec4(x2, 0.0f), + glm::vec4(y2, 0.0f), + glm::vec4(z2, 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + glm::quat rot2 = glm::normalize(glmExtractRotation(rotMat2)); + if (!isNaN(rot2)) { + _inputDevice->_poseStateMap[joint2] = controller::Pose(point2, rot2); + } + + + // 3rd + glm::vec3 x3 = x; // glm::normalize(glm::cross(point4 - point3, point3 - point1)); + glm::vec3 y3 = glm::normalize(point4 - point3); + glm::vec3 z3 = (hand == 0) ? glm::cross(y3, x3) : glm::cross(x3, y3); + + glm::mat4 rotMat3 = glm::mat4(glm::vec4(x3, 0.0f), + glm::vec4(y3, 0.0f), + glm::vec4(z3, 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + glm::quat rot3 = glm::normalize(glmExtractRotation(rotMat3)); + if (!isNaN(rot3)) { + _inputDevice->_poseStateMap[joint3] = controller::Pose(point3, rot3); + } + + + // 4th + glm::quat rot4 = rot3; + if (!isNaN(rot4)) { + _inputDevice->_poseStateMap[joint4] = controller::Pose(point4, rot4); + } +} + + +void ViveControllerManager::updateCameraHandTracker(float deltaTime, + const controller::InputCalibrationData& inputCalibrationData) { + + if (areBothHandControllersActive(_system)) { + // if both hand-controllers are in use, don't do camera hand tracking + disableGestureDetection(); + } else { + enableGestureDetection(); + } + + if (!_viveCameraHandTracker) { + return; + } + + const GestureResult* results = NULL; + int handTrackerFrameIndex { -1 }; + int resultsHandCount = GetGestureResult(&results, &handTrackerFrameIndex); + + if (handTrackerFrameIndex >= 0 /* && handTrackerFrameIndex != _lastHandTrackerFrameIndex */) { +#ifdef HAND_TRACKER_USE_EXTERNAL_TRANSFORM + glm::mat4 trackedHandToAvatar = + glm::inverse(inputCalibrationData.avatarMat) * + inputCalibrationData.sensorToWorldMat * + inputCalibrationData.hmdSensorMat; + // glm::mat4 trackedHandToAvatar = _inputDevice->_poseStateMap[controller::HEAD].getMatrix() * Matrices::Y_180; +#else + DisplayPluginPointer displayPlugin = _container->getActiveDisplayPlugin(); + std::shared_ptr openVRDisplayPlugin = + std::dynamic_pointer_cast(displayPlugin); + glm::mat4 sensorResetMatrix; + if (openVRDisplayPlugin) { + sensorResetMatrix = openVRDisplayPlugin->getSensorResetMatrix(); + } + + glm::mat4 trackedHandToAvatar = + glm::inverse(inputCalibrationData.avatarMat) * + inputCalibrationData.sensorToWorldMat * + sensorResetMatrix; +#endif + + // roll all the old points in the rolling average + memmove(&(_handPoints[1]), + &(_handPoints[0]), + sizeof(_handPoints[0]) * (NUMBER_OF_HAND_TRACKER_SMOOTHING_FRAMES - 1)); + + for (int handIndex = 0; handIndex < resultsHandCount; handIndex++) { + bool isLeftHand = results[handIndex].isLeft; + + vr::ETrackedControllerRole controllerRole = + isLeftHand ? vr::TrackedControllerRole_LeftHand : vr::TrackedControllerRole_RightHand; + if (isHandControllerActive(_system, controllerRole)) { + continue; // if the controller for this hand is tracked, ignore camera hand tracking + } + + int hand = isLeftHand ? 0 : 1; + for (int pointIndex = 0; pointIndex < NUMBER_OF_HAND_POINTS; pointIndex++) { + glm::vec3 pos(results[handIndex].points[3 * pointIndex], + results[handIndex].points[3 * pointIndex + 1], + -results[handIndex].points[3 * pointIndex + 2]); + _handPoints[0][hand][pointIndex] = transformPoint(trackedHandToAvatar, pos); + } + + glm::vec3 wristPos = getRollingAverageHandPoint(hand, 0); + glm::vec3 thumb2 = getRollingAverageHandPoint(hand, 2); + glm::vec3 pinkie1 = getRollingAverageHandPoint(hand, 17); + glm::vec3 palmFacing = glm::cross(pinkie1 - wristPos, thumb2 - wristPos); // z axis + + _inputDevice->_poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = + trackedHandDataToPose(hand, palmFacing, 0, 9); + trackFinger(hand, 1, 2, 3, 4, + isLeftHand ? controller::LEFT_HAND_THUMB1 : controller::RIGHT_HAND_THUMB1, + isLeftHand ? controller::LEFT_HAND_THUMB2 : controller::RIGHT_HAND_THUMB2, + isLeftHand ? controller::LEFT_HAND_THUMB3 : controller::RIGHT_HAND_THUMB3, + isLeftHand ? controller::LEFT_HAND_THUMB4 : controller::RIGHT_HAND_THUMB4); + trackFinger(hand, 5, 6, 7, 8, + isLeftHand ? controller::LEFT_HAND_INDEX1 : controller::RIGHT_HAND_INDEX1, + isLeftHand ? controller::LEFT_HAND_INDEX2 : controller::RIGHT_HAND_INDEX2, + isLeftHand ? controller::LEFT_HAND_INDEX3 : controller::RIGHT_HAND_INDEX3, + isLeftHand ? controller::LEFT_HAND_INDEX4 : controller::RIGHT_HAND_INDEX4); + trackFinger(hand, 9, 10, 11, 12, + isLeftHand ? controller::LEFT_HAND_MIDDLE1 : controller::RIGHT_HAND_MIDDLE1, + isLeftHand ? controller::LEFT_HAND_MIDDLE2 : controller::RIGHT_HAND_MIDDLE2, + isLeftHand ? controller::LEFT_HAND_MIDDLE3 : controller::RIGHT_HAND_MIDDLE3, + isLeftHand ? controller::LEFT_HAND_MIDDLE4 : controller::RIGHT_HAND_MIDDLE4); + trackFinger(hand, 13, 14, 15, 16, + isLeftHand ? controller::LEFT_HAND_RING1 : controller::RIGHT_HAND_RING1, + isLeftHand ? controller::LEFT_HAND_RING2 : controller::RIGHT_HAND_RING2, + isLeftHand ? controller::LEFT_HAND_RING3 : controller::RIGHT_HAND_RING3, + isLeftHand ? controller::LEFT_HAND_RING4 : controller::RIGHT_HAND_RING4); + trackFinger(hand, 17, 18, 19, 20, + isLeftHand ? controller::LEFT_HAND_PINKY1 : controller::RIGHT_HAND_PINKY1, + isLeftHand ? controller::LEFT_HAND_PINKY2 : controller::RIGHT_HAND_PINKY2, + isLeftHand ? controller::LEFT_HAND_PINKY3 : controller::RIGHT_HAND_PINKY3, + isLeftHand ? controller::LEFT_HAND_PINKY4 : controller::RIGHT_HAND_PINKY4); + } + } + _lastHandTrackerFrameIndex = handTrackerFrameIndex; +} + + void ViveControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { if (!_system) { @@ -297,6 +768,12 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; } + + if (_viveProEye) { + updateEyeTracker(deltaTime, inputCalibrationData); + } + + updateCameraHandTracker(deltaTime, inputCalibrationData); } void ViveControllerManager::loadSettings() { @@ -830,9 +1307,7 @@ void ViveControllerManager::InputDevice::handleHmd(uint32_t deviceIndex, const c void ViveControllerManager::InputDevice::handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand) { - if (_system->IsTrackedDeviceConnected(deviceIndex) && - _system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_Controller && - _nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid) { + if (isDeviceIndexActive(_system, deviceIndex) && _nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid) { // process pose const mat4& mat = _nextSimPoseData.poses[deviceIndex]; @@ -1401,9 +1876,52 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI makePair(LEFT_GRIP, "LeftGrip"), makePair(RIGHT_GRIP, "RightGrip"), - // 3d location of controller + // 3d location of left controller and fingers makePair(LEFT_HAND, "LeftHand"), + makePair(LEFT_HAND_THUMB1, "LeftHandThumb1"), + makePair(LEFT_HAND_THUMB2, "LeftHandThumb2"), + makePair(LEFT_HAND_THUMB3, "LeftHandThumb3"), + makePair(LEFT_HAND_THUMB4, "LeftHandThumb4"), + makePair(LEFT_HAND_INDEX1, "LeftHandIndex1"), + makePair(LEFT_HAND_INDEX2, "LeftHandIndex2"), + makePair(LEFT_HAND_INDEX3, "LeftHandIndex3"), + makePair(LEFT_HAND_INDEX4, "LeftHandIndex4"), + makePair(LEFT_HAND_MIDDLE1, "LeftHandMiddle1"), + makePair(LEFT_HAND_MIDDLE2, "LeftHandMiddle2"), + makePair(LEFT_HAND_MIDDLE3, "LeftHandMiddle3"), + makePair(LEFT_HAND_MIDDLE4, "LeftHandMiddle4"), + makePair(LEFT_HAND_RING1, "LeftHandRing1"), + makePair(LEFT_HAND_RING2, "LeftHandRing2"), + makePair(LEFT_HAND_RING3, "LeftHandRing3"), + makePair(LEFT_HAND_RING4, "LeftHandRing4"), + makePair(LEFT_HAND_PINKY1, "LeftHandPinky1"), + makePair(LEFT_HAND_PINKY2, "LeftHandPinky2"), + makePair(LEFT_HAND_PINKY3, "LeftHandPinky3"), + makePair(LEFT_HAND_PINKY4, "LeftHandPinky4"), + + // 3d location of right controller and fingers makePair(RIGHT_HAND, "RightHand"), + makePair(RIGHT_HAND_THUMB1, "RightHandThumb1"), + makePair(RIGHT_HAND_THUMB2, "RightHandThumb2"), + makePair(RIGHT_HAND_THUMB3, "RightHandThumb3"), + makePair(RIGHT_HAND_THUMB4, "RightHandThumb4"), + makePair(RIGHT_HAND_INDEX1, "RightHandIndex1"), + makePair(RIGHT_HAND_INDEX2, "RightHandIndex2"), + makePair(RIGHT_HAND_INDEX3, "RightHandIndex3"), + makePair(RIGHT_HAND_INDEX4, "RightHandIndex4"), + makePair(RIGHT_HAND_MIDDLE1, "RightHandMiddle1"), + makePair(RIGHT_HAND_MIDDLE2, "RightHandMiddle2"), + makePair(RIGHT_HAND_MIDDLE3, "RightHandMiddle3"), + makePair(RIGHT_HAND_MIDDLE4, "RightHandMiddle4"), + makePair(RIGHT_HAND_RING1, "RightHandRing1"), + makePair(RIGHT_HAND_RING2, "RightHandRing2"), + makePair(RIGHT_HAND_RING3, "RightHandRing3"), + makePair(RIGHT_HAND_RING4, "RightHandRing4"), + makePair(RIGHT_HAND_PINKY1, "RightHandPinky1"), + makePair(RIGHT_HAND_PINKY2, "RightHandPinky2"), + makePair(RIGHT_HAND_PINKY3, "RightHandPinky3"), + makePair(RIGHT_HAND_PINKY4, "RightHandPinky4"), + makePair(LEFT_FOOT, "LeftFoot"), makePair(RIGHT_FOOT, "RightFoot"), makePair(HIPS, "Hips"), @@ -1411,6 +1929,10 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI makePair(HEAD, "Head"), makePair(LEFT_ARM, "LeftArm"), makePair(RIGHT_ARM, "RightArm"), + makePair(LEFT_EYE, "LeftEye"), + makePair(RIGHT_EYE, "RightEye"), + makePair(LEFT_EYE_BLINK, "LeftEyeBlink"), + makePair(RIGHT_EYE_BLINK, "RightEyeBlink"), // 16 tracked poses makePair(TRACKED_OBJECT_00, "TrackedObject00"), diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index dbd248dc53..66462cbe85 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -31,6 +31,23 @@ namespace vr { class IVRSystem; } +class ViveProEyeReadThread; + +class EyeDataBuffer { +public: + int getEyeDataResult { 0 }; + bool leftDirectionValid { false }; + bool rightDirectionValid { false }; + bool leftOpennessValid { false }; + bool rightOpennessValid { false }; + glm::vec3 leftEyeGaze; + glm::vec3 rightEyeGaze; + float leftEyeOpenness { 0.0f }; + float rightEyeOpenness { 0.0f }; +}; + + + class ViveControllerManager : public InputPlugin { Q_OBJECT public: @@ -49,12 +66,18 @@ public: bool isHeadController() const override { return true; } bool isHeadControllerMounted() const; + void enableGestureDetection(); + void disableGestureDetection(); + bool activate() override; void deactivate() override; QString getDeviceName() { return QString::fromStdString(_inputDevice->_headsetName); } void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } + void invalidateEyeInputs(); + void updateEyeTracker(float deltaTime, const controller::InputCalibrationData& inputCalibrationData); + void updateCameraHandTracker(float deltaTime, const controller::InputCalibrationData& inputCalibrationData); void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; virtual void saveSettings() const override; @@ -229,6 +252,24 @@ private: vr::IVRSystem* _system { nullptr }; std::shared_ptr _inputDevice { std::make_shared(_system) }; + bool _viveProEye { false }; + mutable std::recursive_mutex _getEyeDataLock; + std::shared_ptr _viveProEyeReadThread; + EyeDataBuffer _prevEyeData; + + bool _viveCameraHandTracker { false }; + int _lastHandTrackerFrameIndex { -1 }; + + const static int NUMBER_OF_HAND_TRACKER_SMOOTHING_FRAMES { 6 }; + const static int NUMBER_OF_HAND_POINTS { 21 }; + glm::vec3 _handPoints[NUMBER_OF_HAND_TRACKER_SMOOTHING_FRAMES][2][NUMBER_OF_HAND_POINTS]; // 2 for number of hands + glm::vec3 getRollingAverageHandPoint(int handIndex, int pointIndex) const; + controller::Pose trackedHandDataToPose(int hand, const glm::vec3& palmFacing, + int nearHandPositionIndex, int farHandPositionIndex); + void trackFinger(int hand, int jointIndex1, int jointIndex2, int jointIndex3, int jointIndex4, + controller::StandardPoseChannel joint1, controller::StandardPoseChannel joint2, + controller::StandardPoseChannel joint3, controller::StandardPoseChannel joint4); + static const char* NAME; }; From 67f4811c2b858bf0b4cab6ff153bc93c5e667582 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 18 Nov 2019 16:59:28 -0800 Subject: [PATCH 17/73] blink actions were renamed --- plugins/openvr/src/ViveControllerManager.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index b1d3b791d6..6e224f21fe 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -431,8 +431,8 @@ bool ViveControllerManager::isHeadControllerMounted() const { void ViveControllerManager::invalidateEyeInputs() { _inputDevice->_poseStateMap[controller::LEFT_EYE].valid = false; _inputDevice->_poseStateMap[controller::RIGHT_EYE].valid = false; - _inputDevice->_axisStateMap[controller::LEFT_EYE_BLINK].valid = false; - _inputDevice->_axisStateMap[controller::RIGHT_EYE_BLINK].valid = false; + _inputDevice->_axisStateMap[controller::EYEBLINK_L].valid = false; + _inputDevice->_axisStateMap[controller::EYEBLINK_R].valid = false; } @@ -510,16 +510,16 @@ void ViveControllerManager::updateEyeTracker(float deltaTime, const controller:: // in hifi, 0 is open 1 is closed. in SRanipal 1 is open, 0 is closed. if (eyeDataBuffer.leftOpennessValid) { - _inputDevice->_axisStateMap[controller::LEFT_EYE_BLINK] = + _inputDevice->_axisStateMap[controller::EYEBLINK_L] = controller::AxisValue(1.0f - eyeDataBuffer.leftEyeOpenness, now); } else { - _inputDevice->_poseStateMap[controller::LEFT_EYE_BLINK].valid = false; + _inputDevice->_poseStateMap[controller::EYEBLINK_L].valid = false; } if (eyeDataBuffer.rightOpennessValid) { - _inputDevice->_axisStateMap[controller::RIGHT_EYE_BLINK] = + _inputDevice->_axisStateMap[controller::EYEBLINK_R] = controller::AxisValue(1.0f - eyeDataBuffer.rightEyeOpenness, now); } else { - _inputDevice->_poseStateMap[controller::RIGHT_EYE_BLINK].valid = false; + _inputDevice->_poseStateMap[controller::EYEBLINK_R].valid = false; } } @@ -1931,8 +1931,8 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI makePair(RIGHT_ARM, "RightArm"), makePair(LEFT_EYE, "LeftEye"), makePair(RIGHT_EYE, "RightEye"), - makePair(LEFT_EYE_BLINK, "LeftEyeBlink"), - makePair(RIGHT_EYE_BLINK, "RightEyeBlink"), + makePair(EYEBLINK_L, "EyeBlink_L"), + makePair(EYEBLINK_R, "EyeBlink_R"), // 16 tracked poses makePair(TRACKED_OBJECT_00, "TrackedObject00"), From 3f249dfcd8cca9688e4748adcc4b530b4de751c8 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 20 Nov 2019 09:48:52 -0800 Subject: [PATCH 18/73] fix controller-module run order so that equip works again --- .../system/controllers/controllerModules/disableOtherModule.js | 2 +- scripts/system/controllers/controllerModules/equipEntity.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/controllerModules/disableOtherModule.js b/scripts/system/controllers/controllerModules/disableOtherModule.js index 7636c56f65..549735b658 100644 --- a/scripts/system/controllers/controllerModules/disableOtherModule.js +++ b/scripts/system/controllers/controllerModules/disableOtherModule.js @@ -17,7 +17,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.hand = hand; this.disableModules = false; this.parameters = makeDispatcherModuleParameters( - 90, + 82, this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"], diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index 54b56ff271..534231f407 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -278,7 +278,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.handHasBeenRightsideUp = false; this.parameters = makeDispatcherModuleParameters( - 115, + 85, this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip"] : ["leftHand", "leftHandEquip"], [], 100); From cb83e4e6c9852abceee7982c716828ef7caf0e85 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 13 Dec 2019 16:22:13 -0800 Subject: [PATCH 19/73] adjust to new eye matrix --- plugins/openvr/src/ViveControllerManager.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 6e224f21fe..24a0c7588b 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -477,17 +477,17 @@ void ViveControllerManager::updateEyeTracker(float deltaTime, const controller:: // in the data from sranipal, left=+x, up=+y, forward=+z mat4 localLeftEyeMat = glm::lookAt(vec3(0.0f, 0.0f, 0.0f), - glm::vec3(-eyeDataBuffer.leftEyeGaze[0], + glm::vec3(eyeDataBuffer.leftEyeGaze[0], eyeDataBuffer.leftEyeGaze[1], - eyeDataBuffer.leftEyeGaze[2]), + -eyeDataBuffer.leftEyeGaze[2]), vec3(0.0f, 1.0f, 0.0f)); quat localLeftEyeRot = glm::quat_cast(localLeftEyeMat); quat avatarLeftEyeRot = _inputDevice->_poseStateMap[controller::HEAD].rotation * localLeftEyeRot; mat4 localRightEyeMat = glm::lookAt(vec3(0.0f, 0.0f, 0.0f), - glm::vec3(-eyeDataBuffer.rightEyeGaze[0], + glm::vec3(eyeDataBuffer.rightEyeGaze[0], eyeDataBuffer.rightEyeGaze[1], - eyeDataBuffer.rightEyeGaze[2]), + -eyeDataBuffer.rightEyeGaze[2]), vec3(0.0f, 1.0f, 0.0f)); quat localRightEyeRot = glm::quat_cast(localRightEyeMat); quat avatarRightEyeRot = _inputDevice->_poseStateMap[controller::HEAD].rotation * localRightEyeRot; From 2fdbd0ab7898520f9e62637fba417fbaed403943 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 18 Feb 2020 09:52:49 -0800 Subject: [PATCH 20/73] update urls for vive eye and hand tracking libraries, fix cmake for same --- cmake/ports/aristo/portfile.cmake | 4 +++- cmake/ports/sranipal/CONTROL | 2 +- cmake/ports/sranipal/portfile.cmake | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cmake/ports/aristo/portfile.cmake b/cmake/ports/aristo/portfile.cmake index 532e1304f4..94efe6f7ea 100644 --- a/cmake/ports/aristo/portfile.cmake +++ b/cmake/ports/aristo/portfile.cmake @@ -2,10 +2,12 @@ include(vcpkg_common_functions) set(ARISTO_VERSION 0.8.1) set(MASTER_COPY_SOURCE_PATH ${CURRENT_BUILDTREES_DIR}/src) +file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_BUILD_ASSETS.txt" EXTERNAL_BUILD_ASSETS) + if (WIN32) vcpkg_download_distfile( ARISTO_SOURCE_ARCHIVE - URLS https://athena-public.s3.amazonaws.com/seth/aristo-0.8.1-windows.zip + URLS "${EXTERNAL_BUILD_ASSETS}/seth/aristo-0.8.1-windows.zip" SHA512 05179c63b72a1c9f5be8a7a2b7389025da683400dbf819e5a6199dd6473c56774d2885182dc5a11cb6324058d228a4ead832222e8b3e1bebaa4c61982e85f0a8 FILENAME aristo-0.8.1-windows.zip ) diff --git a/cmake/ports/sranipal/CONTROL b/cmake/ports/sranipal/CONTROL index 3f878b1c4d..b7d510595e 100644 --- a/cmake/ports/sranipal/CONTROL +++ b/cmake/ports/sranipal/CONTROL @@ -1,3 +1,3 @@ Source: sranipal Version: 1.1.0.1 -Description: SRanipal +Description: super reality animation pal! diff --git a/cmake/ports/sranipal/portfile.cmake b/cmake/ports/sranipal/portfile.cmake index da4646be1a..2e6acea361 100644 --- a/cmake/ports/sranipal/portfile.cmake +++ b/cmake/ports/sranipal/portfile.cmake @@ -2,10 +2,12 @@ include(vcpkg_common_functions) set(SRANIPAL_VERSION 1.1.0.1) set(MASTER_COPY_SOURCE_PATH ${CURRENT_BUILDTREES_DIR}/src) +file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_BUILD_ASSETS.txt" EXTERNAL_BUILD_ASSETS) + if (WIN32) vcpkg_download_distfile( SRANIPAL_SOURCE_ARCHIVE - URLS https://athena-public.s3.amazonaws.com/seth/sranipal-1.1.0.1-windows.zip + URLS "${EXTERNAL_BUILD_ASSETS}/seth/sranipal-1.1.0.1-windows.zip" SHA512 b09ce012abe4e3c71e8e69626bdd7823ff6576601a821ab365275f2764406a3e5f7b65fcf2eb1d0962eff31eb5958a148b00901f67c229dc6ace56eb5e6c9e1b FILENAME sranipal-1.1.0.1-windows.zip ) From 10830cf68cdfab6fc5fe1cf28419704889c6184a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 26 Apr 2020 14:50:43 -0700 Subject: [PATCH 21/73] recover from rebase errors --- cmake/ports/hifi-client-deps/CONTROL | 2 +- scripts/defaultScripts.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cmake/ports/hifi-client-deps/CONTROL b/cmake/ports/hifi-client-deps/CONTROL index 5b9b0bcce0..7070cb6fb9 100644 --- a/cmake/ports/hifi-client-deps/CONTROL +++ b/cmake/ports/hifi-client-deps/CONTROL @@ -1,4 +1,4 @@ Source: hifi-client-deps Version: 0.1 Description: Collected dependencies for High Fidelity applications -Build-Depends: hifi-deps, glslang, nlohmann-json, openvr (windows), sdl2 (!android), spirv-cross (!android), spirv-tools (!android), vulkanmemoryallocator, aristo (windows), sranipal (windows) +Build-Depends: hifi-deps, aristo (windows), glslang, liblo (windows), nlohmann-json, openvr (windows), quazip (!android), sdl2 (!android), spirv-cross (!android), spirv-tools (!android), sranipal (windows), vulkanmemoryallocator diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 0efabd7773..a19bb9c41a 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -34,6 +34,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/emote.js", "system/miniTablet.js", "system/audioMuteOverlay.js", + "system/inspect.js", "system/keyboardShortcuts/keyboardShortcuts.js", "system/hand-track-walk.js" ]; From c0e36b796026515552cea85013faa85feee8ce9f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 16 May 2020 14:37:40 +1200 Subject: [PATCH 22/73] Address code review comments --- .../hifiLeapMotion/src/LeapMotionPlugin.cpp | 9 --- plugins/openvr/src/ViveControllerManager.cpp | 60 ++++++++++++++----- plugins/openvr/src/ViveControllerManager.h | 1 - 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp b/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp index 3f0cad02b4..0989d28d23 100644 --- a/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp +++ b/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp @@ -305,15 +305,6 @@ void LeapMotionPlugin::InputDevice::update(float deltaTime, const controller::In prevRot = controllerToAvatarRotation * prevJoints[i].orientation * ZERO_HAND_ORIENTATION; } - // glm::vec3 linearVelocity, angularVelocity; - // if (i < prevJoints.size()) { - // linearVelocity = (pos - (prevJoints[i].position * METERS_PER_CENTIMETER)) / deltaTime; // m/s - // glm::quat dQ = rot * glm::inverse(prevRot); - // float angle = glm::angle(dQ); - // glm::vec3 axis = glm::axis(dQ); - // angularVelocity = (angle / deltaTime) * axis; - // } - glm::vec3 linearVelocity, angularVelocity; if (i < prevJoints.size()) { linearVelocity = (pos - (prevJoints[i].position * METERS_PER_CENTIMETER)) / deltaTime; // m/s diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 24a0c7588b..2c9eb296ab 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -79,6 +79,32 @@ static const int SECOND_FOOT = 1; static const int HIP = 2; static const int CHEST = 3; +enum ViveHandJointIndex { + HAND = 0, + THUMB_1, + THUMB_2, + THUMB_3, + THUMB_4, + INDEX_1, + INDEX_2, + INDEX_3, + INDEX_4, + MIDDLE_1, + MIDDLE_2, + MIDDLE_3, + MIDDLE_4, + RING_1, + RING_2, + RING_3, + RING_4, + PINKY_1, + PINKY_2, + PINKY_3, + PINKY_4, + + Size +}; + const char* ViveControllerManager::NAME { "OpenVR" }; const std::map TRACKING_RESULT_TO_STRING = { @@ -307,7 +333,7 @@ void ViveControllerManager::enableGestureDetection() { if (_viveCameraHandTracker) { return; } - if (!ViveSR::anipal::Eye::IsViveProEye()) { + if (!ViveSR::anipal::Eye::IsViveProEye()) { return; } @@ -368,7 +394,7 @@ bool ViveControllerManager::activate() { userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; - if (ViveSR::anipal::Eye::IsViveProEye()) { + if (ViveSR::anipal::Eye::IsViveProEye()) { qDebug() << "Vive Pro eye-tracking detected"; int error = ViveSR::anipal::Initial(ViveSR::anipal::Eye::ANIPAL_TYPE_EYE, NULL); @@ -575,9 +601,9 @@ void ViveControllerManager::trackFinger(int hand, int jointIndex1, int jointInde glm::vec3 point3 = getRollingAverageHandPoint(hand, jointIndex3); glm::vec3 point4 = getRollingAverageHandPoint(hand, jointIndex4); - glm::vec3 wristPos = getRollingAverageHandPoint(hand, 0); - glm::vec3 thumb2 = getRollingAverageHandPoint(hand, 2); - glm::vec3 pinkie1 = getRollingAverageHandPoint(hand, 17); + glm::vec3 wristPos = getRollingAverageHandPoint(hand, ViveHandJointIndex::HAND); + glm::vec3 thumb2 = getRollingAverageHandPoint(hand, ViveHandJointIndex::THUMB_2); + glm::vec3 pinkie1 = getRollingAverageHandPoint(hand, ViveHandJointIndex::PINKY_1); // 1st glm::vec3 palmFacing = glm::normalize(glm::cross(pinkie1 - wristPos, thumb2 - wristPos)); @@ -651,6 +677,7 @@ void ViveControllerManager::updateCameraHandTracker(float deltaTime, int handTrackerFrameIndex { -1 }; int resultsHandCount = GetGestureResult(&results, &handTrackerFrameIndex); + // FIXME: Why the commented-out condition? if (handTrackerFrameIndex >= 0 /* && handTrackerFrameIndex != _lastHandTrackerFrameIndex */) { #ifdef HAND_TRACKER_USE_EXTERNAL_TRANSFORM glm::mat4 trackedHandToAvatar = @@ -695,34 +722,39 @@ void ViveControllerManager::updateCameraHandTracker(float deltaTime, _handPoints[0][hand][pointIndex] = transformPoint(trackedHandToAvatar, pos); } - glm::vec3 wristPos = getRollingAverageHandPoint(hand, 0); - glm::vec3 thumb2 = getRollingAverageHandPoint(hand, 2); - glm::vec3 pinkie1 = getRollingAverageHandPoint(hand, 17); + glm::vec3 wristPos = getRollingAverageHandPoint(hand, ViveHandJointIndex::HAND); + glm::vec3 thumb2 = getRollingAverageHandPoint(hand, ViveHandJointIndex::THUMB_2); + glm::vec3 pinkie1 = getRollingAverageHandPoint(hand, ViveHandJointIndex::PINKY_1); glm::vec3 palmFacing = glm::cross(pinkie1 - wristPos, thumb2 - wristPos); // z axis _inputDevice->_poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = - trackedHandDataToPose(hand, palmFacing, 0, 9); - trackFinger(hand, 1, 2, 3, 4, + trackedHandDataToPose(hand, palmFacing, ViveHandJointIndex::HAND, ViveHandJointIndex::MIDDLE_1); + trackFinger(hand, ViveHandJointIndex::THUMB_1, ViveHandJointIndex::THUMB_2, ViveHandJointIndex::THUMB_3, + ViveHandJointIndex::THUMB_4, isLeftHand ? controller::LEFT_HAND_THUMB1 : controller::RIGHT_HAND_THUMB1, isLeftHand ? controller::LEFT_HAND_THUMB2 : controller::RIGHT_HAND_THUMB2, isLeftHand ? controller::LEFT_HAND_THUMB3 : controller::RIGHT_HAND_THUMB3, isLeftHand ? controller::LEFT_HAND_THUMB4 : controller::RIGHT_HAND_THUMB4); - trackFinger(hand, 5, 6, 7, 8, + trackFinger(hand, ViveHandJointIndex::INDEX_1, ViveHandJointIndex::INDEX_2, ViveHandJointIndex::INDEX_3, + ViveHandJointIndex::INDEX_4, isLeftHand ? controller::LEFT_HAND_INDEX1 : controller::RIGHT_HAND_INDEX1, isLeftHand ? controller::LEFT_HAND_INDEX2 : controller::RIGHT_HAND_INDEX2, isLeftHand ? controller::LEFT_HAND_INDEX3 : controller::RIGHT_HAND_INDEX3, isLeftHand ? controller::LEFT_HAND_INDEX4 : controller::RIGHT_HAND_INDEX4); - trackFinger(hand, 9, 10, 11, 12, + trackFinger(hand, ViveHandJointIndex::MIDDLE_1, ViveHandJointIndex::MIDDLE_2, ViveHandJointIndex::MIDDLE_3, + ViveHandJointIndex::MIDDLE_4, isLeftHand ? controller::LEFT_HAND_MIDDLE1 : controller::RIGHT_HAND_MIDDLE1, isLeftHand ? controller::LEFT_HAND_MIDDLE2 : controller::RIGHT_HAND_MIDDLE2, isLeftHand ? controller::LEFT_HAND_MIDDLE3 : controller::RIGHT_HAND_MIDDLE3, isLeftHand ? controller::LEFT_HAND_MIDDLE4 : controller::RIGHT_HAND_MIDDLE4); - trackFinger(hand, 13, 14, 15, 16, + trackFinger(hand, ViveHandJointIndex::RING_1, ViveHandJointIndex::RING_2, ViveHandJointIndex::RING_3, + ViveHandJointIndex::RING_4, isLeftHand ? controller::LEFT_HAND_RING1 : controller::RIGHT_HAND_RING1, isLeftHand ? controller::LEFT_HAND_RING2 : controller::RIGHT_HAND_RING2, isLeftHand ? controller::LEFT_HAND_RING3 : controller::RIGHT_HAND_RING3, isLeftHand ? controller::LEFT_HAND_RING4 : controller::RIGHT_HAND_RING4); - trackFinger(hand, 17, 18, 19, 20, + trackFinger(hand, ViveHandJointIndex::PINKY_1, ViveHandJointIndex::PINKY_2, ViveHandJointIndex::PINKY_3, + ViveHandJointIndex::PINKY_4, isLeftHand ? controller::LEFT_HAND_PINKY1 : controller::RIGHT_HAND_PINKY1, isLeftHand ? controller::LEFT_HAND_PINKY2 : controller::RIGHT_HAND_PINKY2, isLeftHand ? controller::LEFT_HAND_PINKY3 : controller::RIGHT_HAND_PINKY3, diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 66462cbe85..714be87842 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -253,7 +253,6 @@ private: std::shared_ptr _inputDevice { std::make_shared(_system) }; bool _viveProEye { false }; - mutable std::recursive_mutex _getEyeDataLock; std::shared_ptr _viveProEyeReadThread; EyeDataBuffer _prevEyeData; From 2ec541f2effe02ad88c882d78ca49081be2d4d93 Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Wed, 20 May 2020 22:31:02 +0200 Subject: [PATCH 23/73] Fix Linux default audio device debug messages Linux had no default audio implementation, implemented using QAudioDeviceInfo. --- libraries/audio-client/src/AudioClient.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index d9ad82fb51..4e8c88560b 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -610,6 +610,14 @@ QString defaultAudioDeviceName(QAudio::Mode mode) { << " [" << deviceName << "] [" << "]"; #endif +#endif + +#ifdef Q_OS_LINUX + if ( mode == QAudio::AudioInput ) { + deviceName = QAudioDeviceInfo::defaultInputDevice().deviceName(); + } else { + deviceName = QAudioDeviceInfo::defaultOutputDevice().deviceName(); + } #endif return deviceName; } From d406b81b47e9ea64385a638f45c415379c665a25 Mon Sep 17 00:00:00 2001 From: motofckr9k Date: Fri, 22 May 2020 20:35:27 +0200 Subject: [PATCH 24/73] Big rebrand --- BUILD_WIN.md | 4 +- CODING_STANDARD.md | 6 +- INSTALL.md | 4 +- LICENSE | 4 +- cmake/installer/installer-header.bmp | Bin 102656 -> 136938 bytes cmake/installer/installer.ico | Bin 287934 -> 270398 bytes cmake/installer/uninstaller-header.bmp | Bin 102656 -> 136938 bytes cmake/macros/GenerateInstallers.cmake | 12 +- cmake/macros/SetPackagingParameters.cmake | 16 +- cmake/templates/NSIS.template.in | 8 +- interface/CMakeLists.txt | 2 +- .../resources/images/Loading-Inner-H.png | Bin 7484 -> 4048 bytes .../resources/images/about-projectathena.png | Bin 11996 -> 0 bytes interface/resources/images/about-vircadia.png | Bin 0 -> 7004 bytes .../resources/images/hifi-logo-blackish.svg | 123 -------------- interface/resources/images/hifi-logo.svg | 58 ------- .../images/project-athena-banner-color2.svg | 157 ------------------ .../resources/images/vircadia-banner.svg | 95 +++++++++++ interface/resources/images/vircadia-logo.svg | 60 +++++++ interface/resources/qml/LoginDialog.qml | 2 +- .../resources/qml/LoginDialog/SignUpBody.qml | 4 +- .../qml/LoginDialog/UsernameCollisionBody.qml | 4 +- interface/resources/qml/UpdateDialog.qml | 2 +- .../qml/dialogs/TabletLoginDialog.qml | 2 +- .../avatarPackager/AvatarPackagerHeader.qml | 2 +- .../qml/hifi/dialogs/TabletAboutDialog.qml | 6 +- interface/src/Application.cpp | 6 +- interface/src/Menu.cpp | 14 +- interface/src/avatar/AvatarProject.h | 2 +- interface/src/main.cpp | 2 +- libraries/networking/src/MetaverseAPI.cpp | 2 +- libraries/networking/src/MetaverseAPI.h | 2 +- pkg-scripts/athena-server.spec | 10 +- pkg-scripts/server-control | 6 +- .../html/entityProperties.html | 2 +- scripts/system/html/css/tabs.css | 2 +- scripts/system/more/app-more.js | 4 +- scripts/system/more/css/styles.css | 2 +- scripts/system/more/more.html | 2 +- scripts/system/tablet-goto.js | 2 +- 40 files changed, 223 insertions(+), 406 deletions(-) delete mode 100644 interface/resources/images/about-projectathena.png create mode 100644 interface/resources/images/about-vircadia.png delete mode 100644 interface/resources/images/hifi-logo-blackish.svg delete mode 100644 interface/resources/images/hifi-logo.svg delete mode 100644 interface/resources/images/project-athena-banner-color2.svg create mode 100644 interface/resources/images/vircadia-banner.svg create mode 100644 interface/resources/images/vircadia-logo.svg diff --git a/BUILD_WIN.md b/BUILD_WIN.md index bdab7e6e6d..f74895b8a3 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -47,7 +47,7 @@ Download and install the latest version of CMake 3.15. Download the file named win64-x64 Installer from the [CMake Website](https://cmake.org/download/). You can access the installer on this [3.15 Version page](https://cmake.org/files/v3.15/). During installation, make sure to check "Add CMake to system PATH for all users" when prompted. ### Step 4. Create VCPKG environment variable -In the next step, you will use CMake to build Project Athena. By default, the CMake process builds dependency files in Windows' `%TEMP%` directory, which is periodically cleared by the operating system. To prevent you from having to re-build the dependencies in the event that Windows clears that directory, we recommend that you create a `HIFI_VCPKG_BASE` environment variable linked to a directory somewhere on your machine. That directory will contain all dependency files until you manually remove them. +In the next step, you will use CMake to build Vircadia. By default, the CMake process builds dependency files in Windows' `%TEMP%` directory, which is periodically cleared by the operating system. To prevent you from having to re-build the dependencies in the event that Windows clears that directory, we recommend that you create a `HIFI_VCPKG_BASE` environment variable linked to a directory somewhere on your machine. That directory will contain all dependency files until you manually remove them. To create this variable: * Naviagte to 'Edit the System Environment Variables' Through the start menu. @@ -98,7 +98,7 @@ Restart Visual Studio again. In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run from the menu bar `Debug > Start Debugging`. -Now, you should have a full build of Project Athena and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow. +Now, you should have a full build of Vircadia and be able to run the Interface using Visual Studio. Please check our [Docs](https://docs.vircadia.dev/) for more information regarding the programming workflow. Note: You can also run Interface by launching it from command line or File Explorer from `%HIFI_DIR%\build\interface\Release\interface.exe` diff --git a/CODING_STANDARD.md b/CODING_STANDARD.md index fd1843e981..e582ac305f 100644 --- a/CODING_STANDARD.md +++ b/CODING_STANDARD.md @@ -976,9 +976,9 @@ while (true) { #### [4.3.4] Source files (header and implementation) must include a boilerplate. -Boilerplates should include the filename, location, creator, copyright Project Athena contributors, and Apache 2.0 License +Boilerplates should include the filename, location, creator, copyright Vircadia contributors, and Apache 2.0 License information. This should be placed at the top of the file. If editing an existing file that is copyright High Fidelity, add a -second copyright line, copyright Project Athena contributors. +second copyright line, copyright Vircadia contributors. ```cpp // @@ -987,7 +987,7 @@ second copyright line, copyright Project Athena contributors. // // Created by Stephen Birarda on 15 Feb 2013. // Copyright 2013 High Fidelity, Inc. -// Copyright 2020 Project Athena contributors. +// Copyright 2020 Vircadia contributors. // // This is where you could place an optional one line comment about the file. // diff --git a/INSTALL.md b/INSTALL.md index 10858200e7..c0418a7521 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,10 +1,10 @@ # Creating an Installer -Follow the [build guide](BUILD.md) to figure out how to build Project Athena for your platform. +Follow the [build guide](BUILD.md) to figure out how to build Vircadia for your platform. During generation, CMake should produce an `install` target and a `package` target. -The `install` target will copy the Project Athena targets and their dependencies to your `CMAKE_INSTALL_PREFIX`. +The `install` target will copy the Vircadia targets and their dependencies to your `CMAKE_INSTALL_PREFIX`. This variable is set by the `project(hifi)` command in `CMakeLists.txt` to `C:/Program Files/hifi` and stored in `build/CMakeCache.txt` ### Packaging diff --git a/LICENSE b/LICENSE index f88e751de5..51e1023f21 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ Copyright (c) 2013-2019, High Fidelity, Inc. -Copyright (c) 2019-2020, Project Athena Contributors. +Copyright (c) 2019-2020, Vircadia Contributors. All rights reserved. -https://projectathena.io +https://vircadia.com/ Licensed under the Apache License version 2.0 (the "License"); You may not use this software except in compliance with the License. diff --git a/cmake/installer/installer-header.bmp b/cmake/installer/installer-header.bmp index 2d262d3ef9d4b699eead2193bfab5e8ead4bc004..52fd28a9333d86646684d8f301dfd051abea8a41 100644 GIT binary patch literal 136938 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 Tg;ll=tn|;kvd3(B-v9psyu<1U literal 102656 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 Tg;ll=tn|;kvd3(B-v9psyu<1U literal 102656 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 O08o%omadaD4*5Sw2v@WK literal 0 HcmV?d00001 diff --git a/interface/resources/images/hifi-logo-blackish.svg b/interface/resources/images/hifi-logo-blackish.svg deleted file mode 100644 index 60bfb3d418..0000000000 --- a/interface/resources/images/hifi-logo-blackish.svg +++ /dev/null @@ -1,123 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/interface/resources/images/hifi-logo.svg b/interface/resources/images/hifi-logo.svg deleted file mode 100644 index e5d66d8f18..0000000000 --- a/interface/resources/images/hifi-logo.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/images/project-athena-banner-color2.svg b/interface/resources/images/project-athena-banner-color2.svg deleted file mode 100644 index b41a980fe0..0000000000 --- a/interface/resources/images/project-athena-banner-color2.svg +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - image/svg+xml - - Artboard 1 - - - - - - - - - - - - - - - - - - - Artboard 1 - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/images/vircadia-banner.svg b/interface/resources/images/vircadia-banner.svg new file mode 100644 index 0000000000..d954a96192 --- /dev/null +++ b/interface/resources/images/vircadia-banner.svg @@ -0,0 +1,95 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/interface/resources/images/vircadia-logo.svg b/interface/resources/images/vircadia-logo.svg new file mode 100644 index 0000000000..10645c4120 --- /dev/null +++ b/interface/resources/images/vircadia-logo.svg @@ -0,0 +1,60 @@ + +image/svg+xml + + + + + + + + + + + \ No newline at end of file diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index fbc8f9495a..ec98498db6 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -85,7 +85,7 @@ FocusScope { Image { id: banner anchors.centerIn: parent - source: "../images/project-athena-banner-color2.svg" + source: "../images/vircadia-banner.svg" horizontalAlignment: Image.AlignHCenter } } diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index 7347464f4e..fcb47c3534 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -23,7 +23,7 @@ Item { clip: true height: root.height width: root.width - readonly property string termsContainerText: qsTr("By signing up, you agree to Project Athena's Terms of Service") + readonly property string termsContainerText: qsTr("By signing up, you agree to Vircadia's Terms of Service") property int textFieldHeight: 31 property string fontFamily: "Raleway" property int fontSize: 15 @@ -395,7 +395,7 @@ Item { text: signUpBody.termsContainerText Component.onCompleted: { // with the link. - termsText.text = qsTr("By signing up, you agree to Project Athena's Terms of Service") + termsText.text = qsTr("By signing up, you agree to Vircadia's Terms of Service") } } diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index 8b3c878d46..9710723bed 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -19,7 +19,7 @@ import TabletScriptingInterface 1.0 Item { id: usernameCollisionBody clip: true - readonly property string termsContainerText: qsTr("By creating this user profile, you agree to Project Athena's Terms of Service") + readonly property string termsContainerText: qsTr("By creating this user profile, you agree to Vircadia's Terms of Service") width: root.width height: root.height readonly property string fontFamily: "Raleway" @@ -218,7 +218,7 @@ Item { text: usernameCollisionBody.termsContainerText Component.onCompleted: { // with the link. - termsText.text = qsTr("By creating this user profile, you agree to Project Athena's Terms of Service") + termsText.text = qsTr("By creating this user profile, you agree to Vircadia's Terms of Service") } } diff --git a/interface/resources/qml/UpdateDialog.qml b/interface/resources/qml/UpdateDialog.qml index 9c22d0b65b..c3a7a45c69 100644 --- a/interface/resources/qml/UpdateDialog.qml +++ b/interface/resources/qml/UpdateDialog.qml @@ -47,7 +47,7 @@ ScrollingWindow { Image { id: logo - source: "../images/hifi-logo.svg" + source: "../images/vircadia-logo.svg" width: updateDialog.logoSize height: updateDialog.logoSize anchors { diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index a7e5c236ca..12198caf08 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -129,7 +129,7 @@ FocusScope { Image { id: banner anchors.centerIn: parent - source: "../../images/project-athena-banner-color2.svg" + source: "../../images/vircadia-banner.svg" horizontalAlignment: Image.AlignHCenter } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index edb862b023..128ef61c75 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -128,7 +128,7 @@ ShadowRectangle { } } - // FIXME: Link to a Project Athena version of the video. + // FIXME: Link to a Vircadias version of the video. /* RalewayButton { id: video diff --git a/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml b/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml index 2be66442ce..c761312152 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml @@ -25,7 +25,7 @@ Rectangle { Image { sourceSize.width: 295 sourceSize.height: 75 - source: "../../../images/about-projectathena.png" + source: "../../../images/about-vircadia.png" } Item { height: 30; width: 1 } Column { @@ -53,7 +53,7 @@ Rectangle { textFormat: Text.StyledText linkColor: "#00B4EF" color: "white" - text: "Project Athena Github." + text: "Vircadia Github." size: 20 onLinkActivated: { HiFiAbout.openUrl("https:/github.com/kasenvr/project-athena"); @@ -116,7 +116,7 @@ Rectangle { Item { height: 20; width: 1 } RalewayRegular { color: "white" - text: "© 2019 - 2020 Project Athena Contributors." + text: "© 2019 - 2020 Vircadia Contributors." size: 14 } RalewayRegular { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1b22dbd329..5b681faf5e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1139,7 +1139,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Graphik-SemiBold.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Graphik-Regular.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Graphik-Medium.ttf"); - _window->setWindowTitle("Project Athena"); + _window->setWindowTitle("Vircadia"); Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us @@ -3166,7 +3166,7 @@ void Application::showLoginScreen() { QJsonObject loginData = {}; loginData["action"] = "login dialog popped up"; UserActivityLogger::getInstance().logAction("encourageLoginDialog", loginData); - _window->setWindowTitle("Project Athena"); + _window->setWindowTitle("Vircadia"); } else { resumeAfterLoginDialogActionTaken(); } @@ -7063,7 +7063,7 @@ void Application::updateWindowTitle() const { auto accountManager = DependencyManager::get(); auto isInErrorState = nodeList->getDomainHandler().isInErrorState(); - QString buildVersion = " - Project Athena v0.86.0 K2 - " + QString buildVersion = " - Vircadia v0.86.0 K2 - " + (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable ? QString("Version") : QString("Build")) + " " + applicationVersion(); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index b5cacd662b..ceca9debb0 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -782,21 +782,21 @@ Menu::Menu() { // Help/Application menu ---------------------------------- MenuWrapper * helpMenu = addMenu("Help"); - // Help > About Project Athena - action = addActionToQMenuAndActionHash(helpMenu, "About Project Athena"); + // Help > About Vircadia + action = addActionToQMenuAndActionHash(helpMenu, "About Vircadia"); connect(action, &QAction::triggered, [] { qApp->showDialog(QString("hifi/dialogs/AboutDialog.qml"), QString("hifi/dialogs/TabletAboutDialog.qml"), "AboutDialog"); }); helpMenu->addSeparator(); - // Help > Athena Docs + // Help > Vircadia Docs action = addActionToQMenuAndActionHash(helpMenu, "Online Documentation"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://docs.projectathena.dev/")); + QDesktopServices::openUrl(QUrl("https://docs.vircadia.dev/")); }); - // Help > Athena Forum + // Help > Vircadia Forum /* action = addActionToQMenuAndActionHash(helpMenu, "Online Forums"); connect(action, &QAction::triggered, qApp, [] { QDesktopServices::openUrl(QUrl("https://forums.highfidelity.com/")); @@ -805,7 +805,7 @@ Menu::Menu() { // Help > Scripting Reference action = addActionToQMenuAndActionHash(helpMenu, "Online Script Reference"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://apidocs.projectathena.dev/")); + QDesktopServices::openUrl(QUrl("https://apidocs.vircadia.dev/")); }); addActionToQMenuAndActionHash(helpMenu, "Controls Reference", 0, qApp, SLOT(showHelp())); @@ -815,7 +815,7 @@ Menu::Menu() { // Help > Release Notes action = addActionToQMenuAndActionHash(helpMenu, "Release Notes"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://docs.projectathena.dev/release-notes.html")); + QDesktopServices::openUrl(QUrl("https://docs.vircadia.dev/release-notes.html")); }); // Help > Report a Bug! diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 4c1e55fa1c..3e0d69f78b 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -95,7 +95,7 @@ public: static bool isValidNewProjectName(const QString& projectPath, const QString& projectName); static QString getDefaultProjectsPath() { - return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/Project Athena Projects"; + return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/Vircadia Projects"; } signals: diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 81616e5773..46720fea3e 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -379,7 +379,7 @@ int main(int argc, const char* argv[]) { PROFILE_SYNC_END(startup, "app full ctor", ""); #if defined(Q_OS_LINUX) - app.setWindowIcon(QIcon(PathUtils::resourcesPath() + "images/hifi-logo.svg")); + app.setWindowIcon(QIcon(PathUtils::resourcesPath() + "images/vircadia-logo.svg")); #endif QTimer exitTimer; diff --git a/libraries/networking/src/MetaverseAPI.cpp b/libraries/networking/src/MetaverseAPI.cpp index 0fb0bcecad..73316ecda3 100644 --- a/libraries/networking/src/MetaverseAPI.cpp +++ b/libraries/networking/src/MetaverseAPI.cpp @@ -3,7 +3,7 @@ // libraries/networking/src // // Created by Kalila (kasenvr) on 2019-12-16. -// Copyright 2019 Project Athena +// Copyright 2019 Vircadia // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/libraries/networking/src/MetaverseAPI.h b/libraries/networking/src/MetaverseAPI.h index 423f465229..026f8d8b70 100644 --- a/libraries/networking/src/MetaverseAPI.h +++ b/libraries/networking/src/MetaverseAPI.h @@ -3,7 +3,7 @@ // libraries/networking/src // // Created by Kalila (kasenvr) on 2019-12-16. -// Copyright 2019 Project Athena +// Copyright 2019 Vircadia // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/pkg-scripts/athena-server.spec b/pkg-scripts/athena-server.spec index 7910c8114b..6c751a8e50 100644 --- a/pkg-scripts/athena-server.spec +++ b/pkg-scripts/athena-server.spec @@ -5,11 +5,11 @@ Name: athena-server Version: %{version} Release: 1%{?dist} -Summary: Project Athena metaverse platform, based on the High Fidelity Engine. +Summary: Vircadia metaverse platform, based on the High Fidelity Engine. License: ASL 2.0 -URL: https://projectathena.io -Source0: https://github.com/daleglass/athena-builder/blob/master/athena_builder +URL: https://vircadia.com +Source0: https://github.com/daleglass/vircadia-builder/blob/master/vircadia-builder #BuildRequires: systemd-rpm-macros BuildRequires: chrpath @@ -19,8 +19,8 @@ AutoReq: no AutoProv: no %description -Project Athena allows creation and sharing of VR experiences. - The Project Athena metaverse provides built-in social features, including avatar interactions, spatialized audio and interactive physics. Additionally, you have the ability to import any 3D object into your virtual environment. +Vircadia allows creation and sharing of VR experiences. + The Vircadia metaverse provides built-in social features, including avatar interactions, spatialized audio and interactive physics. Additionally, you have the ability to import any 3D object into your virtual environment. %prep diff --git a/pkg-scripts/server-control b/pkg-scripts/server-control index 70383891bd..708b8ce398 100644 --- a/pkg-scripts/server-control +++ b/pkg-scripts/server-control @@ -4,12 +4,12 @@ Priority: optional Maintainer: Heather Anderson Build-Depends: debhelper (>= 10) Standards-Version: 4.1.2 -Homepage: https://www.projectathena.dev +Homepage: https://vircadia.com Vcs-Git: https://github.com/kasenvr/project-athena.git Vcs-Browser: https://github.com/kasenvr/project-athena Package: athena-server Architecture: any Depends: adduser, {DEPENDS} -Description: Project Athena allows creation and sharing of VR experiences. - The Project Athena metaverse provides built-in social features, including avatar interactions, spatialized audio and interactive physics. Additionally, you have the ability to import any 3D object into your virtual environment. +Description: Vircadia allows creation and sharing of VR experiences. + The Vircadia metaverse provides built-in social features, including avatar interactions, spatialized audio and interactive physics. Additionally, you have the ability to import any 3D object into your virtual environment. diff --git a/scripts/system/create/entityProperties/html/entityProperties.html b/scripts/system/create/entityProperties/html/entityProperties.html index 6eadf4d3c0..eef1c33829 100644 --- a/scripts/system/create/entityProperties/html/entityProperties.html +++ b/scripts/system/create/entityProperties/html/entityProperties.html @@ -4,7 +4,7 @@ // // Created by Ryan Huffman on 13 Nov 2014 // Copyright 2014 High Fidelity, Inc. -// Copyright 2020 Project Athena contributors. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/scripts/system/html/css/tabs.css b/scripts/system/html/css/tabs.css index b2d63b5652..6abd910300 100644 --- a/scripts/system/html/css/tabs.css +++ b/scripts/system/html/css/tabs.css @@ -2,7 +2,7 @@ // tabs.css // // Created by Alezia Kurdis on 27 Feb 2020 -// Copyright 2020 Project Athena contributors. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/scripts/system/more/app-more.js b/scripts/system/more/app-more.js index 1902ddf855..97f3377b9d 100644 --- a/scripts/system/more/app-more.js +++ b/scripts/system/more/app-more.js @@ -4,9 +4,9 @@ // VERSION 1.0 // // Created by Keb Helion, February 2020. -// Copyright 2020 Project Athena and contributors. +// Copyright 2020 Vircadia and contributors. // -// This script adds a "More Apps" selector to "Project Athena" to allow the user to add optional functionalities to the tablet. +// This script adds a "More Apps" selector to "Vircadia" to allow the user to add optional functionalities to the tablet. // This application has been designed to work directly from the Github repository. // // Distributed under the Apache License, Version 2.0. diff --git a/scripts/system/more/css/styles.css b/scripts/system/more/css/styles.css index 1f9aba0695..49412d3ccb 100644 --- a/scripts/system/more/css/styles.css +++ b/scripts/system/more/css/styles.css @@ -2,7 +2,7 @@ styles.css Created by Kalila L. on 23 Feb 2020. - Copyright 2020 Project Athena and contributors. + Copyright 2020 Vircadia and contributors. Distributed under the Apache License, Version 2.0. See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/scripts/system/more/more.html b/scripts/system/more/more.html index 218744969e..8cc5352b08 100644 --- a/scripts/system/more/more.html +++ b/scripts/system/more/more.html @@ -3,7 +3,7 @@ // more.html // // Created by Keb Helion, February 2020. -// Copyright 2020 Project Athena and contributors. +// Copyright 2020 Vircadia and contributors. // // App maintained in: https://github.com/kasenvr/community-apps // App copied to: https://github.com/kasenvr/project-athena diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 8c048cc0cc..08c1f14e6d 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -9,7 +9,7 @@ // // Created by Dante Ruiz on 8 February 2017 // Copyright 2016 High Fidelity, Inc. -// Copyright 2020 Project Athena contributors. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html From c05225ca2f4e8d35449a50f5e2b0a42ac61234b9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 26 May 2020 14:58:13 +1200 Subject: [PATCH 25/73] Reinstate nearParentGrabOverlay controller module Fixes regression where tablet becomes large when grabbed if avatar is small. --- scripts/system/controllers/controllerDispatcher.js | 1 - scripts/system/controllers/controllerScripts.js | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index de583b8f0c..5af86d3bbd 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -323,7 +323,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } var nearbyEntityIDs = Entities.findEntities(controllerPosition, findRadius); - nearbyEntityIDs = nearbyEntityIDs.concat(nearbyOverlayIDs[h]); // overlays are now entities for (var j = 0; j < nearbyEntityIDs.length; j++) { var entityID = nearbyEntityIDs[j]; diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index f41dcbd445..fb422ebdf7 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -18,6 +18,7 @@ var CONTOLLER_SCRIPTS = [ //"toggleAdvancedMovementForHandControllers.js", "handTouch.js", "controllerDispatcher.js", + "controllerModules/nearParentGrabOverlay.js", "controllerModules/stylusInput.js", "controllerModules/equipEntity.js", "controllerModules/nearTrigger.js", From b32bdba1115a6cbef30b87579c91de727402c997 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 26 May 2020 14:58:27 +1200 Subject: [PATCH 26/73] Reinstate controller module priorities per master --- .../system/controllers/controllerModules/disableOtherModule.js | 2 +- scripts/system/controllers/controllerModules/equipEntity.js | 2 +- scripts/system/controllers/controllerModules/nearGrabEntity.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/system/controllers/controllerModules/disableOtherModule.js b/scripts/system/controllers/controllerModules/disableOtherModule.js index 549735b658..d49776f06a 100644 --- a/scripts/system/controllers/controllerModules/disableOtherModule.js +++ b/scripts/system/controllers/controllerModules/disableOtherModule.js @@ -17,7 +17,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.hand = hand; this.disableModules = false; this.parameters = makeDispatcherModuleParameters( - 82, + 95, this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"], diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index 534231f407..54b56ff271 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -278,7 +278,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.handHasBeenRightsideUp = false; this.parameters = makeDispatcherModuleParameters( - 85, + 115, this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip"] : ["leftHand", "leftHandEquip"], [], 100); diff --git a/scripts/system/controllers/controllerModules/nearGrabEntity.js b/scripts/system/controllers/controllerModules/nearGrabEntity.js index 381197badf..45d518bb39 100644 --- a/scripts/system/controllers/controllerModules/nearGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearGrabEntity.js @@ -28,7 +28,7 @@ Script.include("/~/system/libraries/controllers.js"); this.grabID = null; this.parameters = makeDispatcherModuleParameters( - 90, + 500, this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], [], 100); From 3850271b97b21201ed89d46f9fac0eaeccb567d8 Mon Sep 17 00:00:00 2001 From: HifiExperiments Date: Thu, 28 May 2020 15:18:22 -0700 Subject: [PATCH 27/73] fix various crashes/issues in controller scripting API --- .../src/controllers/UserInputMapper.cpp | 33 +++++++++++++++++-- .../src/controllers/UserInputMapper.h | 4 +-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index 1eb1a9fa1a..c76ca9dbd3 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -312,7 +312,10 @@ void UserInputMapper::update(float deltaTime) { Input::NamedVector UserInputMapper::getAvailableInputs(uint16 deviceID) const { Locker locker(_lock); auto iterator = _registeredDevices.find(deviceID); - return iterator->second->getAvailableInputs(); + if (iterator != _registeredDevices.end()) { + return iterator->second->getAvailableInputs(); + } + return Input::NamedVector(); } QVector UserInputMapper::getAllActions() const { @@ -366,7 +369,7 @@ bool UserInputMapper::triggerHapticPulse(float strength, float duration, control Locker locker(_lock); bool toReturn = false; for (const auto& device : _registeredDevices) { - toReturn = toReturn || device.second->triggerHapticPulse(strength, duration, hand); + toReturn = device.second->triggerHapticPulse(strength, duration, hand) || toReturn; } return toReturn; } @@ -1237,16 +1240,42 @@ void UserInputMapper::disableMapping(const Mapping::Pointer& mapping) { } void UserInputMapper::setActionState(Action action, float value, bool valid) { + Locker locker(_lock); _actionStates[toInt(action)] = value; _actionStatesValid[toInt(action)] = valid; } void UserInputMapper::deltaActionState(Action action, float delta, bool valid) { + Locker locker(_lock); _actionStates[toInt(action)] += delta; bool wasValid = _actionStatesValid[toInt(action)]; _actionStatesValid[toInt(action)] = wasValid & valid; } +float UserInputMapper::getActionState(Action action) const { + Locker locker(_lock); + + int index = toInt(action); + if (index < _actionStates.size()) { + return _actionStates[index]; + } + + qCDebug(controllers) << "UserInputMapper::getActionState invalid action:" << index; + return 0.0f; +} + +bool UserInputMapper::getActionStateValid(Action action) const { + Locker locker(_lock); + + int index = toInt(action); + if (index < _actionStatesValid.size()) { + return _actionStatesValid[index]; + } + + qCDebug(controllers) << "UserInputMapper::getActionStateValid invalid action:" << index; + return false; +} + } diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index cd44f3226c..79fcf6e64c 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -81,8 +81,8 @@ namespace controller { QVector getAllActions() const; QString getActionName(Action action) const; QString getStandardPoseName(uint16_t pose); - float getActionState(Action action) const { return _actionStates[toInt(action)]; } - bool getActionStateValid(Action action) const { return _actionStatesValid[toInt(action)]; } + float getActionState(Action action) const; + bool getActionStateValid(Action action) const; Pose getPoseState(Action action) const; int findAction(const QString& actionName) const; QVector getActionNames() const; From d15527dd04225157f18a6de4dd50878efe930897 Mon Sep 17 00:00:00 2001 From: HifiExperiments Date: Thu, 28 May 2020 15:47:05 -0700 Subject: [PATCH 28/73] no negatives --- libraries/controllers/src/controllers/UserInputMapper.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index c76ca9dbd3..604a4f9c73 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -1256,7 +1256,7 @@ float UserInputMapper::getActionState(Action action) const { Locker locker(_lock); int index = toInt(action); - if (index < _actionStates.size()) { + if (index >= 0 && index < _actionStates.size()) { return _actionStates[index]; } @@ -1268,7 +1268,7 @@ bool UserInputMapper::getActionStateValid(Action action) const { Locker locker(_lock); int index = toInt(action); - if (index < _actionStatesValid.size()) { + if (index >= 0 && index < _actionStatesValid.size()) { return _actionStatesValid[index]; } From f26c7d3ceefcf9cfa6cbb0a7088080b0956a305e Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Mon, 1 Jun 2020 20:34:35 -0700 Subject: [PATCH 29/73] Move metaverse server URL info into NetworkingConstants.h (for C++ code) and into shared.js (for JS code). Modify references to the metaverse server from constants to references to the new central definitions. --- domain-server/resources/web/js/shared.js | 1 + domain-server/src/DomainServer.cpp | 8 +------ interface/src/Application.cpp | 5 ++--- interface/src/Menu.cpp | 10 ++++----- libraries/auto-updater/src/AutoUpdater.cpp | 8 +++---- libraries/ktx/src/khronos/KHR.h | 1 + .../networking/src/NetworkingConstants.h | 22 +++++++++++++++++++ libraries/shared/src/shared/Storage.h | 1 + libraries/ui/src/ui/types/RequestFilters.cpp | 2 +- script-archive/avatarSelector.js | 2 +- .../entityScripts/recordingEntityScript.js | 4 ++-- script-archive/lobby.js | 2 +- .../targetPractice/targetPracticeGame.js | 2 +- scripts/system/html/js/SnapshotReview.js | 2 +- server-console/src/main.js | 2 +- .../src/modules/hf-notifications.js | 2 +- 16 files changed, 45 insertions(+), 29 deletions(-) diff --git a/domain-server/resources/web/js/shared.js b/domain-server/resources/web/js/shared.js index f4053ebddc..3c7dd2705c 100644 --- a/domain-server/resources/web/js/shared.js +++ b/domain-server/resources/web/js/shared.js @@ -52,6 +52,7 @@ var URLs = { // STABLE METAVERSE_URL: https://metaverse.highfidelity.com // STAGING METAVERSE_URL: https://staging.highfidelity.com METAVERSE_URL: 'https://metaverse.highfidelity.com', + CDN_URL: 'https://cdn.highfidelity.com', PLACE_URL: 'https://hifi.place', }; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 9fea49d2da..0d30560691 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -70,13 +70,7 @@ const QString DomainServer::REPLACEMENT_FILE_EXTENSION = ".replace"; int const DomainServer::EXIT_CODE_REBOOT = 234923; -#if USE_STABLE_GLOBAL_SERVICES -const QString ICE_SERVER_DEFAULT_HOSTNAME = "ice.highfidelity.com"; -#else -const QString ICE_SERVER_DEFAULT_HOSTNAME = "dev-ice.highfidelity.com"; -#endif - -QString DomainServer::_iceServerAddr { ICE_SERVER_DEFAULT_HOSTNAME }; +QString DomainServer::_iceServerAddr { NetworkingConstants::ICE_SERVER_DEFAULT_HOSTNAME }; int DomainServer::_iceServerPort { ICE_SERVER_DEFAULT_PORT }; bool DomainServer::_overrideDomainID { false }; QUuid DomainServer::_overridingDomainID; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3a3a19bcc6..dd969c4f75 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -347,7 +347,6 @@ static const QString STANDARD_TO_ACTION_MAPPING_NAME = "Standard to Action"; static const QString NO_MOVEMENT_MAPPING_NAME = "Standard to Action (No Movement)"; static const QString NO_MOVEMENT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_nomovement.json"; -static const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin"; @@ -7621,7 +7620,7 @@ bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { QUrl scriptURL { scriptFilenameOrURL }; - if (scriptURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + if (scriptURL.host().endsWith(NetworkingConstants::MARKETPLACE_CDN_HOSTNAME)) { int startIndex = shortName.lastIndexOf('/') + 1; int endIndex = shortName.lastIndexOf('?'); shortName = shortName.mid(startIndex, endIndex - startIndex); @@ -7744,7 +7743,7 @@ bool Application::askToReplaceDomainContent(const QString& url) { const int MAX_CHARACTERS_PER_LINE = 90; if (DependencyManager::get()->getThisNodeCanReplaceContent()) { QUrl originURL { url }; - if (originURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + if (originURL.host().endsWith(NetworkingConstants::MARKETPLACE_CDN_HOSTNAME)) { // Create a confirmation dialog when this call is made static const QString infoText = simpleWordWrap("Your domain's content will be replaced with a new content set. " "If you want to save what you have now, create a backup before proceeding. For more information about backing up " diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 23a145be52..a3843ea0e7 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -799,19 +799,19 @@ Menu::Menu() { // Help > Athena Docs action = addActionToQMenuAndActionHash(helpMenu, "Online Documentation"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://docs.vircadia.dev/")); + QDesktopServices::openUrl(NetworkingConstants::HELP_DOCS_URL); }); // Help > Athena Forum /* action = addActionToQMenuAndActionHash(helpMenu, "Online Forums"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://forums.highfidelity.com/")); + QDesktopServices::openUrl(NetworkingConstants::HELP_FORUM_URL)); }); */ // Help > Scripting Reference action = addActionToQMenuAndActionHash(helpMenu, "Online Script Reference"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://apidocs.vircadia.dev/")); + QDesktopServices::openUrl(NetworkingConstants::HELP_SCRIPTING_REFERENCE_URL); }); addActionToQMenuAndActionHash(helpMenu, "Controls Reference", 0, qApp, SLOT(showHelp())); @@ -821,13 +821,13 @@ Menu::Menu() { // Help > Release Notes action = addActionToQMenuAndActionHash(helpMenu, "Release Notes"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://docs.vircadia.dev/release-notes.html")); + QDesktopServices::openUrl(NetworkingConstants::HELP_RELEASE_NOTES_URL); }); // Help > Report a Bug! action = addActionToQMenuAndActionHash(helpMenu, "Report a Bug!"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://github.com/kasenvr/project-athena/issues")); + QDesktopServices::openUrl(NetworkingConstants::HELP_BUG_REPORT_URL); }); } diff --git a/libraries/auto-updater/src/AutoUpdater.cpp b/libraries/auto-updater/src/AutoUpdater.cpp index 300a22983a..d8afac59b2 100644 --- a/libraries/auto-updater/src/AutoUpdater.cpp +++ b/libraries/auto-updater/src/AutoUpdater.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include AutoUpdater::AutoUpdater() : @@ -36,18 +37,15 @@ void AutoUpdater::checkForUpdate() { this->getLatestVersionData(); } -const QUrl BUILDS_XML_URL("https://highfidelity.com/builds.xml"); -const QUrl MASTER_BUILDS_XML_URL("https://highfidelity.com/dev-builds.xml"); - void AutoUpdater::getLatestVersionData() { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QUrl buildsURL; if (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable) { - buildsURL = BUILDS_XML_URL; + buildsURL = NetworkingConstants::BUILDS_XML_URL; } else if (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Master) { - buildsURL = MASTER_BUILDS_XML_URL; + buildsURL = NetworkingConstants::MASTER_BUILDS_XML_URL; } QNetworkRequest latestVersionRequest(buildsURL); diff --git a/libraries/ktx/src/khronos/KHR.h b/libraries/ktx/src/khronos/KHR.h index 617e40ce06..cd3eb109d7 100644 --- a/libraries/ktx/src/khronos/KHR.h +++ b/libraries/ktx/src/khronos/KHR.h @@ -11,6 +11,7 @@ #define khronos_khr_hpp #include +#include namespace khronos { diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index 3bd84bc977..1d28205310 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -27,6 +27,28 @@ namespace NetworkingConstants { const QUrl METAVERSE_SERVER_URL_STABLE { "https://metaverse.highfidelity.com" }; const QUrl METAVERSE_SERVER_URL_STAGING { "https://staging-metaverse.vircadia.com" }; + + // Web Engine requests to this parent domain have an account authorization header added + const QString AUTH_HOSTNAME_BASE = "highfidelity.com"; + + const QUrl BUILDS_XML_URL("https://highfidelity.com/builds.xml"); + const QUrl MASTER_BUILDS_XML_URL("https://highfidelity.com/dev-builds.xml"); + + +#if USE_STABLE_GLOBAL_SERVICES + const QString ICE_SERVER_DEFAULT_HOSTNAME = "ice.highfidelity.com"; +#else + const QString ICE_SERVER_DEFAULT_HOSTNAME = "dev-ice.highfidelity.com"; +#endif + + const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; + + const QUrl HELP_DOCS_URL { "https://docs.vircadia.dev" }; + const QUrl HELP_FORUM_URL { "https://forums.vircadia.dev" }; + const QUrl HELP_SCRIPTING_REFERENCE_URL{ "https://apidocs.vircadia.dev/" }; + const QUrl HELP_RELEASE_NOTES_URL{ "https://docs.vircadia.dev/release-notes.html" }; + const QUrl HELP_BUG_REPORT_URL{ "https://github.com/kasenvr/project-athena/issues" }; + } const QString HIFI_URL_SCHEME_ABOUT = "about"; diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h index 6a2cecf8b9..f64f2758c3 100644 --- a/libraries/shared/src/shared/Storage.h +++ b/libraries/shared/src/shared/Storage.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include diff --git a/libraries/ui/src/ui/types/RequestFilters.cpp b/libraries/ui/src/ui/types/RequestFilters.cpp index 943dd02c29..9287559289 100644 --- a/libraries/ui/src/ui/types/RequestFilters.cpp +++ b/libraries/ui/src/ui/types/RequestFilters.cpp @@ -29,7 +29,7 @@ namespace { auto metaverseServerURL = MetaverseAPI::getCurrentMetaverseServerURL(); static const QStringList HF_HOSTS = { "highfidelity.com", "highfidelity.io", - metaverseServerURL.toString(), "metaverse.highfidelity.io" + metaverseServerURL.toString(), }; const auto& scheme = url.scheme(); const auto& host = url.host(); diff --git a/script-archive/avatarSelector.js b/script-archive/avatarSelector.js index 119044e35a..9dca3f6494 100644 --- a/script-archive/avatarSelector.js +++ b/script-archive/avatarSelector.js @@ -155,7 +155,7 @@ var avatars = {}; function changeLobbyTextures() { var req = new XMLHttpRequest(); - req.open("GET", "https://metaverse.highfidelity.com/api/v1/marketplace?category=head+%26+body&limit=21", false); + req.open("GET", URLs.METAVERSE_URL + "/api/v1/marketplace?category=head+%26+body&limit=21", false); req.send(); // Data returned is randomized. avatars = JSON.parse(req.responseText).data.items; diff --git a/script-archive/entityScripts/recordingEntityScript.js b/script-archive/entityScripts/recordingEntityScript.js index 3d1b6f46df..4281fbc64e 100644 --- a/script-archive/entityScripts/recordingEntityScript.js +++ b/script-archive/entityScripts/recordingEntityScript.js @@ -21,8 +21,8 @@ var START_MESSAGE = "recordingStarted"; var STOP_MESSAGE = "recordingEnded"; var PARTICIPATING_MESSAGE = "participatingToRecording"; - var RECORDING_ICON_URL = "http://cdn.highfidelity.com/alan/production/icons/ICO_rec-active.svg"; - var NOT_RECORDING_ICON_URL = "http://cdn.highfidelity.com/alan/production/icons/ICO_rec-inactive.svg"; + var RECORDING_ICON_URL = URLs.CDN_URL + "/alan/production/icons/ICO_rec-active.svg"; + var NOT_RECORDING_ICON_URL = URLs.CDN_URL + "/alan/production/icons/ICO_rec-inactive.svg"; var ICON_WIDTH = 60; var ICON_HEIGHT = 60; var overlay = null; diff --git a/script-archive/lobby.js b/script-archive/lobby.js index 7a06cdd906..d89fbe1f9d 100644 --- a/script-archive/lobby.js +++ b/script-archive/lobby.js @@ -153,7 +153,7 @@ var places = {}; function changeLobbyTextures() { var req = new XMLHttpRequest(); - req.open("GET", "https://metaverse.highfidelity.com/api/v1/places?limit=21", false); + req.open("GET", URLs.METAVERSE_URL + "/api/v1/places?limit=21", false); req.send(); places = JSON.parse(req.responseText).data.places; diff --git a/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js b/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js index 5e2612ded6..4e7f39821d 100644 --- a/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js +++ b/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js @@ -14,7 +14,7 @@ const GAME_CHANNEL = 'winterSmashUpGame'; const SCORE_POST_URL = 'https://script.google.com/macros/s/AKfycbwZAMx6cMBx6-8NGEhR8ELUA-dldtpa_4P55z38Q4vYHW6kneg/exec'; -const MODEL_URL = 'http://cdn.highfidelity.com/chris/production/winter/game/'; +const MODEL_URL = URLs.CDN_URL + '/chris/production/winter/game/'; const MAX_GAME_TIME = 120; //seconds const TARGET_CLOSE_OFFSET = 0.5; const MILLISECS_IN_SEC = 1000; diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index 1e8be9d644..71e468265f 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -440,7 +440,7 @@ function updateShareInfo(containerID, storyID) { } var shareBar = document.getElementById(containerID + "shareBar"), parentDiv = document.getElementById(containerID), - shareURL = "https://highfidelity.com/user_stories/" + storyID, + shareURL = URLs.METAVERSE_URL + "/user_stories/" + storyID, facebookButton = document.getElementById(containerID + "facebookButton"), twitterButton = document.getElementById(containerID + "twitterButton"); diff --git a/server-console/src/main.js b/server-console/src/main.js index d8d6fea4bf..f645d6af4c 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -56,7 +56,7 @@ const menuNotificationIcon = path.join(__dirname, '../resources/tray-menu-notifi const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/; -const HOME_CONTENT_URL = "http://cdn.highfidelity.com/content-sets/home-tutorial-RC40.tar.gz"; +const HOME_CONTENT_URL = URLs.CDN_URL + "/content-sets/home-tutorial-RC40.tar.gz"; const buildInfo = GetBuildInfo(); diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js index 1ddbd1d307..dfc07ed77c 100644 --- a/server-console/src/modules/hf-notifications.js +++ b/server-console/src/modules/hf-notifications.js @@ -19,7 +19,7 @@ const MARKETPLACE_NOTIFICATION_POLL_TIME_MS = 600 * 1000; const OSX_CLICK_DELAY_TIMEOUT = 500; -const METAVERSE_SERVER_URL= process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : 'https://metaverse.highfidelity.com' +const METAVERSE_SERVER_URL = process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : URLs.METAVERSE_URL const STORIES_URL= '/api/v1/user_stories'; const USERS_URL= '/api/v1/users'; const ECONOMIC_ACTIVITY_URL= '/api/v1/commerce/history'; From 4b27f80c89398651ddc907e4175ab2ccb26926b3 Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Thu, 4 Jun 2020 09:01:51 -0700 Subject: [PATCH 30/73] Move metaverse server URL update: remove unnecessary spaces. Remove added "include " (for VS compiling) to separate PR. --- libraries/ktx/src/khronos/KHR.h | 1 - libraries/shared/src/shared/Storage.h | 1 - script-archive/lobby.js | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/ktx/src/khronos/KHR.h b/libraries/ktx/src/khronos/KHR.h index cd3eb109d7..617e40ce06 100644 --- a/libraries/ktx/src/khronos/KHR.h +++ b/libraries/ktx/src/khronos/KHR.h @@ -11,7 +11,6 @@ #define khronos_khr_hpp #include -#include namespace khronos { diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h index f64f2758c3..6a2cecf8b9 100644 --- a/libraries/shared/src/shared/Storage.h +++ b/libraries/shared/src/shared/Storage.h @@ -14,7 +14,6 @@ #include #include #include -#include #include #include diff --git a/script-archive/lobby.js b/script-archive/lobby.js index d89fbe1f9d..54f3dc526e 100644 --- a/script-archive/lobby.js +++ b/script-archive/lobby.js @@ -153,7 +153,7 @@ var places = {}; function changeLobbyTextures() { var req = new XMLHttpRequest(); - req.open("GET", URLs.METAVERSE_URL + "/api/v1/places?limit=21", false); + req.open("GET", URLs.METAVERSE_URL + "/api/v1/places?limit=21", false); req.send(); places = JSON.parse(req.responseText).data.places; From 0bc6296591475068c9aa8d88ca4218f7163d0840 Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Thu, 4 Jun 2020 12:58:28 -0700 Subject: [PATCH 31/73] Move metaverse server URL update: revert nearly all changes to Javascript scripts because the 'require' of the central definition file was not included. --- script-archive/avatarSelector.js | 2 +- script-archive/entityScripts/recordingEntityScript.js | 4 ++-- script-archive/lobby.js | 2 +- .../winterSmashUp/targetPractice/targetPracticeGame.js | 2 +- scripts/system/html/js/SnapshotReview.js | 2 +- server-console/src/main.js | 2 +- server-console/src/modules/hf-notifications.js | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/script-archive/avatarSelector.js b/script-archive/avatarSelector.js index 9dca3f6494..119044e35a 100644 --- a/script-archive/avatarSelector.js +++ b/script-archive/avatarSelector.js @@ -155,7 +155,7 @@ var avatars = {}; function changeLobbyTextures() { var req = new XMLHttpRequest(); - req.open("GET", URLs.METAVERSE_URL + "/api/v1/marketplace?category=head+%26+body&limit=21", false); + req.open("GET", "https://metaverse.highfidelity.com/api/v1/marketplace?category=head+%26+body&limit=21", false); req.send(); // Data returned is randomized. avatars = JSON.parse(req.responseText).data.items; diff --git a/script-archive/entityScripts/recordingEntityScript.js b/script-archive/entityScripts/recordingEntityScript.js index 4281fbc64e..3d1b6f46df 100644 --- a/script-archive/entityScripts/recordingEntityScript.js +++ b/script-archive/entityScripts/recordingEntityScript.js @@ -21,8 +21,8 @@ var START_MESSAGE = "recordingStarted"; var STOP_MESSAGE = "recordingEnded"; var PARTICIPATING_MESSAGE = "participatingToRecording"; - var RECORDING_ICON_URL = URLs.CDN_URL + "/alan/production/icons/ICO_rec-active.svg"; - var NOT_RECORDING_ICON_URL = URLs.CDN_URL + "/alan/production/icons/ICO_rec-inactive.svg"; + var RECORDING_ICON_URL = "http://cdn.highfidelity.com/alan/production/icons/ICO_rec-active.svg"; + var NOT_RECORDING_ICON_URL = "http://cdn.highfidelity.com/alan/production/icons/ICO_rec-inactive.svg"; var ICON_WIDTH = 60; var ICON_HEIGHT = 60; var overlay = null; diff --git a/script-archive/lobby.js b/script-archive/lobby.js index 54f3dc526e..7a06cdd906 100644 --- a/script-archive/lobby.js +++ b/script-archive/lobby.js @@ -153,7 +153,7 @@ var places = {}; function changeLobbyTextures() { var req = new XMLHttpRequest(); - req.open("GET", URLs.METAVERSE_URL + "/api/v1/places?limit=21", false); + req.open("GET", "https://metaverse.highfidelity.com/api/v1/places?limit=21", false); req.send(); places = JSON.parse(req.responseText).data.places; diff --git a/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js b/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js index 4e7f39821d..5e2612ded6 100644 --- a/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js +++ b/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js @@ -14,7 +14,7 @@ const GAME_CHANNEL = 'winterSmashUpGame'; const SCORE_POST_URL = 'https://script.google.com/macros/s/AKfycbwZAMx6cMBx6-8NGEhR8ELUA-dldtpa_4P55z38Q4vYHW6kneg/exec'; -const MODEL_URL = URLs.CDN_URL + '/chris/production/winter/game/'; +const MODEL_URL = 'http://cdn.highfidelity.com/chris/production/winter/game/'; const MAX_GAME_TIME = 120; //seconds const TARGET_CLOSE_OFFSET = 0.5; const MILLISECS_IN_SEC = 1000; diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index 71e468265f..1e8be9d644 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -440,7 +440,7 @@ function updateShareInfo(containerID, storyID) { } var shareBar = document.getElementById(containerID + "shareBar"), parentDiv = document.getElementById(containerID), - shareURL = URLs.METAVERSE_URL + "/user_stories/" + storyID, + shareURL = "https://highfidelity.com/user_stories/" + storyID, facebookButton = document.getElementById(containerID + "facebookButton"), twitterButton = document.getElementById(containerID + "twitterButton"); diff --git a/server-console/src/main.js b/server-console/src/main.js index f645d6af4c..d8d6fea4bf 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -56,7 +56,7 @@ const menuNotificationIcon = path.join(__dirname, '../resources/tray-menu-notifi const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/; -const HOME_CONTENT_URL = URLs.CDN_URL + "/content-sets/home-tutorial-RC40.tar.gz"; +const HOME_CONTENT_URL = "http://cdn.highfidelity.com/content-sets/home-tutorial-RC40.tar.gz"; const buildInfo = GetBuildInfo(); diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js index dfc07ed77c..1ddbd1d307 100644 --- a/server-console/src/modules/hf-notifications.js +++ b/server-console/src/modules/hf-notifications.js @@ -19,7 +19,7 @@ const MARKETPLACE_NOTIFICATION_POLL_TIME_MS = 600 * 1000; const OSX_CLICK_DELAY_TIMEOUT = 500; -const METAVERSE_SERVER_URL = process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : URLs.METAVERSE_URL +const METAVERSE_SERVER_URL= process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : 'https://metaverse.highfidelity.com' const STORIES_URL= '/api/v1/user_stories'; const USERS_URL= '/api/v1/users'; const ECONOMIC_ACTIVITY_URL= '/api/v1/commerce/history'; From a8bd627cda1c9b35d5c2c9b1701fc664c64293c1 Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Tue, 7 Apr 2020 20:53:52 +0200 Subject: [PATCH 32/73] Add Prometheus exporter Exports domain statistics for the domain on port 9703 (officially reserved) --- assignment-client/src/assets/AssetServer.cpp | 11 +- assignment-client/src/octree/OctreeServer.cpp | 14 +- assignment-client/src/octree/OctreeServer.h | 2 + .../resources/prometheus_exporter/index.html | 14 + domain-server/src/DomainServer.cpp | 4 +- domain-server/src/DomainServer.h | 5 +- domain-server/src/DomainServerExporter.cpp | 450 ++++++++++++++++++ domain-server/src/DomainServerExporter.h | 55 +++ libraries/networking/src/DomainHandler.h | 17 +- 9 files changed, 561 insertions(+), 11 deletions(-) create mode 100644 domain-server/resources/prometheus_exporter/index.html create mode 100644 domain-server/src/DomainServerExporter.cpp create mode 100644 domain-server/src/DomainServerExporter.h diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 502cf15aa2..b3344e3832 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -176,7 +176,7 @@ std::pair AssetServer::getAssetStatus(const A } else if (loaded && meta.failedLastBake) { return { AssetUtils::Error, meta.lastBakeErrors }; } - + return { AssetUtils::Pending, "" }; } @@ -199,7 +199,7 @@ void AssetServer::maybeBake(const AssetUtils::AssetPath& path, const AssetUtils: void AssetServer::createEmptyMetaFile(const AssetUtils::AssetHash& hash) { QString metaFilePath = "atp:/" + hash + "/meta.json"; QFile metaFile { metaFilePath }; - + if (!metaFile.exists()) { qDebug() << "Creating metafile for " << hash; if (metaFile.open(QFile::WriteOnly)) { @@ -285,7 +285,7 @@ void updateConsumedCores() { auto coreCount = std::thread::hardware_concurrency(); if (isInterfaceRunning) { coreCount = coreCount > MIN_CORES_FOR_MULTICORE ? CPU_AFFINITY_COUNT_HIGH : CPU_AFFINITY_COUNT_LOW; - } + } qCDebug(asset_server) << "Setting max consumed cores to " << coreCount; setMaxCores(coreCount); } @@ -931,6 +931,9 @@ void AssetServer::sendStatsPacket() { connectionStats["5. Period (us)"] = stats.packetSendPeriod; connectionStats["6. Up (Mb/s)"] = stats.sentBytes * megabitsPerSecPerByte; connectionStats["7. Down (Mb/s)"] = stats.receivedBytes * megabitsPerSecPerByte; + connectionStats["last_heard_time_msecs"] = date.toUTC().toMSecsSinceEpoch(); + connectionStats["last_heard_ago_msecs"] = date.msecsTo(QDateTime::currentDateTime()); + nodeStats["Connection Stats"] = connectionStats; using Events = udt::ConnectionStats::Stats::Event; @@ -1147,7 +1150,7 @@ bool AssetServer::deleteMappings(const AssetUtils::AssetPathList& paths) { hashesToCheckForDeletion << it->second; qCDebug(asset_server) << "Deleted a mapping:" << path << "=>" << it->second; - + _fileMappings.erase(it); } else { qCDebug(asset_server) << "Unable to delete a mapping that was not found:" << path; diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 80e0060299..c1cf3d2297 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -1197,7 +1197,7 @@ void OctreeServer::domainSettingsRequestComplete() { } else { beginRunning(); } -} +} void OctreeServer::beginRunning() { auto nodeList = DependencyManager::get(); @@ -1344,6 +1344,11 @@ QString OctreeServer::getUptime() { return formattedUptime; } +double OctreeServer::getUptimeSeconds() +{ + return (usecTimestampNow() - _startedUSecs) / 1000000.0; +} + QString OctreeServer::getFileLoadTime() { QString result; if (isInitialLoadComplete()) { @@ -1386,6 +1391,11 @@ QString OctreeServer::getFileLoadTime() { return result; } +double OctreeServer::getFileLoadTimeSeconds() +{ + return getLoadElapsedTime() / 1000000.0; +} + QString OctreeServer::getConfiguration() { QString result; for (int i = 1; i < _argc; i++) { @@ -1421,6 +1431,8 @@ void OctreeServer::sendStatsPacket() { statsArray1["4. persistFileLoadTime"] = getFileLoadTime(); statsArray1["5. clients"] = getCurrentClientCount(); statsArray1["6. threads"] = threadsStats; + statsArray1["uptime_seconds"] = getUptimeSeconds(); + statsArray1["persistFileLoadTime_seconds"] = getFileLoadTimeSeconds(); // Octree Stats QJsonObject octreeStats; diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 07b1e334b1..3ae4dddee9 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -158,7 +158,9 @@ protected: void initHTTPManager(int port); void resetSendingStats(); QString getUptime(); + double getUptimeSeconds(); QString getFileLoadTime(); + double getFileLoadTimeSeconds(); QString getConfiguration(); QString getStatusLink(); diff --git a/domain-server/resources/prometheus_exporter/index.html b/domain-server/resources/prometheus_exporter/index.html new file mode 100644 index 0000000000..5a23c78858 --- /dev/null +++ b/domain-server/resources/prometheus_exporter/index.html @@ -0,0 +1,14 @@ + + + Vircadia Prometheus exporter + + + +

Vircadia Prometheus exporter

+ +

This is the Prometheus exporter, used to export stats about the domain server for graphing and analysis.

+

+ Metrics +

+ + diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 9fea49d2da..9c6361faef 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -163,7 +163,8 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection, DomainServer::DomainServer(int argc, char* argv[]) : QCoreApplication(argc, argv), _gatekeeper(this), - _httpManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTP_PORT, QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this) + _httpManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTP_PORT, QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this), + _httpExporterManager(QHostAddress::Any, DOMAIN_SERVER_EXPORTER_PORT, QString("%1/resources/prometheus_exporter/").arg(QCoreApplication::applicationDirPath()), &_exporter) { if (_parentPID != -1) { watchParentProcess(_parentPID); @@ -1977,6 +1978,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url const QString URI_API_BACKUPS_ID = "/api/backups/"; const QString URI_API_BACKUPS_DOWNLOAD_ID = "/api/backups/download/"; const QString URI_API_BACKUPS_RECOVER = "/api/backups/recover/"; + const QString URI_EXPORTER_= "/metrics"; const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 95b4b784cb..f6bb9bc7ae 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -36,6 +36,7 @@ #include "DomainContentBackupManager.h" #include "PendingAssignedNodeData.h" +#include "DomainServerExporter.h" #include @@ -115,7 +116,7 @@ private slots: void sendHeartbeatToIceServer(); void nodePingMonitor(); - void handleConnectedNode(SharedNodePointer newNode, quint64 requestReceiveTime); + void handleConnectedNode(SharedNodePointer newNode, quint64 requestReceiveTime); void handleTempDomainSuccess(QNetworkReply* requestReply); void handleTempDomainError(QNetworkReply* requestReply); @@ -234,8 +235,10 @@ private: std::vector _replicatedUsernames; DomainGatekeeper _gatekeeper; + DomainServerExporter _exporter; HTTPManager _httpManager; + HTTPManager _httpExporterManager; std::unique_ptr _httpsManager; QHash _allAssignments; diff --git a/domain-server/src/DomainServerExporter.cpp b/domain-server/src/DomainServerExporter.cpp new file mode 100644 index 0000000000..3ecccfc8b4 --- /dev/null +++ b/domain-server/src/DomainServerExporter.cpp @@ -0,0 +1,450 @@ +// +// DomainServerExporter.cpp +// domain-server/src +// +// Created by Dale Glass on 3 Apr 2020. +// Copyright 2020 Dale Glass +// +// Prometheus exporter +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +// TODO: +// +// Look into the data provided by OctreeServer::handleHTTPRequest in assignment-client/src/octree/OctreeServer.cpp +// Turns out the octree server (entity server) can optionally deliver additional statistics via another HTTP server +// that is disabled by default. This functionality can be enabled by setting statusPort to a port number. +// +// Look into what appears in Audio Mixer -> z_listeners -> jitter -> injectors, so far it's been an empty list. + +#include +#include +#include +#include +#include +#include + +#include "DomainServerExporter.h" +#include "DependencyManager.h" +#include "LimitedNodeList.h" +#include "HTTPConnection.h" +#include "DomainServerNodeData.h" + +Q_LOGGING_CATEGORY(domain_server_exporter, "hifi.domain_server.exporter") + + + +static const QMap TYPE_MAP { + { "asset_server_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_cw_p" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_down_mb_s" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_est_max_p_s" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_last_heard_ago_msecs" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_last_heard_time_msecs" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_period_us" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_rtt_ms" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_up_mb_s" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_downstream_stats_duplicates" , DomainServerExporter::MetricType::Counter }, + { "asset_server_downstream_stats_recvd_packets" , DomainServerExporter::MetricType::Counter }, + { "asset_server_downstream_stats_recvd_p_s" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_downstream_stats_sent_ack" , DomainServerExporter::MetricType::Counter }, + { "asset_server_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_io_stats_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_upstream_stats_procd_ack" , DomainServerExporter::MetricType::Counter }, + { "asset_server_upstream_stats_recvd_ack" , DomainServerExporter::MetricType::Counter }, + { "asset_server_upstream_stats_retransmitted" , DomainServerExporter::MetricType::Counter }, + { "asset_server_upstream_stats_sent_packets" , DomainServerExporter::MetricType::Counter }, + { "asset_server_upstream_stats_sent_p_s" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_listeners_per_frame" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_listeners_silent_per_frame" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_streams_per_frame" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_check_time" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_check_time_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_events" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_events_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_frame" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_frame_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_mix" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_mix_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_packets" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_packets_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_sleep" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_sleep_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_tic" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_tic_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_io_stats_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_available" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_available_avg_10s" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_desired" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_lost_percent" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_lost_percent_30s" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_not_mixed" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_downstream_overflows" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_downstream_starves" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_downstream_unplayed" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_injectors" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_active_streams" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_active_to_inactive" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_active_to_skippped" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_avg_mixes_per_block" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_hrtf_renders" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_hrtf_resets" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_hrtf_updates" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_inactive_streams" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_inactive_to_active" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_inactive_to_skippped" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_percent_hrtf_mixes" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_percent_manual_echo_mixes" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_percent_manual_stereo_mixes" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_skipped_streams" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_skippped_to_active" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_skippped_to_inactive" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_total_mixes" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_silent_packets_per_frame" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_threads" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_throttling_ratio" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_trailing_mix_ratio" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_use_dynamic_jitter_buffers" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_av_data_receive_rate" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_avg_other_av_skips_per_second" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_avg_other_av_starves_per_second" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_delta_full_vs_avatar_data_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_inbound_av_data_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_num_avs_sent_last_frame" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_outbound_av_data_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_outbound_av_traits_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_recent_other_av_in_view" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_recent_other_av_out_of_view" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_total_num_out_of_order_sends" , DomainServerExporter::MetricType::Counter }, + { "avatar_mixer_average_listeners_last_second" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_broadcast_loop_rate" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_io_stats_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_parallel_tasks_broadcast_avatar_data_functor" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_parallel_tasks_broadcast_avatar_data_innner" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_parallel_tasks_broadcast_avatar_data_lock_wait" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_parallel_tasks_broadcast_avatar_data_node_transform" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_parallel_tasks_broadcast_avatar_data_total" , DomainServerExporter::MetricType::Counter }, + { "avatar_mixer_parallel_tasks_display_name_management_total" , DomainServerExporter::MetricType::Counter }, + { "avatar_mixer_parallel_tasks_process_queued_avatar_data_packets_lock_wait" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_parallel_tasks_process_queued_avatar_data_packets_total" , DomainServerExporter::MetricType::Counter }, + { "avatar_mixer_single_core_tasks_incoming_packets_handle_avatar_identity_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_incoming_packets_handle_avatar_query_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_incoming_packets_handle_kill_avatar_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_incoming_packets_handle_node_ignore_request_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_incoming_packets_handle_radius_ignore_request_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_incoming_packets_handle_requests_domain_list_data_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_process_events" , DomainServerExporter::MetricType::Counter }, + { "avatar_mixer_single_core_tasks_queue_incoming_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_send_stats" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_received_1_nodes_processed" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_1_nodes_broadcasted_to" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_2_average_others_included" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_3_average_over_budget_avatars" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_4_average_data_bytes" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_5_average_traits_bytes" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_6_average_identity_bytes" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_7_average_hero_avatars" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_timing_1_process_incoming_packets" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_timing_2_ignore_calculation" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_timing_3_to_byte_array" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_timing_4_avatar_data_packing" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_timing_5_packet_sending" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_timing_6_job_elapsed_time" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_threads" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_throttling_ratio" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_trailing_mix_ratio" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_io_stats_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_nodes_inbound_kbit_s" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_nodes_outbound_kbit_s" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_nodes_reliable_packet_s" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_nodes_unreliable_packet_s" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_octree_stats_element_count" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_octree_stats_internal_element_count" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_octree_stats_leaf_element_count" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_script_engine_stats_number_running_scripts" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_data_packet_queue" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_data_total_elements" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_data_total_packets" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_timing_avg_lock_wait_time_per_element" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_timing_avg_lock_wait_time_per_packet" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_timing_avg_process_time_per_element" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_timing_avg_process_time_per_packet" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_timing_avg_transit_time_per_packet" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_clients" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_persist_file_load_time_seconds" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_threads_handle_pacekt_send" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_threads_packet_distributor" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_threads_processing" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_threads_write_datagram" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_uptime_seconds" , DomainServerExporter::MetricType::Counter }, + { "entity_server_entity_server_octree_element_count" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_octree_internal_element_count" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_octree_leaf_element_count" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_data_total_bytes" , DomainServerExporter::MetricType::Counter }, + { "entity_server_entity_server_outbound_data_total_bytes_bit_masks" , DomainServerExporter::MetricType::Counter }, + { "entity_server_entity_server_outbound_data_total_bytes_octal_codes" , DomainServerExporter::MetricType::Counter }, + { "entity_server_entity_server_outbound_data_total_bytes_wasted" , DomainServerExporter::MetricType::Counter }, + { "entity_server_entity_server_outbound_data_total_packets" , DomainServerExporter::MetricType::Counter }, + { "entity_server_entity_server_outbound_timing_avg_compress_and_write_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_timing_avg_encode_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_timing_avg_inside_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_timing_avg_loop_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_timing_avg_send_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_timing_avg_tree_traverse_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_timing_node_wait_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_io_stats_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_io_stats_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_messages_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_messages_outbound_kbps" , DomainServerExporter::MetricType::Gauge } +}; + + +// Things we're not going to convert for various reasons, such as containing text, +// or having a value followed by an unit ("5.2 seconds"). +// +// Things like text like usernames have no place in the Prometheus model, so they can be skipped. +// +// For numeric values with an unit, instead of trying to parse it, the stats will just need to +// have a second copy of the metric added, with the value expressed as a number, with the original +// being blacklisted here. + +static const QSet BLACKLIST = { + "asset_server_connection_stats_last_heard", // Timestamp as a string + "asset_server_username", // Username + "audio_mixer_listeners_jitter_downstream_avg_gap", // Number as string with unit name + "audio_mixer_listeners_jitter_downstream_avg_gap_30s", // Number as string with unit name + "audio_mixer_listeners_jitter_downstream_max_gap", // Number as string with unit name + "audio_mixer_listeners_jitter_downstream_max_gap_30s", // Number as string with unit name + "audio_mixer_listeners_jitter_downstream_min_gap", // Number as string with unit name + "audio_mixer_listeners_jitter_downstream_min_gap_30s", // Number as string with unit name + "audio_mixer_listeners_jitter_injectors", // Array, empty. TODO: check if this ever contains anything. + "audio_mixer_listeners_jitter_upstream", // Number as string with unit name + "audio_mixer_listeners_username", // Username + "avatar_mixer_avatars_display_name", // Username + "avatar_mixer_avatars_username", // Username + "entity_script_server_nodes_node_type", // Username + "entity_script_server_nodes_username", // Username + "entity_server_entity_server_misc_configuration", // Text + "entity_server_entity_server_misc_detailed_stats_url", // URL + "entity_server_entity_server_misc_persist_file_load_time", // Number as string with unit name + "entity_server_entity_server_misc_uptime", // Number as string with unit name + "messages_mixer_messages_username" // Username +}; + + +DomainServerExporter::DomainServerExporter() +{ + +} + +bool DomainServerExporter::handleHTTPRequest(HTTPConnection *connection, const QUrl &url, bool skipSubHandler) +{ + const QString URI_METRICS = "/metrics"; + const QString EXPORTER_MIME_TYPE = "text/plain"; + + qCDebug(domain_server_exporter) << "Request on URL " << url; + + if ( url.path() == URI_METRICS ) { + auto nodeList = DependencyManager::get(); + QString output = ""; + QTextStream out_stream(&output); + + nodeList->eachNode([this, &out_stream](const SharedNodePointer& node){ + generateMetricsForNode(out_stream, node); + }); + + connection->respond(HTTPConnection::StatusCode200, output.toUtf8(), qPrintable(EXPORTER_MIME_TYPE)); + return true; + } + + return false; +} + +QString DomainServerExporter::escapeName(const QString &name) +{ + QRegularExpression invalid_characters("[^A-Za-z0-9_]"); + + QString ret = name; + + // If a key is named something like: "6. threads", turn it into just "threads" + ret.replace(QRegularExpression("^\\d+\\. "), ""); + ret.replace(QRegularExpression("^\\d+_"), ""); + + // If a key is named something like "z_listeners", turn it into just "listeners" + ret.replace(QRegularExpression("^z_"), ""); + + // If a key is named something like "lost%", change it to "lost_percent_". + // redundant underscores will be removed below. + ret.replace(QRegularExpression("%"), "_percent_"); + + // change mixedCaseNames to mixed_case_names + ret.replace(QRegularExpression("([a-z])([A-Z])"), "\\1_\\2"); + + // Replace all invalid characters with a _ + ret.replace(invalid_characters, "_"); + + // Remove any "_" characters at the beginning or end + ret.replace(QRegularExpression("^_+"), ""); + ret.replace(QRegularExpression("_+$"), ""); + + // Replace any duplicated _ characters with a single one + ret.replace(QRegularExpression("_+"), "_"); + + ret = ret.toLower(); + + return ret; +} + +void DomainServerExporter::generateMetricsForNode( QTextStream &stream, const SharedNodePointer &node ) +{ + QString ret = ""; + QJsonObject statsObject = static_cast(node->getLinkedData())->getStatsJSONObject(); + QString node_type = NodeType::getNodeTypeName(static_cast(node->getType())); + + + stream << "\n\n\n"; + stream << "###############################################################\n"; + stream << "# " << node_type << "\n"; + stream << "###############################################################\n"; + + generateMetricsFromJson(stream, node_type, escapeName(node_type), QHash(), statsObject); + + QJsonDocument doc(statsObject); + ret.append( doc.toJson() ); + +} + +void DomainServerExporter::generateMetricsFromJson(QTextStream &stream, QString original_path, QString path, QHash labels, const QJsonObject &obj) +{ + for(auto iter = obj.constBegin(); iter != obj.constEnd(); ++iter) { + auto key = escapeName(iter.key()); + auto val = iter.value(); + auto metric_name = path + "_" + key; + auto orig_metric_name = original_path + " -> " + iter.key(); + + if ( val.isObject() ) { + QUuid possible_uuid = QUuid::fromString(iter.key()); + + if ( possible_uuid.isNull() ) { + generateMetricsFromJson(stream, original_path + " -> " + iter.key(), path + "_" + key, labels, iter.value().toObject()); + } else { + labels.insert("uuid", possible_uuid.toString(QUuid::WithoutBraces)); + generateMetricsFromJson(stream, original_path, path, labels, iter.value().toObject()); + } + + + continue; + } + + if ( BLACKLIST.contains(metric_name)) { + continue; + } + + bool conversion_ok = false; + double converted = 0; + + if ( val.isString() ) { + // Prometheus only deals with numeric values. See if this string contains a valid one + + QString tmp = val.toString(); + converted = tmp.toDouble(&conversion_ok); + + if ( !conversion_ok ) { + qCWarning(domain_server_exporter) << "Failed to convert value of " << orig_metric_name << " (" << metric_name << ") to double: " << tmp << "'"; + continue; + } + + } + + stream << QString("\n# HELP %1 %2 -> %3\n").arg(metric_name).arg(original_path).arg(iter.key()); + + if ( TYPE_MAP.contains(metric_name )) { + stream << "# TYPE " << metric_name << " "; + switch( TYPE_MAP[metric_name ]) { + case DomainServerExporter::MetricType::Untyped: + stream << "untyped"; break; + case DomainServerExporter::MetricType::Counter: + stream << "counter"; break; + case DomainServerExporter::MetricType::Gauge: + stream << "gauge"; break; + case DomainServerExporter::MetricType::Histogram: + stream << "histogram"; break; + case DomainServerExporter::MetricType::Summary: + stream << "summary"; break; + } + stream << "\n"; + } else { + qCWarning(domain_server_exporter) << "Type for metric " << orig_metric_name << " (" << metric_name << ") not known."; + } + + stream << path << "_" << key; + if (!labels.isEmpty() ) { + stream << "{"; + + bool is_first = true; + QHashIterator iter(labels); + + while( iter.hasNext() ) { + iter.next(); + + if ( ! is_first ) { + stream << ","; + } + + QString value = iter.value(); + value.replace("\\", "\\\\"); + value.replace("\"", "\\\""); + value.replace("\n", "\\\n"); + + stream << iter.key() << "=\"" << value << "\""; + + is_first = false; + } + stream << "}"; + } + + stream << " "; + + if ( val.isBool() ) { + stream << ( iter.value().toBool() ? "1" : "0" ); + } else if ( val.isDouble() ) { + stream << val.toDouble(); + } else if ( val.isString() ) { + // Converted above + stream << converted; + } else { + qCWarning(domain_server_exporter) << "Can't convert metric " << orig_metric_name << "(" << metric_name << ") with value " << val; + } + + stream << "\n"; + } +} + diff --git a/domain-server/src/DomainServerExporter.h b/domain-server/src/DomainServerExporter.h new file mode 100644 index 0000000000..d818ad8114 --- /dev/null +++ b/domain-server/src/DomainServerExporter.h @@ -0,0 +1,55 @@ +// +// DomainServerExporter.h +// domain-server/src +// +// Created by Dale Glass on 3 Apr 2020. +// Copyright 2020 Dale Glass +// +// Prometheus exporter +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef DOMAINSERVEREXPORTER_H +#define DOMAINSERVEREXPORTER_H + +#include +#include "HTTPManager.h" +#include "Node.h" +#include +#include +#include +#include + + + +/** + * @brief Prometheus exporter for domain stats + * + * This class exportors the statistics that can be seen on the domain's page in + * a format that can be parsed by Prometheus. This is useful for troubleshooting, + * monitoring performance, and making pretty graphs. + */ +class DomainServerExporter : public HTTPRequestHandler +{ +public: + typedef enum { + Untyped, /* Works the same as Gauge, with the difference of signalling that the actual type is unknown */ + Counter, /* Value only goes up. Eg, number of packets received */ + Gauge, /* Current numerical value that can go up or down. Current temperature, memory usage, etc */ + Histogram, /* Samples sorted in buckets gathered over time */ + Summary + } MetricType; + + DomainServerExporter(); + ~DomainServerExporter() = default; + bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override; + +private: + QString escapeName(const QString &name); + void generateMetricsForNode( QTextStream &stream, const SharedNodePointer &node ); + void generateMetricsFromJson(QTextStream &stream, QString original_path, QString path, QHash labels, const QJsonObject &obj); +}; + +#endif // DOMAINSERVEREXPORTER_H diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 68059fb158..178c56c34a 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -33,7 +33,7 @@ #include "NetworkingConstants.h" #include "MetaverseAPI.h" -const unsigned short DEFAULT_DOMAIN_SERVER_PORT = +const unsigned short DEFAULT_DOMAIN_SERVER_PORT = QProcessEnvironment::systemEnvironment() .contains("HIFI_DOMAIN_SERVER_PORT") ? QProcessEnvironment::systemEnvironment() @@ -41,7 +41,7 @@ const unsigned short DEFAULT_DOMAIN_SERVER_PORT = .toUShort() : 40102; -const unsigned short DEFAULT_DOMAIN_SERVER_DTLS_PORT = +const unsigned short DEFAULT_DOMAIN_SERVER_DTLS_PORT = QProcessEnvironment::systemEnvironment() .contains("HIFI_DOMAIN_SERVER_DTLS_PORT") ? QProcessEnvironment::systemEnvironment() @@ -49,7 +49,7 @@ const unsigned short DEFAULT_DOMAIN_SERVER_DTLS_PORT = .toUShort() : 40103; -const quint16 DOMAIN_SERVER_HTTP_PORT = +const quint16 DOMAIN_SERVER_HTTP_PORT = QProcessEnvironment::systemEnvironment() .contains("HIFI_DOMAIN_SERVER_HTTP_PORT") ? QProcessEnvironment::systemEnvironment() @@ -57,7 +57,7 @@ const quint16 DOMAIN_SERVER_HTTP_PORT = .toUInt() : 40100; -const quint16 DOMAIN_SERVER_HTTPS_PORT = +const quint16 DOMAIN_SERVER_HTTPS_PORT = QProcessEnvironment::systemEnvironment() .contains("HIFI_DOMAIN_SERVER_HTTPS_PORT") ? QProcessEnvironment::systemEnvironment() @@ -65,6 +65,15 @@ const quint16 DOMAIN_SERVER_HTTPS_PORT = .toUInt() : 40101; +const quint16 DOMAIN_SERVER_EXPORTER_PORT = + QProcessEnvironment::systemEnvironment() + .contains("VIRCADIA_DOMAIN_SERVER_EXPORTER_PORT") + ? QProcessEnvironment::systemEnvironment() + .value("VIRCADIA_DOMAIN_SERVER_EXPORTER_PORT") + .toUInt() + : 9703; + + const int MAX_SILENT_DOMAIN_SERVER_CHECK_INS = 5; class DomainHandler : public QObject { From 443d769eacf398e6f21aaa21ebf96c2a89716b14 Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Fri, 10 Apr 2020 17:01:40 +0200 Subject: [PATCH 33/73] Make it possible to turn the Prometheus exporter on and off The settings are available from the domain settings page. --- .../resources/describe-settings.json | 23 +++++++++++++ domain-server/src/DomainServer.cpp | 32 +++++++++++++++++-- domain-server/src/DomainServer.h | 3 +- .../embedded-webserver/src/HTTPManager.h | 2 +- 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index cdf92918c6..284dd344e7 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -57,6 +57,29 @@ } ] }, + { + "label": "Monitoring", + "name": "monitoring", + "restart": false, + "settings": [ + { + "name": "enable_prometheus_exporter", + "label": "Enable Prometheus Exporter", + "help": "Enable a Prometheus exporter to make it possible to gather the stats that are available at Nodes tab with a Prometheus server. This makes it possible to keep track of long-term domain statistics for graphing, troubleshooting, and performance monitoring.", + "default": false, + "type": "checkbox", + "advanced": true + }, + { + "name": "prometheus_exporter_port", + "label": "Prometheus TCP Port", + "help": "This is the port where the Prometheus exporter accepts connections. It listens both on IPv4 and IPv6 and can be accessed remotely, so you should make sure to restrict access with a firewall as needed.", + "default": "9703", + "type": "int", + "advanced": true + } + ] + }, { "label": "Paths", "html_id": "paths", diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 9c6361faef..284281a64f 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -163,8 +163,7 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection, DomainServer::DomainServer(int argc, char* argv[]) : QCoreApplication(argc, argv), _gatekeeper(this), - _httpManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTP_PORT, QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this), - _httpExporterManager(QHostAddress::Any, DOMAIN_SERVER_EXPORTER_PORT, QString("%1/resources/prometheus_exporter/").arg(QCoreApplication::applicationDirPath()), &_exporter) + _httpManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTP_PORT, QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this) { if (_parentPID != -1) { watchParentProcess(_parentPID); @@ -230,7 +229,6 @@ DomainServer::DomainServer(int argc, char* argv[]) : this, &DomainServer::updateDownstreamNodes); connect(&_settingsManager, &DomainServerSettingsManager::settingsUpdated, this, &DomainServer::updateUpstreamNodes); - setupGroupCacheRefresh(); optionallySetupOAuth(); @@ -331,6 +329,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : _nodePingMonitorTimer = new QTimer{ this }; connect(_nodePingMonitorTimer, &QTimer::timeout, this, &DomainServer::nodePingMonitor); _nodePingMonitorTimer->start(NODE_PING_MONITOR_INTERVAL_MSECS); + + initializeExporter(); } void DomainServer::parseCommandLine(int argc, char* argv[]) { @@ -425,6 +425,11 @@ DomainServer::~DomainServer() { _contentManager->terminate(); } + if ( _httpExporterManager ) { + _httpExporterManager->close(); + delete _httpExporterManager; + } + DependencyManager::destroy(); // cleanup the AssetClient thread @@ -3039,6 +3044,27 @@ void DomainServer::updateUpstreamNodes() { updateReplicationNodes(Upstream); } +void DomainServer::initializeExporter() +{ + static const QString ENABLE_EXPORTER = "monitoring.enable_prometheus_exporter"; + static const QString EXPORTER_PORT = "monitoring.prometheus_exporter_port"; + + bool isExporterEnabled = _settingsManager.valueOrDefaultValueForKeyPath(ENABLE_EXPORTER).toBool(); + int exporterPort = _settingsManager.valueOrDefaultValueForKeyPath(EXPORTER_PORT).toInt(); + + if ( exporterPort < 1 || exporterPort > 65535 ) { + qCWarning(domain_server) << "Prometheus exporter port " << exporterPort << " is out of range."; + isExporterEnabled = false; + } + + qCDebug(domain_server) << "Setting up Prometheus exporter"; + + if ( isExporterEnabled && !_httpExporterManager) { + qCInfo(domain_server) << "Starting Prometheus exporter on port " << exporterPort; + _httpExporterManager = new HTTPManager(QHostAddress::Any, static_cast(exporterPort), QString("%1/resources/prometheus_exporter/").arg(QCoreApplication::applicationDirPath()), &_exporter); + } +} + void DomainServer::updateReplicatedNodes() { // Make sure we have downstream nodes in our list static const QString REPLICATED_USERS_KEY = "users"; diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index f6bb9bc7ae..46968c7a0c 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -139,6 +139,7 @@ private slots: void updateReplicatedNodes(); void updateDownstreamNodes(); void updateUpstreamNodes(); + void initializeExporter(); void tokenGrantFinished(); void profileRequestFinished(); @@ -238,7 +239,7 @@ private: DomainServerExporter _exporter; HTTPManager _httpManager; - HTTPManager _httpExporterManager; + HTTPManager *_httpExporterManager { nullptr }; std::unique_ptr _httpsManager; QHash _allAssignments; diff --git a/libraries/embedded-webserver/src/HTTPManager.h b/libraries/embedded-webserver/src/HTTPManager.h index 597f6921cc..d25cde413e 100644 --- a/libraries/embedded-webserver/src/HTTPManager.h +++ b/libraries/embedded-webserver/src/HTTPManager.h @@ -34,7 +34,7 @@ class HTTPManager : public QTcpServer, public HTTPRequestHandler { public: /// Initializes the manager. HTTPManager(const QHostAddress& listenAddress, quint16 port, const QString& documentRoot, HTTPRequestHandler* requestHandler = nullptr); - + bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override; private slots: From 8700214239881a36fa0af706424d24dc65afe8ca Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Sat, 11 Apr 2020 17:14:58 +0200 Subject: [PATCH 34/73] Add alternate audio stats without text formatting for the exporter --- .../src/audio/AudioMixerClientData.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index e2f28c221e..470ab3f233 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -668,6 +668,12 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { downstreamStats["min_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMin); downstreamStats["max_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMax); downstreamStats["avg_gap_30s"] = formatUsecTime(streamStats._timeGapWindowAverage); + downstreamStats["min_gap_usecs"] = static_cast(streamStats._timeGapMin); + downstreamStats["max_gap_usecs"] = static_cast(streamStats._timeGapMax); + downstreamStats["avg_gap_usecs"] = static_cast(streamStats._timeGapAverage); + downstreamStats["min_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowMin); + downstreamStats["max_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowMax); + downstreamStats["avg_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowAverage); result["downstream"] = downstreamStats; @@ -695,6 +701,13 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { upstreamStats["max_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMax); upstreamStats["avg_gap_30s"] = formatUsecTime(streamStats._timeGapWindowAverage); + upstreamStats["min_gap_usecs"] = static_cast(streamStats._timeGapMin); + upstreamStats["max_gap_usecs"] = static_cast(streamStats._timeGapMax); + upstreamStats["avg_gap_usecs"] = static_cast(streamStats._timeGapAverage); + upstreamStats["min_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowMin); + upstreamStats["max_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowMax); + upstreamStats["avg_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowAverage); + result["upstream"] = upstreamStats; } else { result["upstream"] = "mic unknown"; @@ -725,6 +738,12 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { upstreamStats["max_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMax); upstreamStats["avg_gap_30s"] = formatUsecTime(streamStats._timeGapWindowAverage); + upstreamStats["min_gap_usecs"] = static_cast(streamStats._timeGapMin); + upstreamStats["max_gap_usecs"] = static_cast(streamStats._timeGapMax); + upstreamStats["avg_gap_usecs"] = static_cast(streamStats._timeGapAverage); + upstreamStats["min_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowMin); + upstreamStats["max_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowMax); + upstreamStats["avg_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowAverage); injectorArray.push_back(upstreamStats); } } From bc40bc5e5244963abe2effb2e64472dfe54da954 Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Sat, 11 Apr 2020 17:15:44 +0200 Subject: [PATCH 35/73] Update stats backlist for the exporter --- domain-server/src/DomainServerExporter.cpp | 24 ++++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/domain-server/src/DomainServerExporter.cpp b/domain-server/src/DomainServerExporter.cpp index 3ecccfc8b4..47a1c52d1c 100644 --- a/domain-server/src/DomainServerExporter.cpp +++ b/domain-server/src/DomainServerExporter.cpp @@ -239,14 +239,20 @@ static const QMap TYPE_MAP { static const QSet BLACKLIST = { "asset_server_connection_stats_last_heard", // Timestamp as a string "asset_server_username", // Username - "audio_mixer_listeners_jitter_downstream_avg_gap", // Number as string with unit name - "audio_mixer_listeners_jitter_downstream_avg_gap_30s", // Number as string with unit name - "audio_mixer_listeners_jitter_downstream_max_gap", // Number as string with unit name - "audio_mixer_listeners_jitter_downstream_max_gap_30s", // Number as string with unit name - "audio_mixer_listeners_jitter_downstream_min_gap", // Number as string with unit name - "audio_mixer_listeners_jitter_downstream_min_gap_30s", // Number as string with unit name + "audio_mixer_listeners_jitter_downstream_avg_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_avg_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_max_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_max_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_min_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_min_gap_30s", // Number as string with unit name, alternative added "audio_mixer_listeners_jitter_injectors", // Array, empty. TODO: check if this ever contains anything. - "audio_mixer_listeners_jitter_upstream", // Number as string with unit name + "audio_mixer_listeners_jitter_upstream", // Only exists in the absence of a connection + "audio_mixer_listeners_jitter_upstream_avg_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_avg_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_max_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_max_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_min_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_min_gap_30s", // Number as string with unit name, alternative added "audio_mixer_listeners_username", // Username "avatar_mixer_avatars_display_name", // Username "avatar_mixer_avatars_username", // Username @@ -254,8 +260,8 @@ static const QSet BLACKLIST = { "entity_script_server_nodes_username", // Username "entity_server_entity_server_misc_configuration", // Text "entity_server_entity_server_misc_detailed_stats_url", // URL - "entity_server_entity_server_misc_persist_file_load_time", // Number as string with unit name - "entity_server_entity_server_misc_uptime", // Number as string with unit name + "entity_server_entity_server_misc_persist_file_load_time", // Number as string with unit name, alternative added + "entity_server_entity_server_misc_uptime", // Number as string with unit name, alternative added "messages_mixer_messages_username" // Username }; From da5e4d3cfc55d087af69303b0f7e1b9bee7b4f1f Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Sun, 17 May 2020 18:38:37 +0200 Subject: [PATCH 36/73] Review fixes --- domain-server/src/DomainServer.cpp | 8 +- domain-server/src/DomainServerExporter.cpp | 164 ++++++++++----------- 2 files changed, 84 insertions(+), 88 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 284281a64f..03af46d841 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -425,7 +425,7 @@ DomainServer::~DomainServer() { _contentManager->terminate(); } - if ( _httpExporterManager ) { + if (_httpExporterManager) { _httpExporterManager->close(); delete _httpExporterManager; } @@ -3052,16 +3052,16 @@ void DomainServer::initializeExporter() bool isExporterEnabled = _settingsManager.valueOrDefaultValueForKeyPath(ENABLE_EXPORTER).toBool(); int exporterPort = _settingsManager.valueOrDefaultValueForKeyPath(EXPORTER_PORT).toInt(); - if ( exporterPort < 1 || exporterPort > 65535 ) { + if (exporterPort < 1 || exporterPort > 65535) { qCWarning(domain_server) << "Prometheus exporter port " << exporterPort << " is out of range."; isExporterEnabled = false; } qCDebug(domain_server) << "Setting up Prometheus exporter"; - if ( isExporterEnabled && !_httpExporterManager) { + if (isExporterEnabled && !_httpExporterManager) { qCInfo(domain_server) << "Starting Prometheus exporter on port " << exporterPort; - _httpExporterManager = new HTTPManager(QHostAddress::Any, static_cast(exporterPort), QString("%1/resources/prometheus_exporter/").arg(QCoreApplication::applicationDirPath()), &_exporter); + _httpExporterManager = new HTTPManager(QHostAddress::Any, (quint16)exporterPort, QString("%1/resources/prometheus_exporter/").arg(QCoreApplication::applicationDirPath()), &_exporter); } } diff --git a/domain-server/src/DomainServerExporter.cpp b/domain-server/src/DomainServerExporter.cpp index 47a1c52d1c..087f8300b1 100644 --- a/domain-server/src/DomainServerExporter.cpp +++ b/domain-server/src/DomainServerExporter.cpp @@ -11,7 +11,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - // TODO: // // Look into the data provided by OctreeServer::handleHTTPRequest in assignment-client/src/octree/OctreeServer.cpp @@ -33,9 +32,7 @@ #include "HTTPConnection.h" #include "DomainServerNodeData.h" -Q_LOGGING_CATEGORY(domain_server_exporter, "hifi.domain_server.exporter") - - +Q_LOGGING_CATEGORY(domain_server_exporter, "hifi.domain_server.prometheus_exporter") static const QMap TYPE_MAP { { "asset_server_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, @@ -237,55 +234,49 @@ static const QMap TYPE_MAP { // being blacklisted here. static const QSet BLACKLIST = { - "asset_server_connection_stats_last_heard", // Timestamp as a string - "asset_server_username", // Username - "audio_mixer_listeners_jitter_downstream_avg_gap", // Number as string with unit name, alternative added - "audio_mixer_listeners_jitter_downstream_avg_gap_30s", // Number as string with unit name, alternative added - "audio_mixer_listeners_jitter_downstream_max_gap", // Number as string with unit name, alternative added - "audio_mixer_listeners_jitter_downstream_max_gap_30s", // Number as string with unit name, alternative added - "audio_mixer_listeners_jitter_downstream_min_gap", // Number as string with unit name, alternative added - "audio_mixer_listeners_jitter_downstream_min_gap_30s", // Number as string with unit name, alternative added - "audio_mixer_listeners_jitter_injectors", // Array, empty. TODO: check if this ever contains anything. - "audio_mixer_listeners_jitter_upstream", // Only exists in the absence of a connection - "audio_mixer_listeners_jitter_upstream_avg_gap", // Number as string with unit name, alternative added - "audio_mixer_listeners_jitter_upstream_avg_gap_30s", // Number as string with unit name, alternative added - "audio_mixer_listeners_jitter_upstream_max_gap", // Number as string with unit name, alternative added - "audio_mixer_listeners_jitter_upstream_max_gap_30s", // Number as string with unit name, alternative added - "audio_mixer_listeners_jitter_upstream_min_gap", // Number as string with unit name, alternative added - "audio_mixer_listeners_jitter_upstream_min_gap_30s", // Number as string with unit name, alternative added - "audio_mixer_listeners_username", // Username - "avatar_mixer_avatars_display_name", // Username - "avatar_mixer_avatars_username", // Username - "entity_script_server_nodes_node_type", // Username - "entity_script_server_nodes_username", // Username - "entity_server_entity_server_misc_configuration", // Text - "entity_server_entity_server_misc_detailed_stats_url", // URL - "entity_server_entity_server_misc_persist_file_load_time", // Number as string with unit name, alternative added - "entity_server_entity_server_misc_uptime", // Number as string with unit name, alternative added - "messages_mixer_messages_username" // Username + "asset_server_connection_stats_last_heard", // Timestamp as a string + "asset_server_username", // Username + "audio_mixer_listeners_jitter_downstream_avg_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_avg_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_max_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_max_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_min_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_min_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_injectors", // Array, empty. TODO: check if this ever contains anything. + "audio_mixer_listeners_jitter_upstream", // Only exists in the absence of a connection + "audio_mixer_listeners_jitter_upstream_avg_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_avg_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_max_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_max_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_min_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_min_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_username", // Username + "avatar_mixer_avatars_display_name", // Username + "avatar_mixer_avatars_username", // Username + "entity_script_server_nodes_node_type", // Username + "entity_script_server_nodes_username", // Username + "entity_server_entity_server_misc_configuration", // Text + "entity_server_entity_server_misc_detailed_stats_url", // URL + "entity_server_entity_server_misc_persist_file_load_time", // Number as string with unit name, alternative added + "entity_server_entity_server_misc_uptime", // Number as string with unit name, alternative added + "messages_mixer_messages_username" // Username }; - -DomainServerExporter::DomainServerExporter() -{ - +DomainServerExporter::DomainServerExporter() { } -bool DomainServerExporter::handleHTTPRequest(HTTPConnection *connection, const QUrl &url, bool skipSubHandler) -{ +bool DomainServerExporter::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) { const QString URI_METRICS = "/metrics"; const QString EXPORTER_MIME_TYPE = "text/plain"; qCDebug(domain_server_exporter) << "Request on URL " << url; - if ( url.path() == URI_METRICS ) { + if (url.path() == URI_METRICS) { auto nodeList = DependencyManager::get(); QString output = ""; QTextStream out_stream(&output); - nodeList->eachNode([this, &out_stream](const SharedNodePointer& node){ - generateMetricsForNode(out_stream, node); - }); + nodeList->eachNode([this, &out_stream](const SharedNodePointer& node) { generateMetricsForNode(out_stream, node); }); connection->respond(HTTPConnection::StatusCode200, output.toUtf8(), qPrintable(EXPORTER_MIME_TYPE)); return true; @@ -294,8 +285,7 @@ bool DomainServerExporter::handleHTTPRequest(HTTPConnection *connection, const Q return false; } -QString DomainServerExporter::escapeName(const QString &name) -{ +QString DomainServerExporter::escapeName(const QString& name) { QRegularExpression invalid_characters("[^A-Za-z0-9_]"); QString ret = name; @@ -329,99 +319,105 @@ QString DomainServerExporter::escapeName(const QString &name) return ret; } -void DomainServerExporter::generateMetricsForNode( QTextStream &stream, const SharedNodePointer &node ) -{ +void DomainServerExporter::generateMetricsForNode(QTextStream& stream, const SharedNodePointer& node) { QString ret = ""; QJsonObject statsObject = static_cast(node->getLinkedData())->getStatsJSONObject(); QString node_type = NodeType::getNodeTypeName(static_cast(node->getType())); - stream << "\n\n\n"; stream << "###############################################################\n"; stream << "# " << node_type << "\n"; stream << "###############################################################\n"; - generateMetricsFromJson(stream, node_type, escapeName(node_type), QHash(), statsObject); + generateMetricsFromJson(stream, node_type, escapeName(node_type), QHash(), statsObject); QJsonDocument doc(statsObject); - ret.append( doc.toJson() ); - + ret.append(doc.toJson()); } -void DomainServerExporter::generateMetricsFromJson(QTextStream &stream, QString original_path, QString path, QHash labels, const QJsonObject &obj) -{ - for(auto iter = obj.constBegin(); iter != obj.constEnd(); ++iter) { +void DomainServerExporter::generateMetricsFromJson(QTextStream& stream, + QString original_path, + QString path, + QHash labels, + const QJsonObject& obj) { + for (auto iter = obj.constBegin(); iter != obj.constEnd(); ++iter) { auto key = escapeName(iter.key()); auto val = iter.value(); auto metric_name = path + "_" + key; auto orig_metric_name = original_path + " -> " + iter.key(); - if ( val.isObject() ) { + if (val.isObject()) { QUuid possible_uuid = QUuid::fromString(iter.key()); - if ( possible_uuid.isNull() ) { - generateMetricsFromJson(stream, original_path + " -> " + iter.key(), path + "_" + key, labels, iter.value().toObject()); + if (possible_uuid.isNull()) { + generateMetricsFromJson(stream, original_path + " -> " + iter.key(), path + "_" + key, labels, + iter.value().toObject()); } else { - labels.insert("uuid", possible_uuid.toString(QUuid::WithoutBraces)); + labels.insert("uuid", possible_uuid.toString(QUuid::WithoutBraces)); generateMetricsFromJson(stream, original_path, path, labels, iter.value().toObject()); } - continue; } - if ( BLACKLIST.contains(metric_name)) { + if (BLACKLIST.contains(metric_name)) { continue; } bool conversion_ok = false; double converted = 0; - if ( val.isString() ) { + if (val.isString()) { // Prometheus only deals with numeric values. See if this string contains a valid one QString tmp = val.toString(); converted = tmp.toDouble(&conversion_ok); - if ( !conversion_ok ) { - qCWarning(domain_server_exporter) << "Failed to convert value of " << orig_metric_name << " (" << metric_name << ") to double: " << tmp << "'"; + if (!conversion_ok) { + qCWarning(domain_server_exporter) << "Failed to convert value of " << orig_metric_name << " (" << metric_name + << ") to double: " << tmp << "'"; continue; } - } stream << QString("\n# HELP %1 %2 -> %3\n").arg(metric_name).arg(original_path).arg(iter.key()); - if ( TYPE_MAP.contains(metric_name )) { + if (TYPE_MAP.contains(metric_name)) { stream << "# TYPE " << metric_name << " "; - switch( TYPE_MAP[metric_name ]) { - case DomainServerExporter::MetricType::Untyped: - stream << "untyped"; break; - case DomainServerExporter::MetricType::Counter: - stream << "counter"; break; - case DomainServerExporter::MetricType::Gauge: - stream << "gauge"; break; - case DomainServerExporter::MetricType::Histogram: - stream << "histogram"; break; - case DomainServerExporter::MetricType::Summary: - stream << "summary"; break; + switch (TYPE_MAP[metric_name]) { + case DomainServerExporter::MetricType::Untyped: + stream << "untyped"; + break; + case DomainServerExporter::MetricType::Counter: + stream << "counter"; + break; + case DomainServerExporter::MetricType::Gauge: + stream << "gauge"; + break; + case DomainServerExporter::MetricType::Histogram: + stream << "histogram"; + break; + case DomainServerExporter::MetricType::Summary: + stream << "summary"; + break; } stream << "\n"; } else { - qCWarning(domain_server_exporter) << "Type for metric " << orig_metric_name << " (" << metric_name << ") not known."; + qCWarning(domain_server_exporter) + << "Type for metric " << orig_metric_name << " (" << metric_name << ") not known."; } stream << path << "_" << key; - if (!labels.isEmpty() ) { + if (!labels.isEmpty()) { stream << "{"; bool is_first = true; - QHashIterator iter(labels); + QHashIterator iter(labels); - while( iter.hasNext() ) { + while (iter.hasNext()) { iter.next(); - if ( ! is_first ) { + if (!is_first) { stream << ","; } @@ -439,18 +435,18 @@ void DomainServerExporter::generateMetricsFromJson(QTextStream &stream, QString stream << " "; - if ( val.isBool() ) { - stream << ( iter.value().toBool() ? "1" : "0" ); - } else if ( val.isDouble() ) { + if (val.isBool()) { + stream << (iter.value().toBool() ? "1" : "0"); + } else if (val.isDouble()) { stream << val.toDouble(); - } else if ( val.isString() ) { + } else if (val.isString()) { // Converted above stream << converted; } else { - qCWarning(domain_server_exporter) << "Can't convert metric " << orig_metric_name << "(" << metric_name << ") with value " << val; + qCWarning(domain_server_exporter) + << "Can't convert metric " << orig_metric_name << "(" << metric_name << ") with value " << val; } stream << "\n"; } } - From 303f10dada255544cb7c9170d8876734fa130a1d Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Fri, 29 May 2020 01:37:20 +0200 Subject: [PATCH 37/73] Review fixes --- assignment-client/src/octree/OctreeServer.cpp | 11 +++--- domain-server/src/DomainServer.cpp | 5 ++- domain-server/src/DomainServerExporter.cpp | 36 +++++++++---------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index c1cf3d2297..2aec19b6b1 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -89,6 +89,7 @@ int OctreeServer::_shortProcessWait = 0; int OctreeServer::_noProcessWait = 0; static const QString PERSIST_FILE_DOWNLOAD_PATH = "/models.json.gz"; +static const double NANOSECONDS_PER_SECOND = 1000000.0;; void OctreeServer::resetSendingStats() { @@ -1344,9 +1345,8 @@ QString OctreeServer::getUptime() { return formattedUptime; } -double OctreeServer::getUptimeSeconds() -{ - return (usecTimestampNow() - _startedUSecs) / 1000000.0; +double OctreeServer::getUptimeSeconds() { + return (usecTimestampNow() - _startedUSecs) / NANOSECONDS_PER_SECOND; } QString OctreeServer::getFileLoadTime() { @@ -1391,9 +1391,8 @@ QString OctreeServer::getFileLoadTime() { return result; } -double OctreeServer::getFileLoadTimeSeconds() -{ - return getLoadElapsedTime() / 1000000.0; +double OctreeServer::getFileLoadTimeSeconds() { + return getLoadElapsedTime() / NANOSECONDS_PER_SECOND; } QString OctreeServer::getConfiguration() { diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 03af46d841..75a1c5c7c0 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -67,6 +67,9 @@ Q_LOGGING_CATEGORY(domain_server_ice, "hifi.domain_server.ice") const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token"; const QString DomainServer::REPLACEMENT_FILE_EXTENSION = ".replace"; +const int MIN_PORT = 1; +const int MAX_PORT = 65535; + int const DomainServer::EXIT_CODE_REBOOT = 234923; @@ -3052,7 +3055,7 @@ void DomainServer::initializeExporter() bool isExporterEnabled = _settingsManager.valueOrDefaultValueForKeyPath(ENABLE_EXPORTER).toBool(); int exporterPort = _settingsManager.valueOrDefaultValueForKeyPath(EXPORTER_PORT).toInt(); - if (exporterPort < 1 || exporterPort > 65535) { + if (exporterPort < MIN_PORT || exporterPort > MAX_PORT) { qCWarning(domain_server) << "Prometheus exporter port " << exporterPort << " is out of range."; isExporterEnabled = false; } diff --git a/domain-server/src/DomainServerExporter.cpp b/domain-server/src/DomainServerExporter.cpp index 087f8300b1..3dc2023548 100644 --- a/domain-server/src/DomainServerExporter.cpp +++ b/domain-server/src/DomainServerExporter.cpp @@ -341,16 +341,16 @@ void DomainServerExporter::generateMetricsFromJson(QTextStream& stream, QHash labels, const QJsonObject& obj) { for (auto iter = obj.constBegin(); iter != obj.constEnd(); ++iter) { - auto key = escapeName(iter.key()); - auto val = iter.value(); - auto metric_name = path + "_" + key; + auto escaped_key = escapeName(iter.key()); + auto metric_value = iter.value(); + auto metric_name = path + "_" + escaped_key; auto orig_metric_name = original_path + " -> " + iter.key(); - if (val.isObject()) { + if (metric_value.isObject()) { QUuid possible_uuid = QUuid::fromString(iter.key()); if (possible_uuid.isNull()) { - generateMetricsFromJson(stream, original_path + " -> " + iter.key(), path + "_" + key, labels, + generateMetricsFromJson(stream, original_path + " -> " + iter.key(), path + "_" + escaped_key, labels, iter.value().toObject()); } else { labels.insert("uuid", possible_uuid.toString(QUuid::WithoutBraces)); @@ -367,10 +367,10 @@ void DomainServerExporter::generateMetricsFromJson(QTextStream& stream, bool conversion_ok = false; double converted = 0; - if (val.isString()) { + if (metric_value.isString()) { // Prometheus only deals with numeric values. See if this string contains a valid one - QString tmp = val.toString(); + QString tmp = metric_value.toString(); converted = tmp.toDouble(&conversion_ok); if (!conversion_ok) { @@ -407,7 +407,7 @@ void DomainServerExporter::generateMetricsFromJson(QTextStream& stream, << "Type for metric " << orig_metric_name << " (" << metric_name << ") not known."; } - stream << path << "_" << key; + stream << path << "_" << escaped_key; if (!labels.isEmpty()) { stream << "{"; @@ -421,12 +421,12 @@ void DomainServerExporter::generateMetricsFromJson(QTextStream& stream, stream << ","; } - QString value = iter.value(); - value.replace("\\", "\\\\"); - value.replace("\"", "\\\""); - value.replace("\n", "\\\n"); + QString escaped_value = iter.value(); + escaped_value.replace("\\", "\\\\"); + escaped_value.replace("\"", "\\\""); + escaped_value.replace("\n", "\\\n"); - stream << iter.key() << "=\"" << value << "\""; + stream << iter.key() << "=\"" << escaped_value << "\""; is_first = false; } @@ -435,16 +435,16 @@ void DomainServerExporter::generateMetricsFromJson(QTextStream& stream, stream << " "; - if (val.isBool()) { + if (metric_value.isBool()) { stream << (iter.value().toBool() ? "1" : "0"); - } else if (val.isDouble()) { - stream << val.toDouble(); - } else if (val.isString()) { + } else if (metric_value.isDouble()) { + stream << metric_value.toDouble(); + } else if (metric_value.isString()) { // Converted above stream << converted; } else { qCWarning(domain_server_exporter) - << "Can't convert metric " << orig_metric_name << "(" << metric_name << ") with value " << val; + << "Can't convert metric " << orig_metric_name << "(" << metric_name << ") with value " << metric_value; } stream << "\n"; From 01b03173481bc9bcedbe2c17646cdb82de8b185e Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Fri, 29 May 2020 01:42:46 +0200 Subject: [PATCH 38/73] Add missing gauges --- domain-server/src/DomainServerExporter.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/domain-server/src/DomainServerExporter.cpp b/domain-server/src/DomainServerExporter.cpp index 3dc2023548..80af5f37db 100644 --- a/domain-server/src/DomainServerExporter.cpp +++ b/domain-server/src/DomainServerExporter.cpp @@ -84,6 +84,12 @@ static const QMap TYPE_MAP { { "audio_mixer_listeners_jitter_downstream_desired" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_listeners_jitter_downstream_lost_percent" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_listeners_jitter_downstream_lost_percent_30s" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_avg_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_avg_gap_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_max_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_max_gap_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_min_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_min_gap_usecs" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_listeners_jitter_downstream_not_mixed" , DomainServerExporter::MetricType::Counter }, { "audio_mixer_listeners_jitter_downstream_overflows" , DomainServerExporter::MetricType::Counter }, { "audio_mixer_listeners_jitter_downstream_starves" , DomainServerExporter::MetricType::Counter }, From 9e478ba4587ae7c53f3c28a8e3ca4882373fe35e Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Fri, 29 May 2020 20:07:51 +0200 Subject: [PATCH 39/73] More variable renaming --- domain-server/src/DomainServer.h | 2 +- domain-server/src/DomainServerExporter.cpp | 86 +++++++++++----------- domain-server/src/DomainServerExporter.h | 2 +- 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 46968c7a0c..7c0fa5fb15 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -239,7 +239,7 @@ private: DomainServerExporter _exporter; HTTPManager _httpManager; - HTTPManager *_httpExporterManager { nullptr }; + HTTPManager* _httpExporterManager { nullptr }; std::unique_ptr _httpsManager; QHash _allAssignments; diff --git a/domain-server/src/DomainServerExporter.cpp b/domain-server/src/DomainServerExporter.cpp index 80af5f37db..f48f77422f 100644 --- a/domain-server/src/DomainServerExporter.cpp +++ b/domain-server/src/DomainServerExporter.cpp @@ -280,9 +280,9 @@ bool DomainServerExporter::handleHTTPRequest(HTTPConnection* connection, const Q if (url.path() == URI_METRICS) { auto nodeList = DependencyManager::get(); QString output = ""; - QTextStream out_stream(&output); + QTextStream outStream(&output); - nodeList->eachNode([this, &out_stream](const SharedNodePointer& node) { generateMetricsForNode(out_stream, node); }); + nodeList->eachNode([this, &outStream](const SharedNodePointer& node) { generateMetricsForNode(outStream, node); }); connection->respond(HTTPConnection::StatusCode200, output.toUtf8(), qPrintable(EXPORTER_MIME_TYPE)); return true; @@ -292,7 +292,7 @@ bool DomainServerExporter::handleHTTPRequest(HTTPConnection* connection, const Q } QString DomainServerExporter::escapeName(const QString& name) { - QRegularExpression invalid_characters("[^A-Za-z0-9_]"); + QRegularExpression invalidCharacters("[^A-Za-z0-9_]"); QString ret = name; @@ -311,7 +311,7 @@ QString DomainServerExporter::escapeName(const QString& name) { ret.replace(QRegularExpression("([a-z])([A-Z])"), "\\1_\\2"); // Replace all invalid characters with a _ - ret.replace(invalid_characters, "_"); + ret.replace(invalidCharacters, "_"); // Remove any "_" characters at the beginning or end ret.replace(QRegularExpression("^_+"), ""); @@ -328,69 +328,69 @@ QString DomainServerExporter::escapeName(const QString& name) { void DomainServerExporter::generateMetricsForNode(QTextStream& stream, const SharedNodePointer& node) { QString ret = ""; QJsonObject statsObject = static_cast(node->getLinkedData())->getStatsJSONObject(); - QString node_type = NodeType::getNodeTypeName(static_cast(node->getType())); + QString nodeType = NodeType::getNodeTypeName(static_cast(node->getType())); stream << "\n\n\n"; stream << "###############################################################\n"; - stream << "# " << node_type << "\n"; + stream << "# " << nodeType << "\n"; stream << "###############################################################\n"; - generateMetricsFromJson(stream, node_type, escapeName(node_type), QHash(), statsObject); + generateMetricsFromJson(stream, nodeType, escapeName(nodeType), QHash(), statsObject); QJsonDocument doc(statsObject); ret.append(doc.toJson()); } void DomainServerExporter::generateMetricsFromJson(QTextStream& stream, - QString original_path, + QString originalPath, QString path, QHash labels, - const QJsonObject& obj) { - for (auto iter = obj.constBegin(); iter != obj.constEnd(); ++iter) { - auto escaped_key = escapeName(iter.key()); - auto metric_value = iter.value(); - auto metric_name = path + "_" + escaped_key; - auto orig_metric_name = original_path + " -> " + iter.key(); + const QJsonObject& root) { + for (auto iter = root.constBegin(); iter != root.constEnd(); ++iter) { + auto escapedKey = escapeName(iter.key()); + auto metricValue = iter.value(); + auto metricName = path + "_" + escapedKey; + auto origMetricName = originalPath + " -> " + iter.key(); - if (metric_value.isObject()) { + if (metricValue.isObject()) { QUuid possible_uuid = QUuid::fromString(iter.key()); if (possible_uuid.isNull()) { - generateMetricsFromJson(stream, original_path + " -> " + iter.key(), path + "_" + escaped_key, labels, + generateMetricsFromJson(stream, originalPath + " -> " + iter.key(), path + "_" + escapedKey, labels, iter.value().toObject()); } else { labels.insert("uuid", possible_uuid.toString(QUuid::WithoutBraces)); - generateMetricsFromJson(stream, original_path, path, labels, iter.value().toObject()); + generateMetricsFromJson(stream, originalPath, path, labels, iter.value().toObject()); } continue; } - if (BLACKLIST.contains(metric_name)) { + if (BLACKLIST.contains(metricName)) { continue; } - bool conversion_ok = false; + bool conversionOk = false; double converted = 0; - if (metric_value.isString()) { + if (metricValue.isString()) { // Prometheus only deals with numeric values. See if this string contains a valid one - QString tmp = metric_value.toString(); - converted = tmp.toDouble(&conversion_ok); + QString tmp = metricValue.toString(); + converted = tmp.toDouble(&conversionOk); - if (!conversion_ok) { - qCWarning(domain_server_exporter) << "Failed to convert value of " << orig_metric_name << " (" << metric_name + if (!conversionOk) { + qCWarning(domain_server_exporter) << "Failed to convert value of " << origMetricName << " (" << metricName << ") to double: " << tmp << "'"; continue; } } - stream << QString("\n# HELP %1 %2 -> %3\n").arg(metric_name).arg(original_path).arg(iter.key()); + stream << QString("\n# HELP %1 %2 -> %3\n").arg(metricName).arg(originalPath).arg(iter.key()); - if (TYPE_MAP.contains(metric_name)) { - stream << "# TYPE " << metric_name << " "; - switch (TYPE_MAP[metric_name]) { + if (TYPE_MAP.contains(metricName)) { + stream << "# TYPE " << metricName << " "; + switch (TYPE_MAP[metricName]) { case DomainServerExporter::MetricType::Untyped: stream << "untyped"; break; @@ -410,47 +410,47 @@ void DomainServerExporter::generateMetricsFromJson(QTextStream& stream, stream << "\n"; } else { qCWarning(domain_server_exporter) - << "Type for metric " << orig_metric_name << " (" << metric_name << ") not known."; + << "Type for metric " << origMetricName << " (" << metricName << ") not known."; } - stream << path << "_" << escaped_key; + stream << path << "_" << escapedKey; if (!labels.isEmpty()) { stream << "{"; - bool is_first = true; + bool isFirst = true; QHashIterator iter(labels); while (iter.hasNext()) { iter.next(); - if (!is_first) { + if (!isFirst) { stream << ","; } - QString escaped_value = iter.value(); - escaped_value.replace("\\", "\\\\"); - escaped_value.replace("\"", "\\\""); - escaped_value.replace("\n", "\\\n"); + QString escapedValue = iter.value(); + escapedValue.replace("\\", "\\\\"); + escapedValue.replace("\"", "\\\""); + escapedValue.replace("\n", "\\\n"); - stream << iter.key() << "=\"" << escaped_value << "\""; + stream << iter.key() << "=\"" << escapedValue << "\""; - is_first = false; + isFirst = false; } stream << "}"; } stream << " "; - if (metric_value.isBool()) { + if (metricValue.isBool()) { stream << (iter.value().toBool() ? "1" : "0"); - } else if (metric_value.isDouble()) { - stream << metric_value.toDouble(); - } else if (metric_value.isString()) { + } else if (metricValue.isDouble()) { + stream << metricValue.toDouble(); + } else if (metricValue.isString()) { // Converted above stream << converted; } else { qCWarning(domain_server_exporter) - << "Can't convert metric " << orig_metric_name << "(" << metric_name << ") with value " << metric_value; + << "Can't convert metric " << origMetricName << "(" << metricName << ") with value " << metricValue; } stream << "\n"; diff --git a/domain-server/src/DomainServerExporter.h b/domain-server/src/DomainServerExporter.h index d818ad8114..d823c44c99 100644 --- a/domain-server/src/DomainServerExporter.h +++ b/domain-server/src/DomainServerExporter.h @@ -48,7 +48,7 @@ public: private: QString escapeName(const QString &name); - void generateMetricsForNode( QTextStream &stream, const SharedNodePointer &node ); + void generateMetricsForNode(QTextStream &stream, const SharedNodePointer &node); void generateMetricsFromJson(QTextStream &stream, QString original_path, QString path, QHash labels, const QJsonObject &obj); }; From c0a0d39ce5df6772a063eb390c292b4ec6ae30a7 Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Fri, 29 May 2020 22:20:56 +0200 Subject: [PATCH 40/73] More style fixes --- domain-server/src/DomainServer.cpp | 2 -- domain-server/src/DomainServerExporter.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 75a1c5c7c0..49b319e31d 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1986,8 +1986,6 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url const QString URI_API_BACKUPS_ID = "/api/backups/"; const QString URI_API_BACKUPS_DOWNLOAD_ID = "/api/backups/download/"; const QString URI_API_BACKUPS_RECOVER = "/api/backups/recover/"; - const QString URI_EXPORTER_= "/metrics"; - const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; QPointer connectionPtr { connection }; diff --git a/domain-server/src/DomainServerExporter.h b/domain-server/src/DomainServerExporter.h index d823c44c99..427ad256d5 100644 --- a/domain-server/src/DomainServerExporter.h +++ b/domain-server/src/DomainServerExporter.h @@ -49,7 +49,7 @@ public: private: QString escapeName(const QString &name); void generateMetricsForNode(QTextStream &stream, const SharedNodePointer &node); - void generateMetricsFromJson(QTextStream &stream, QString original_path, QString path, QHash labels, const QJsonObject &obj); + void generateMetricsFromJson(QTextStream &stream, QString original_path, QString path, QHash labels, const QJsonObject &obj); }; #endif // DOMAINSERVEREXPORTER_H From 45f1640b5a6a10e9353ea056d6e80a69b367be8c Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Fri, 29 May 2020 22:22:30 +0200 Subject: [PATCH 41/73] Remove useless code --- domain-server/src/DomainServerExporter.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/domain-server/src/DomainServerExporter.cpp b/domain-server/src/DomainServerExporter.cpp index f48f77422f..2f0d750b9d 100644 --- a/domain-server/src/DomainServerExporter.cpp +++ b/domain-server/src/DomainServerExporter.cpp @@ -326,7 +326,6 @@ QString DomainServerExporter::escapeName(const QString& name) { } void DomainServerExporter::generateMetricsForNode(QTextStream& stream, const SharedNodePointer& node) { - QString ret = ""; QJsonObject statsObject = static_cast(node->getLinkedData())->getStatsJSONObject(); QString nodeType = NodeType::getNodeTypeName(static_cast(node->getType())); @@ -336,9 +335,6 @@ void DomainServerExporter::generateMetricsForNode(QTextStream& stream, const Sha stream << "###############################################################\n"; generateMetricsFromJson(stream, nodeType, escapeName(nodeType), QHash(), statsObject); - - QJsonDocument doc(statsObject); - ret.append(doc.toJson()); } void DomainServerExporter::generateMetricsFromJson(QTextStream& stream, From 172ae454202e50ea91f6e5f73313d2e7280beade Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Wed, 3 Jun 2020 21:41:40 +0200 Subject: [PATCH 42/73] Another variable renaming --- domain-server/src/DomainServerExporter.cpp | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/domain-server/src/DomainServerExporter.cpp b/domain-server/src/DomainServerExporter.cpp index 2f0d750b9d..8d6dc348ff 100644 --- a/domain-server/src/DomainServerExporter.cpp +++ b/domain-server/src/DomainServerExporter.cpp @@ -294,35 +294,35 @@ bool DomainServerExporter::handleHTTPRequest(HTTPConnection* connection, const Q QString DomainServerExporter::escapeName(const QString& name) { QRegularExpression invalidCharacters("[^A-Za-z0-9_]"); - QString ret = name; + QString result = name; // If a key is named something like: "6. threads", turn it into just "threads" - ret.replace(QRegularExpression("^\\d+\\. "), ""); - ret.replace(QRegularExpression("^\\d+_"), ""); + result.replace(QRegularExpression("^\\d+\\. "), ""); + result.replace(QRegularExpression("^\\d+_"), ""); // If a key is named something like "z_listeners", turn it into just "listeners" - ret.replace(QRegularExpression("^z_"), ""); + result.replace(QRegularExpression("^z_"), ""); // If a key is named something like "lost%", change it to "lost_percent_". // redundant underscores will be removed below. - ret.replace(QRegularExpression("%"), "_percent_"); + result.replace(QRegularExpression("%"), "_percent_"); // change mixedCaseNames to mixed_case_names - ret.replace(QRegularExpression("([a-z])([A-Z])"), "\\1_\\2"); + result.replace(QRegularExpression("([a-z])([A-Z])"), "\\1_\\2"); // Replace all invalid characters with a _ - ret.replace(invalidCharacters, "_"); + result.replace(invalidCharacters, "_"); // Remove any "_" characters at the beginning or end - ret.replace(QRegularExpression("^_+"), ""); - ret.replace(QRegularExpression("_+$"), ""); + result.replace(QRegularExpression("^_+"), ""); + result.replace(QRegularExpression("_+$"), ""); // Replace any duplicated _ characters with a single one - ret.replace(QRegularExpression("_+"), "_"); + result.replace(QRegularExpression("_+"), "_"); - ret = ret.toLower(); + result = result.toLower(); - return ret; + return result; } void DomainServerExporter::generateMetricsForNode(QTextStream& stream, const SharedNodePointer& node) { From b2a5e7fdd16a3c7513aecf687c553c1eb6220e18 Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Mon, 8 Jun 2020 17:53:27 +0200 Subject: [PATCH 43/73] Additional review fixes --- assignment-client/src/octree/OctreeServer.cpp | 2 +- domain-server/src/DomainServerExporter.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 2aec19b6b1..63520262cd 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -1392,7 +1392,7 @@ QString OctreeServer::getFileLoadTime() { } double OctreeServer::getFileLoadTimeSeconds() { - return getLoadElapsedTime() / NANOSECONDS_PER_SECOND; + return getLoadElapsedTime() / NANOSECONDS_PER_SECOND; } QString OctreeServer::getConfiguration() { diff --git a/domain-server/src/DomainServerExporter.h b/domain-server/src/DomainServerExporter.h index 427ad256d5..500532621a 100644 --- a/domain-server/src/DomainServerExporter.h +++ b/domain-server/src/DomainServerExporter.h @@ -48,8 +48,8 @@ public: private: QString escapeName(const QString &name); - void generateMetricsForNode(QTextStream &stream, const SharedNodePointer &node); - void generateMetricsFromJson(QTextStream &stream, QString original_path, QString path, QHash labels, const QJsonObject &obj); + void generateMetricsForNode(QTextStream& stream, const SharedNodePointer& node); + void generateMetricsFromJson(QTextStream& stream, QString originalPath, QString path, QHash labels, const QJsonObject& obj); }; #endif // DOMAINSERVEREXPORTER_H From 49f72f0ca637eb7a0aac96b172af2796fc21095a Mon Sep 17 00:00:00 2001 From: Kasen IO Date: Mon, 8 Jun 2020 23:38:15 -0400 Subject: [PATCH 44/73] 1st pass at disabling certified auto-deletion. --- libraries/entities/src/EntityTree.cpp | 58 +++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index fedda7a42e..c57bc7b1de 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1446,14 +1446,14 @@ void EntityTree::addCertifiedEntityOnServer(EntityItemPointer entity) { entityList << entityItemID; // adds to list within hash because entityList is a reference. qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID << "total" << entityList.size() << "entities."; } - // Delete an already-existing entity from the tree if it has the same + // Handle an already-existing entity from the tree if it has the same // CertificateID as the entity we're trying to add. if (!existingEntityItemID.isNull()) { qCDebug(entities) << "Certificate ID" << certID << "already exists on entity with ID" - << existingEntityItemID << ". Deleting existing entity."; - withWriteLock([&] { - deleteEntity(existingEntityItemID, true); - }); + << existingEntityItemID << ". No action will be taken to remove it."; + // withWriteLock([&] { + // deleteEntity(existingEntityItemID, true); + // }); } } @@ -1527,10 +1527,10 @@ void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove continue; } qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() - << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << entityID; - withWriteLock([&] { - deleteEntity(entityID, true); - }); + << "doesn't match the current Domain ID" << thisDomainID << ". No action will be taken to remove it: " << entityID; + // withWriteLock([&] { + // deleteEntity(entityID, true); + // }); } { QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); @@ -1555,10 +1555,10 @@ void EntityTree::startChallengeOwnershipTimer(const EntityItemID& entityItemID) } }); connect(_challengeOwnershipTimeoutTimer, &QTimer::timeout, this, [=]() { - qCDebug(entities) << "Ownership challenge timed out, deleting entity" << entityItemID; - withWriteLock([&] { - deleteEntity(entityItemID, true); - }); + qCDebug(entities) << "Ownership challenge timed out for entity " << entityItemID << ". No action will be taken to remove it."; + // withWriteLock([&] { + // deleteEntity(entityItemID, true); + // }); if (_challengeOwnershipTimeoutTimer) { _challengeOwnershipTimeoutTimer->stop(); _challengeOwnershipTimeoutTimer->deleteLater(); @@ -1650,10 +1650,10 @@ void EntityTree::sendChallengeOwnershipPacket(const QString& certID, const QStri QByteArray text = computeNonce(entityItemID, ownerKey); if (text == "") { - qCDebug(entities) << "CRITICAL ERROR: Couldn't compute nonce. Deleting entity..."; - withWriteLock([&] { - deleteEntity(entityItemID, true); - }); + qCDebug(entities) << "CRITICAL ERROR: Couldn't compute nonce. No action will be taken to remove this entity."; + // withWriteLock([&] { + // deleteEntity(entityItemID, true); + // }); } else { qCDebug(entities) << "Challenging ownership of Cert ID" << certID; // 2. Send the nonce to the rezzing avatar's node @@ -1724,15 +1724,15 @@ void EntityTree::validatePop(const QString& certID, const EntityItemID& entityIt if (networkReply->error() == QNetworkReply::NoError) { if (!jsonObject["invalid_reason"].toString().isEmpty()) { - qCDebug(entities) << "invalid_reason not empty, deleting entity" << entityItemID; - withWriteLock([&] { - deleteEntity(entityItemID, true); - }); + qCDebug(entities) << "invalid_reason not empty, no action will be taken to delete entity" << entityItemID; + // withWriteLock([&] { + // deleteEntity(entityItemID, true); + // }); } else if (jsonObject["transfer_status"].toArray().first().toString() == "failed") { - qCDebug(entities) << "'transfer_status' is 'failed', deleting entity" << entityItemID; - withWriteLock([&] { - deleteEntity(entityItemID, true); - }); + qCDebug(entities) << "'transfer_status' is 'failed', no action will be taken to delete entity" << entityItemID; + // withWriteLock([&] { + // deleteEntity(entityItemID, true); + // }); } else { // Second, challenge ownership of the PoP cert // (ignore pending status; a failure will be cleaned up during DDV) @@ -1742,11 +1742,11 @@ void EntityTree::validatePop(const QString& certID, const EntityItemID& entityIt senderNode); } } else { - qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; deleting entity" << entityItemID + qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; no action will be taken to delete entity" << entityItemID << "More info:" << jsonObject; - withWriteLock([&] { - deleteEntity(entityItemID, true); - }); + // withWriteLock([&] { + // deleteEntity(entityItemID, true); + // }); } networkReply->deleteLater(); From f6214d46d00b011938c93b7e1963a65e7bf211f1 Mon Sep 17 00:00:00 2001 From: Kasen IO Date: Tue, 9 Jun 2020 15:07:39 -0400 Subject: [PATCH 45/73] Add comments indicating intent. --- libraries/entities/src/EntityTree.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index c57bc7b1de..1ce19033e5 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1451,6 +1451,9 @@ void EntityTree::addCertifiedEntityOnServer(EntityItemPointer entity) { if (!existingEntityItemID.isNull()) { qCDebug(entities) << "Certificate ID" << certID << "already exists on entity with ID" << existingEntityItemID << ". No action will be taken to remove it."; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. // withWriteLock([&] { // deleteEntity(existingEntityItemID, true); // }); @@ -1528,6 +1531,9 @@ void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove } qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() << "doesn't match the current Domain ID" << thisDomainID << ". No action will be taken to remove it: " << entityID; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. // withWriteLock([&] { // deleteEntity(entityID, true); // }); @@ -1556,6 +1562,9 @@ void EntityTree::startChallengeOwnershipTimer(const EntityItemID& entityItemID) }); connect(_challengeOwnershipTimeoutTimer, &QTimer::timeout, this, [=]() { qCDebug(entities) << "Ownership challenge timed out for entity " << entityItemID << ". No action will be taken to remove it."; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. // withWriteLock([&] { // deleteEntity(entityItemID, true); // }); @@ -1651,6 +1660,9 @@ void EntityTree::sendChallengeOwnershipPacket(const QString& certID, const QStri if (text == "") { qCDebug(entities) << "CRITICAL ERROR: Couldn't compute nonce. No action will be taken to remove this entity."; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. // withWriteLock([&] { // deleteEntity(entityItemID, true); // }); @@ -1725,11 +1737,17 @@ void EntityTree::validatePop(const QString& certID, const EntityItemID& entityIt if (networkReply->error() == QNetworkReply::NoError) { if (!jsonObject["invalid_reason"].toString().isEmpty()) { qCDebug(entities) << "invalid_reason not empty, no action will be taken to delete entity" << entityItemID; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. // withWriteLock([&] { // deleteEntity(entityItemID, true); // }); } else if (jsonObject["transfer_status"].toArray().first().toString() == "failed") { qCDebug(entities) << "'transfer_status' is 'failed', no action will be taken to delete entity" << entityItemID; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. // withWriteLock([&] { // deleteEntity(entityItemID, true); // }); @@ -1744,6 +1762,9 @@ void EntityTree::validatePop(const QString& certID, const EntityItemID& entityIt } else { qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; no action will be taken to delete entity" << entityItemID << "More info:" << jsonObject; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. // withWriteLock([&] { // deleteEntity(entityItemID, true); // }); From 170c78e274f0b0dea825831615cbe6d5e52ed29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Wed, 10 Jun 2020 02:53:22 +0200 Subject: [PATCH 46/73] Delete unused athena-server.spec --- pkg-scripts/athena-server.spec | 128 --------------------------------- 1 file changed, 128 deletions(-) delete mode 100644 pkg-scripts/athena-server.spec diff --git a/pkg-scripts/athena-server.spec b/pkg-scripts/athena-server.spec deleted file mode 100644 index 6c751a8e50..0000000000 --- a/pkg-scripts/athena-server.spec +++ /dev/null @@ -1,128 +0,0 @@ -#ATHENA=~/Athena rpmbuild --target x86_64 -bb athena-server.spec -%define version %{lua:print(os.getenv("VERSION"))} -%define depends %{lua:print(os.getenv("DEPENDS"))} - -Name: athena-server -Version: %{version} -Release: 1%{?dist} -Summary: Vircadia metaverse platform, based on the High Fidelity Engine. - -License: ASL 2.0 -URL: https://vircadia.com -Source0: https://github.com/daleglass/vircadia-builder/blob/master/vircadia-builder - -#BuildRequires: systemd-rpm-macros -BuildRequires: chrpath -Requires: %{depends} -BuildArch: x86_64 -AutoReq: no -AutoProv: no - -%description -Vircadia allows creation and sharing of VR experiences. - The Vircadia metaverse provides built-in social features, including avatar interactions, spatialized audio and interactive physics. Additionally, you have the ability to import any 3D object into your virtual environment. - - -%prep - - -%build - - -%install -rm -rf $RPM_BUILD_ROOT -install -d $RPM_BUILD_ROOT/opt/athena -install -m 0755 -t $RPM_BUILD_ROOT/opt/athena $ATHENA/build/assignment-client/assignment-client -install -m 0755 -t $RPM_BUILD_ROOT/opt/athena $ATHENA/build/domain-server/domain-server -install -m 0755 -t $RPM_BUILD_ROOT/opt/athena $ATHENA/build/tools/oven/oven -#install -m 0755 -t $RPM_BUILD_ROOT/opt/athena $ATHENA/build/ice-server/ice-server -strip --strip-all $RPM_BUILD_ROOT/opt/athena/* -chrpath -d $RPM_BUILD_ROOT/opt/athena/* -install -m 0755 -t $RPM_BUILD_ROOT/opt/athena $ATHENA/source/pkg-scripts/new-server -install -d $RPM_BUILD_ROOT/opt/athena/lib -install -m 0644 -t $RPM_BUILD_ROOT/opt/athena/lib $ATHENA/build/libraries/*/*.so -strip --strip-all $RPM_BUILD_ROOT/opt/athena/lib/* -chrpath -d $RPM_BUILD_ROOT/opt/athena/lib/* -install -m 0644 -t $RPM_BUILD_ROOT/opt/athena/lib $ATHENA/qt5-install/lib/libQt5Network.so.*.*.* -install -m 0644 -t $RPM_BUILD_ROOT/opt/athena/lib $ATHENA/qt5-install/lib/libQt5Core.so.*.*.* -install -m 0644 -t $RPM_BUILD_ROOT/opt/athena/lib $ATHENA/qt5-install/lib/libQt5Widgets.so.*.*.* -install -m 0644 -t $RPM_BUILD_ROOT/opt/athena/lib $ATHENA/qt5-install/lib/libQt5Gui.so.*.*.* -install -m 0644 -t $RPM_BUILD_ROOT/opt/athena/lib $ATHENA/qt5-install/lib/libQt5Script.so.*.*.* -install -m 0644 -t $RPM_BUILD_ROOT/opt/athena/lib $ATHENA/qt5-install/lib/libQt5Quick.so.*.*.* -install -m 0644 -t $RPM_BUILD_ROOT/opt/athena/lib $ATHENA/qt5-install/lib/libQt5WebSockets.so.*.*.* -install -m 0644 -t $RPM_BUILD_ROOT/opt/athena/lib $ATHENA/qt5-install/lib/libQt5Qml.so.*.*.* -install -m 0644 -t $RPM_BUILD_ROOT/opt/athena/lib $ATHENA/qt5-install/lib/libQt5ScriptTools.so.*.*.* -install -m 0644 -t $RPM_BUILD_ROOT/opt/athena/lib $ATHENA/build/ext/makefiles/quazip/project/lib/libquazip5.so.*.*.* -install -d $RPM_BUILD_ROOT/usr/lib/systemd/system -install -m 0644 -t $RPM_BUILD_ROOT/usr/lib/systemd/system $ATHENA/source/pkg-scripts/athena-assignment-client.service -install -m 0644 -t $RPM_BUILD_ROOT/usr/lib/systemd/system $ATHENA/source/pkg-scripts/athena-assignment-client@.service -install -m 0644 -t $RPM_BUILD_ROOT/usr/lib/systemd/system $ATHENA/source/pkg-scripts/athena-domain-server.service -install -m 0644 -t $RPM_BUILD_ROOT/usr/lib/systemd/system $ATHENA/source/pkg-scripts/athena-domain-server@.service -#install -m 0644 -t $RPM_BUILD_ROOT/usr/lib/systemd/system $ATHENA/source/pkg-scripts/athena-ice-server.service -#install -m 0644 -t $RPM_BUILD_ROOT/usr/lib/systemd/system $ATHENA/source/pkg-scripts/athena-ice-server@.service -install -m 0644 -t $RPM_BUILD_ROOT/usr/lib/systemd/system $ATHENA/source/pkg-scripts/athena-server.target -install -m 0644 -t $RPM_BUILD_ROOT/usr/lib/systemd/system $ATHENA/source/pkg-scripts/athena-server@.target -cp -a $ATHENA/source/domain-server/resources $RPM_BUILD_ROOT/opt/athena -cp -a $ATHENA/build/assignment-client/plugins $RPM_BUILD_ROOT/opt/athena -chrpath -d $RPM_BUILD_ROOT/opt/athena/plugins/*.so -chrpath -d $RPM_BUILD_ROOT/opt/athena/plugins/*/*.so -strip --strip-all $RPM_BUILD_ROOT/opt/athena/plugins/*.so -strip --strip-all $RPM_BUILD_ROOT/opt/athena/plugins/*/*.so -find $RPM_BUILD_ROOT/opt/athena/resources -name ".gitignore" -delete - - -%files -%license $ATHENA/source/LICENSE -/opt/athena -/usr/lib/systemd/system - - -%changelog - - -%post -# create users -getent passwd athena >/dev/numm 2>&1 || useradd -r -c "Project Athena" -d /var/lib/athena -U -M athena -#getent group athena >/dev/null 2>&1 || groupadd -r athena - -# create data folder -mkdir -p /etc/opt/athena -mkdir -p /var/lib/athena && chown athena:athena /var/lib/athena && chmod 775 /var/lib/athena - -ldconfig -n /opt/athena/lib -if [ ! -d "/var/lib/athena/default" ]; then - /opt/athena/new-server default 40100 - systemctl enable athena-server@default.target - systemctl start athena-server@default.target -fi - -%systemd_post athena-assignment-client.service -%systemd_post athena-assignment-client@.service -%systemd_post athena-domain-server.service -%systemd_post athena-domain-server@.service -#%systemd_post athena-ice-server.service -#%systemd_post athena-ice-server@.service -%systemd_post athena-server.target -%systemd_post athena-server@.target - - -%preun -%systemd_preun athena-server.target -%systemd_preun athena-server@.target -%systemd_preun athena-assignment-client.service -%systemd_preun athena-assignment-client@.service -%systemd_preun athena-domain-server.service -%systemd_preun athena-domain-server@.service -#%systemd_preun athena-ice-server.service -#%systemd_preun athena-ice-server@.service - - -%postun -%systemd_postun_with_restart athena-server.target -%systemd_postun_with_restart athena-server@.target -%systemd_postun_with_restart athena-assignment-client.service -%systemd_postun_with_restart athena-assignment-client@.service -%systemd_postun_with_restart athena-domain-server.service -%systemd_postun_with_restart athena-domain-server@.service -#%systemd_postun_with_restart athena-ice-server.service -#%systemd_postun_with_restart athena-ice-server@.service From 3910448367b2b1d936be503c320f7073d4df12bd Mon Sep 17 00:00:00 2001 From: Kasen IO Date: Thu, 11 Jun 2020 15:36:13 -0400 Subject: [PATCH 47/73] Fix models not scaling correctly when loading due to timeout. --- scripts/system/create/edit.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index 3d8578715d..82d6ee9953 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -603,15 +603,19 @@ var toolBar = (function () { Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL); } // Make sure the model entity is loaded before we try to figure out - // its dimensions. - var MAX_LOADED_CHECKS = 10; + // its dimensions. We need to give ample time to load the entity. + var MAX_LOADED_CHECKS = 100; // 100 * 100ms = 10 seconds. var LOADED_CHECK_INTERVAL = 100; var isLoadedCheckCount = 0; var entityIsLoadedCheck = function() { isLoadedCheckCount++; if (isLoadedCheckCount === MAX_LOADED_CHECKS || Entities.isLoaded(entityID)) { var naturalDimensions = Entities.getEntityProperties(entityID, "naturalDimensions").naturalDimensions; - + + if (isLoadedCheckCount === MAX_LOADED_CHECKS) { + console.log("Model entity failed to load in time: " + (MAX_LOADED_CHECKS * LOADED_CHECK_INTERVAL) + " ... setting dimensions to: " + JSON.stringify(naturalDimensions)) + } + Entities.editEntity(entityID, { visible: true, dimensions: naturalDimensions From fd59665e633c408f3b980fa0e94f18f546795687 Mon Sep 17 00:00:00 2001 From: Dale Glass Date: Thu, 11 Jun 2020 22:23:27 +0200 Subject: [PATCH 48/73] Add missing Prometheus stats to type map --- domain-server/src/DomainServerExporter.cpp | 25 ++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/domain-server/src/DomainServerExporter.cpp b/domain-server/src/DomainServerExporter.cpp index 8d6dc348ff..a7affaf4dd 100644 --- a/domain-server/src/DomainServerExporter.cpp +++ b/domain-server/src/DomainServerExporter.cpp @@ -45,8 +45,8 @@ static const QMap TYPE_MAP { { "asset_server_connection_stats_rtt_ms" , DomainServerExporter::MetricType::Gauge }, { "asset_server_connection_stats_up_mb_s" , DomainServerExporter::MetricType::Gauge }, { "asset_server_downstream_stats_duplicates" , DomainServerExporter::MetricType::Counter }, - { "asset_server_downstream_stats_recvd_packets" , DomainServerExporter::MetricType::Counter }, { "asset_server_downstream_stats_recvd_p_s" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_downstream_stats_recvd_packets" , DomainServerExporter::MetricType::Counter }, { "asset_server_downstream_stats_sent_ack" , DomainServerExporter::MetricType::Counter }, { "asset_server_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, { "asset_server_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, @@ -55,8 +55,8 @@ static const QMap TYPE_MAP { { "asset_server_upstream_stats_procd_ack" , DomainServerExporter::MetricType::Counter }, { "asset_server_upstream_stats_recvd_ack" , DomainServerExporter::MetricType::Counter }, { "asset_server_upstream_stats_retransmitted" , DomainServerExporter::MetricType::Counter }, - { "asset_server_upstream_stats_sent_packets" , DomainServerExporter::MetricType::Counter }, { "asset_server_upstream_stats_sent_p_s" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_upstream_stats_sent_packets" , DomainServerExporter::MetricType::Counter }, { "audio_mixer_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_avg_listeners_per_frame" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_avg_listeners_silent_per_frame" , DomainServerExporter::MetricType::Gauge }, @@ -81,11 +81,11 @@ static const QMap TYPE_MAP { { "audio_mixer_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_listeners_jitter_downstream_available" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_listeners_jitter_downstream_available_avg_10s" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_avg_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_avg_gap_usecs" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_listeners_jitter_downstream_desired" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_listeners_jitter_downstream_lost_percent" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_listeners_jitter_downstream_lost_percent_30s" , DomainServerExporter::MetricType::Gauge }, - { "audio_mixer_listeners_jitter_downstream_avg_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, - { "audio_mixer_listeners_jitter_downstream_avg_gap_usecs" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_listeners_jitter_downstream_max_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_listeners_jitter_downstream_max_gap_usecs" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_listeners_jitter_downstream_min_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, @@ -95,6 +95,23 @@ static const QMap TYPE_MAP { { "audio_mixer_listeners_jitter_downstream_starves" , DomainServerExporter::MetricType::Counter }, { "audio_mixer_listeners_jitter_downstream_unplayed" , DomainServerExporter::MetricType::Counter }, { "audio_mixer_listeners_jitter_injectors" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_upstream_available" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_available_avg_10s" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_avg_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_avg_gap_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_desired_calc" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_lost_percent" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_lost_percent_30s" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_max_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_max_gap_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_mic_desired" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_min_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_min_gap_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_not_mixed" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_upstream_overflows" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_upstream_silents_dropped" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_upstream_starves" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_upstream_unplayed" , DomainServerExporter::MetricType::Counter }, { "audio_mixer_listeners_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_mix_stats_active_streams" , DomainServerExporter::MetricType::Gauge }, { "audio_mixer_mix_stats_active_to_inactive" , DomainServerExporter::MetricType::Counter }, From 2bc3047db1ec05b3660c5adc052905a8660a4816 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 13 Jun 2020 11:03:19 +1200 Subject: [PATCH 49/73] Fix reloading content in serverless domain --- libraries/networking/src/DomainHandler.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index fa4e3dc29b..d34b5cf090 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -211,6 +211,7 @@ void DomainHandler::setURLAndID(QUrl domainURL, QUuid domainID) { // if it's in the error state, reset and try again. if (_domainURL != domainURL || (_sockAddr.getPort() != domainPort && domainURL.scheme() == URL_SCHEME_HIFI) + || isServerless() // For reloading content in serverless domain. || _isInErrorState) { // re-set the domain info so that auth information is reloaded hardReset("Changing domain URL"); From 3878ad6df61094b6f5b20ca278c4e149d782226a Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Mon, 1 Jun 2020 20:34:35 -0700 Subject: [PATCH 50/73] Move metaverse server URL info into NetworkingConstants.h (for C++ code) and into shared.js (for JS code). Modify references to the metaverse server from constants to references to the new central definitions. --- domain-server/resources/web/js/shared.js | 1 + domain-server/src/DomainServer.cpp | 8 +------ interface/src/Application.cpp | 5 ++--- interface/src/Menu.cpp | 10 ++++----- libraries/auto-updater/src/AutoUpdater.cpp | 8 +++---- .../networking/src/NetworkingConstants.h | 22 +++++++++++++++++++ libraries/ui/src/ui/types/RequestFilters.cpp | 2 +- script-archive/avatarSelector.js | 2 +- .../entityScripts/recordingEntityScript.js | 4 ++-- script-archive/lobby.js | 2 +- .../targetPractice/targetPracticeGame.js | 2 +- scripts/system/html/js/SnapshotReview.js | 2 +- server-console/src/main.js | 2 +- .../src/modules/hf-notifications.js | 2 +- 14 files changed, 43 insertions(+), 29 deletions(-) diff --git a/domain-server/resources/web/js/shared.js b/domain-server/resources/web/js/shared.js index f4053ebddc..3c7dd2705c 100644 --- a/domain-server/resources/web/js/shared.js +++ b/domain-server/resources/web/js/shared.js @@ -52,6 +52,7 @@ var URLs = { // STABLE METAVERSE_URL: https://metaverse.highfidelity.com // STAGING METAVERSE_URL: https://staging.highfidelity.com METAVERSE_URL: 'https://metaverse.highfidelity.com', + CDN_URL: 'https://cdn.highfidelity.com', PLACE_URL: 'https://hifi.place', }; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 9fea49d2da..0d30560691 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -70,13 +70,7 @@ const QString DomainServer::REPLACEMENT_FILE_EXTENSION = ".replace"; int const DomainServer::EXIT_CODE_REBOOT = 234923; -#if USE_STABLE_GLOBAL_SERVICES -const QString ICE_SERVER_DEFAULT_HOSTNAME = "ice.highfidelity.com"; -#else -const QString ICE_SERVER_DEFAULT_HOSTNAME = "dev-ice.highfidelity.com"; -#endif - -QString DomainServer::_iceServerAddr { ICE_SERVER_DEFAULT_HOSTNAME }; +QString DomainServer::_iceServerAddr { NetworkingConstants::ICE_SERVER_DEFAULT_HOSTNAME }; int DomainServer::_iceServerPort { ICE_SERVER_DEFAULT_PORT }; bool DomainServer::_overrideDomainID { false }; QUuid DomainServer::_overridingDomainID; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 85c2d4abe0..05ddcd5bd3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -347,7 +347,6 @@ static const QString STANDARD_TO_ACTION_MAPPING_NAME = "Standard to Action"; static const QString NO_MOVEMENT_MAPPING_NAME = "Standard to Action (No Movement)"; static const QString NO_MOVEMENT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_nomovement.json"; -static const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin"; @@ -7620,7 +7619,7 @@ bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { QUrl scriptURL { scriptFilenameOrURL }; - if (scriptURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + if (scriptURL.host().endsWith(NetworkingConstants::MARKETPLACE_CDN_HOSTNAME)) { int startIndex = shortName.lastIndexOf('/') + 1; int endIndex = shortName.lastIndexOf('?'); shortName = shortName.mid(startIndex, endIndex - startIndex); @@ -7743,7 +7742,7 @@ bool Application::askToReplaceDomainContent(const QString& url) { const int MAX_CHARACTERS_PER_LINE = 90; if (DependencyManager::get()->getThisNodeCanReplaceContent()) { QUrl originURL { url }; - if (originURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + if (originURL.host().endsWith(NetworkingConstants::MARKETPLACE_CDN_HOSTNAME)) { // Create a confirmation dialog when this call is made static const QString infoText = simpleWordWrap("Your domain's content will be replaced with a new content set. " "If you want to save what you have now, create a backup before proceeding. For more information about backing up " diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index b07117b4be..54ed23d80d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -799,19 +799,19 @@ Menu::Menu() { // Help > Vircadia Docs action = addActionToQMenuAndActionHash(helpMenu, "Online Documentation"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://docs.vircadia.dev/")); + QDesktopServices::openUrl(NetworkingConstants::HELP_DOCS_URL); }); // Help > Vircadia Forum /* action = addActionToQMenuAndActionHash(helpMenu, "Online Forums"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://forums.highfidelity.com/")); + QDesktopServices::openUrl(NetworkingConstants::HELP_FORUM_URL)); }); */ // Help > Scripting Reference action = addActionToQMenuAndActionHash(helpMenu, "Online Script Reference"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://apidocs.vircadia.dev/")); + QDesktopServices::openUrl(NetworkingConstants::HELP_SCRIPTING_REFERENCE_URL); }); addActionToQMenuAndActionHash(helpMenu, "Controls Reference", 0, qApp, SLOT(showHelp())); @@ -821,13 +821,13 @@ Menu::Menu() { // Help > Release Notes action = addActionToQMenuAndActionHash(helpMenu, "Release Notes"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://docs.vircadia.dev/release-notes.html")); + QDesktopServices::openUrl(NetworkingConstants::HELP_RELEASE_NOTES_URL); }); // Help > Report a Bug! action = addActionToQMenuAndActionHash(helpMenu, "Report a Bug!"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://github.com/kasenvr/project-athena/issues")); + QDesktopServices::openUrl(NetworkingConstants::HELP_BUG_REPORT_URL); }); } diff --git a/libraries/auto-updater/src/AutoUpdater.cpp b/libraries/auto-updater/src/AutoUpdater.cpp index 300a22983a..d8afac59b2 100644 --- a/libraries/auto-updater/src/AutoUpdater.cpp +++ b/libraries/auto-updater/src/AutoUpdater.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include AutoUpdater::AutoUpdater() : @@ -36,18 +37,15 @@ void AutoUpdater::checkForUpdate() { this->getLatestVersionData(); } -const QUrl BUILDS_XML_URL("https://highfidelity.com/builds.xml"); -const QUrl MASTER_BUILDS_XML_URL("https://highfidelity.com/dev-builds.xml"); - void AutoUpdater::getLatestVersionData() { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QUrl buildsURL; if (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable) { - buildsURL = BUILDS_XML_URL; + buildsURL = NetworkingConstants::BUILDS_XML_URL; } else if (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Master) { - buildsURL = MASTER_BUILDS_XML_URL; + buildsURL = NetworkingConstants::MASTER_BUILDS_XML_URL; } QNetworkRequest latestVersionRequest(buildsURL); diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index 3bd84bc977..1d28205310 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -27,6 +27,28 @@ namespace NetworkingConstants { const QUrl METAVERSE_SERVER_URL_STABLE { "https://metaverse.highfidelity.com" }; const QUrl METAVERSE_SERVER_URL_STAGING { "https://staging-metaverse.vircadia.com" }; + + // Web Engine requests to this parent domain have an account authorization header added + const QString AUTH_HOSTNAME_BASE = "highfidelity.com"; + + const QUrl BUILDS_XML_URL("https://highfidelity.com/builds.xml"); + const QUrl MASTER_BUILDS_XML_URL("https://highfidelity.com/dev-builds.xml"); + + +#if USE_STABLE_GLOBAL_SERVICES + const QString ICE_SERVER_DEFAULT_HOSTNAME = "ice.highfidelity.com"; +#else + const QString ICE_SERVER_DEFAULT_HOSTNAME = "dev-ice.highfidelity.com"; +#endif + + const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; + + const QUrl HELP_DOCS_URL { "https://docs.vircadia.dev" }; + const QUrl HELP_FORUM_URL { "https://forums.vircadia.dev" }; + const QUrl HELP_SCRIPTING_REFERENCE_URL{ "https://apidocs.vircadia.dev/" }; + const QUrl HELP_RELEASE_NOTES_URL{ "https://docs.vircadia.dev/release-notes.html" }; + const QUrl HELP_BUG_REPORT_URL{ "https://github.com/kasenvr/project-athena/issues" }; + } const QString HIFI_URL_SCHEME_ABOUT = "about"; diff --git a/libraries/ui/src/ui/types/RequestFilters.cpp b/libraries/ui/src/ui/types/RequestFilters.cpp index 943dd02c29..9287559289 100644 --- a/libraries/ui/src/ui/types/RequestFilters.cpp +++ b/libraries/ui/src/ui/types/RequestFilters.cpp @@ -29,7 +29,7 @@ namespace { auto metaverseServerURL = MetaverseAPI::getCurrentMetaverseServerURL(); static const QStringList HF_HOSTS = { "highfidelity.com", "highfidelity.io", - metaverseServerURL.toString(), "metaverse.highfidelity.io" + metaverseServerURL.toString(), }; const auto& scheme = url.scheme(); const auto& host = url.host(); diff --git a/script-archive/avatarSelector.js b/script-archive/avatarSelector.js index 119044e35a..9dca3f6494 100644 --- a/script-archive/avatarSelector.js +++ b/script-archive/avatarSelector.js @@ -155,7 +155,7 @@ var avatars = {}; function changeLobbyTextures() { var req = new XMLHttpRequest(); - req.open("GET", "https://metaverse.highfidelity.com/api/v1/marketplace?category=head+%26+body&limit=21", false); + req.open("GET", URLs.METAVERSE_URL + "/api/v1/marketplace?category=head+%26+body&limit=21", false); req.send(); // Data returned is randomized. avatars = JSON.parse(req.responseText).data.items; diff --git a/script-archive/entityScripts/recordingEntityScript.js b/script-archive/entityScripts/recordingEntityScript.js index 3d1b6f46df..4281fbc64e 100644 --- a/script-archive/entityScripts/recordingEntityScript.js +++ b/script-archive/entityScripts/recordingEntityScript.js @@ -21,8 +21,8 @@ var START_MESSAGE = "recordingStarted"; var STOP_MESSAGE = "recordingEnded"; var PARTICIPATING_MESSAGE = "participatingToRecording"; - var RECORDING_ICON_URL = "http://cdn.highfidelity.com/alan/production/icons/ICO_rec-active.svg"; - var NOT_RECORDING_ICON_URL = "http://cdn.highfidelity.com/alan/production/icons/ICO_rec-inactive.svg"; + var RECORDING_ICON_URL = URLs.CDN_URL + "/alan/production/icons/ICO_rec-active.svg"; + var NOT_RECORDING_ICON_URL = URLs.CDN_URL + "/alan/production/icons/ICO_rec-inactive.svg"; var ICON_WIDTH = 60; var ICON_HEIGHT = 60; var overlay = null; diff --git a/script-archive/lobby.js b/script-archive/lobby.js index 7a06cdd906..d89fbe1f9d 100644 --- a/script-archive/lobby.js +++ b/script-archive/lobby.js @@ -153,7 +153,7 @@ var places = {}; function changeLobbyTextures() { var req = new XMLHttpRequest(); - req.open("GET", "https://metaverse.highfidelity.com/api/v1/places?limit=21", false); + req.open("GET", URLs.METAVERSE_URL + "/api/v1/places?limit=21", false); req.send(); places = JSON.parse(req.responseText).data.places; diff --git a/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js b/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js index 5e2612ded6..4e7f39821d 100644 --- a/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js +++ b/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js @@ -14,7 +14,7 @@ const GAME_CHANNEL = 'winterSmashUpGame'; const SCORE_POST_URL = 'https://script.google.com/macros/s/AKfycbwZAMx6cMBx6-8NGEhR8ELUA-dldtpa_4P55z38Q4vYHW6kneg/exec'; -const MODEL_URL = 'http://cdn.highfidelity.com/chris/production/winter/game/'; +const MODEL_URL = URLs.CDN_URL + '/chris/production/winter/game/'; const MAX_GAME_TIME = 120; //seconds const TARGET_CLOSE_OFFSET = 0.5; const MILLISECS_IN_SEC = 1000; diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index 1e8be9d644..71e468265f 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -440,7 +440,7 @@ function updateShareInfo(containerID, storyID) { } var shareBar = document.getElementById(containerID + "shareBar"), parentDiv = document.getElementById(containerID), - shareURL = "https://highfidelity.com/user_stories/" + storyID, + shareURL = URLs.METAVERSE_URL + "/user_stories/" + storyID, facebookButton = document.getElementById(containerID + "facebookButton"), twitterButton = document.getElementById(containerID + "twitterButton"); diff --git a/server-console/src/main.js b/server-console/src/main.js index d8d6fea4bf..f645d6af4c 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -56,7 +56,7 @@ const menuNotificationIcon = path.join(__dirname, '../resources/tray-menu-notifi const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/; -const HOME_CONTENT_URL = "http://cdn.highfidelity.com/content-sets/home-tutorial-RC40.tar.gz"; +const HOME_CONTENT_URL = URLs.CDN_URL + "/content-sets/home-tutorial-RC40.tar.gz"; const buildInfo = GetBuildInfo(); diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js index 1ddbd1d307..dfc07ed77c 100644 --- a/server-console/src/modules/hf-notifications.js +++ b/server-console/src/modules/hf-notifications.js @@ -19,7 +19,7 @@ const MARKETPLACE_NOTIFICATION_POLL_TIME_MS = 600 * 1000; const OSX_CLICK_DELAY_TIMEOUT = 500; -const METAVERSE_SERVER_URL= process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : 'https://metaverse.highfidelity.com' +const METAVERSE_SERVER_URL = process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : URLs.METAVERSE_URL const STORIES_URL= '/api/v1/user_stories'; const USERS_URL= '/api/v1/users'; const ECONOMIC_ACTIVITY_URL= '/api/v1/commerce/history'; From 245b9b80363ee77ec9552641da69792dcdaee7e2 Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Thu, 4 Jun 2020 09:01:51 -0700 Subject: [PATCH 51/73] Move metaverse server URL update: remove unnecessary spaces. Remove added "include " (for VS compiling) to separate PR. --- libraries/ktx/src/khronos/KHR.h | 1 - libraries/shared/src/shared/Storage.h | 1 - script-archive/lobby.js | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/ktx/src/khronos/KHR.h b/libraries/ktx/src/khronos/KHR.h index cd3eb109d7..617e40ce06 100644 --- a/libraries/ktx/src/khronos/KHR.h +++ b/libraries/ktx/src/khronos/KHR.h @@ -11,7 +11,6 @@ #define khronos_khr_hpp #include -#include namespace khronos { diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h index f64f2758c3..6a2cecf8b9 100644 --- a/libraries/shared/src/shared/Storage.h +++ b/libraries/shared/src/shared/Storage.h @@ -14,7 +14,6 @@ #include #include #include -#include #include #include diff --git a/script-archive/lobby.js b/script-archive/lobby.js index d89fbe1f9d..54f3dc526e 100644 --- a/script-archive/lobby.js +++ b/script-archive/lobby.js @@ -153,7 +153,7 @@ var places = {}; function changeLobbyTextures() { var req = new XMLHttpRequest(); - req.open("GET", URLs.METAVERSE_URL + "/api/v1/places?limit=21", false); + req.open("GET", URLs.METAVERSE_URL + "/api/v1/places?limit=21", false); req.send(); places = JSON.parse(req.responseText).data.places; From 5ad1b5cf5ac3edac01a8ad290a880573f0a1fd62 Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Thu, 4 Jun 2020 12:58:28 -0700 Subject: [PATCH 52/73] Move metaverse server URL update: revert nearly all changes to Javascript scripts because the 'require' of the central definition file was not included. --- script-archive/avatarSelector.js | 2 +- script-archive/entityScripts/recordingEntityScript.js | 4 ++-- script-archive/lobby.js | 2 +- .../winterSmashUp/targetPractice/targetPracticeGame.js | 2 +- scripts/system/html/js/SnapshotReview.js | 2 +- server-console/src/main.js | 2 +- server-console/src/modules/hf-notifications.js | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/script-archive/avatarSelector.js b/script-archive/avatarSelector.js index 9dca3f6494..119044e35a 100644 --- a/script-archive/avatarSelector.js +++ b/script-archive/avatarSelector.js @@ -155,7 +155,7 @@ var avatars = {}; function changeLobbyTextures() { var req = new XMLHttpRequest(); - req.open("GET", URLs.METAVERSE_URL + "/api/v1/marketplace?category=head+%26+body&limit=21", false); + req.open("GET", "https://metaverse.highfidelity.com/api/v1/marketplace?category=head+%26+body&limit=21", false); req.send(); // Data returned is randomized. avatars = JSON.parse(req.responseText).data.items; diff --git a/script-archive/entityScripts/recordingEntityScript.js b/script-archive/entityScripts/recordingEntityScript.js index 4281fbc64e..3d1b6f46df 100644 --- a/script-archive/entityScripts/recordingEntityScript.js +++ b/script-archive/entityScripts/recordingEntityScript.js @@ -21,8 +21,8 @@ var START_MESSAGE = "recordingStarted"; var STOP_MESSAGE = "recordingEnded"; var PARTICIPATING_MESSAGE = "participatingToRecording"; - var RECORDING_ICON_URL = URLs.CDN_URL + "/alan/production/icons/ICO_rec-active.svg"; - var NOT_RECORDING_ICON_URL = URLs.CDN_URL + "/alan/production/icons/ICO_rec-inactive.svg"; + var RECORDING_ICON_URL = "http://cdn.highfidelity.com/alan/production/icons/ICO_rec-active.svg"; + var NOT_RECORDING_ICON_URL = "http://cdn.highfidelity.com/alan/production/icons/ICO_rec-inactive.svg"; var ICON_WIDTH = 60; var ICON_HEIGHT = 60; var overlay = null; diff --git a/script-archive/lobby.js b/script-archive/lobby.js index 54f3dc526e..7a06cdd906 100644 --- a/script-archive/lobby.js +++ b/script-archive/lobby.js @@ -153,7 +153,7 @@ var places = {}; function changeLobbyTextures() { var req = new XMLHttpRequest(); - req.open("GET", URLs.METAVERSE_URL + "/api/v1/places?limit=21", false); + req.open("GET", "https://metaverse.highfidelity.com/api/v1/places?limit=21", false); req.send(); places = JSON.parse(req.responseText).data.places; diff --git a/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js b/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js index 4e7f39821d..5e2612ded6 100644 --- a/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js +++ b/script-archive/winterSmashUp/targetPractice/targetPracticeGame.js @@ -14,7 +14,7 @@ const GAME_CHANNEL = 'winterSmashUpGame'; const SCORE_POST_URL = 'https://script.google.com/macros/s/AKfycbwZAMx6cMBx6-8NGEhR8ELUA-dldtpa_4P55z38Q4vYHW6kneg/exec'; -const MODEL_URL = URLs.CDN_URL + '/chris/production/winter/game/'; +const MODEL_URL = 'http://cdn.highfidelity.com/chris/production/winter/game/'; const MAX_GAME_TIME = 120; //seconds const TARGET_CLOSE_OFFSET = 0.5; const MILLISECS_IN_SEC = 1000; diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index 71e468265f..1e8be9d644 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -440,7 +440,7 @@ function updateShareInfo(containerID, storyID) { } var shareBar = document.getElementById(containerID + "shareBar"), parentDiv = document.getElementById(containerID), - shareURL = URLs.METAVERSE_URL + "/user_stories/" + storyID, + shareURL = "https://highfidelity.com/user_stories/" + storyID, facebookButton = document.getElementById(containerID + "facebookButton"), twitterButton = document.getElementById(containerID + "twitterButton"); diff --git a/server-console/src/main.js b/server-console/src/main.js index f645d6af4c..d8d6fea4bf 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -56,7 +56,7 @@ const menuNotificationIcon = path.join(__dirname, '../resources/tray-menu-notifi const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/; -const HOME_CONTENT_URL = URLs.CDN_URL + "/content-sets/home-tutorial-RC40.tar.gz"; +const HOME_CONTENT_URL = "http://cdn.highfidelity.com/content-sets/home-tutorial-RC40.tar.gz"; const buildInfo = GetBuildInfo(); diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js index dfc07ed77c..1ddbd1d307 100644 --- a/server-console/src/modules/hf-notifications.js +++ b/server-console/src/modules/hf-notifications.js @@ -19,7 +19,7 @@ const MARKETPLACE_NOTIFICATION_POLL_TIME_MS = 600 * 1000; const OSX_CLICK_DELAY_TIMEOUT = 500; -const METAVERSE_SERVER_URL = process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : URLs.METAVERSE_URL +const METAVERSE_SERVER_URL= process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : 'https://metaverse.highfidelity.com' const STORIES_URL= '/api/v1/user_stories'; const USERS_URL= '/api/v1/users'; const ECONOMIC_ACTIVITY_URL= '/api/v1/commerce/history'; From ad1825c2fb2036587315fdce5e4a7be211e7156e Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Sat, 13 Jun 2020 11:47:21 -0700 Subject: [PATCH 53/73] Add "include " so Storage.h builds under Windows. This was accidentially removed because of interaction of several PRs. --- libraries/shared/src/shared/Storage.h | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h index 6a2cecf8b9..f64f2758c3 100644 --- a/libraries/shared/src/shared/Storage.h +++ b/libraries/shared/src/shared/Storage.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include From ef056ecfbd5b3d76e9fbd103c9fa776da295769c Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Sat, 13 Jun 2020 13:07:06 -0700 Subject: [PATCH 54/73] Add another missing #include for building under Windows. --- libraries/ktx/src/khronos/KHR.h | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/ktx/src/khronos/KHR.h b/libraries/ktx/src/khronos/KHR.h index 617e40ce06..cd3eb109d7 100644 --- a/libraries/ktx/src/khronos/KHR.h +++ b/libraries/ktx/src/khronos/KHR.h @@ -11,6 +11,7 @@ #define khronos_khr_hpp #include +#include namespace khronos { From 189f6f544061a3a65abdcc88b32261dc20b0f55e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 15 Jun 2020 08:04:51 +1200 Subject: [PATCH 55/73] Code review --- .../controllers/controllerModules/trackedHandTablet.js | 8 +------- .../controllers/controllerModules/trackedHandWalk.js | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/scripts/system/controllers/controllerModules/trackedHandTablet.js b/scripts/system/controllers/controllerModules/trackedHandTablet.js index 6bb9d67ef8..66cf408af8 100644 --- a/scripts/system/controllers/controllerModules/trackedHandTablet.js +++ b/scripts/system/controllers/controllerModules/trackedHandTablet.js @@ -94,13 +94,7 @@ Script.include("/~/system/libraries/controllers.js"); }; this.isReady = function (controllerData) { - if (!handsAreTracked()) { - return makeRunningValues(false, [], []); - } else if (this.gestureCompleted) { - return makeRunningValues(true, [], []); - } else { - return makeRunningValues(false, [], []); - } + return makeRunningValues(handsAreTracked() && this.gestureCompleted, [], []); }; this.run = function (controllerData) { diff --git a/scripts/system/controllers/controllerModules/trackedHandWalk.js b/scripts/system/controllers/controllerModules/trackedHandWalk.js index 92549eaa81..9ecc53a1fa 100644 --- a/scripts/system/controllers/controllerModules/trackedHandWalk.js +++ b/scripts/system/controllers/controllerModules/trackedHandWalk.js @@ -105,13 +105,7 @@ Script.include("/~/system/libraries/controllers.js"); }; this.isReady = function (controllerData) { - if (!handsAreTracked()) { - return makeRunningValues(false, [], []); - } else if (this.walkingForward || this.walkingBackward) { - return makeRunningValues(true, [], []); - } else { - return makeRunningValues(false, [], []); - } + return makeRunningValues(handsAreTracked() && (this.walkingForward || this.walkingBackward), [], []); }; this.run = function (controllerData) { From 3e585267e4db09be79a755d0f0b093bdde922911 Mon Sep 17 00:00:00 2001 From: Kasen IO Date: Thu, 18 Jun 2020 01:11:07 -0400 Subject: [PATCH 56/73] Fix gizmo size issue post auto-resize of a new model. --- scripts/system/create/edit.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index 82d6ee9953..ab21c5776c 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -621,6 +621,10 @@ var toolBar = (function () { dimensions: naturalDimensions }) dimensionsCheckCallback(); + // We want to update the selection manager again since the script has moved on without us. + selectionManager.clearSelections(this); + entityListTool.sendUpdate(); + selectionManager.setSelections([entityID], this); return; } Script.setTimeout(entityIsLoadedCheck, LOADED_CHECK_INTERVAL); From 256b466e84f844e50b4aafb9a4c909a3d527acd1 Mon Sep 17 00:00:00 2001 From: motofckr9k Date: Thu, 18 Jun 2020 09:12:01 +0200 Subject: [PATCH 57/73] Add vircadia-banner.svg for login dialogs --- .../resources/images/vircadia-banner.svg | 23 +++++++++++++++++++ interface/resources/qml/LoginDialog.qml | 2 +- .../resources/qml/OverlayLoginDialog.qml | 2 +- .../qml/dialogs/TabletLoginDialog.qml | 2 +- .../qml/hifi/dialogs/TabletAboutDialog.qml | 2 +- 5 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 interface/resources/images/vircadia-banner.svg diff --git a/interface/resources/images/vircadia-banner.svg b/interface/resources/images/vircadia-banner.svg new file mode 100644 index 0000000000..b53dd79040 --- /dev/null +++ b/interface/resources/images/vircadia-banner.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index 375d68ad26..ec239206ae 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -87,7 +87,7 @@ FocusScope { anchors.centerIn: parent sourceSize.width: 500 sourceSize.height: 91 - source: "../images/vircadia-logo.svg" + source: "../images/vircadia-banner.svg" horizontalAlignment: Image.AlignHCenter } } diff --git a/interface/resources/qml/OverlayLoginDialog.qml b/interface/resources/qml/OverlayLoginDialog.qml index 0ad2c57e5f..4e578efb92 100644 --- a/interface/resources/qml/OverlayLoginDialog.qml +++ b/interface/resources/qml/OverlayLoginDialog.qml @@ -81,7 +81,7 @@ FocusScope { Image { id: banner anchors.centerIn: parent - source: "../images/high-fidelity-banner.svg" + source: "../images/vircadia-banner.svg" horizontalAlignment: Image.AlignHCenter } } diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index 533fd1197c..f794fea66a 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -131,7 +131,7 @@ FocusScope { anchors.centerIn: parent sourceSize.width: 400 sourceSize.height: 73 - source: "../../images/vircadia-logo.svg" + source: "../../images/vircadia-banner.svg" horizontalAlignment: Image.AlignHCenter } } diff --git a/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml b/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml index 2fd464e55f..e7a9e0cef2 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml @@ -25,7 +25,7 @@ Rectangle { Image { width: 400; height: 73 fillMode: Image.PreserveAspectFit - source: "../../../images/vircadia-logo.svg" + source: "../../../images/vircadia-banner.svg" } Item { height: 30; width: 1 } Column { From bf230a415a9eb0b7554e344765993295793ab72f Mon Sep 17 00:00:00 2001 From: motofckr9k Date: Sun, 21 Jun 2020 00:33:52 +0200 Subject: [PATCH 58/73] Initial commit --- interface/resources/qml/LoginDialog.qml | 4 +- .../qml/LoginDialog/LinkAccountBody.qml | 67 ++++++++++--------- .../qml/LoginDialog/LoggingInBody.qml | 1 + 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index ec239206ae..a27bf0c3bb 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -3,10 +3,10 @@ // // Created by David Rowe on 3 Jun 2015 // Copyright 2015 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. +// // Distributed under the Apache License, Version 2.0. -// // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// import Hifi 1.0 import QtQuick 2.4 diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 4f0ea2e607..f3e10b53c6 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -3,6 +3,7 @@ // // Created by Clement on 7/18/16 // Copyright 2015 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -485,7 +486,41 @@ Item { } } } + + TextMetrics { + id: dismissButtonTextMetrics + font: loginErrorMessage.font + text: dismissButton.text + } + HifiControlsUit.Button { + id: dismissButton + width: loginButton.width + height: d.minHeightButton + anchors { + top: cantAccessText.bottom + topMargin: hifi.dimensions.contentSpacing.y + left: loginButton.left + } +// color: hifi.buttons.noneBorderlessWhite + text: qsTr("Skip Log In") + fontCapitalization: Font.MixedCase + fontFamily: linkAccountBody.fontFamily + fontSize: linkAccountBody.fontSize + fontBold: linkAccountBody.fontBold + visible: loginDialog.getLoginDialogPoppedUp() && !linkAccountBody.linkSteam && !linkAccountBody.linkOculus; + onClicked: { + if (linkAccountBody.loginDialogPoppedUp) { + var data = { + "action": "user dismissed login screen" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + loginDialog.dismissLoginDialog(); + } + root.tryDestroy(); + } + } } + Item { id: signUpContainer width: loginContainer.width @@ -543,38 +578,6 @@ Item { } } } - TextMetrics { - id: dismissButtonTextMetrics - font: loginErrorMessage.font - text: dismissButton.text - } - HifiControlsUit.Button { - id: dismissButton - width: dismissButtonTextMetrics.width - height: d.minHeightButton - anchors { - bottom: parent.bottom - right: parent.right - margins: 3 * hifi.dimensions.contentSpacing.y - } - color: hifi.buttons.noneBorderlessWhite - text: qsTr("No thanks, take me in-world! >") - fontCapitalization: Font.MixedCase - fontFamily: linkAccountBody.fontFamily - fontSize: linkAccountBody.fontSize - fontBold: linkAccountBody.fontBold - visible: loginDialog.getLoginDialogPoppedUp() && !linkAccountBody.linkSteam && !linkAccountBody.linkOculus; - onClicked: { - if (linkAccountBody.loginDialogPoppedUp) { - var data = { - "action": "user dismissed login screen" - }; - UserActivityLogger.logAction("encourageLoginDialog", data); - loginDialog.dismissLoginDialog(); - } - root.tryDestroy(); - } - } } Connections { diff --git a/interface/resources/qml/LoginDialog/LoggingInBody.qml b/interface/resources/qml/LoginDialog/LoggingInBody.qml index a0029dc40b..beeab74d40 100644 --- a/interface/resources/qml/LoginDialog/LoggingInBody.qml +++ b/interface/resources/qml/LoginDialog/LoggingInBody.qml @@ -3,6 +3,7 @@ // // Created by Wayne Chen on 10/18/18 // Copyright 2018 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html From e8a3009b96fabdc32aa737283893f4529cdfe713 Mon Sep 17 00:00:00 2001 From: motofckr9k Date: Sun, 21 Jun 2020 01:04:52 +0200 Subject: [PATCH 59/73] Remove color from dismissButton --- interface/resources/qml/LoginDialog/LinkAccountBody.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index f3e10b53c6..064fbafa0b 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -501,7 +501,6 @@ Item { topMargin: hifi.dimensions.contentSpacing.y left: loginButton.left } -// color: hifi.buttons.noneBorderlessWhite text: qsTr("Skip Log In") fontCapitalization: Font.MixedCase fontFamily: linkAccountBody.fontFamily From 3da755fb951c194bbe9cb3f81477f58cedcc23b9 Mon Sep 17 00:00:00 2001 From: motofckr9k Date: Sun, 21 Jun 2020 02:53:05 +0200 Subject: [PATCH 60/73] Change button text, and move it to answer "Don't have an account?" --- .../qml/LoginDialog/LinkAccountBody.qml | 80 ++++++++++++------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 064fbafa0b..1111116a28 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -487,37 +487,7 @@ Item { } } - TextMetrics { - id: dismissButtonTextMetrics - font: loginErrorMessage.font - text: dismissButton.text - } - HifiControlsUit.Button { - id: dismissButton - width: loginButton.width - height: d.minHeightButton - anchors { - top: cantAccessText.bottom - topMargin: hifi.dimensions.contentSpacing.y - left: loginButton.left - } - text: qsTr("Skip Log In") - fontCapitalization: Font.MixedCase - fontFamily: linkAccountBody.fontFamily - fontSize: linkAccountBody.fontSize - fontBold: linkAccountBody.fontBold - visible: loginDialog.getLoginDialogPoppedUp() && !linkAccountBody.linkSteam && !linkAccountBody.linkOculus; - onClicked: { - if (linkAccountBody.loginDialogPoppedUp) { - var data = { - "action": "user dismissed login screen" - }; - UserActivityLogger.logAction("encourageLoginDialog", data); - loginDialog.dismissLoginDialog(); - } - root.tryDestroy(); - } - } + } Item { @@ -576,6 +546,54 @@ Item { "errorString": "" }); } } + + Text { + id: signUpTextSecond + text: qsTr("or..") + anchors { + left: signUpShortcutText.right + leftMargin: hifi.dimensions.contentSpacing.x + } + lineHeight: 1 + color: "white" + font.family: linkAccountBody.fontFamily + font.pixelSize: linkAccountBody.textFieldFontSize + font.bold: linkAccountBody.fontBold + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + TextMetrics { + id: dismissButtonTextMetrics + font: loginErrorMessage.font + text: dismissButton.text + } + HifiControlsUit.Button { + id: dismissButton + width: loginButton.width + height: d.minHeightButton + anchors { + top: signUpText.bottom + topMargin: hifi.dimensions.contentSpacing.y + left: loginButton.left + } + text: qsTr("Use without account, log in anonymously") + fontCapitalization: Font.MixedCase + fontFamily: linkAccountBody.fontFamily + fontSize: linkAccountBody.fontSize + fontBold: linkAccountBody.fontBold + visible: loginDialog.getLoginDialogPoppedUp() && !linkAccountBody.linkSteam && !linkAccountBody.linkOculus; + onClicked: { + if (linkAccountBody.loginDialogPoppedUp) { + var data = { + "action": "user dismissed login screen" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + loginDialog.dismissLoginDialog(); + } + root.tryDestroy(); + } + } } } From 41563b14a86a21e2d0e94cc3956eddb612082d87 Mon Sep 17 00:00:00 2001 From: motofckr9k Date: Sun, 21 Jun 2020 03:05:54 +0200 Subject: [PATCH 61/73] Move signUpContainer closer to loginContainer --- interface/resources/qml/LoginDialog/LinkAccountBody.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 1111116a28..8232a78eea 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -498,7 +498,7 @@ Item { anchors { left: loginContainer.left top: loginContainer.bottom - topMargin: 0.15 * parent.height + topMargin: 0.10 * parent.height } TextMetrics { id: signUpTextMetrics From 75f6246ac9e09e61cd804293d79eb86aeda69646 Mon Sep 17 00:00:00 2001 From: motofckr9k Date: Sun, 21 Jun 2020 03:16:19 +0200 Subject: [PATCH 62/73] Move them even closer --- interface/resources/qml/LoginDialog/LinkAccountBody.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 8232a78eea..e37941f20d 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -498,7 +498,7 @@ Item { anchors { left: loginContainer.left top: loginContainer.bottom - topMargin: 0.10 * parent.height + topMargin: 0.05 * parent.height } TextMetrics { id: signUpTextMetrics From 7b34872b81c3c8c7b3e92859ab98b9542d03f70a Mon Sep 17 00:00:00 2001 From: motofckr9k Date: Sun, 21 Jun 2020 03:59:33 +0200 Subject: [PATCH 63/73] Replace domain-server favicon --- domain-server/resources/web/favicon.ico | Bin 5430 -> 4286 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/domain-server/resources/web/favicon.ico b/domain-server/resources/web/favicon.ico index becc1b8e8b5bc3bd677093a21f348fe077c712a6..cbcf3649027f248f37e92b956e2cb32716f91814 100644 GIT binary patch literal 4286 zcmb_fTWnO<6}^y3Zok@(s{Lw;T2*R4B2}tNuuZUm1Ons%#COIs9($gC%)(oj{ZN~0Yb6@uanE(=Z-rK5Ug{x6hWet-<}VFMJlY3eVKK>ih6WY%sSrehm-T z5v%Hn2b08#G*RA2lx-p&XeLTqh?2G`tGHu$dP#?T^iOpr*PCCnDymQAHq_WyA0Zx# z5|6})wFzn*4a7>pks->Ph-I6Jr7c8h8&TXr6mNMACGFq(e?ZIz0|8V8Z=$k_@P!Do zn%G!FtPc~93XV9jww733M?BO(tVn4tmbqNC5hV)4lvUiG?EGeC;m5%tJ+@-LK13^t za3xV4AgY2yKyXye;dnekJStqoiHBU~6=k4X55n4gGL5;YDEmye1XVndjCEJCb{5o;0!x^OXUb~K?eSjAfoepr2E z%{VrhM59TheMHhE>itB*N5tpxvA2)dbAagXC!QK0%#_xJ>W8DvvPNP)h9&LkyJ5gq z^Io)7+(4^IWKE*kB$`YjV-hK!gTvutZ#O;XpVx?u(Yb!)HIb$lLkDxkTPAPa-};UD z7;@Xqv)E=5TTNn%NwifEE%R_FK76#_`KzZqhE5O_)pOcZns9aLU|1&p6g?IgHw+S=k#UQZ(?sHv8R&wv7dO_Pwe&)yBto059v#Po2NFBBU)>TxS#fgbLuth z>ee20b3%AnB^VrUs*K7qSJHM1-`i1y{y;eff<%9i=nD|N0p_s?CxSzI)8(W6)@4t9 z``16$K534U)3?=Y_Lbl1UMM}d86o*lud`BeQaquup|#vP6w2aIh;$_T+AnJpQma#H#r7_ziVvuO-Pj^c$dakPdw zTup^#K8`(=wEys$r-uFKD@2F(>K&SRJ*M_K09OvfJ|-b2%cri3k?twep0UkuodPygG`J#0Z8H z#Hl!OB1RmK5<_9)d54dKA=+0)Jonr0|3zy;yegT|VUpM0^S`Y?$;g>UAh=f^?BYSdPJsH_rV_` zo^blGF0N;$;vmy&jV7}gO%mrCsBpYoH;3bRl=hVuy!TH2o7f-JUKO7T2FZyX0dn#K z&wF(?1m^l6^Gar>%&LbPXst}9F_tdJc!n5D6Bkl*IE0Uv>dC3=-aCJFM0-_yDtRH9 zB^V?lF1+Y@FDEC7`kJ{u$jp?vExUu75vg*VZz#%5G+xI9N=`{`cbc>>pYi+;Z(Ju5!Fe1+X|0Q?{FO63np&K%?O?Fq z-0!`|&Jkyuv@XRPl3CAG5&NA!xLW8*-kDQJ1~dn*A2)<~du=#Bf8^xGGiTk}^4tE` zWpeTza`IjBKkw3h`)5=>6>t0`LX{DcnbL=zfSwz|L6=GUjWe|Wah3M_S82a@mG3VX_LTfzJm#^#Am*0hbki3?eQOJS#@kf5WH%KN)4`hdv9wg1fmsYR* z$o<I(hGWPk7#51V*0zY=Nb}trEZ^fIzDI}j}BVT*9=53WX! z`H8-NGgn5weh+CFv<;(c+AzNUfnj`69)B~8qSp=M9(n9Ej0J^{`Pd3yljrXL$sO)} Oa<|lwTB>HQjQ<5&xQ$H! literal 5430 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 From 56362461fcd062e9b1b9c91907dcd4c3db8a8783 Mon Sep 17 00:00:00 2001 From: motofckr9k Date: Sun, 21 Jun 2020 05:51:37 +0200 Subject: [PATCH 64/73] Fix "or" not dissappearing in popup window --- interface/resources/qml/LoginDialog/LinkAccountBody.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index e37941f20d..af888936ae 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -549,7 +549,7 @@ Item { Text { id: signUpTextSecond - text: qsTr("or..") + text: qsTr("or") anchors { left: signUpShortcutText.right leftMargin: hifi.dimensions.contentSpacing.x @@ -561,6 +561,7 @@ Item { font.bold: linkAccountBody.fontBold verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + visible: loginDialog.getLoginDialogPoppedUp() && !linkAccountBody.linkSteam && !linkAccountBody.linkOculus; } TextMetrics { From 34887ec604818d0da0e5956f6a37942e6faeb8cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Mon, 22 Jun 2020 04:10:21 +0200 Subject: [PATCH 65/73] Remove unnessesary whitespaces --- interface/resources/qml/LoginDialog/LinkAccountBody.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index af888936ae..6ac5be6fd4 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -486,8 +486,6 @@ Item { } } } - - } Item { From ef5d6bfc2f8cac67f8f04e9175986739ba2f1e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Mon, 22 Jun 2020 04:11:34 +0200 Subject: [PATCH 66/73] Remove unnessesary copyright --- interface/resources/qml/LoginDialog/LoggingInBody.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/resources/qml/LoginDialog/LoggingInBody.qml b/interface/resources/qml/LoginDialog/LoggingInBody.qml index beeab74d40..a0029dc40b 100644 --- a/interface/resources/qml/LoginDialog/LoggingInBody.qml +++ b/interface/resources/qml/LoginDialog/LoggingInBody.qml @@ -3,7 +3,6 @@ // // Created by Wayne Chen on 10/18/18 // Copyright 2018 High Fidelity, Inc. -// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html From 3fe1dddee20107f34a7255999b1218c5b872ccfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Mon, 22 Jun 2020 04:19:10 +0200 Subject: [PATCH 67/73] Replace tabs with spaces --- .../qml/LoginDialog/LinkAccountBody.qml | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 6ac5be6fd4..e679202455 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -563,36 +563,36 @@ Item { } TextMetrics { - id: dismissButtonTextMetrics - font: loginErrorMessage.font - text: dismissButton.text - } - HifiControlsUit.Button { - id: dismissButton - width: loginButton.width - height: d.minHeightButton - anchors { - top: signUpText.bottom - topMargin: hifi.dimensions.contentSpacing.y + id: dismissButtonTextMetrics + font: loginErrorMessage.font + text: dismissButton.text + } + HifiControlsUit.Button { + id: dismissButton + width: loginButton.width + height: d.minHeightButton + anchors { + top: signUpText.bottom + topMargin: hifi.dimensions.contentSpacing.y left: loginButton.left - } - text: qsTr("Use without account, log in anonymously") - fontCapitalization: Font.MixedCase - fontFamily: linkAccountBody.fontFamily - fontSize: linkAccountBody.fontSize - fontBold: linkAccountBody.fontBold - visible: loginDialog.getLoginDialogPoppedUp() && !linkAccountBody.linkSteam && !linkAccountBody.linkOculus; - onClicked: { - if (linkAccountBody.loginDialogPoppedUp) { - var data = { - "action": "user dismissed login screen" - }; - UserActivityLogger.logAction("encourageLoginDialog", data); - loginDialog.dismissLoginDialog(); - } - root.tryDestroy(); - } - } + } + text: qsTr("Use without account, log in anonymously") + fontCapitalization: Font.MixedCase + fontFamily: linkAccountBody.fontFamily + fontSize: linkAccountBody.fontSize + fontBold: linkAccountBody.fontBold + visible: loginDialog.getLoginDialogPoppedUp() && !linkAccountBody.linkSteam && !linkAccountBody.linkOculus; + onClicked: { + if (linkAccountBody.loginDialogPoppedUp) { + var data = { + "action": "user dismissed login screen" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + loginDialog.dismissLoginDialog(); + } + root.tryDestroy(); + } + } } } From 5b0855fa6e72166ef44c98b0daa9873edcc82c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Mon, 22 Jun 2020 06:18:05 +0200 Subject: [PATCH 68/73] Fix indentation --- interface/resources/qml/LoginDialog/LinkAccountBody.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index e679202455..209dd631a8 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -586,10 +586,10 @@ Item { if (linkAccountBody.loginDialogPoppedUp) { var data = { "action": "user dismissed login screen" - }; - UserActivityLogger.logAction("encourageLoginDialog", data); - loginDialog.dismissLoginDialog(); - } + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + loginDialog.dismissLoginDialog(); + } root.tryDestroy(); } } From 147bea6ba9d4e6cc168ba21412143823f54d6e3b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 22 Jun 2020 21:12:46 +1200 Subject: [PATCH 69/73] Fix indentation --- interface/resources/qml/LoginDialog/LinkAccountBody.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 209dd631a8..571b7e074c 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -585,10 +585,10 @@ Item { onClicked: { if (linkAccountBody.loginDialogPoppedUp) { var data = { - "action": "user dismissed login screen" - }; - UserActivityLogger.logAction("encourageLoginDialog", data); - loginDialog.dismissLoginDialog(); + "action": "user dismissed login screen" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + loginDialog.dismissLoginDialog(); } root.tryDestroy(); } From 84a2fe061f113b24cf83917e928742fa866ba777 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 23 Jun 2020 20:27:08 +1200 Subject: [PATCH 70/73] Update More app from community apps repo --- scripts/system/more/app-more.js | 2 +- scripts/system/more/css/styles.css | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/system/more/app-more.js b/scripts/system/more/app-more.js index 8dc0603385..1077db70ba 100644 --- a/scripts/system/more/app-more.js +++ b/scripts/system/more/app-more.js @@ -6,7 +6,7 @@ // Created by Keb Helion, February 2020. // Copyright 2020 Vircadia contributors. // -// This script adds a "More Apps" selector to "Vircadia" to allow the user to add optional functionalities to the tablet. +// This script adds a "More Apps" selector to Vircadia to allow the user to add optional functionalities to the tablet. // This application has been designed to work directly from the Github repository. // // Distributed under the Apache License, Version 2.0. diff --git a/scripts/system/more/css/styles.css b/scripts/system/more/css/styles.css index e4e6bfba24..c1f0f5d6ae 100644 --- a/scripts/system/more/css/styles.css +++ b/scripts/system/more/css/styles.css @@ -42,6 +42,10 @@ p a { color: SteelBlue; } +.appdesc a { + color: LightBlue; +} + font.appname { font-family: 'Merriweather', sans-serif; font-size: 18px; From fc418fa9e47e5959585aec71f88bd181ffc278dd Mon Sep 17 00:00:00 2001 From: Kasen IO Date: Tue, 23 Jun 2020 15:57:15 -0400 Subject: [PATCH 71/73] Remove script that no longer exists created by the Vive Pro Eye PR. --- scripts/defaultScripts.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 830785d89b..5e7e120bf3 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -34,8 +34,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/miniTablet.js", "system/audioMuteOverlay.js", "system/inspect.js", - "system/keyboardShortcuts/keyboardShortcuts.js", - "system/hand-track-walk.js" + "system/keyboardShortcuts/keyboardShortcuts.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", From 69b407f001d9169924b9066a8d86cb0071d9fb5c Mon Sep 17 00:00:00 2001 From: kasenvr <52365539+kasenvr@users.noreply.github.com> Date: Sat, 27 Jun 2020 20:26:35 -0400 Subject: [PATCH 72/73] Update BUILD.md I removed a build variable that does not exist and added one that does after doing a codebase search. --- BUILD.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BUILD.md b/BUILD.md index b30160e7e4..e17e52a76f 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,13 +1,13 @@ # General Build Information -*Last Updated on May 17, 2020* +*Last Updated on June 27, 2020* ### OS Specific Build Guides * [Build Windows](BUILD_WIN.md) - complete instructions for Windows. * [Build Linux](BUILD_LINUX.md) - additional instructions for Linux. * [Build OSX](BUILD_OSX.md) - additional instructions for OS X. -* [Build Android](BUILD_ANDROID.md) - additional instructions for Android +* [Build Android](BUILD_ANDROID.md) - additional instructions for Android. ### Dependencies - [git](https://git-scm.com/downloads): >= 1.6 @@ -79,10 +79,10 @@ Where /path/to/directory is the path to a directory where you wish the build fil // The type of release. RELEASE_TYPE=PRODUCTION|PR - RELEASE_BUILD=PRODUCTION|PR // TODO: What do these do? PRODUCTION_BUILD=0|1 + PR_BUILD=0|1 STABLE_BUILD=0|1 // TODO: What do these do? @@ -150,4 +150,4 @@ The following build options can be used when running CMake #### Devices You can support external input/output devices such as Leap Motion, MIDI, and more by adding each individual SDK in the visible building path. Refer to the readme file available in each device folder in [interface/external/](interface/external) for the detailed explanation of the requirements to use the device. - \ No newline at end of file + From 6a159fd34f3d8636a93de641ac25982b912dea1c Mon Sep 17 00:00:00 2001 From: kasenvr <52365539+kasenvr@users.noreply.github.com> Date: Sat, 27 Jun 2020 20:39:19 -0400 Subject: [PATCH 73/73] Add missing release_type --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index e17e52a76f..c8d4785d08 100644 --- a/BUILD.md +++ b/BUILD.md @@ -78,7 +78,7 @@ Where /path/to/directory is the path to a directory where you wish the build fil BUILD_NUMBER // The type of release. - RELEASE_TYPE=PRODUCTION|PR + RELEASE_TYPE=PRODUCTION|PR|DEV // TODO: What do these do? PRODUCTION_BUILD=0|1