LONGITUDINAL_BACKWARD | number | number | Deprecated: Use
diff --git a/libraries/controllers/src/controllers/InputDevice.h b/libraries/controllers/src/controllers/InputDevice.h
index 7c3c31cb38..95d0524a4a 100644
--- a/libraries/controllers/src/controllers/InputDevice.h
+++ b/libraries/controllers/src/controllers/InputDevice.h
@@ -54,10 +54,9 @@ enum Hand {
/**jsdoc
* The Controller.Hardware object has properties representing standard and hardware-specific controller and
- * computer outputs, plus predefined actions on Interface and the user's avatar. Read-only. The outputs can be mapped
- * to actions or functions in a {@link RouteObject} mapping. Additionally, hardware-specific controller outputs can be mapped
- * to standard controller outputs.
- *
+ * computer outputs, plus predefined actions on Interface and the user's avatar. Read-only.
+ * The outputs can be mapped to actions or functions in a {@link RouteObject} mapping. Additionally, hardware-specific
+ * controller outputs can be mapped to standard controller outputs.
* Controllers typically implement a subset of the {@link Controller.Standard} controls, plus they may implement some extras.
* Some common controllers are included in the table. You can see the outputs provided by these and others by
* viewing their {@link Controller.MappingJSON|MappingJSON} files at
diff --git a/libraries/controllers/src/controllers/ScriptingInterface.cpp b/libraries/controllers/src/controllers/ScriptingInterface.cpp
index 07c59e1aaa..fd32b2eb43 100644
--- a/libraries/controllers/src/controllers/ScriptingInterface.cpp
+++ b/libraries/controllers/src/controllers/ScriptingInterface.cpp
@@ -227,6 +227,7 @@ namespace controller {
}
QObject* ScriptingInterface::loadMapping(const QString& jsonUrl) {
+ // FIXME: Implement. https://highfidelity.manuscript.com/f/cases/14188/Implement-Controller-loadMappping
return nullptr;
}
diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h
index 157730e7c6..de1cada97b 100644
--- a/libraries/controllers/src/controllers/ScriptingInterface.h
+++ b/libraries/controllers/src/controllers/ScriptingInterface.h
@@ -73,7 +73,7 @@ namespace controller {
virtual ~ScriptingInterface() {};
/**jsdoc
- * Get a list of all available actions.
+ * Gets a list of all available actions.
* @function Controller.getAllActions
* @returns {Action[]} All available actions.
* @deprecated This function no longer works.
@@ -82,7 +82,7 @@ namespace controller {
Q_INVOKABLE QVector getAllActions();
/**jsdoc
- * Get a list of all available inputs for a hardware device.
+ * Gets a list of all available inputs for a hardware device.
* @function Controller.getAvailableInputs
* @param {number} deviceID - Integer ID of the hardware device.
* @returns {NamedPair[]} All available inputs for the device.
@@ -92,7 +92,7 @@ namespace controller {
Q_INVOKABLE QVector getAvailableInputs(unsigned int device);
/**jsdoc
- * Find the name of a particular controller from its device ID.
+ * Finds the name of a particular controller from its device ID.
* @function Controller.getDeviceName
* @param {number} deviceID - The integer ID of the device.
* @returns {string} The name of the device if found, otherwise "unknown" .
@@ -106,7 +106,7 @@ namespace controller {
Q_INVOKABLE QString getDeviceName(unsigned int device);
/**jsdoc
- * Get the current value of an action.
+ * Gets the current value of an action.
* @function Controller.getActionValue
* @param {number} actionID - The integer ID of the action.
* @returns {number} The current value of the action.
@@ -121,7 +121,7 @@ namespace controller {
Q_INVOKABLE float getActionValue(int action);
/**jsdoc
- * Find the ID of a specific controller from its device name.
+ * Finds the ID of a specific controller from its device name.
* @function Controller.findDevice
* @param {string} deviceName - The name of the device to find.
* @returns {number} The integer ID of the device if available, otherwise 65535 .
@@ -132,7 +132,7 @@ namespace controller {
Q_INVOKABLE int findDevice(QString name);
/**jsdoc
- * Get the names of all currently available controller devices plus "Actions", "Application", and "Standard".
+ * Gets the names of all currently available controller devices plus "Actions", "Application", and "Standard".
* @function Controller.getDeviceNames
* @returns {string[]} An array of device names.
* @example Get the names of all currently available controller devices.
@@ -143,7 +143,7 @@ namespace controller {
Q_INVOKABLE QVector getDeviceNames();
/**jsdoc
- * Find the ID of an action from its name.
+ * Finds the ID of an action from its name.
* @function Controller.findAction
* @param {string} actionName - The name of the action: one of the {@link Controller.Actions} property names.
* @returns {number} The integer ID of the action if found, otherwise 4095 . Note that this value is not
@@ -156,7 +156,7 @@ namespace controller {
Q_INVOKABLE int findAction(QString actionName);
/**jsdoc
- * Get the names of all actions available as properties of {@link Controller.Actions}.
+ * Gets the names of all actions available as properties of {@link Controller.Actions}.
* @function Controller.getActionNames
* @returns {string[]} An array of action names.
* @example Get the names of all actions.
@@ -167,7 +167,7 @@ namespace controller {
Q_INVOKABLE QVector getActionNames() const;
/**jsdoc
- * Get the value of a controller button or axis output. Note: Also gets the value of a controller axis output.
+ * Gets the value of a controller button or axis output. Note: Also gets the value of a controller axis output.
* @function Controller.getValue
* @param {number} source - The {@link Controller.Standard} or {@link Controller.Hardware} item.
* @returns {number} The current value of the controller item output if source is valid, otherwise
@@ -186,7 +186,7 @@ namespace controller {
Q_INVOKABLE float getValue(const int& source) const;
/**jsdoc
- * Get the value of a controller axis output. Note: Also gets the value of a controller button output.
+ * Gets the value of a controller axis output. Note: Also gets the value of a controller button output.
* @function Controller.getAxisValue
* @param {number} source - The {@link Controller.Standard} or {@link Controller.Hardware} item.
* @returns {number} The current value of the controller item output if source is valid, otherwise
@@ -196,7 +196,7 @@ namespace controller {
Q_INVOKABLE float getAxisValue(int source) const;
/**jsdoc
- * Get the value of a controller pose output.
+ * Gets the value of a controller pose output.
* @function Controller.getPoseValue
* @param {number} source - The {@link Controller.Standard} or {@link Controller.Hardware} pose output.
* @returns {Pose} The current value of the controller pose output if source is a pose output, otherwise
@@ -212,7 +212,7 @@ namespace controller {
* @function Controller.triggerHapticPulse
* @param {number} strength - The strength of the haptic pulse, 0.0 – 1.0 .
* @param {number} duration - The duration of the haptic pulse, in milliseconds.
- * @param {Controller.Hand} hand=2 - The hand or hands to trigger the haptic pulse on.
+ * @param {Controller.Hand} [hand=2] - The hand or hands to trigger the haptic pulse on.
* @example Trigger a haptic pulse on the right hand.
* var HAPTIC_STRENGTH = 0.5;
* var HAPTIC_DURATION = 10;
@@ -225,7 +225,7 @@ namespace controller {
* Triggers a 250ms haptic pulse on connected and enabled devices that have the capability.
* @function Controller.triggerShortHapticPulse
* @param {number} strength - The strength of the haptic pulse, 0.0 – 1.0 .
- * @param {Controller.Hand} hand=2 - The hand or hands to trigger the haptic pulse on.
+ * @param {Controller.Hand} [hand=2] - The hand or hands to trigger the haptic pulse on.
*/
Q_INVOKABLE bool triggerShortHapticPulse(float strength, controller::Hand hand = BOTH) const;
@@ -235,7 +235,7 @@ namespace controller {
* @param {number} deviceID - The ID of the device to trigger the haptic pulse on.
* @param {number} strength - The strength of the haptic pulse, 0.0 – 1.0 .
* @param {number} duration - The duration of the haptic pulse, in milliseconds.
- * @param {Controller.Hand} hand=2 - The hand or hands to trigger the haptic pulse on.
+ * @param {Controller.Hand} [hand=2] - The hand or hands to trigger the haptic pulse on.
* @example Trigger a haptic pulse on an Oculus Touch controller.
* var HAPTIC_STRENGTH = 0.5;
* var deviceID = Controller.findDevice("OculusTouch");
@@ -251,18 +251,18 @@ namespace controller {
* @function Controller.triggerShortHapticPulseOnDevice
* @param {number} deviceID - The ID of the device to trigger the haptic pulse on.
* @param {number} strength - The strength of the haptic pulse, 0.0 – 1.0 .
- * @param {Controller.Hand} hand=2 - The hand or hands to trigger the haptic pulse on.
+ * @param {Controller.Hand} [hand=2] - The hand or hands to trigger the haptic pulse on.
*/
Q_INVOKABLE bool triggerShortHapticPulseOnDevice(unsigned int device, float strength, controller::Hand hand = BOTH)
const;
/**jsdoc
- * Create a new controller mapping. Routes can then be added to the mapping using {@link MappingObject} methods and
+ * Creates a new controller mapping. Routes can then be added to the mapping using {@link MappingObject} methods and
* routed to Standard controls, Actions , or script functions using {@link RouteObject}
* methods. The mapping can then be enabled using {@link Controller.enableMapping|enableMapping} for it to take effect.
* @function Controller.newMapping
- * @param {string} mappingName=Uuid.generate() - A unique name for the mapping. If not specified a new UUID generated
+ * @param {string} [mappingName=Uuid.generate()] - A unique name for the mapping. If not specified a new UUID generated
* by {@link Uuid.generate} is used.
* @returns {MappingObject} A controller mapping object.
* @example Create a simple mapping that makes the right trigger move your avatar up.
@@ -279,22 +279,22 @@ namespace controller {
Q_INVOKABLE QObject* newMapping(const QString& mappingName = QUuid::createUuid().toString());
/**jsdoc
- * Enable or disable a controller mapping. When enabled, the routes in the mapping have effect.
+ * Enables or disables a controller mapping. When enabled, the routes in the mapping have effect.
* @function Controller.enableMapping
* @param {string} mappingName - The name of the mapping.
- * @param {boolean} enable=true - If true then the mapping is enabled, otherwise it is disabled.
+ * @param {boolean} [[enable=true] - If true then the mapping is enabled, otherwise it is disabled.
*/
Q_INVOKABLE void enableMapping(const QString& mappingName, bool enable = true);
/**jsdoc
- * Disable a controller mapping. When disabled, the routes in the mapping have no effect.
+ * Disables a controller mapping. When disabled, the routes in the mapping have no effect.
* @function Controller.disableMapping
* @param {string} mappingName - The name of the mapping.
*/
Q_INVOKABLE void disableMapping(const QString& mappingName) { enableMapping(mappingName, false); }
/**jsdoc
- * Create a new controller mapping from a {@link Controller.MappingJSON|MappingJSON} string. Use
+ * Creates a new controller mapping from a {@link Controller.MappingJSON|MappingJSON} string. Use
* {@link Controller.enableMapping|enableMapping} to enable the mapping for it to take effect.
* @function Controller.parseMapping
* @param {string} jsonString - A JSON string of the {@link Controller.MappingJSON|MappingJSON}.
@@ -317,19 +317,19 @@ namespace controller {
Q_INVOKABLE QObject* parseMapping(const QString& json);
/**jsdoc
- * Create a new controller mapping from a {@link Controller.MappingJSON|MappingJSON} JSON file at a URL. Use
+ * Creates a new controller mapping from a {@link Controller.MappingJSON|MappingJSON} JSON file at a URL. Use
* {@link Controller.enableMapping|enableMapping} to enable the mapping for it to take effect.
+ * Warning: This function is not yet implemented; it doesn't load a mapping and just returns
+ * null .
* @function Controller.loadMapping
* @param {string} jsonURL - The URL the {@link Controller.MappingJSON|MappingJSON} JSON file.
* @returns {MappingObject} A controller mapping object.
- * @todo Implement this function. It currently does not load the mapping from the file; it just returns
- * null .
*/
Q_INVOKABLE QObject* loadMapping(const QString& jsonUrl);
/**jsdoc
- * Get the {@link Controller.Hardware} property tree. Calling this function is the same as using the {@link Controller}
+ * Gets the {@link Controller.Hardware} property tree. Calling this function is the same as using the {@link Controller}
* property, Controller.Hardware .
* @function Controller.getHardware
* @returns {Controller.Hardware} The {@link Controller.Hardware} property tree.
@@ -337,7 +337,7 @@ namespace controller {
Q_INVOKABLE const QVariantMap getHardware() { return _hardware; }
/**jsdoc
- * Get the {@link Controller.Actions} property tree. Calling this function is the same as using the {@link Controller}
+ * Gets the {@link Controller.Actions} property tree. Calling this function is the same as using the {@link Controller}
* property, Controller.Actions .
* @function Controller.getActions
* @returns {Controller.Actions} The {@link Controller.Actions} property tree.
@@ -345,7 +345,7 @@ namespace controller {
Q_INVOKABLE const QVariantMap getActions() { return _actions; } //undefined
/**jsdoc
- * Get the {@link Controller.Standard} property tree. Calling this function is the same as using the {@link Controller}
+ * Gets the {@link Controller.Standard} property tree. Calling this function is the same as using the {@link Controller}
* property, Controller.Standard .
* @function Controller.getStandard
* @returns {Controller.Standard} The {@link Controller.Standard} property tree.
@@ -354,7 +354,7 @@ namespace controller {
/**jsdoc
- * Start making a recording of currently active controllers.
+ * Starts making a recording of currently active controllers.
* @function Controller.startInputRecording
* @example Make a controller recording.
* // Delay start of recording for 2s.
@@ -374,13 +374,13 @@ namespace controller {
Q_INVOKABLE void startInputRecording();
/**jsdoc
- * Stop making a recording started by {@link Controller.startInputRecording|startInputRecording}.
+ * Stops making a recording started by {@link Controller.startInputRecording|startInputRecording}.
* @function Controller.stopInputRecording
*/
Q_INVOKABLE void stopInputRecording();
/**jsdoc
- * Play back the current recording from the beginning. The current recording may have been recorded by
+ * Plays back the current recording from the beginning. The current recording may have been recorded by
* {@link Controller.startInputRecording|startInputRecording} and
* {@link Controller.stopInputRecording|stopInputRecording}, or loaded by
* {@link Controller.loadInputRecording|loadInputRecording}. Playback repeats in a loop until
@@ -403,13 +403,13 @@ namespace controller {
Q_INVOKABLE void startInputPlayback();
/**jsdoc
- * Stop play back of a recording started by {@link Controller.startInputPlayback|startInputPlayback}.
+ * Stops play back of a recording started by {@link Controller.startInputPlayback|startInputPlayback}.
* @function Controller.stopInputPlayback
*/
Q_INVOKABLE void stopInputPlayback();
/**jsdoc
- * Save the current recording to a file. The current recording may have been recorded by
+ * Saves the current recording to a file. The current recording may have been recorded by
* {@link Controller.startInputRecording|startInputRecording} and
* {@link Controller.stopInputRecording|stopInputRecording}, or loaded by
* {@link Controller.loadInputRecording|loadInputRecording}. It is saved in the directory returned by
@@ -419,24 +419,26 @@ namespace controller {
Q_INVOKABLE void saveInputRecording();
/**jsdoc
- * Load an input recording, ready for play back.
+ * Loads an input recording, ready for play back.
* @function Controller.loadInputRecording
* @param {string} file - The path to the recording file, prefixed by "file:///" .
*/
Q_INVOKABLE void loadInputRecording(const QString& file);
/**jsdoc
- * Get the directory in which input recordings are saved.
+ * Gets the directory in which input recordings are saved.
* @function Controller.getInputRecorderSaveDirectory
* @returns {string} The directory in which input recordings are saved.
*/
Q_INVOKABLE QString getInputRecorderSaveDirectory();
/**jsdoc
- * Get all the active and enabled (running) input devices
- * @function Controller.getRunningInputDevices
- * @returns {string[]} An array of strings with the names
- */
+ * Gets the names of all the active and running (enabled) input devices.
+ * @function Controller.getRunningInputDevices
+ * @returns {string[]} The list of current active and running input devices.
+ * @example List all active and running input devices.
+ * print("Running devices: " + JSON.stringify(Controller.getRunningInputDeviceNames()));
+ */
Q_INVOKABLE QStringList getRunningInputDeviceNames();
bool isMouseCaptured() const { return _mouseCaptured; }
@@ -447,7 +449,7 @@ namespace controller {
public slots:
/**jsdoc
- * Disable processing of mouse "move", "press", "double-press", and "release" events into
+ * Disables processing of mouse "move", "press", "double-press", and "release" events into
* {@link Controller.Hardware|Controller.Hardware.Keyboard} outputs.
* @function Controller.captureMouseEvents
* @example Disable Controller.Hardware.Keyboard mouse events for a short period.
@@ -475,7 +477,7 @@ namespace controller {
virtual void captureMouseEvents() { _mouseCaptured = true; }
/**jsdoc
- * Enable processing of mouse "move", "press", "double-press", and "release" events into
+ * Enables processing of mouse "move", "press", "double-press", and "release" events into
* {@link Controller.Hardware-Keyboard|Controller.Hardware.Keyboard} outputs that were disabled using
* {@link Controller.captureMouseEvents|captureMouseEvents}.
* @function Controller.releaseMouseEvents
@@ -484,7 +486,7 @@ namespace controller {
/**jsdoc
- * Disable processing of touch "begin", "update", and "end" events into
+ * Disables processing of touch "begin", "update", and "end" events into
* {@link Controller.Hardware|Controller.Hardware.Keyboard},
* {@link Controller.Hardware|Controller.Hardware.Touchscreen}, and
* {@link Controller.Hardware|Controller.Hardware.TouchscreenVirtualPad} outputs.
@@ -493,7 +495,7 @@ namespace controller {
virtual void captureTouchEvents() { _touchCaptured = true; }
/**jsdoc
- * Enable processing of touch "begin", "update", and "end" events into
+ * Enables processing of touch "begin", "update", and "end" events into
* {@link Controller.Hardware|Controller.Hardware.Keyboard},
* {@link Controller.Hardware|Controller.Hardware.Touchscreen}, and
* {@link Controller.Hardware|Controller.Hardware.TouchscreenVirtualPad} outputs that were disabled using
@@ -504,14 +506,14 @@ namespace controller {
/**jsdoc
- * Disable processing of mouse wheel rotation events into {@link Controller.Hardware|Controller.Hardware.Keyboard}
+ * Disables processing of mouse wheel rotation events into {@link Controller.Hardware|Controller.Hardware.Keyboard}
* outputs.
* @function Controller.captureWheelEvents
*/
virtual void captureWheelEvents() { _wheelCaptured = true; }
/**jsdoc
- * Enable processing of mouse wheel rotation events into {@link Controller.Hardware|Controller.Hardware.Keyboard}
+ * Enables processing of mouse wheel rotation events into {@link Controller.Hardware|Controller.Hardware.Keyboard}
* outputs that wer disabled using {@link Controller.captureWheelEvents|captureWheelEvents}.
* @function Controller.releaseWheelEvents
*/
@@ -519,7 +521,7 @@ namespace controller {
/**jsdoc
- * Disable translating and rotating the user's avatar in response to keyboard and controller controls.
+ * Disables translating and rotating the user's avatar in response to keyboard and controller controls.
* @function Controller.captureActionEvents
* @example Disable avatar translation and rotation for a short period.
* Script.setTimeout(function () {
@@ -533,12 +535,19 @@ namespace controller {
virtual void captureActionEvents() { _actionsCaptured = true; }
/**jsdoc
- * Enable translating and rotating the user's avatar in response to keyboard and controller controls that were disabled
+ * Enables translating and rotating the user's avatar in response to keyboard and controller controls that were disabled
* using {@link Controller.captureActionEvents|captureActionEvents}.
* @function Controller.releaseActionEvents
*/
virtual void releaseActionEvents() { _actionsCaptured = false; }
+ /**jsdoc
+ * @function Controller.updateRunningInputDevices
+ * @param {string} deviceName - Device name.
+ * @param {boolean} isRunning - Is running.
+ * @param {string[]} runningDevices - Running devices.
+ * @deprecated This function is deprecated and will be removed.
+ */
void updateRunningInputDevices(const QString& deviceName, bool isRunning, const QStringList& runningDevices);
signals:
@@ -593,7 +602,7 @@ namespace controller {
/**jsdoc
* Triggered when a device is registered or unregistered by a plugin. Not all plugins generate
- * hardwareChanged events: for example connecting or disconnecting a mouse will not generate an event but
+ * hardwareChanged events: for example, connecting or disconnecting a mouse will not generate an event but
* connecting or disconnecting an Xbox controller will.
* @function Controller.hardwareChanged
* @returns {Signal}
@@ -601,13 +610,13 @@ namespace controller {
void hardwareChanged();
/**jsdoc
- * Triggered when a device is enabled/disabled
- * Enabling/Disabling Leapmotion on settings/controls will trigger this signal.
- * @function Controller.deviceRunningChanged
- * @param {string} deviceName - The name of the device that is getting enabled/disabled
- * @param {boolean} isEnabled - Return if the device is enabled.
- * @returns {Signal}
- */
+ * Triggered when an input device starts or stops being active and running (enabled). For example, enabling or
+ * disabling the LeapMotion in Settings > Controls > Calibration will trigger this signal.
+ * @function Controller.inputDeviceRunningChanged
+ * @param {string} deviceName - The name of the device.
+ * @param {boolean} isRunning - true if the device is active and running, false if it isn't.
+ * @returns {Signal}
+ */
void inputDeviceRunningChanged(QString deviceName, bool isRunning);
diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp
index e1733d2524..a7ae1aae98 100644
--- a/libraries/controllers/src/controllers/StandardController.cpp
+++ b/libraries/controllers/src/controllers/StandardController.cpp
@@ -30,17 +30,16 @@ void StandardController::focusOutEvent() {
/**jsdoc
* The Controller.Standard object has properties representing standard controller outputs. Those for physical
* controllers are based on the XBox controller, with aliases for PlayStation. The property values are integer IDs, uniquely
- * identifying each output. Read-only. These can be mapped to actions or functions in a {@link RouteObject}
- * mapping.
- *
- * The data value provided by each control is either a number or a {@link Pose}. Numbers are typically normalized to
- * 0.0 or 1.0 for button states, the range 0.0 – 1.0 for unidirectional scales,
- * and the range -1.0 – 1.0 for bidirectional scales.
- *
- * Each hardware device has a mapping from its outputs to Controller.Standard items, specified in a JSON file.
- * For example,
- * leapmotion.json and
- * vive.json.
+ * identifying each output. Read-only.
+ * These outputs can be mapped to actions or functions in a {@link RouteObject} mapping. The data value provided by each
+ * control is either a number or a {@link Pose}. Numbers are typically normalized to 0.0 or 1.0 for
+ * button states, the range 0.0 – 1.0 for unidirectional scales, and the range -1.0 – 1.0
+ * for bidirectional scales.
+ * Each hardware device has a mapping from its outputs to a subset of Controller.Standard items, specified in a
+ * JSON file. For example,
+ * vive.json
+ * and
+ * leapmotion.json.
*
*
*
@@ -119,12 +118,12 @@ void StandardController::focusOutEvent() {
* button.
* RightThumbUp | number | number | Right thumb not touching primary or secondary
* thumb buttons. |
- * LeftPrimaryIndex | number | number | Left primary index control pressed.
- * To Do: Implement this for current controllers. |
+ * LeftPrimaryIndex | number | number | Left primary index control
+ * pressed. |
* LeftSecondaryIndex | number | number | Left secondary index control pressed.
* |
* RightPrimaryIndex | number | number | Right primary index control pressed.
- * To Do: Implement this for current controllers. |
+ *
* RightSecondaryIndex | number | number | Right secondary index control pressed.
* |
* LeftPrimaryIndexTouch | number | number | Left index finger is touching primary
diff --git a/libraries/controllers/src/controllers/impl/MappingBuilderProxy.h b/libraries/controllers/src/controllers/impl/MappingBuilderProxy.h
index 845e19f6c3..b51f484f7d 100644
--- a/libraries/controllers/src/controllers/impl/MappingBuilderProxy.h
+++ b/libraries/controllers/src/controllers/impl/MappingBuilderProxy.h
@@ -134,7 +134,7 @@ public:
: _parent(parent), _mapping(mapping) { }
/**jsdoc
- * Create a new {@link RouteObject} from a controller output, ready to be mapped to a standard control, action, or
+ * Creates a new {@link RouteObject} from a controller output, ready to be mapped to a standard control, action, or
* function.
* This is a QML-specific version of {@link MappingObject#from|from}: use this version in QML files.
* @function MappingObject#fromQml
@@ -145,7 +145,7 @@ public:
Q_INVOKABLE QObject* fromQml(const QJSValue& source);
/**jsdoc
- * Create a new {@link RouteObject} from two numeric {@link Controller.Hardware} outputs, one applied in the negative
+ * Creates a new {@link RouteObject} from two numeric {@link Controller.Hardware} outputs, one applied in the negative
* direction and the other in the positive direction, ready to be mapped to a standard control, action, or function.
* This is a QML-specific version of {@link MappingObject#makeAxis|makeAxis}: use this version in QML files.
* @function MappingObject#makeAxisQml
@@ -157,7 +157,7 @@ public:
Q_INVOKABLE QObject* makeAxisQml(const QJSValue& source1, const QJSValue& source2);
/**jsdoc
- * Create a new {@link RouteObject} from a controller output, ready to be mapped to a standard control, action, or
+ * Creates a new {@link RouteObject} from a controller output, ready to be mapped to a standard control, action, or
* function.
* @function MappingObject#from
* @param {Controller.Standard|Controller.Hardware|function} source - The controller output or function that is the source
@@ -167,7 +167,7 @@ public:
Q_INVOKABLE QObject* from(const QScriptValue& source);
/**jsdoc
- * Create a new {@link RouteObject} from two numeric {@link Controller.Hardware} outputs, one applied in the negative
+ * Creates a new {@link RouteObject} from two numeric {@link Controller.Hardware} outputs, one applied in the negative
* direction and the other in the positive direction, ready to be mapped to a standard control, action, or function.
* @function MappingObject#makeAxis
* @param {Controller.Hardware} source1 - The first, negative-direction controller output.
@@ -189,7 +189,7 @@ public:
Q_INVOKABLE QObject* makeAxis(const QScriptValue& source1, const QScriptValue& source2);
/**jsdoc
- * Enable or disable the mapping. When enabled, the routes in the mapping take effect.
+ * Enables or disables the mapping. When enabled, the routes in the mapping take effect.
* Synonymous with {@link Controller.enableMapping}.
* @function MappingObject#enable
* @param {boolean} enable=true - If true then the mapping is enabled, otherwise it is disabled.
@@ -198,7 +198,7 @@ public:
Q_INVOKABLE QObject* enable(bool enable = true);
/**jsdoc
- * Disable the mapping. When disabled, the routes in the mapping have no effect.
+ * Disables the mapping. When disabled, the routes in the mapping have no effect.
* Synonymous with {@link Controller.disableMapping}.
* @function MappingObject#disable
* @returns {MappingObject} The mapping object, so that further routes can be added.
diff --git a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp
index 048e23be1c..56ace23335 100644
--- a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp
+++ b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp
@@ -66,6 +66,8 @@ QObject* RouteBuilderProxy::peek(bool enable) {
}
QObject* RouteBuilderProxy::when(const QScriptValue& expression) {
+ // FIXME: Support "!" conditional in simple expression and array expression.
+ // Note that "!" is supported when parsing a JSON file, in UserInputMapper::parseConditional().
auto newConditional = _parent.conditionalFor(expression);
if (_route->conditional) {
_route->conditional = ConditionalPointer(new AndConditional(_route->conditional, newConditional));
diff --git a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h
index eb610af78a..e7ff04d72c 100644
--- a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h
+++ b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h
@@ -51,7 +51,7 @@ class RouteBuilderProxy : public QObject {
: _parent(parent), _mapping(mapping), _route(route) { }
/**jsdoc
- * Terminate the route with a standard control, an action, or a script function. The output value from the route is
+ * Terminates the route with a standard control, an action, or a script function. The output value from the route is
* sent to the specified destination.
* This is a QML-specific version of {@link MappingObject#to|to}: use this version in QML files.
* @function RouteObject#toQml
@@ -62,7 +62,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE void toQml(const QJSValue& destination);
/**jsdoc
- * Process the route only if a condition is satisfied. The condition is evaluated before the route input is read, and
+ * Processes the route only if a condition is satisfied. The condition is evaluated before the route input is read, and
* the input is read only if the condition is true . Thus, if the condition is not met then subsequent
* routes using the same input are processed.
* This is a QML-specific version of {@link MappingObject#to|to}: use this version in QML files.
@@ -81,7 +81,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* whenQml(const QJSValue& expression);
/**jsdoc
- * Terminate the route with a standard control, an action, or a script function. The output value from the route is
+ * Terminates the route with a standard control, an action, or a script function. The output value from the route is
* sent to the specified destination.
* @function RouteObject#to
* @param {Controller.Standard|Controller.Actions|function} destination - The standard control, action, or JavaScript
@@ -117,7 +117,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE void to(const QScriptValue& destination);
/**jsdoc
- * Enable and disabling writing debug information for a route to the program log.
+ * Enables or disables writing debug information for a route to the program log.
* @function RouteObject#debug
* @param {boolean} [enable=true] - If true then writing debug information is enabled for the route,
* otherwise it is disabled.
@@ -147,7 +147,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* debug(bool enable = true);
/**jsdoc
- * Process the route without marking the controller output as having been read, so that other routes from the same
+ * Processes the route without marking the controller output as having been read, so that other routes from the same
* controller output can also process.
* @function RouteObject#peek
* @param {boolean} [enable=true] - If true then the route is processed without marking the route's
@@ -157,7 +157,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* peek(bool enable = true);
/**jsdoc
- * Process the route only if a condition is satisfied. The condition is evaluated before the route input is read, and
+ * Processes the route only if a condition is satisfied. The condition is evaluated before the route input is read, and
* the input is read only if the condition is true . Thus, if the condition is not met then subsequent
* routes using the same input are processed.
* @function RouteObject#when
@@ -170,6 +170,8 @@ class RouteBuilderProxy : public QObject {
* definition.
*
* If an array of conditions is provided, their values are ANDed together.
+ * Warning: The use of ! is not currently supported in JavaScript .when()
+ * calls.
* @returns {RouteObject} The RouteObject with the condition added.
* @example Process the right trigger differently in HMD and desktop modes.
* var MAPPING_NAME = "com.highfidelity.controllers.example.newMapping";
@@ -193,7 +195,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* when(const QScriptValue& expression);
/**jsdoc
- * Filter numeric route values to lie between two values; values outside this range are not passed on through the
+ * Filters numeric route values to lie between two values; values outside this range are not passed on through the
* route.
* @function RouteObject#clamp
* @param {number} min - The minimum value to pass through.
@@ -214,7 +216,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* clamp(float min, float max);
/**jsdoc
- * Filter numeric route values such that they are rounded to 0 or 1 without output values
+ * Filters numeric route values such that they are rounded to 0 or 1 without output values
* flickering when the input value hovers around 0.5 . For example, this enables you to use an analog input
* as if it were a toggle.
* @function RouteObject#hysteresis
@@ -239,7 +241,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* hysteresis(float min, float max);
/**jsdoc
- * Filter numeric route values to send at a specified interval.
+ * Filters numeric route values to send at a specified interval.
* @function RouteObject#pulse
* @param {number} interval - The interval between sending values, in seconds.
* @returns {RouteObject} The RouteObject with the filter applied.
@@ -258,7 +260,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* pulse(float interval);
/**jsdoc
- * Filter numeric and {@link Pose} route values to be scaled by a constant amount.
+ * Filters numeric and {@link Pose} route values to be scaled by a constant amount.
* @function RouteObject#scale
* @param {number} multiplier - The scale to multiply the value by.
* @returns {RouteObject} The RouteObject with the filter applied.
@@ -280,7 +282,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* scale(float multiplier);
/**jsdoc
- * Filter numeric and {@link Pose} route values to have the opposite sign, e.g., 0.5 is changed to
+ * Filters numeric and {@link Pose} route values to have the opposite sign, e.g., 0.5 is changed to
* -0.5 .
* @function RouteObject#invert
* @returns {RouteObject} The RouteObject with the filter applied.
@@ -302,7 +304,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* invert();
/**jsdoc
- * Filter numeric route values such that they're sent only when the input value is outside a dead-zone. When the input
+ * Filters numeric route values such that they're sent only when the input value is outside a dead-zone. When the input
* passes the dead-zone value, output is sent starting at 0.0 and catching up with the input value. As the
* input returns toward the dead-zone value, output values reduce to 0.0 at the dead-zone value.
* @function RouteObject#deadZone
@@ -324,7 +326,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* deadZone(float min);
/**jsdoc
- * Filter numeric route values such that they are rounded to -1 , 0 , or 1 .
+ * Filters numeric route values such that they are rounded to -1 , 0 , or 1 .
* For example, this enables you to use an analog input as if it were a toggle or, in the case of a bidirectional axis,
* a tri-state switch.
* @function RouteObject#constrainToInteger
@@ -345,7 +347,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* constrainToInteger();
/**jsdoc
- * Filter numeric route values such that they are rounded to 0 or 1 . For example, this
+ * Filters numeric route values such that they are rounded to 0 or 1 . For example, this
* enables you to use an analog input as if it were a toggle.
* @function RouteObject#constrainToPositiveInteger
* @returns {RouteObject} The RouteObject with the filter applied.
@@ -364,7 +366,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* constrainToPositiveInteger();
/**jsdoc
- * Filter {@link Pose} route values to have a pre-translation applied.
+ * Filters {@link Pose} route values to have a pre-translation applied.
* @function RouteObject#translate
* @param {Vec3} translate - The pre-translation to add to the pose.
* @returns {RouteObject} The RouteObject with the pre-translation applied.
@@ -373,7 +375,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* translate(glm::vec3 translate);
/**jsdoc
- * Filter {@link Pose} route values to have a pre-transform applied.
+ * Filters {@link Pose} route values to have a pre-transform applied.
* @function RouteObject#transform
* @param {Mat4} transform - The pre-transform to apply.
* @returns {RouteObject} The RouteObject with the pre-transform applied.
@@ -382,7 +384,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* transform(glm::mat4 transform);
/**jsdoc
- * Filter {@link Pose} route values to have a post-transform applied.
+ * Filters {@link Pose} route values to have a post-transform applied.
* @function RouteObject#postTransform
* @param {Mat4} transform - The post-transform to apply.
* @returns {RouteObject} The RouteObject with the post-transform applied.
@@ -391,7 +393,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* postTransform(glm::mat4 transform);
/**jsdoc
- * Filter {@link Pose} route values to have a pre-rotation applied.
+ * Filters {@link Pose} route values to have a pre-rotation applied.
* @function RouteObject#rotate
* @param {Quat} rotation - The pre-rotation to add to the pose.
* @returns {RouteObject} The RouteObject with the pre-rotation applied.
@@ -400,7 +402,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* rotate(glm::quat rotation);
/**jsdoc
- * Filter {@link Pose} route values to be smoothed by a low velocity filter. The filter's rotation and translation
+ * Filters {@link Pose} route values to be smoothed by a low velocity filter. The filter's rotation and translation
* values are calculated as: (1 - f) * currentValue + f * previousValue where
* f = currentVelocity / filterConstant . At low velocities, the filter value is largely the previous
* value; at high velocities the value is wholly the current controller value.
@@ -415,7 +417,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* lowVelocity(float rotationConstant, float translationConstant);
/**jsdoc
- * Filter {@link Pose} route values to be smoothed by an exponential decay filter. The filter's rotation and
+ * Filters {@link Pose} route values to be smoothed by an exponential decay filter. The filter's rotation and
* translation values are calculated as: filterConstant * currentValue + (1 - filterConstant) *
* previousValue . Values near 1 are less smooth with lower latency; values near 0 are more smooth with higher
* latency.
@@ -428,7 +430,7 @@ class RouteBuilderProxy : public QObject {
Q_INVOKABLE QObject* exponentialSmoothing(float rotationConstant, float translationConstant);
/**jsdoc
- * Filter numeric route values such that a value of 0.0 is changed to 1.0 , and other values
+ * Filters numeric route values such that a value of 0.0 is changed to 1.0 , and other values
* are changed to 0.0 .
* @function RouteObject#logicalNot
* @returns {RouteObject} The RouteObject with the filter applied.
diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp
index 3383d71a52..78dd14868c 100755
--- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp
+++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp
@@ -219,9 +219,10 @@ controller::Input KeyboardMouseDevice::InputDevice::makeInput(KeyboardMouseDevic
/**jsdoc
* The Controller.Hardware.Keyboard object has properties representing keyboard, mouse, and display touch
- * events. The property values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to
- * actions or functions or Controller.Standard items in a {@link RouteObject} mapping. For presses, each data
- * value is either 1.0 for "true" or 0.0 for "false".
+ * events. The property values are integer IDs, uniquely identifying each output. Read-only.
+ * These events can be mapped to actions or functions or Controller.Standard items in a {@link RouteObject}
+ * mapping. For presses, each data value is either 1.0 for "true" or 0.0 for "false".
+ *
*
*
* Property | Type | Data | Description |
@@ -274,9 +275,13 @@ controller::Input KeyboardMouseDevice::InputDevice::makeInput(KeyboardMouseDevic
* MouseWheelLeft | number | number | The mouse wheel rotated left. The data value
* is the number of units rotated (typically 1.0 ). |
* MouseWheelUp | number | number | The mouse wheel rotated up. The data value
- * is the number of units rotated (typically 1.0 ). |
+ * is the number of units rotated (typically 1.0 ).
+ * Warning: The mouse wheel in an ordinary mouse generates left/right wheel events instead of
+ * up/down.
* MouseWheelDown | number | number | The mouse wheel rotated down. The data value
- * is the number of units rotated (typically 1.0 ). |
+ * is the number of units rotated (typically 1.0 ).
+ * Warning: The mouse wheel in an ordinary mouse generates left/right wheel events instead of
+ * up/down.
* TouchpadRight | number | number | The average touch on a touch-enabled device
* moved right. The data value is how far the average position of all touch points moved. |
* TouchpadLeft | number | number | The average touch on a touch-enabled device
@@ -288,7 +293,6 @@ controller::Input KeyboardMouseDevice::InputDevice::makeInput(KeyboardMouseDevic
*
* |
* @typedef {object} Controller.Hardware-Keyboard
- * @todo Currently, the mouse wheel in an ordinary mouse generates left/right wheel events instead of up/down.
*/
controller::Input::NamedVector KeyboardMouseDevice::InputDevice::getAvailableInputs() const {
using namespace controller;
diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp
index 8d97ff78af..14830f3f04 100644
--- a/plugins/oculus/src/OculusControllerManager.cpp
+++ b/plugins/oculus/src/OculusControllerManager.cpp
@@ -409,9 +409,10 @@ void OculusControllerManager::TouchDevice::stopHapticPulse(bool leftHand) {
}
/**jsdoc
- * The Controller.Hardware.OculusTouch object has properties representing Oculus Rift. The property values are
- * integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or
- * Controller.Standard items in a {@link RouteObject} mapping.
+ * The Controller.Hardware.OculusTouch object has properties representing the Oculus Rift. The property values
+ * are integer IDs, uniquely identifying each output. Read-only.
+ * These outputs can be mapped to actions or functions or Controller.Standard items in a {@link RouteObject}
+ * mapping.
*
*
* Property | Type | Data | Description |
diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp
index 34ebb73fda..c21a9ae4df 100644
--- a/plugins/openvr/src/ViveControllerManager.cpp
+++ b/plugins/openvr/src/ViveControllerManager.cpp
@@ -1299,14 +1299,20 @@ void ViveControllerManager::InputDevice::setConfigFromString(const QString& valu
}
/**jsdoc
- * The Controller.Hardware.Vive object has properties representing Vive. The property values are integer IDs,
- * uniquely identifying each output. Read-only. These can be mapped to actions or functions or
- * Controller.Standard items in a {@link RouteObject} mapping.
+ * The Controller.Hardware.Vive object has properties representing the Vive. The property values are integer
+ * IDs, uniquely identifying each output. Read-only.
+ * These outputs can be mapped to actions or functions or Controller.Standard items in a {@link RouteObject}
+ * mapping.
*
*
* Property | Type | Data | Description |
*
*
+ * Buttons |
+ * LeftApplicationMenu | number | number | Left application menu button pressed.
+ * |
+ * RightApplicationMenu | number | number | Right application menu button pressed.
+ * |
* Touch Pad (Sticks) |
* LX | number | number | Left touch pad x-axis scale. |
* LY | number | number | Left touch pad y-axis scale. |
From 9a70416f2b89f9a923210eab81b0ac89ccf70112 Mon Sep 17 00:00:00 2001
From: David Rowe
Date: Wed, 10 Apr 2019 12:04:53 +1200
Subject: [PATCH 05/54] Update Clipboard JSDoc
---
.../scripting/ClipboardScriptingInterface.h | 84 +++++++++++++------
1 file changed, 60 insertions(+), 24 deletions(-)
diff --git a/interface/src/scripting/ClipboardScriptingInterface.h b/interface/src/scripting/ClipboardScriptingInterface.h
index 42f2205861..9e72d9ea15 100644
--- a/interface/src/scripting/ClipboardScriptingInterface.h
+++ b/interface/src/scripting/ClipboardScriptingInterface.h
@@ -18,7 +18,7 @@
#include
/**jsdoc
- * The Clipboard API enables you to export and import entities to and from JSON files.
+ * The Clipboard API enables you to export and import entities to and from JSON files.
*
* @namespace Clipboard
*
@@ -33,56 +33,92 @@ public:
public:
/**jsdoc
- * Compute the extents of the contents held in the clipboard.
+ * Gets the extents of the entities held in the clipboard.
* @function Clipboard.getContentsDimensions
- * @returns {Vec3} The extents of the contents held in the clipboard.
+ * @returns {Vec3} The extents of the content held in the clipboard.
+ * @example Import entities to the clipboard and report their overall dimensions.
+ * var filename = Window.browse("Import entities to clipboard", "", "*.json");
+ * if (filename) {
+ * if (Clipboard.importEntities(filename)) {
+ * print("Clipboard dimensions: " + JSON.stringify(Clipboard.getContentsDimensions()));
+ * }
+ * }
*/
Q_INVOKABLE glm::vec3 getContentsDimensions();
/**jsdoc
- * Compute the largest dimension of the extents of the contents held in the clipboard.
+ * Gets the largest dimension of the extents of the entities held in the clipboard.
* @function Clipboard.getClipboardContentsLargestDimension
- * @returns {number} The largest dimension computed.
+ * @returns {number} The largest dimension of the extents of the content held in the clipboard.
*/
Q_INVOKABLE float getClipboardContentsLargestDimension();
/**jsdoc
- * Import entities from a JSON file containing entity data into the clipboard.
- * You can generate a JSON file using {@link Clipboard.exportEntities}.
+ * Imports entities from a JSON file into the clipboard.
* @function Clipboard.importEntities
- * @param {string} filename Path and name of file to import.
- * @param {boolean} does the ResourceRequestObserver observe this request?
- * @param {number} optional internal id of object causing this import.
+ * @param {string} filename - The path and name of the JSON file to import.
+ * @param {boolean} [isObservable=true] - true if the {@link ResourceRequestObserver} can observe this
+ * request, false if it can't.
+ * @param {number} [callerID=-1] - An integer ID that is passed through to the {@link ResourceRequestObserver}.
* @returns {boolean} true if the import was successful, otherwise false .
+ * @example Import entities and paste into the domain.
+ * var filename = Window.browse("Import entities to clipboard", "", "*.json");
+ * if (filename) {
+ * if (Clipboard.importEntities(filename)) {
+ * pastedEntities = Clipboard.pasteEntities(Vec3.sum(MyAvatar.position,
+ * Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })));
+ * print("Entities pasted: " + JSON.stringify(pastedEntities));
+ * }
+ * }
*/
Q_INVOKABLE bool importEntities(const QString& filename, const bool isObservable = true, const qint64 callerId = -1);
/**jsdoc
- * Export the entities specified to a JSON file.
+ * Exports specified entities to a JSON file.
* @function Clipboard.exportEntities
- * @param {string} filename Path and name of the file to export the entities to. Should have the extension ".json".
- * @param {Uuid[]} entityIDs Array of IDs of the entities to export.
- * @returns {boolean} true if the export was successful, otherwise false .
+ * @param {string} filename - Path and name of the file to export the entities to. Should have the extension ".json".
+ * @param {Uuid[]} entityIDs - The IDs of the entities to export.
+ * @returns {boolean} true if entities were found and the file was written, otherwise false .
+ * @example Create and export a cube and a sphere.
+ * // Create entities.
+ * var box = Entities.addEntity({
+ * type: "Box",
+ * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: -0.2, y: 0, z: -3 })),
+ * lifetime: 300 // Delete after 5 minutes.
+ * });
+ * var sphere = Entities.addEntity({
+ * type: "Sphere",
+ * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0.2, y: 0, z: -3 })),
+ * lifetime: 300 // Delete after 5 minutes.
+ * });
+ *
+ * // Export entities.
+ * var filename = Window.save("Export entities to JSON file", Paths.resources, "*.json");
+ * if (filename) {
+ * Clipboard.exportEntities(filename, [box, sphere]);
+ * }
*/
Q_INVOKABLE bool exportEntities(const QString& filename, const QVector& entityIDs);
/**jsdoc
- * Export the entities with centers within a cube to a JSON file.
+ * Exports all entities that have centers within a cube to a JSON file.
* @function Clipboard.exportEntities
- * @param {string} filename Path and name of the file to export the entities to. Should have the extension ".json".
- * @param {number} x X-coordinate of the cube center.
- * @param {number} y Y-coordinate of the cube center.
- * @param {number} z Z-coordinate of the cube center.
- * @param {number} scale Half dimension of the cube.
- * @returns {boolean} true if the export was successful, otherwise false .
+ * @variation 0
+ * @param {string} filename - Path and name of the file to export the entities to. Should have the extension ".json".
+ * @param {number} x - X-coordinate of the cube center.
+ * @param {number} y - Y-coordinate of the cube center.
+ * @param {number} z - Z-coordinate of the cube center.
+ * @param {number} scale - Half dimension of the cube.
+ * @returns {boolean} true if entities were found and the file was written, otherwise false .
*/
Q_INVOKABLE bool exportEntities(const QString& filename, float x, float y, float z, float scale);
/**jsdoc
- * Paste the contents of the clipboard into the world.
+ * Pastes the contents of the clipboard into the domain.
* @function Clipboard.pasteEntities
- * @param {Vec3} position Position to paste the clipboard contents at.
- * @returns {Uuid[]} Array of entity IDs for the new entities that were created as a result of the paste operation.
+ * @param {Vec3} position - The position to paste the clipboard contents at.
+ * @returns {Uuid[]} The IDs of the new entities that were created as a result of the paste operation. If entities couldn't
+ * be created then an empty array is returned.
*/
Q_INVOKABLE QVector pasteEntities(glm::vec3 position);
};
From d587f1227d9bfccbf64228bd8f97de8ac825fc98 Mon Sep 17 00:00:00 2001
From: David Rowe
Date: Wed, 10 Apr 2019 12:05:15 +1200
Subject: [PATCH 06/54] Add ResourceRequestObserver JSDoc
---
.../shared/src/ResourceRequestObserver.cpp | 7 ++++
.../shared/src/ResourceRequestObserver.h | 34 ++++++++++++++++++-
2 files changed, 40 insertions(+), 1 deletion(-)
diff --git a/libraries/shared/src/ResourceRequestObserver.cpp b/libraries/shared/src/ResourceRequestObserver.cpp
index 608d6905c5..21f4d56173 100644
--- a/libraries/shared/src/ResourceRequestObserver.cpp
+++ b/libraries/shared/src/ResourceRequestObserver.cpp
@@ -16,6 +16,13 @@
#include
#include
+/**jsdoc
+ * Information about a resource request.
+ * @typedef {object} ResourceRequestObserver.ResourceRequest
+ * @property {string} url - The URL of the resource request.
+ * @property {number} callerId - An ID identifying the request.
+ * @property {string} extra - Extra information about the request.
+ */
void ResourceRequestObserver::update(const QUrl& requestUrl,
const qint64 callerId,
const QString& extra) {
diff --git a/libraries/shared/src/ResourceRequestObserver.h b/libraries/shared/src/ResourceRequestObserver.h
index edf3c617cb..352f01c3a5 100644
--- a/libraries/shared/src/ResourceRequestObserver.h
+++ b/libraries/shared/src/ResourceRequestObserver.h
@@ -16,7 +16,15 @@
#include "DependencyManager.h"
-
+/**jsdoc
+ * The ResourceRequestObserver API provides notifications when an observable resource request is made.
+ *
+ * @namespace ResourceRequestObserver
+ *
+ * @hifi-interface
+ * @hifi-client-entity
+ * @hifi-avatar
+ */
class ResourceRequestObserver : public QObject, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY
@@ -25,5 +33,29 @@ public:
void update(const QUrl& requestUrl, const qint64 callerId = -1, const QString& extra = "");
signals:
+ /**jsdoc
+ * Triggered when an observable resource request is made.
+ * @function ResourceRequestObserver.resourceRequestEvent
+ * @param {ResourceRequestObserver.ResourceRequest} request - Information about the resource request.
+ * @returns {Signal}
+ * @example Report when a particular Clipboard.importEntities() resource request is made.
+ * ResourceRequestObserver.resourceRequestEvent.connect(function (request) {
+ * if (request.callerId === 100) {
+ * print("Resource request: " + JSON.stringify(request));
+ * }
+ * });
+ *
+ * function importEntities() {
+ * var filename = Window.browse("Import entities to clipboard", "", "*.json");
+ * if (filename) {
+ * Clipboard.importEntities(filename, true, 100);
+ * pastedEntities = Clipboard.pasteEntities(Vec3.sum(MyAvatar.position,
+ * Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })));
+ * print("Entities pasted: " + JSON.stringify(pastedEntities));
+ * }
+ * }
+ *
+ * Script.setTimeout(importEntities, 2000);
+ */
void resourceRequestEvent(QVariantMap result);
};
From 5c31fcc920706f403565ed7701bbed0e14f66123 Mon Sep 17 00:00:00 2001
From: Simon Walton
Date: Wed, 10 Apr 2019 23:44:16 -0700
Subject: [PATCH 07/54] Best effort at certificate json
---
assignment-client/src/avatars/MixerAvatar.cpp | 59 +++++++++++++++----
assignment-client/src/avatars/MixerAvatar.h | 1 +
2 files changed, 47 insertions(+), 13 deletions(-)
diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp
index 3757a1367e..19f09f7c36 100644
--- a/assignment-client/src/avatars/MixerAvatar.cpp
+++ b/assignment-client/src/avatars/MixerAvatar.cpp
@@ -11,6 +11,7 @@
#include
#include
+#include
#include
#include
#include
@@ -30,9 +31,11 @@ void MixerAvatar::fetchAvatarFST() {
if (avatarURL.isEmpty()) {
return;
}
+ auto avatarURLString = avatarURL.toDisplayString();
// Match UUID + version
- static const QRegularExpression marketIdRegex
- { "^https://https://metaverse.highfidelity.com/api/v.+/commerce/entity_edition/([-0-9a-z]{36}).*certificate_id=([\\w/+%]+)" };
+ static const QRegularExpression marketIdRegex{
+ "^https://metaverse.highfidelity.com/api/v.+/commerce/entity_edition/([-0-9a-z]{36}).*certificate_id=([\\w/+%]+)"
+ };
auto marketIdMatch = marketIdRegex.match(avatarURL.toDisplayString());
if (marketIdMatch.hasMatch()) {
QMutexLocker certifyLocker(&_avatarCertifyLock);
@@ -59,8 +62,9 @@ void MixerAvatar::fetchAvatarFST() {
}
// TESTING
-static const QString PLANT_CERTID
- { "MEYCIQDxKA62xq/G/x1aWpXyJbGjIHm6SU4ceQu2ljtFRfeu/QIhAKw2uEfLId8sqLfEoErOlvu2UV2wbP3ttrYP1hoZT0Ge" };
+static const QString PLANT_CERTID{
+ "MEYCIQDxKA62xq/G/x1aWpXyJbGjIHm6SU4ceQu2ljtFRfeu/QIhAKw2uEfLId8sqLfEoErOlvu2UV2wbP3ttrYP1hoZT0Ge"
+};
void MixerAvatar::fstRequestComplete() {
ResourceRequest* fstRequest = static_cast(QObject::sender());
@@ -79,7 +83,7 @@ void MixerAvatar::fstRequestComplete() {
_verifyState = staticVerification ? kStaticValidation : kNoncertified;
if (_verifyState == kStaticValidation) {
- static const QString POP_MARKETPLACE_API { "/api/v1/commerce/proof_of_purchase_status/transfer" };
+ static const QString POP_MARKETPLACE_API{ "/api/v1/commerce/proof_of_purchase_status/transfer" };
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
@@ -105,16 +109,14 @@ void MixerAvatar::fstRequestComplete() {
}
} else {
auto jsonData = responseJson["data"];
- if (!jsonData.isUndefined()
- && !jsonData.toObject()["message"].isUndefined()) {
+ if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) {
qCDebug(avatars) << "Certificate Id lookup failed for" << getDisplayName() << ":"
- << jsonData.toObject()["message"].toString();
+ << jsonData.toObject()["message"].toString();
_verifyState = kError;
}
}
networkReply->deleteLater();
});
-
}
}
_avatarRequest->deleteLater();
@@ -128,14 +130,45 @@ bool MixerAvatar::generateFSTHash() {
if (_avatarFSTContents.length() == 0) {
return false;
}
- QCryptographicHash fstHash(QCryptographicHash::Sha224);
- fstHash.addData(_avatarFSTContents);
+ QByteArray hashJson = canonicalJson(_avatarFSTContents);
+ QCryptographicHash fstHash(QCryptographicHash::Sha256);
+ fstHash.addData(hashJson);
_certificateHash = fstHash.result();
return true;
}
bool MixerAvatar::validateFSTHash(const QString& publicKey) {
// Guess we should refactor this stuff into a Authorization namespace ...
- return EntityItemProperties::verifySignature(publicKey, _certificateHash,
- QByteArray::fromBase64(_certificateId.toUtf8()));
+ return EntityItemProperties::verifySignature(publicKey, _certificateHash, QByteArray::fromBase64(_certificateId.toUtf8()));
+}
+
+QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
+ QStringList fstLines = fstFile.split("\n", QString::SkipEmptyParts);
+ static const QString fstKeywordsReg{
+ "(marketplaceID|itemDescription|itemCategories|itemArtist|itemLicenseUrl|limitedRun|itemName|"
+ "filename|texdir|script|editionNumber)"
+ };
+ QRegularExpression fstLineRegExp{ QString("^\\s*") + fstKeywordsReg + "\\s*=\\s*(.*)$" };
+ QStringListIterator fstLineIter(fstLines);
+
+ QJsonObject certifiedItems;
+ QJsonArray scriptsArray;
+ while (fstLineIter.hasNext()) {
+ auto line = fstLineIter.next();
+ auto lineMatch = fstLineRegExp.match(line);
+ if (lineMatch.hasMatch()) {
+ QString key = lineMatch.captured(1);
+ if (key == "limitedRun" || key == "editionNumber") {
+ certifiedItems[key] = QJsonValue(lineMatch.captured(2).toDouble());
+ } else {
+ certifiedItems[key] = QJsonValue(lineMatch.captured(2));
+ }
+ }
+ }
+ if (!scriptsArray.empty()) {
+ certifiedItems["script"] = scriptsArray;
+ }
+ QJsonDocument jsonDocCertifiedItems(certifiedItems);
+ // return R"({"editionNumber":34,"filename":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/Hifi_Toon_Male_3.fbx","itemArtist":"EgyMax","itemCategories":"Avatars","itemDescription":"This is my first avatar. I hope you like it. More will come","itemLicenseUrl":"","itemName":"Bridger","limitedRun":"-1","marketplaceID":"7f142fde-541a-4902-b33a-25fa89dfba21","texdir":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/textures"})";
+ return jsonDocCertifiedItems.toJson(QJsonDocument::Compact);
}
diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h
index 74309db461..d9e6d4259e 100644
--- a/assignment-client/src/avatars/MixerAvatar.h
+++ b/assignment-client/src/avatars/MixerAvatar.h
@@ -44,6 +44,7 @@ private:
bool generateFSTHash();
bool validateFSTHash(const QString& publicKey);
+ static QByteArray canonicalJson(const QString fstFile);
private slots:
void fstRequestComplete();
From b2e54f46e139689b70379f465754d9fb0c33bf6c Mon Sep 17 00:00:00 2001
From: Simon Walton
Date: Thu, 11 Apr 2019 23:33:56 -0700
Subject: [PATCH 08/54] Static FST verification now working
---
assignment-client/src/avatars/MixerAvatar.cpp | 38 ++++++++++---------
assignment-client/src/avatars/MixerAvatar.h | 5 ++-
2 files changed, 24 insertions(+), 19 deletions(-)
diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp
index 19f09f7c36..0e2db965d7 100644
--- a/assignment-client/src/avatars/MixerAvatar.cpp
+++ b/assignment-client/src/avatars/MixerAvatar.cpp
@@ -40,9 +40,7 @@ void MixerAvatar::fetchAvatarFST() {
if (marketIdMatch.hasMatch()) {
QMutexLocker certifyLocker(&_avatarCertifyLock);
_marketplaceIdString = marketIdMatch.captured(1);
- _certificateId = QUrl::fromPercentEncoding(marketIdMatch.captured(2).toUtf8());
- } else {
- _marketplaceIdString = "2119142f-0cd6-4126-b18e-06b53afcc0a9"; // XXX: plants entity, for testing
+ _certificateIdFromURL = QUrl::fromPercentEncoding(marketIdMatch.captured(2).toUtf8());
}
ResourceRequest* fstRequest = resourceManager->createResourceRequest(this, avatarURL);
@@ -61,11 +59,6 @@ void MixerAvatar::fetchAvatarFST() {
}
}
-// TESTING
-static const QString PLANT_CERTID{
- "MEYCIQDxKA62xq/G/x1aWpXyJbGjIHm6SU4ceQu2ljtFRfeu/QIhAKw2uEfLId8sqLfEoErOlvu2UV2wbP3ttrYP1hoZT0Ge"
-};
-
void MixerAvatar::fstRequestComplete() {
ResourceRequest* fstRequest = static_cast(QObject::sender());
QMutexLocker certifyLocker(&_avatarCertifyLock);
@@ -93,7 +86,7 @@ void MixerAvatar::fstRequestComplete() {
networkRequest.setUrl(requestURL);
QJsonObject request;
- request["certificate_id"] = _certificateId;
+ request["certificate_id"] = _certificateIdFromURL;
_verifyState = kRequestingOwner;
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
networkReply->setParent(this);
@@ -139,36 +132,47 @@ bool MixerAvatar::generateFSTHash() {
bool MixerAvatar::validateFSTHash(const QString& publicKey) {
// Guess we should refactor this stuff into a Authorization namespace ...
- return EntityItemProperties::verifySignature(publicKey, _certificateHash, QByteArray::fromBase64(_certificateId.toUtf8()));
+ return EntityItemProperties::verifySignature(publicKey, _certificateHash,
+ QByteArray::fromBase64(_certificateIdFromURL.toUtf8()));
}
QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
QStringList fstLines = fstFile.split("\n", QString::SkipEmptyParts);
static const QString fstKeywordsReg{
"(marketplaceID|itemDescription|itemCategories|itemArtist|itemLicenseUrl|limitedRun|itemName|"
- "filename|texdir|script|editionNumber)"
+ "filename|texdir|script|editionNumber|certificateID)"
};
- QRegularExpression fstLineRegExp{ QString("^\\s*") + fstKeywordsReg + "\\s*=\\s*(.*)$" };
+ QRegularExpression fstLineRegExp{ QString("^\\s*") + fstKeywordsReg + "\\s*=\\s*(\\S.*)$" };
QStringListIterator fstLineIter(fstLines);
QJsonObject certifiedItems;
QJsonArray scriptsArray;
+ QStringList scripts;
while (fstLineIter.hasNext()) {
auto line = fstLineIter.next();
auto lineMatch = fstLineRegExp.match(line);
if (lineMatch.hasMatch()) {
QString key = lineMatch.captured(1);
- if (key == "limitedRun" || key == "editionNumber") {
- certifiedItems[key] = QJsonValue(lineMatch.captured(2).toDouble());
+ if (key == "certificateID") {
+ _certificateIdFromFST = lineMatch.captured(2);
+ } else if (key == "limitedRun" || key == "editionNumber") {
+ double value = lineMatch.captured(2).toDouble();
+ if (value != 0.0) {
+ certifiedItems[key] = QJsonValue(value);
+ }
+ } else if (key == "script") {
+ scripts.append(lineMatch.captured(2));
} else {
certifiedItems[key] = QJsonValue(lineMatch.captured(2));
}
}
}
- if (!scriptsArray.empty()) {
- certifiedItems["script"] = scriptsArray;
+ if (!scripts.empty()) {
+ scripts.sort();
+ certifiedItems["script"] = QJsonArray::fromStringList(scripts);
}
QJsonDocument jsonDocCertifiedItems(certifiedItems);
- // return R"({"editionNumber":34,"filename":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/Hifi_Toon_Male_3.fbx","itemArtist":"EgyMax","itemCategories":"Avatars","itemDescription":"This is my first avatar. I hope you like it. More will come","itemLicenseUrl":"","itemName":"Bridger","limitedRun":"-1","marketplaceID":"7f142fde-541a-4902-b33a-25fa89dfba21","texdir":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/textures"})";
+ //OK - this one works
+ //return R"({"editionNumber":34,"filename":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/Hifi_Toon_Male_3.fbx","itemArtist":"EgyMax","itemCategories":"Avatars","itemDescription":"This is my first avatar. I hope you like it. More will come","itemName":"Bridger","limitedRun":-1,"marketplaceID":"7f142fde-541a-4902-b33a-25fa89dfba21","texdir":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/textures"})";
return jsonDocCertifiedItems.toJson(QJsonDocument::Compact);
}
diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h
index d9e6d4259e..8ac60c334e 100644
--- a/assignment-client/src/avatars/MixerAvatar.h
+++ b/assignment-client/src/avatars/MixerAvatar.h
@@ -39,12 +39,13 @@ private:
QString _marketplaceIdString;
QByteArray _avatarFSTContents;
QByteArray _certificateHash;
- QString _certificateId;
+ QString _certificateIdFromURL;
+ QString _certificateIdFromFST;
bool _avatarFSTValid { false };
bool generateFSTHash();
bool validateFSTHash(const QString& publicKey);
- static QByteArray canonicalJson(const QString fstFile);
+ QByteArray canonicalJson(const QString fstFile);
private slots:
void fstRequestComplete();
From fb8f9e302dd3f54d3e0b709173ec401494c57a88 Mon Sep 17 00:00:00 2001
From: Simon Walton
Date: Mon, 15 Apr 2019 01:11:23 -0700
Subject: [PATCH 09/54] Static verification changes - WIP
---
assignment-client/src/avatars/MixerAvatar.cpp | 32 ++++++++++++++-----
assignment-client/src/avatars/MixerAvatar.h | 11 ++++---
2 files changed, 31 insertions(+), 12 deletions(-)
diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp
index 0e2db965d7..a4f646edf6 100644
--- a/assignment-client/src/avatars/MixerAvatar.cpp
+++ b/assignment-client/src/avatars/MixerAvatar.cpp
@@ -25,12 +25,16 @@
#include "AvatarLogging.h"
void MixerAvatar::fetchAvatarFST() {
- _avatarFSTValid = false;
+ _verifyState = kNoncertified;
+ _certificateIdFromURL.clear();
+ _certificateIdFromFST.clear();
+ _marketplaceIdString.clear();
auto resourceManager = DependencyManager::get();
QUrl avatarURL = getSkeletonModelURL();
if (avatarURL.isEmpty()) {
return;
}
+
auto avatarURLString = avatarURL.toDisplayString();
// Match UUID + version
static const QRegularExpression marketIdRegex{
@@ -56,6 +60,7 @@ void MixerAvatar::fetchAvatarFST() {
fstRequest->send();
} else {
qCDebug(avatars) << "Couldn't create FST request for" << avatarURL;
+ _verifyState = kError;
}
}
@@ -73,7 +78,7 @@ void MixerAvatar::fstRequestComplete() {
generateFSTHash();
QString& marketplacePublicKey = EntityItem::_marketplacePublicKey;
bool staticVerification = validateFSTHash(marketplacePublicKey);
- _verifyState = staticVerification ? kStaticValidation : kNoncertified;
+ _verifyState = staticVerification ? kStaticValidation : kVerificationFailed;
if (_verifyState == kStaticValidation) {
static const QString POP_MARKETPLACE_API{ "/api/v1/commerce/proof_of_purchase_status/transfer" };
@@ -86,7 +91,7 @@ void MixerAvatar::fstRequestComplete() {
networkRequest.setUrl(requestURL);
QJsonObject request;
- request["certificate_id"] = _certificateIdFromURL;
+ request["certificate_id"] = _certificateIdFromFST;
_verifyState = kRequestingOwner;
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
networkReply->setParent(this);
@@ -96,6 +101,8 @@ void MixerAvatar::fstRequestComplete() {
QJsonDocument responseJson = QJsonDocument::fromJson(responseString.toUtf8());
if (networkReply->error() == QNetworkReply::NoError) {
QMutexLocker certifyLocker(&_avatarCertifyLock);
+ // TODO: move processing to slave thread.
+ _verifyState = kOwnerResponse;
if (responseJson["status"].toString() == "success") {
auto jsonData = responseJson["data"];
// owner, owner key?
@@ -110,6 +117,9 @@ void MixerAvatar::fstRequestComplete() {
}
networkReply->deleteLater();
});
+ } else {
+ _verifyState = kVerificationFailed;
+ qCDebug(avatars) << "Avatar" << getDisplayName() << "FAILED static certification";
}
}
_avatarRequest->deleteLater();
@@ -133,7 +143,7 @@ bool MixerAvatar::generateFSTHash() {
bool MixerAvatar::validateFSTHash(const QString& publicKey) {
// Guess we should refactor this stuff into a Authorization namespace ...
return EntityItemProperties::verifySignature(publicKey, _certificateHash,
- QByteArray::fromBase64(_certificateIdFromURL.toUtf8()));
+ QByteArray::fromBase64(_certificateIdFromFST.toUtf8()));
}
QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
@@ -146,7 +156,6 @@ QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
QStringListIterator fstLineIter(fstLines);
QJsonObject certifiedItems;
- QJsonArray scriptsArray;
QStringList scripts;
while (fstLineIter.hasNext()) {
auto line = fstLineIter.next();
@@ -155,15 +164,22 @@ QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
QString key = lineMatch.captured(1);
if (key == "certificateID") {
_certificateIdFromFST = lineMatch.captured(2);
+ } else if (key == "itemDescription") {
+ // Item description can be multiline - intermediate lines end in
+ QString itemDesc = lineMatch.captured(2);
+ while (itemDesc.endsWith('\r') && fstLineIter.hasNext()) {
+ itemDesc += '\n' + fstLineIter.next();
+ }
+ certifiedItems[key] = QJsonValue(itemDesc);
} else if (key == "limitedRun" || key == "editionNumber") {
double value = lineMatch.captured(2).toDouble();
if (value != 0.0) {
certifiedItems[key] = QJsonValue(value);
}
} else if (key == "script") {
- scripts.append(lineMatch.captured(2));
+ scripts.append(lineMatch.captured(2).trimmed());
} else {
- certifiedItems[key] = QJsonValue(lineMatch.captured(2));
+ certifiedItems[key] = QJsonValue(lineMatch.captured(2).trimmed());
}
}
}
@@ -172,7 +188,7 @@ QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
certifiedItems["script"] = QJsonArray::fromStringList(scripts);
}
QJsonDocument jsonDocCertifiedItems(certifiedItems);
- //OK - this one works
+ //Example working form:
//return R"({"editionNumber":34,"filename":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/Hifi_Toon_Male_3.fbx","itemArtist":"EgyMax","itemCategories":"Avatars","itemDescription":"This is my first avatar. I hope you like it. More will come","itemName":"Bridger","limitedRun":-1,"marketplaceID":"7f142fde-541a-4902-b33a-25fa89dfba21","texdir":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/textures"})";
return jsonDocCertifiedItems.toJson(QJsonDocument::Compact);
}
diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h
index 8ac60c334e..08c632a131 100644
--- a/assignment-client/src/avatars/MixerAvatar.h
+++ b/assignment-client/src/avatars/MixerAvatar.h
@@ -15,6 +15,8 @@
#ifndef hifi_MixerAvatar_h
#define hifi_MixerAvatar_h
+#include
+
#include
class ResourceRequest;
@@ -25,13 +27,14 @@ public:
void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; }
void fetchAvatarFST();
+ bool isCertifyFailed() const { return _verifyState == kVerificationFailed; }
private:
bool _needsHeroCheck{ false };
- // Avatar certification/verification
- enum VerifyState { kNoncertified, kRequestingFST, kReceivedFST, kStaticValidation, kRequestingOwner, kChallengeClient, kVerified,
- kVerificationFailed, kVerificationSucceeded, kError };
+ // Avatar certification/verification:
+ enum VerifyState { kNoncertified, kRequestingFST, kReceivedFST, kStaticValidation, kRequestingOwner, kOwnerResponse,
+ kChallengeClient, kVerified, kVerificationFailed, kVerificationSucceeded, kError };
Q_ENUM(VerifyState);
VerifyState _verifyState { kNoncertified };
QMutex _avatarCertifyLock;
@@ -41,7 +44,7 @@ private:
QByteArray _certificateHash;
QString _certificateIdFromURL;
QString _certificateIdFromFST;
- bool _avatarFSTValid { false };
+ QJsonDocument _dynamicMarketResponse;
bool generateFSTHash();
bool validateFSTHash(const QString& publicKey);
From 1e7669dd0c86773e4381be9aeb484ae45b75a85b Mon Sep 17 00:00:00 2001
From: Simon Walton
Date: Tue, 16 Apr 2019 18:28:14 -0700
Subject: [PATCH 10/54] Static verification working
---
.../src/avatars/AvatarMixerClientData.cpp | 4 +
assignment-client/src/avatars/MixerAvatar.cpp | 74 ++++++++++++++-----
assignment-client/src/avatars/MixerAvatar.h | 7 +-
3 files changed, 64 insertions(+), 21 deletions(-)
diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp
index 3eff0d90f9..97b2cabe9a 100644
--- a/assignment-client/src/avatars/AvatarMixerClientData.cpp
+++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp
@@ -81,6 +81,10 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
}
assert(_packetQueue.empty());
+ if (_avatar) {
+ _avatar->processCertifyEvents();
+ }
+
return packetsProcessed;
}
diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp
index a4f646edf6..46c98b7532 100644
--- a/assignment-client/src/avatars/MixerAvatar.cpp
+++ b/assignment-client/src/avatars/MixerAvatar.cpp
@@ -28,7 +28,7 @@ void MixerAvatar::fetchAvatarFST() {
_verifyState = kNoncertified;
_certificateIdFromURL.clear();
_certificateIdFromFST.clear();
- _marketplaceIdString.clear();
+ _marketplaceIdFromURL.clear();
auto resourceManager = DependencyManager::get();
QUrl avatarURL = getSkeletonModelURL();
if (avatarURL.isEmpty()) {
@@ -38,13 +38,15 @@ void MixerAvatar::fetchAvatarFST() {
auto avatarURLString = avatarURL.toDisplayString();
// Match UUID + version
static const QRegularExpression marketIdRegex{
- "^https://metaverse.highfidelity.com/api/v.+/commerce/entity_edition/([-0-9a-z]{36}).*certificate_id=([\\w/+%]+)"
+ "^https://metaverse.highfidelity.com/api/v.+/commerce/entity_edition/([-0-9a-z]{36}).*?(certificate_id=([\\w/+%]+)).*$"
};
auto marketIdMatch = marketIdRegex.match(avatarURL.toDisplayString());
if (marketIdMatch.hasMatch()) {
QMutexLocker certifyLocker(&_avatarCertifyLock);
- _marketplaceIdString = marketIdMatch.captured(1);
- _certificateIdFromURL = QUrl::fromPercentEncoding(marketIdMatch.captured(2).toUtf8());
+ _marketplaceIdFromURL = marketIdMatch.captured(1);
+ if (marketIdMatch.lastCapturedIndex() == 3) {
+ _certificateIdFromURL = QUrl::fromPercentEncoding(marketIdMatch.captured(3).toUtf8());
+ }
}
ResourceRequest* fstRequest = resourceManager->createResourceRequest(this, avatarURL);
@@ -96,21 +98,14 @@ void MixerAvatar::fstRequestComplete() {
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
networkReply->setParent(this);
connect(networkReply, &QNetworkReply::finished, [this, networkReply]() {
- QString responseString = networkReply->readAll();
- qCDebug(avatars) << "Marketplace response for avatar" << getDisplayName() << ":" << responseString;
- QJsonDocument responseJson = QJsonDocument::fromJson(responseString.toUtf8());
+ QMutexLocker certifyLocker(&_avatarCertifyLock);
if (networkReply->error() == QNetworkReply::NoError) {
- QMutexLocker certifyLocker(&_avatarCertifyLock);
- // TODO: move processing to slave thread.
+ _dynamicMarketResponse = networkReply->readAll();
_verifyState = kOwnerResponse;
- if (responseJson["status"].toString() == "success") {
- auto jsonData = responseJson["data"];
- // owner, owner key?
- }
} else {
- auto jsonData = responseJson["data"];
+ auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"];
if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) {
- qCDebug(avatars) << "Certificate Id lookup failed for" << getDisplayName() << ":"
+ qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":"
<< jsonData.toObject()["message"].toString();
_verifyState = kError;
}
@@ -179,7 +174,7 @@ QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
} else if (key == "script") {
scripts.append(lineMatch.captured(2).trimmed());
} else {
- certifiedItems[key] = QJsonValue(lineMatch.captured(2).trimmed());
+ certifiedItems[key] = QJsonValue(lineMatch.captured(2));
}
}
}
@@ -187,8 +182,53 @@ QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
scripts.sort();
certifiedItems["script"] = QJsonArray::fromStringList(scripts);
}
+
QJsonDocument jsonDocCertifiedItems(certifiedItems);
//Example working form:
- //return R"({"editionNumber":34,"filename":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/Hifi_Toon_Male_3.fbx","itemArtist":"EgyMax","itemCategories":"Avatars","itemDescription":"This is my first avatar. I hope you like it. More will come","itemName":"Bridger","limitedRun":-1,"marketplaceID":"7f142fde-541a-4902-b33a-25fa89dfba21","texdir":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/textures"})";
+ //return R"({"editionNumber":34,"filename":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/Hifi_Toon_Male_3.fbx","itemArtist":"EgyMax",
+ //"itemCategories":"Avatars","itemDescription":"This is my first avatar. I hope you like it. More will come","itemName":"Bridger","limitedRun":-1,
+ //"marketplaceID":"7f142fde-541a-4902-b33a-25fa89dfba21","texdir":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/textures"})";
return jsonDocCertifiedItems.toJson(QJsonDocument::Compact);
}
+
+void MixerAvatar::processCertifyEvents() {
+ QMutexLocker certifyLocker(&_avatarCertifyLock);
+ if (_verifyState != kOwnerResponse) {
+ return;
+ }
+
+ switch (_verifyState) {
+ case kOwnerResponse:
+ {
+ QJsonDocument responseJson = QJsonDocument::fromJson(_dynamicMarketResponse.toUtf8());
+ _verifyState = kChallengeClient;
+ QString ownerPublicKey;
+ bool ownerValid = false;
+ qCDebug(avatars) << "Marketplace response for avatar" << getDisplayName() << ":" << _dynamicMarketResponse;
+ if (responseJson["status"].toString() == "success") {
+ QJsonValue jsonData = responseJson["data"];
+ if (jsonData.isObject()) {
+ auto ownerJson = jsonData["transfer_recipient_key"];
+ if (ownerJson.isString()) {
+ ownerPublicKey = ownerJson.toString();
+ }
+ auto transferStatusJson = jsonData["transfer_status"];
+ if (transferStatusJson.isArray() && transferStatusJson.toArray()[0].toString() == "confirmed") {
+ ownerValid = true;
+ }
+ }
+ if (ownerValid && !ownerPublicKey.isEmpty()) {
+ // Challenge owner ...
+ } else {
+ _verifyState = kError;
+ }
+ } else {
+ qCDebug(avatars) << "Get owner status failed for " << getDisplayName() << _marketplaceIdFromURL <<
+ "message:" << responseJson["message"].toString();
+ _verifyState = kError;
+ }
+ break;
+ }
+
+ } // close switch
+}
diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h
index 08c632a131..e0b7b14429 100644
--- a/assignment-client/src/avatars/MixerAvatar.h
+++ b/assignment-client/src/avatars/MixerAvatar.h
@@ -15,8 +15,6 @@
#ifndef hifi_MixerAvatar_h
#define hifi_MixerAvatar_h
-#include
-
#include
class ResourceRequest;
@@ -28,6 +26,7 @@ public:
void fetchAvatarFST();
bool isCertifyFailed() const { return _verifyState == kVerificationFailed; }
+ void processCertifyEvents();
private:
bool _needsHeroCheck{ false };
@@ -39,12 +38,12 @@ private:
VerifyState _verifyState { kNoncertified };
QMutex _avatarCertifyLock;
ResourceRequest* _avatarRequest { nullptr };
- QString _marketplaceIdString;
+ QString _marketplaceIdFromURL;
QByteArray _avatarFSTContents;
QByteArray _certificateHash;
QString _certificateIdFromURL;
QString _certificateIdFromFST;
- QJsonDocument _dynamicMarketResponse;
+ QString _dynamicMarketResponse;
bool generateFSTHash();
bool validateFSTHash(const QString& publicKey);
From a3cd5ad3c5b45a5f8577adfcb1aeb540249f2515 Mon Sep 17 00:00:00 2001
From: Simon Walton
Date: Wed, 17 Apr 2019 18:33:30 -0700
Subject: [PATCH 11/54] Avatar-owner challenge now working
---
assignment-client/src/avatars/AvatarMixer.cpp | 11 +++
assignment-client/src/avatars/AvatarMixer.h | 1 +
assignment-client/src/avatars/MixerAvatar.cpp | 82 +++++++++++++++++--
assignment-client/src/avatars/MixerAvatar.h | 8 +-
4 files changed, 95 insertions(+), 7 deletions(-)
diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp
index 9816cebf43..b804e4a20f 100644
--- a/assignment-client/src/avatars/AvatarMixer.cpp
+++ b/assignment-client/src/avatars/AvatarMixer.cpp
@@ -82,6 +82,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket");
packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase },
this, "handleOctreePacket");
+ packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "handleChallengeOwnership");
packetReceiver.registerListenerForTypes({
PacketType::ReplicatedAvatarIdentity,
@@ -1123,6 +1124,16 @@ void AvatarMixer::entityChange() {
_dirtyHeroStatus = true;
}
+void AvatarMixer::handleChallengeOwnership(QSharedPointer message, SharedNodePointer senderNode) {
+ if (senderNode->getType() == NodeType::Agent && senderNode->getLinkedData()) {
+ auto clientData = static_cast(senderNode->getLinkedData());
+ auto avatar = clientData->getAvatarSharedPointer();
+ if (avatar) {
+ avatar->handleChallengeResponse(message.data());
+ }
+ }
+}
+
void AvatarMixer::aboutToFinish() {
DependencyManager::destroy();
DependencyManager::destroy();
diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h
index 10dff5e8a4..93dc755f51 100644
--- a/assignment-client/src/avatars/AvatarMixer.h
+++ b/assignment-client/src/avatars/AvatarMixer.h
@@ -65,6 +65,7 @@ private slots:
void domainSettingsRequestComplete();
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
void handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode);
+ void handleChallengeOwnership(QSharedPointer message, SharedNodePointer senderNode);
void start();
private:
diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp
index 46c98b7532..8165a7d91d 100644
--- a/assignment-client/src/avatars/MixerAvatar.cpp
+++ b/assignment-client/src/avatars/MixerAvatar.cpp
@@ -24,6 +24,15 @@
#include "MixerAvatar.h"
#include "AvatarLogging.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+
void MixerAvatar::fetchAvatarFST() {
_verifyState = kNoncertified;
_certificateIdFromURL.clear();
@@ -137,17 +146,17 @@ bool MixerAvatar::generateFSTHash() {
bool MixerAvatar::validateFSTHash(const QString& publicKey) {
// Guess we should refactor this stuff into a Authorization namespace ...
- return EntityItemProperties::verifySignature(publicKey, _certificateHash,
- QByteArray::fromBase64(_certificateIdFromFST.toUtf8()));
+return EntityItemProperties::verifySignature(publicKey, _certificateHash,
+ QByteArray::fromBase64(_certificateIdFromFST.toUtf8()));
}
QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
QStringList fstLines = fstFile.split("\n", QString::SkipEmptyParts);
- static const QString fstKeywordsReg{
+ static const QString fstKeywordsReg {
"(marketplaceID|itemDescription|itemCategories|itemArtist|itemLicenseUrl|limitedRun|itemName|"
"filename|texdir|script|editionNumber|certificateID)"
};
- QRegularExpression fstLineRegExp{ QString("^\\s*") + fstKeywordsReg + "\\s*=\\s*(\\S.*)$" };
+ QRegularExpression fstLineRegExp { QString("^\\s*") + fstKeywordsReg + "\\s*=\\s*(\\S.*)$" };
QStringListIterator fstLineIter(fstLines);
QJsonObject certifiedItems;
@@ -193,7 +202,7 @@ QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
void MixerAvatar::processCertifyEvents() {
QMutexLocker certifyLocker(&_avatarCertifyLock);
- if (_verifyState != kOwnerResponse) {
+ if (_verifyState != kOwnerResponse && _verifyState != kChallengeResponse) {
return;
}
@@ -218,7 +227,10 @@ void MixerAvatar::processCertifyEvents() {
}
}
if (ownerValid && !ownerPublicKey.isEmpty()) {
- // Challenge owner ...
+ _ownerPublicKey = "-----BEGIN PUBLIC KEY-----\n"
+ + ownerPublicKey
+ + "\n-----END PUBLIC KEY-----\n";
+ challengeOwner();
} else {
_verifyState = kError;
}
@@ -230,5 +242,63 @@ void MixerAvatar::processCertifyEvents() {
break;
}
+ case kChallengeResponse:
+ {
+ int avatarIDLength;
+ int signedNonceLength;
+ if (_challengeResponse.length() < 8) {
+ _verifyState = kError;
+ break;
+ }
+
+ QDataStream responseStream(_challengeResponse);
+ responseStream.setByteOrder(QDataStream::LittleEndian);
+ responseStream >> avatarIDLength >> signedNonceLength;
+ QByteArray avatarID(_challengeResponse.data() + 2 * sizeof(int), avatarIDLength);
+ QByteArray signedNonce(_challengeResponse.data() + 2 * sizeof(int) + avatarIDLength, signedNonceLength);
+ QCryptographicHash nonceHash(QCryptographicHash::Sha256);
+ nonceHash.addData(_challengeNonce);
+
+ bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, nonceHash.result(),
+ QByteArray::fromBase64(signedNonce));
+ _verifyState = challengeResult ? kVerificationSucceeded : kVerificationFailed;
+ if (_verifyState == kVerificationFailed) {
+ qCDebug(avatars) << "Dynamic verification FAILED for " << getDisplayName() << getSessionUUID();
+ }
+ }
+
} // close switch
}
+
+void MixerAvatar::challengeOwner() {
+ auto nodeList = DependencyManager::get();
+ QByteArray avatarID = ("{" + _marketplaceIdFromURL + "}").toUtf8();
+ QByteArray nonce = QUuid::createUuid().toByteArray();
+
+ auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership,
+ 2 * sizeof(int) + nonce.length() + avatarID.length(), true);
+ challengeOwnershipPacket->writePrimitive(avatarID.length());
+ challengeOwnershipPacket->writePrimitive(nonce.length());
+ challengeOwnershipPacket->write(avatarID);
+ challengeOwnershipPacket->write(nonce);
+
+ nodeList->sendPacket(std::move(challengeOwnershipPacket), *(nodeList->nodeWithUUID(getSessionUUID())) );
+ _challengeNonce = nonce;
+
+ static constexpr int CHALLENGE_TIMEOUT_MS = 10 * 1000; // 10 s
+ _challengeTimeout.setInterval(CHALLENGE_TIMEOUT_MS);
+ _challengeTimeout.connect(&_challengeTimeout, &QTimer::timeout, [this]() {
+ _verifyState = kVerificationFailed;
+ });
+}
+
+void MixerAvatar::handleChallengeResponse(ReceivedMessage * response) {
+ QByteArray avatarID;
+ QByteArray encryptedNonce;
+ QMutexLocker certifyLocker(&_avatarCertifyLock);
+ if (_verifyState == kChallengeClient) {
+ _challengeTimeout.stop();
+ _challengeResponse = response->readAll();
+ _verifyState = kChallengeResponse;
+ }
+}
diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h
index e0b7b14429..57896b2876 100644
--- a/assignment-client/src/avatars/MixerAvatar.h
+++ b/assignment-client/src/avatars/MixerAvatar.h
@@ -27,13 +27,14 @@ public:
void fetchAvatarFST();
bool isCertifyFailed() const { return _verifyState == kVerificationFailed; }
void processCertifyEvents();
+ void handleChallengeResponse(ReceivedMessage * response);
private:
bool _needsHeroCheck{ false };
// Avatar certification/verification:
enum VerifyState { kNoncertified, kRequestingFST, kReceivedFST, kStaticValidation, kRequestingOwner, kOwnerResponse,
- kChallengeClient, kVerified, kVerificationFailed, kVerificationSucceeded, kError };
+ kChallengeClient, kChallengeResponse, kVerified, kVerificationFailed, kVerificationSucceeded, kError };
Q_ENUM(VerifyState);
VerifyState _verifyState { kNoncertified };
QMutex _avatarCertifyLock;
@@ -44,10 +45,15 @@ private:
QString _certificateIdFromURL;
QString _certificateIdFromFST;
QString _dynamicMarketResponse;
+ QString _ownerPublicKey;
+ QByteArray _challengeNonce;
+ QByteArray _challengeResponse;
+ QTimer _challengeTimeout;
bool generateFSTHash();
bool validateFSTHash(const QString& publicKey);
QByteArray canonicalJson(const QString fstFile);
+ void challengeOwner();
private slots:
void fstRequestComplete();
From 6837d7e5e8a269bd5d229f12e0a3447408b5a149 Mon Sep 17 00:00:00 2001
From: David Rowe
Date: Fri, 19 Apr 2019 12:26:21 +1200
Subject: [PATCH 12/54] Midi JSDoc
---
libraries/midi/src/Midi.cpp | 55 ++++++++++++++++++
libraries/midi/src/Midi.h | 112 ++++++++++++++++++++++++------------
2 files changed, 130 insertions(+), 37 deletions(-)
diff --git a/libraries/midi/src/Midi.cpp b/libraries/midi/src/Midi.cpp
index 1f190111f2..964fd4419a 100644
--- a/libraries/midi/src/Midi.cpp
+++ b/libraries/midi/src/Midi.cpp
@@ -261,6 +261,61 @@ void Midi::MidiCleanup() {
}
#endif
+/**jsdoc
+ * A MIDI message.
+ * Warning: The status property is NOT a MIDI status value.
+ * @typedef {object} Midi.MidiMessage
+ * @property {number} device - Device number.
+ * @property {Midi.RawMidiMessage} raw - Raw MIDI message - {@link Midi.RawMidiMessage}.
+ * @property {number} status - Channel + status. Legacy value.
+ * @property {number} channel - Channel: 1 – 16 .
+ * @property {number} type - Status: {@link Midi.MidiStatus}; 8 – 15 .
+ * @property {number} note - Note: 0 – 127 .
+ * @property {number} velocity - Note velocity: 0 – 127 . (0 means "note off".)
+ * @property {number} bend - Pitch bend: -8192 – 8191 .
+ * @property {number} program - Program change: 0 – 127 .
+ */
+/**jsdoc
+ * An integer DWORD (unsigned 32 bit) message with bits having values as follows:
+ *
+ *
+ *
+ * 00000000 |
+ * 0vvvvvvv |
+ * 0nnnnnnn |
+ * 1sss |
+ * cccc |
+ *
+ *
+ * Where:
+ *
+ * v = Velocity.
+ * n = Note.
+ * s = Status - {@link Midi.MidiStatus}
+ * c = Channel.
+ *
+ * The number in the first bit of each byte denotes whether it is a command (1) or data (0).
+ * @typedef {number} Midi.RawMidiMessage
+ */
+/**jsdoc
+ * A MIDI status value. The following MIDI status values are supported:
+ *
+ *
+ * Value | Description |
+ *
+ *
+ * 8 | Note off. |
+ * 9 | Note on. |
+ * 10 | Polyphonic key pressure. |
+ * 11 | Control change. |
+ * 12 | Program change. |
+ * 13 | Channel pressure. |
+ * 14 | Pitch bend. |
+ * 15 | System message. |
+ *
+ *
+ * @typedef {number} Midi.MidiStatus
+ */
void Midi::midiReceived(int device, int raw, int channel, int status, int type, int note, int velocity, int bend, int program) {
QVariantMap eventData;
eventData["device"] = device;
diff --git a/libraries/midi/src/Midi.h b/libraries/midi/src/Midi.h
index 081a44f7b6..57c6962b48 100644
--- a/libraries/midi/src/Midi.h
+++ b/libraries/midi/src/Midi.h
@@ -21,6 +21,12 @@
#include
/**jsdoc
+ * The Midi API provides the ability to connect Interface with musical instruments and other external or virtual
+ * devices via the MIDI protocol. For further information and examples, see the tutorial:
+ * Use MIDI to Control Your Environment.
+ *
+ * Note: Only works on Windows.
+ *
* @namespace Midi
*
* @hifi-interface
@@ -49,88 +55,112 @@ private:
void MidiCleanup();
signals:
- void midiNote(QVariantMap eventData);
- void midiMessage(QVariantMap eventData);
- void midiReset();
-
- public slots:
/**jsdoc
- * Send Raw MIDI packet to a particular device.
+ * Triggered when a connected device sends an output.
+ * @function Midi.midiNote
+ * @param {MidiMessage} message - The MIDI message.
+ * @returns {Signal}
+ * @deprecated Use {@link Midi.midiMessage|midiMessage} instead.
+ */
+ void midiNote(QVariantMap eventData);
+
+ /**jsdoc
+ * Triggered when a connected device sends an output.
+ * @function Midi.midiMessage
+ * @param {MidiMessage} message - The MIDI message.
+ * @returns {Signal}
+ */
+ void midiMessage(QVariantMap eventData);
+
+ /**jsdoc
+ * Triggered when the system detects there was a reset such as when a device is plugged in or unplugged.
+ * @function Midi.midiReset
+ * @returns {Signal}
+ */
+ void midiReset();
+
+public slots:
+
+ /**jsdoc
+ * Sends a raw MIDI packet to a particular device.
* @function Midi.sendRawDword
* @param {number} device - Integer device number.
- * @param {number} raw - Integer (DWORD) raw MIDI message.
+ * @param {Midi.RawMidiMessage} raw - Raw MIDI message.
*/
Q_INVOKABLE void sendRawDword(int device, int raw);
/**jsdoc
- * Send MIDI message to a particular device.
+ * Sends a MIDI message to a particular device.
* @function Midi.sendMidiMessage
* @param {number} device - Integer device number.
* @param {number} channel - Integer channel number.
- * @param {number} type - 0x8 is note off, 0x9 is note on (if velocity=0, note off), etc.
- * @param {number} note - MIDI note number.
- * @param {number} velocity - Note velocity (0 means note off).
+ * @param {Midi.MidiStatus} type - Integer status value.
+ * @param {number} note - Note number.
+ * @param {number} velocity - Note velocity. (0 means "note off".)
+ * @comment The "type" parameter has that name to match up with {@link Midi.MidiMessage}.
*/
Q_INVOKABLE void sendMidiMessage(int device, int channel, int type, int note, int velocity);
/**jsdoc
- * Play a note on all connected devices.
+ * Plays a note on all connected devices.
* @function Midi.playMidiNote
- * @param {number} status - 0x80 is note off, 0x90 is note on (if velocity=0, note off), etc.
- * @param {number} note - MIDI note number.
- * @param {number} velocity - Note velocity (0 means note off).
+ * @param {MidiStatus} status - Note status.
+ * @param {number} note - Note number.
+ * @param {number} velocity - Note velocity. (0 means "note off".)
*/
Q_INVOKABLE void playMidiNote(int status, int note, int velocity);
/**jsdoc
- * Turn off all notes on all connected devices.
+ * Turns off all notes on all connected MIDI devices.
* @function Midi.allNotesOff
*/
Q_INVOKABLE void allNotesOff();
/**jsdoc
- * Clean up and re-discover attached devices.
+ * Cleans up and rediscovers attached MIDI devices.
* @function Midi.resetDevices
*/
Q_INVOKABLE void resetDevices();
/**jsdoc
- * Get a list of inputs/outputs.
+ * Gets a list of MIDI input or output devices.
* @function Midi.listMidiDevices
- * @param {boolean} output
+ * @param {boolean} output - true to list output devices, false to list input devices.
* @returns {string[]}
*/
Q_INVOKABLE QStringList listMidiDevices(bool output);
/**jsdoc
- * Block an input/output by name.
+ * Blocks a MIDI device's input or output.
* @function Midi.blockMidiDevice
- * @param {string} name
- * @param {boolean} output
+ * @param {string} name - The name of the MIDI device to block.
+ * @param {boolean} output - true to block the device's output, false to block its input.
*/
Q_INVOKABLE void blockMidiDevice(QString name, bool output);
/**jsdoc
- * Unblock an input/output by name.
+ * Unblocks a MIDI device's input or output.
* @function Midi.unblockMidiDevice
- * @param {string} name
- * @param {boolean} output
+ * @param {string} name- The name of the MIDI device to unblock.
+ * @param {boolean} output - true to block the device's output, false to block its input.
*/
Q_INVOKABLE void unblockMidiDevice(QString name, bool output);
/**jsdoc
- * Repeat all incoming notes to all outputs (default disabled).
+ * Enables or disables repeating all incoming notes to all outputs. (Default is disabled.)
* @function Midi.thruModeEnable
- * @param {boolean} enable
+ * @param {boolean} enable - true to enable repeating all incoming notes to all output, false to
+ * disable.
*/
Q_INVOKABLE void thruModeEnable(bool enable);
/**jsdoc
- * Broadcast on all unblocked devices.
+ * Enables or disables broadcasts to all unblocked devices.
* @function Midi.broadcastEnable
- * @param {boolean} enable
+ * @param {boolean} enable - true to have "send" functions broadcast to all devices, false to
+ * have them send to specific output devices.
*/
Q_INVOKABLE void broadcastEnable(bool enable);
@@ -138,50 +168,58 @@ signals:
/// filter by event types
/**jsdoc
+ * Enables or disables note off events.
* @function Midi.typeNoteOffEnable
- * @param {boolean} enable
+ * @param {boolean} enable - true to enable, false to disable.
*/
Q_INVOKABLE void typeNoteOffEnable(bool enable);
/**jsdoc
+ * Enables or disabled note on events.
* @function Midi.typeNoteOnEnable
- * @param {boolean} enable
+ * @param {boolean} enable - true to enable, false to disable.
*/
Q_INVOKABLE void typeNoteOnEnable(bool enable);
/**jsdoc
+ * Enables or disabled ply key pressure events.
* @function Midi.typePolyKeyPressureEnable
- * @param {boolean} enable
+ * @param {boolean} enable - true to enable, false to disable.
*/
Q_INVOKABLE void typePolyKeyPressureEnable(bool enable);
/**jsdoc
+ * Enables or disabled control change events.
* @function Midi.typeControlChangeEnable
- * @param {boolean} enable
+ * @param {boolean} enable - true to enable, false to disable.
*/
Q_INVOKABLE void typeControlChangeEnable(bool enable);
/**jsdoc
+ * Enables or disabled program change events.
* @function Midi.typeProgramChangeEnable
- * @param {boolean} enable
+ * @param {boolean} enable - true to enable, false to disable.
*/
Q_INVOKABLE void typeProgramChangeEnable(bool enable);
/**jsdoc
+ * Enables or disables channel pressure events.
* @function Midi.typeChanPressureEnable
- * @param {boolean} enable
+ * @param {boolean} enable - true to enable, false to disable.
*/
Q_INVOKABLE void typeChanPressureEnable(bool enable);
/**jsdoc
+ * Enables or disabled pitch bend events.
* @function Midi.typePitchBendEnable
- * @param {boolean} enable
+ * @param {boolean} enable - true to enable, false to disable.
*/
Q_INVOKABLE void typePitchBendEnable(bool enable);
/**jsdoc
+ * Enables or disables system message events.
* @function Midi.typeSystemMessageEnable
- * @param {boolean} enable
+ * @param {boolean} enable - true to enable, false to disable.
*/
Q_INVOKABLE void typeSystemMessageEnable(bool enable);
From 703a4a570028adec099ab99c6d0d78b0942f8379 Mon Sep 17 00:00:00 2001
From: SamGondelman
Date: Fri, 19 Apr 2019 14:53:23 -0700
Subject: [PATCH 13/54] call leaveEntity when script stops
---
.../src/EntityTreeRenderer.cpp | 59 ++++++++++---------
.../src/EntityTreeRenderer.h | 5 +-
2 files changed, 35 insertions(+), 29 deletions(-)
diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp
index 6cfff7bc41..7d130125b3 100644
--- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp
+++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp
@@ -205,8 +205,11 @@ void EntityTreeRenderer::stopDomainAndNonOwnedEntities() {
foreach (const EntityItemID& entityID, entitiesWithEntityScripts) {
EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID);
- if (entityItem) {
+ if (entityItem && !entityItem->getScript().isEmpty()) {
if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) {
+ if (entityItem->contains(_avatarPosition)) {
+ _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
+ }
_entitiesScriptEngine->unloadEntityScript(entityID, true);
}
}
@@ -534,7 +537,7 @@ void EntityTreeRenderer::handleSpaceUpdate(std::pair proxyUp
_spaceUpdates.emplace_back(proxyUpdate.first, proxyUpdate.second);
}
-bool EntityTreeRenderer::findBestZoneAndMaybeContainingEntities(QVector* entitiesContainingAvatar) {
+bool EntityTreeRenderer::findBestZoneAndMaybeContainingEntities(QSet& entitiesContainingAvatar) {
bool didUpdate = false;
float radius = 0.01f; // for now, assume 0.01 meter radius, because we actually check the point inside later
QVector entityIDs;
@@ -580,9 +583,7 @@ bool EntityTreeRenderer::findBestZoneAndMaybeContainingEntities(QVectorgetEntityItemID();
- }
+ entitiesContainingAvatar << entity->getEntityItemID();
}
}
}
@@ -616,36 +617,36 @@ bool EntityTreeRenderer::checkEnterLeaveEntities() {
auto movedEnough = glm::distance(avatarPosition, _avatarPosition) > ZONE_CHECK_DISTANCE;
auto enoughTimeElapsed = (now - _lastZoneCheck) > ZONE_CHECK_INTERVAL;
- if (movedEnough || enoughTimeElapsed) {
+ if (_forceRecheckEntities || movedEnough || enoughTimeElapsed) {
_avatarPosition = avatarPosition;
_lastZoneCheck = now;
- QVector entitiesContainingAvatar;
- didUpdate = findBestZoneAndMaybeContainingEntities(&entitiesContainingAvatar);
-
+ _forceRecheckEntities = false;
+
+ QSet entitiesContainingAvatar;
+ didUpdate = findBestZoneAndMaybeContainingEntities(entitiesContainingAvatar);
+
// Note: at this point we don't need to worry about the tree being locked, because we only deal with
// EntityItemIDs from here. The callEntityScriptMethod() method is robust against attempting to call scripts
// for entity IDs that no longer exist.
- // for all of our previous containing entities, if they are no longer containing then send them a leave event
- foreach(const EntityItemID& entityID, _currentEntitiesInside) {
- if (!entitiesContainingAvatar.contains(entityID)) {
- emit leaveEntity(entityID);
- if (_entitiesScriptEngine) {
+ if (_entitiesScriptEngine) {
+ // for all of our previous containing entities, if they are no longer containing then send them a leave event
+ foreach(const EntityItemID& entityID, _currentEntitiesInside) {
+ if (!entitiesContainingAvatar.contains(entityID)) {
+ emit leaveEntity(entityID);
_entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
}
}
- }
- // for all of our new containing entities, if they weren't previously containing then send them an enter event
- foreach(const EntityItemID& entityID, entitiesContainingAvatar) {
- if (!_currentEntitiesInside.contains(entityID)) {
- emit enterEntity(entityID);
- if (_entitiesScriptEngine) {
+ // for all of our new containing entities, if they weren't previously containing then send them an enter event
+ foreach(const EntityItemID& entityID, entitiesContainingAvatar) {
+ if (!_currentEntitiesInside.contains(entityID)) {
+ emit enterEntity(entityID);
_entitiesScriptEngine->callEntityScriptMethod(entityID, "enterEntity");
}
}
+ _currentEntitiesInside = entitiesContainingAvatar;
}
- _currentEntitiesInside = entitiesContainingAvatar;
}
}
return didUpdate;
@@ -653,7 +654,7 @@ bool EntityTreeRenderer::checkEnterLeaveEntities() {
void EntityTreeRenderer::leaveDomainAndNonOwnedEntities() {
if (_tree && !_shuttingDown) {
- QVector currentEntitiesInsideToSave;
+ QSet currentEntitiesInsideToSave;
foreach (const EntityItemID& entityID, _currentEntitiesInside) {
EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID);
if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) {
@@ -662,7 +663,7 @@ void EntityTreeRenderer::leaveDomainAndNonOwnedEntities() {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
}
} else {
- currentEntitiesInsideToSave.push_back(entityID);
+ currentEntitiesInsideToSave.insert(entityID);
}
}
@@ -687,9 +688,7 @@ void EntityTreeRenderer::leaveAllEntities() {
}
void EntityTreeRenderer::forceRecheckEntities() {
- // make sure our "last avatar position" is something other than our current position,
- // so that on our next chance, we'll check for enter/leave entity events.
- _avatarPosition = _viewState->getAvatarPosition() + glm::vec3((float)TREE_SCALE);
+ _forceRecheckEntities = true;
}
bool EntityTreeRenderer::applyLayeredZones() {
@@ -992,7 +991,10 @@ void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) {
return;
}
- if (_tree && !_shuttingDown && _entitiesScriptEngine) {
+ if (_tree && !_shuttingDown && _entitiesScriptEngine && !itr->second->getEntity()->getScript().isEmpty()) {
+ if (itr->second->getEntity()->contains(_avatarPosition)) {
+ _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
+ }
_entitiesScriptEngine->unloadEntityScript(entityID, true);
}
@@ -1038,6 +1040,9 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, bool
QString scriptUrl = entity->getScript();
if ((shouldLoad && unloadFirst) || scriptUrl.isEmpty()) {
if (_entitiesScriptEngine) {
+ if (entity->contains(_avatarPosition)) {
+ _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
+ }
_entitiesScriptEngine->unloadEntityScript(entityID);
}
entity->scriptHasUnloaded();
diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h
index cee91ad1c7..05105a2c7f 100644
--- a/libraries/entities-renderer/src/EntityTreeRenderer.h
+++ b/libraries/entities-renderer/src/EntityTreeRenderer.h
@@ -169,7 +169,7 @@ private:
void resetEntitiesScriptEngine();
- bool findBestZoneAndMaybeContainingEntities(QVector* entitiesContainingAvatar = nullptr);
+ bool findBestZoneAndMaybeContainingEntities(QSet& entitiesContainingAvatar);
bool applyLayeredZones();
void stopDomainAndNonOwnedEntities();
@@ -186,7 +186,8 @@ private:
void forceRecheckEntities();
glm::vec3 _avatarPosition { 0.0f };
- QVector _currentEntitiesInside;
+ bool _forceRecheckEntities { true };
+ QSet _currentEntitiesInside;
bool _wantScripts;
ScriptEnginePointer _entitiesScriptEngine;
From 7bca3c76bbf0c0be08e992f468915f655a54f877 Mon Sep 17 00:00:00 2001
From: Simon Walton
Date: Mon, 22 Apr 2019 11:49:23 -0700
Subject: [PATCH 14/54] Move more processing to slave thread; other WIP
---
assignment-client/src/avatars/MixerAvatar.cpp | 146 +++++++++---------
assignment-client/src/avatars/MixerAvatar.h | 10 +-
2 files changed, 83 insertions(+), 73 deletions(-)
diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp
index 8165a7d91d..13b612db5d 100644
--- a/assignment-client/src/avatars/MixerAvatar.cpp
+++ b/assignment-client/src/avatars/MixerAvatar.cpp
@@ -24,30 +24,22 @@
#include "MixerAvatar.h"
#include "AvatarLogging.h"
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-
void MixerAvatar::fetchAvatarFST() {
_verifyState = kNoncertified;
_certificateIdFromURL.clear();
_certificateIdFromFST.clear();
_marketplaceIdFromURL.clear();
+ _marketplaceIdFromFST.clear();
auto resourceManager = DependencyManager::get();
QUrl avatarURL = getSkeletonModelURL();
if (avatarURL.isEmpty()) {
return;
}
- auto avatarURLString = avatarURL.toDisplayString();
- // Match UUID + version
+ //auto avatarURLString = avatarURL.toDisplayString();
+ // Match UUID + (optionally) URL cert
static const QRegularExpression marketIdRegex{
- "^https://metaverse.highfidelity.com/api/v.+/commerce/entity_edition/([-0-9a-z]{36}).*?(certificate_id=([\\w/+%]+)).*$"
+ "^https://metaverse.highfidelity.com/api/v.+/commerce/entity_edition/([-0-9a-z]{36})(.*?certificate_id=([\\w/+%]+)|.*).*$"
};
auto marketIdMatch = marketIdRegex.match(avatarURL.toDisplayString());
if (marketIdMatch.hasMatch()) {
@@ -86,45 +78,6 @@ void MixerAvatar::fstRequestComplete() {
} else {
_avatarFSTContents = fstRequest->getData();
_verifyState = kReceivedFST;
- generateFSTHash();
- QString& marketplacePublicKey = EntityItem::_marketplacePublicKey;
- bool staticVerification = validateFSTHash(marketplacePublicKey);
- _verifyState = staticVerification ? kStaticValidation : kVerificationFailed;
-
- if (_verifyState == kStaticValidation) {
- static const QString POP_MARKETPLACE_API{ "/api/v1/commerce/proof_of_purchase_status/transfer" };
- auto& networkAccessManager = NetworkAccessManager::getInstance();
- QNetworkRequest networkRequest;
- networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
- networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL();
- requestURL.setPath(POP_MARKETPLACE_API);
- networkRequest.setUrl(requestURL);
-
- QJsonObject request;
- request["certificate_id"] = _certificateIdFromFST;
- _verifyState = kRequestingOwner;
- QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
- networkReply->setParent(this);
- connect(networkReply, &QNetworkReply::finished, [this, networkReply]() {
- QMutexLocker certifyLocker(&_avatarCertifyLock);
- if (networkReply->error() == QNetworkReply::NoError) {
- _dynamicMarketResponse = networkReply->readAll();
- _verifyState = kOwnerResponse;
- } else {
- auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"];
- if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) {
- qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":"
- << jsonData.toObject()["message"].toString();
- _verifyState = kError;
- }
- }
- networkReply->deleteLater();
- });
- } else {
- _verifyState = kVerificationFailed;
- qCDebug(avatars) << "Avatar" << getDisplayName() << "FAILED static certification";
- }
}
_avatarRequest->deleteLater();
_avatarRequest = nullptr;
@@ -184,6 +137,9 @@ QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
scripts.append(lineMatch.captured(2).trimmed());
} else {
certifiedItems[key] = QJsonValue(lineMatch.captured(2));
+ if (key == "marketplaceID") {
+ _marketplaceIdFromFST = lineMatch.captured(2);
+ }
}
}
}
@@ -207,10 +163,53 @@ void MixerAvatar::processCertifyEvents() {
}
switch (_verifyState) {
+
+ case kReceivedFST:
+ {
+ generateFSTHash();
+ QString& marketplacePublicKey = EntityItem::_marketplacePublicKey;
+ bool staticVerification = validateFSTHash(marketplacePublicKey);
+ _verifyState = staticVerification ? kStaticValidation : kVerificationFailed;
+
+ if (_verifyState == kStaticValidation) {
+ static const QString POP_MARKETPLACE_API { "/api/v1/commerce/proof_of_purchase_status/transfer" };
+ auto& networkAccessManager = NetworkAccessManager::getInstance();
+ QNetworkRequest networkRequest;
+ networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL();
+ requestURL.setPath(POP_MARKETPLACE_API);
+ networkRequest.setUrl(requestURL);
+
+ QJsonObject request;
+ request["certificate_id"] = _certificateIdFromFST;
+ _verifyState = kRequestingOwner;
+ QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
+ networkReply->setParent(this);
+ connect(networkReply, &QNetworkReply::finished, [this, networkReply]() {
+ QMutexLocker certifyLocker(&_avatarCertifyLock);
+ if (networkReply->error() == QNetworkReply::NoError) {
+ _dynamicMarketResponse = networkReply->readAll();
+ _verifyState = kOwnerResponse;
+ } else {
+ auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"];
+ if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) {
+ qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":"
+ << jsonData.toObject()["message"].toString();
+ _verifyState = kError;
+ }
+ }
+ networkReply->deleteLater();
+ });
+ } else {
+ _verifyState = kVerificationFailedPending;
+ qCDebug(avatars) << "Avatar" << getDisplayName() << "FAILED static certification";
+ }
+ }
+
case kOwnerResponse:
{
QJsonDocument responseJson = QJsonDocument::fromJson(_dynamicMarketResponse.toUtf8());
- _verifyState = kChallengeClient;
QString ownerPublicKey;
bool ownerValid = false;
qCDebug(avatars) << "Marketplace response for avatar" << getDisplayName() << ":" << _dynamicMarketResponse;
@@ -227,10 +226,15 @@ void MixerAvatar::processCertifyEvents() {
}
}
if (ownerValid && !ownerPublicKey.isEmpty()) {
- _ownerPublicKey = "-----BEGIN PUBLIC KEY-----\n"
- + ownerPublicKey
- + "\n-----END PUBLIC KEY-----\n";
- challengeOwner();
+ if (ownerPublicKey.startsWith("-----BEGIN ")){
+ _ownerPublicKey = ownerPublicKey;
+ } else {
+ _ownerPublicKey = "-----BEGIN PUBLIC KEY-----\n"
+ + ownerPublicKey
+ + "\n-----END PUBLIC KEY-----\n";
+ }
+ sendOwnerChallenge();
+ _verifyState = kChallengeClient;
} else {
_verifyState = kError;
}
@@ -244,35 +248,37 @@ void MixerAvatar::processCertifyEvents() {
case kChallengeResponse:
{
- int avatarIDLength;
- int signedNonceLength;
if (_challengeResponse.length() < 8) {
_verifyState = kError;
break;
}
- QDataStream responseStream(_challengeResponse);
- responseStream.setByteOrder(QDataStream::LittleEndian);
- responseStream >> avatarIDLength >> signedNonceLength;
+ int avatarIDLength;
+ int signedNonceLength;
+ {
+ QDataStream responseStream(_challengeResponse);
+ responseStream.setByteOrder(QDataStream::LittleEndian);
+ responseStream >> avatarIDLength >> signedNonceLength;
+ }
QByteArray avatarID(_challengeResponse.data() + 2 * sizeof(int), avatarIDLength);
QByteArray signedNonce(_challengeResponse.data() + 2 * sizeof(int) + avatarIDLength, signedNonceLength);
- QCryptographicHash nonceHash(QCryptographicHash::Sha256);
- nonceHash.addData(_challengeNonce);
- bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, nonceHash.result(),
+ bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, _challengeNonceHash,
QByteArray::fromBase64(signedNonce));
- _verifyState = challengeResult ? kVerificationSucceeded : kVerificationFailed;
- if (_verifyState == kVerificationFailed) {
+ _verifyState = challengeResult ? kVerificationSucceeded : kVerificationFailedPending;
+ if (_verifyState == kVerificationFailedPending) {
qCDebug(avatars) << "Dynamic verification FAILED for " << getDisplayName() << getSessionUUID();
+ } else {
+ qCDebug(avatars) << "Dynamic verification SUCCEEDED for " << getDisplayName() << getSessionUUID();
}
}
} // close switch
}
-void MixerAvatar::challengeOwner() {
+void MixerAvatar::sendOwnerChallenge() {
auto nodeList = DependencyManager::get();
- QByteArray avatarID = ("{" + _marketplaceIdFromURL + "}").toUtf8();
+ QByteArray avatarID = ("{" + _marketplaceIdFromFST + "}").toUtf8();
QByteArray nonce = QUuid::createUuid().toByteArray();
auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership,
@@ -283,7 +289,9 @@ void MixerAvatar::challengeOwner() {
challengeOwnershipPacket->write(nonce);
nodeList->sendPacket(std::move(challengeOwnershipPacket), *(nodeList->nodeWithUUID(getSessionUUID())) );
- _challengeNonce = nonce;
+ QCryptographicHash nonceHash(QCryptographicHash::Sha256);
+ nonceHash.addData(nonce);
+ _challengeNonceHash = nonceHash.result();
static constexpr int CHALLENGE_TIMEOUT_MS = 10 * 1000; // 10 s
_challengeTimeout.setInterval(CHALLENGE_TIMEOUT_MS);
diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h
index 57896b2876..1d5dbc62ae 100644
--- a/assignment-client/src/avatars/MixerAvatar.h
+++ b/assignment-client/src/avatars/MixerAvatar.h
@@ -25,7 +25,7 @@ public:
void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; }
void fetchAvatarFST();
- bool isCertifyFailed() const { return _verifyState == kVerificationFailed; }
+ bool isCertifyFailed() const { return _verifyState == kVerificationFailed || _verifyState == kVerificationFailedPending; }
void processCertifyEvents();
void handleChallengeResponse(ReceivedMessage * response);
@@ -34,26 +34,28 @@ private:
// Avatar certification/verification:
enum VerifyState { kNoncertified, kRequestingFST, kReceivedFST, kStaticValidation, kRequestingOwner, kOwnerResponse,
- kChallengeClient, kChallengeResponse, kVerified, kVerificationFailed, kVerificationSucceeded, kError };
+ kChallengeClient, kChallengeResponse, kVerified, kVerificationFailedPending, kVerificationFailed,
+ kVerificationSucceeded, kError };
Q_ENUM(VerifyState);
VerifyState _verifyState { kNoncertified };
QMutex _avatarCertifyLock;
ResourceRequest* _avatarRequest { nullptr };
QString _marketplaceIdFromURL;
+ QString _marketplaceIdFromFST;
QByteArray _avatarFSTContents;
QByteArray _certificateHash;
QString _certificateIdFromURL;
QString _certificateIdFromFST;
QString _dynamicMarketResponse;
QString _ownerPublicKey;
- QByteArray _challengeNonce;
+ QByteArray _challengeNonceHash;
QByteArray _challengeResponse;
QTimer _challengeTimeout;
bool generateFSTHash();
bool validateFSTHash(const QString& publicKey);
QByteArray canonicalJson(const QString fstFile);
- void challengeOwner();
+ void sendOwnerChallenge();
private slots:
void fstRequestComplete();
From c03839e49f8bb6870f8a12827f86ea3786856cec Mon Sep 17 00:00:00 2001
From: Simon Walton
Date: Tue, 23 Apr 2019 09:26:17 -0700
Subject: [PATCH 15/54] Fix threaded issues; add verify failed flag to avatar
identity packet
Bump packet version
---
assignment-client/src/avatars/MixerAvatar.cpp | 16 ++++++--
assignment-client/src/avatars/MixerAvatar.h | 2 +-
libraries/avatars/src/AvatarData.cpp | 40 ++++++++++++++-----
libraries/avatars/src/AvatarData.h | 7 ++++
.../networking/src/udt/PacketHeaders.cpp | 2 +-
libraries/networking/src/udt/PacketHeaders.h | 3 +-
6 files changed, 53 insertions(+), 17 deletions(-)
diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp
index 13b612db5d..3f57bbe3e9 100644
--- a/assignment-client/src/avatars/MixerAvatar.cpp
+++ b/assignment-client/src/avatars/MixerAvatar.cpp
@@ -158,7 +158,7 @@ QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
void MixerAvatar::processCertifyEvents() {
QMutexLocker certifyLocker(&_avatarCertifyLock);
- if (_verifyState != kOwnerResponse && _verifyState != kChallengeResponse) {
+ if (_verifyState != kReceivedFST && _verifyState != kOwnerResponse && _verifyState != kChallengeResponse && _verifyState != kRequestingOwner) {
return;
}
@@ -185,8 +185,8 @@ void MixerAvatar::processCertifyEvents() {
request["certificate_id"] = _certificateIdFromFST;
_verifyState = kRequestingOwner;
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
- networkReply->setParent(this);
- connect(networkReply, &QNetworkReply::finished, [this, networkReply]() {
+ //networkReply->setParent(this);
+ connect(networkReply, &QNetworkReply::readyRead, [this, networkReply]() {
QMutexLocker certifyLocker(&_avatarCertifyLock);
if (networkReply->error() == QNetworkReply::NoError) {
_dynamicMarketResponse = networkReply->readAll();
@@ -205,6 +205,7 @@ void MixerAvatar::processCertifyEvents() {
_verifyState = kVerificationFailedPending;
qCDebug(avatars) << "Avatar" << getDisplayName() << "FAILED static certification";
}
+ break;
}
case kOwnerResponse:
@@ -271,6 +272,15 @@ void MixerAvatar::processCertifyEvents() {
} else {
qCDebug(avatars) << "Dynamic verification SUCCEEDED for " << getDisplayName() << getSessionUUID();
}
+
+ break;
+ }
+
+ case kRequestingOwner:
+ {
+ certifyLocker.unlock();
+ QCoreApplication::processEvents();
+ break;
}
} // close switch
diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h
index 1d5dbc62ae..8979d5c9ad 100644
--- a/assignment-client/src/avatars/MixerAvatar.h
+++ b/assignment-client/src/avatars/MixerAvatar.h
@@ -25,7 +25,7 @@ public:
void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; }
void fetchAvatarFST();
- bool isCertifyFailed() const { return _verifyState == kVerificationFailed || _verifyState == kVerificationFailedPending; }
+ virtual bool isCertifyFailed() const override { return _verifyState == kVerificationFailed || _verifyState == kVerificationFailedPending; }
void processCertifyEvents();
void handleChallengeResponse(ReceivedMessage * response);
diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp
index f460881a45..f179a7dc67 100755
--- a/libraries/avatars/src/AvatarData.cpp
+++ b/libraries/avatars/src/AvatarData.cpp
@@ -1935,8 +1935,7 @@ void AvatarData::processAvatarIdentity(QDataStream& packetStream, bool& identity
>> identity.attachmentData
>> identity.displayName
>> identity.sessionDisplayName
- >> identity.isReplicated
- >> identity.lookAtSnappingEnabled
+ >> identity.identityFlags
;
if (incomingSequenceNumber > _identitySequenceNumber) {
@@ -1951,8 +1950,22 @@ void AvatarData::processAvatarIdentity(QDataStream& packetStream, bool& identity
}
maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName);
- if (identity.isReplicated != _isReplicated) {
- _isReplicated = identity.isReplicated;
+ bool flagValue;
+ flagValue = identity.identityFlags.testFlag(AvatarDataPacket::IdentityFlag::isReplicated);
+ if ( flagValue != _isReplicated) {
+ _isReplicated = flagValue;
+ identityChanged = true;
+ }
+
+ flagValue = identity.identityFlags.testFlag(AvatarDataPacket::IdentityFlag::lookAtSnapping);
+ if ( flagValue != _lookAtSnappingEnabled) {
+ setProperty("lookAtSnappingEnabled", flagValue);
+ identityChanged = true;
+ }
+
+ flagValue = identity.identityFlags.testFlag(AvatarDataPacket::IdentityFlag::verificationFailed);
+ if (flagValue != _verificationFailed) {
+ _verificationFailed = flagValue;
identityChanged = true;
}
@@ -1961,11 +1974,6 @@ void AvatarData::processAvatarIdentity(QDataStream& packetStream, bool& identity
identityChanged = true;
}
- if (identity.lookAtSnappingEnabled != _lookAtSnappingEnabled) {
- setProperty("lookAtSnappingEnabled", identity.lookAtSnappingEnabled);
- identityChanged = true;
- }
-
#ifdef WANT_DEBUG
qCDebug(avatars) << __FUNCTION__
<< "identity.uuid:" << identity.uuid
@@ -2084,17 +2092,27 @@ void AvatarData::prepareResetTraitInstances() {
QByteArray AvatarData::identityByteArray(bool setIsReplicated) const {
QByteArray identityData;
QDataStream identityStream(&identityData, QIODevice::Append);
+ using namespace AvatarDataPacket;
// when mixers send identity packets to agents, they simply forward along the last incoming sequence number they received
// whereas agents send a fresh outgoing sequence number when identity data has changed
+ IdentityFlags identityFlags = IdentityFlag::none;
+ if (_isReplicated || setIsReplicated) {
+ identityFlags.setFlag(IdentityFlag::isReplicated);
+ }
+ if (_lookAtSnappingEnabled) {
+ identityFlags.setFlag(IdentityFlag::lookAtSnapping);
+ }
+ if (isCertifyFailed()) {
+ identityFlags.setFlag(IdentityFlag::verificationFailed);
+ }
identityStream << getSessionUUID()
<< (udt::SequenceNumber::Type) _identitySequenceNumber
<< _attachmentData
<< _displayName
<< getSessionDisplayNameForTransport() // depends on _sessionDisplayName
- << (_isReplicated || setIsReplicated)
- << _lookAtSnappingEnabled;
+ << identityFlags;
return identityData;
}
diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h
index 79c82d4f29..0403998d82 100755
--- a/libraries/avatars/src/AvatarData.h
+++ b/libraries/avatars/src/AvatarData.h
@@ -329,6 +329,10 @@ namespace AvatarDataPacket {
static const size_t MIN_BULK_PACKET_SIZE = NUM_BYTES_RFC4122_UUID + HEADER_SIZE;
static const size_t FAUX_JOINTS_SIZE = 2 * (sizeof(SixByteQuat) + sizeof(SixByteTrans));
+ // AvatarIdentity packet:
+ enum class IdentityFlag: quint32 {none, isReplicated = 0x1, lookAtSnapping = 0x2, verificationFailed = 0x4};
+ Q_DECLARE_FLAGS(IdentityFlags, IdentityFlag)
+
struct SendStatus {
HasFlags itemFlags { 0 };
bool sendUUID { false };
@@ -1132,6 +1136,7 @@ public:
QString sessionDisplayName;
bool isReplicated;
bool lookAtSnappingEnabled;
+ AvatarDataPacket::IdentityFlags identityFlags;
};
// identityChanged returns true if identity has changed, false otherwise.
@@ -1163,6 +1168,7 @@ public:
_sessionDisplayName = sessionDisplayName;
markIdentityDataChanged();
}
+ virtual bool isCertifyFailed() const { return _verificationFailed; }
/**jsdoc
* Gets information about the models currently attached to your avatar.
@@ -1639,6 +1645,7 @@ protected:
QString _displayName;
QString _sessionDisplayName { };
bool _lookAtSnappingEnabled { true };
+ bool _verificationFailed { false };
quint64 _errorLogExpiry; ///< time in future when to log an error
diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp
index b21c200ef2..40f73ce5e3 100644
--- a/libraries/networking/src/udt/PacketHeaders.cpp
+++ b/libraries/networking/src/udt/PacketHeaders.cpp
@@ -38,7 +38,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
return static_cast(EntityQueryPacketVersion::ConicalFrustums);
case PacketType::AvatarIdentity:
case PacketType::AvatarData:
- return static_cast(AvatarMixerPacketVersion::FBXJointOrderChange);
+ return static_cast(AvatarMixerPacketVersion::SendVerificationFailed);
case PacketType::BulkAvatarData:
case PacketType::KillAvatar:
return static_cast(AvatarMixerPacketVersion::FBXJointOrderChange);
diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h
index 1dafc561f6..1b73eae992 100644
--- a/libraries/networking/src/udt/PacketHeaders.h
+++ b/libraries/networking/src/udt/PacketHeaders.h
@@ -331,7 +331,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
AvatarTraitsAck,
FasterAvatarEntities,
SendMaxTranslationDimension,
- FBXJointOrderChange
+ FBXJointOrderChange,
+ SendVerificationFailed
};
enum class DomainConnectRequestVersion : PacketVersion {
From de97af5c022e5c8217c20d1a5d63cd10e052d399 Mon Sep 17 00:00:00 2001
From: Simon Walton
Date: Wed, 24 Apr 2019 17:42:51 -0700
Subject: [PATCH 16/54] Minimal working version for avatar's client
Also merge of Wayne's proof-of-concept
---
assignment-client/src/avatars/AvatarMixer.cpp | 8 +-
assignment-client/src/avatars/MixerAvatar.cpp | 58 ++++---
assignment-client/src/avatars/MixerAvatar.h | 15 +-
.../resources/images/AvatarTheftBanner.png | Bin 0 -> 49298 bytes
interface/resources/qml/AvatarTheft.qml | 70 +++++++++
interface/resources/qml/AvatarTheftBanner.qml | 67 ++++++++
.../resources/qml/AvatarTheftSettings.qml | 139 +++++++++++++++++
interface/src/Application.cpp | 1 +
interface/src/avatar/AvatarManager.cpp | 8 +
.../developer/tests/avatarTheftPrototype.js | 144 ++++++++++++++++++
10 files changed, 481 insertions(+), 29 deletions(-)
create mode 100644 interface/resources/images/AvatarTheftBanner.png
create mode 100644 interface/resources/qml/AvatarTheft.qml
create mode 100644 interface/resources/qml/AvatarTheftBanner.qml
create mode 100644 interface/resources/qml/AvatarTheftSettings.qml
create mode 100644 scripts/developer/tests/avatarTheftPrototype.js
diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp
index b804e4a20f..96e225c7e7 100644
--- a/assignment-client/src/avatars/AvatarMixer.cpp
+++ b/assignment-client/src/avatars/AvatarMixer.cpp
@@ -368,9 +368,10 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
return;
}
- bool sendIdentity = false;
+ MixerAvatar& avatar = nodeData->getAvatar();
+ bool sendIdentity = avatar.needsIdentityUpdate();
if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) {
- AvatarData& avatar = nodeData->getAvatar();
+ MixerAvatar& avatar = nodeData->getAvatar();
const QString& existingBaseDisplayName = nodeData->getAvatar().getSessionDisplayName();
if (!existingBaseDisplayName.isEmpty()) {
SessionDisplayName existingDisplayName { existingBaseDisplayName };
@@ -415,10 +416,11 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name or avatar.
// since this packet includes a change to either the skeleton model URL or the display name
// it needs a new sequence number
- nodeData->getAvatar().pushIdentitySequenceNumber();
+ avatar.pushIdentitySequenceNumber();
// tell node whose name changed about its new session display name or avatar.
sendIdentityPacket(nodeData, node);
+ avatar.clearIdentityUpdate();
}
}
diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp
index 3f57bbe3e9..9ad4a0cfd3 100644
--- a/assignment-client/src/avatars/MixerAvatar.cpp
+++ b/assignment-client/src/avatars/MixerAvatar.cpp
@@ -15,6 +15,7 @@
#include
#include
#include
+#include
#include
#include
@@ -78,6 +79,7 @@ void MixerAvatar::fstRequestComplete() {
} else {
_avatarFSTContents = fstRequest->getData();
_verifyState = kReceivedFST;
+ _pendingEvent = true;
}
_avatarRequest->deleteLater();
_avatarRequest = nullptr;
@@ -156,12 +158,31 @@ QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
return jsonDocCertifiedItems.toJson(QJsonDocument::Compact);
}
-void MixerAvatar::processCertifyEvents() {
+void MixerAvatar::ownerRequestComplete() {
QMutexLocker certifyLocker(&_avatarCertifyLock);
- if (_verifyState != kReceivedFST && _verifyState != kOwnerResponse && _verifyState != kChallengeResponse && _verifyState != kRequestingOwner) {
+ QNetworkReply* networkReply = static_cast(QObject::sender());
+
+ if (networkReply->error() == QNetworkReply::NoError) {
+ _dynamicMarketResponse = networkReply->readAll();
+ _verifyState = kOwnerResponse;
+ _pendingEvent = true;
+ } else {
+ auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"];
+ if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) {
+ qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":"
+ << jsonData.toObject()["message"].toString();
+ _verifyState = kError;
+ }
+ }
+ networkReply->deleteLater();
+}
+
+void MixerAvatar::processCertifyEvents() {
+ if (!_pendingEvent) {
return;
}
+ QMutexLocker certifyLocker(&_avatarCertifyLock);
switch (_verifyState) {
case kReceivedFST:
@@ -185,24 +206,10 @@ void MixerAvatar::processCertifyEvents() {
request["certificate_id"] = _certificateIdFromFST;
_verifyState = kRequestingOwner;
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
- //networkReply->setParent(this);
- connect(networkReply, &QNetworkReply::readyRead, [this, networkReply]() {
- QMutexLocker certifyLocker(&_avatarCertifyLock);
- if (networkReply->error() == QNetworkReply::NoError) {
- _dynamicMarketResponse = networkReply->readAll();
- _verifyState = kOwnerResponse;
- } else {
- auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"];
- if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) {
- qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":"
- << jsonData.toObject()["message"].toString();
- _verifyState = kError;
- }
- }
- networkReply->deleteLater();
- });
+ connect(networkReply, &QNetworkReply::finished, this, &MixerAvatar::ownerRequestComplete);
} else {
- _verifyState = kVerificationFailedPending;
+ _verifyState = kVerificationFailed;
+ _pendingEvent = false;
qCDebug(avatars) << "Avatar" << getDisplayName() << "FAILED static certification";
}
break;
@@ -244,6 +251,7 @@ void MixerAvatar::processCertifyEvents() {
"message:" << responseJson["message"].toString();
_verifyState = kError;
}
+ _pendingEvent = false;
break;
}
@@ -266,19 +274,19 @@ void MixerAvatar::processCertifyEvents() {
bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, _challengeNonceHash,
QByteArray::fromBase64(signedNonce));
- _verifyState = challengeResult ? kVerificationSucceeded : kVerificationFailedPending;
- if (_verifyState == kVerificationFailedPending) {
+ _verifyState = challengeResult ? kVerificationSucceeded : kVerificationFailed;
+ _needsIdentityUpdate = true;
+ if (_verifyState == kVerificationFailed) {
qCDebug(avatars) << "Dynamic verification FAILED for " << getDisplayName() << getSessionUUID();
} else {
qCDebug(avatars) << "Dynamic verification SUCCEEDED for " << getDisplayName() << getSessionUUID();
}
-
+ _pendingEvent = false;
break;
}
case kRequestingOwner:
- {
- certifyLocker.unlock();
+ { // Qt networking done on this thread:
QCoreApplication::processEvents();
break;
}
@@ -307,6 +315,7 @@ void MixerAvatar::sendOwnerChallenge() {
_challengeTimeout.setInterval(CHALLENGE_TIMEOUT_MS);
_challengeTimeout.connect(&_challengeTimeout, &QTimer::timeout, [this]() {
_verifyState = kVerificationFailed;
+ _needsIdentityUpdate = true;
});
}
@@ -318,5 +327,6 @@ void MixerAvatar::handleChallengeResponse(ReceivedMessage * response) {
_challengeTimeout.stop();
_challengeResponse = response->readAll();
_verifyState = kChallengeResponse;
+ _pendingEvent = true;
}
}
diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h
index 8979d5c9ad..5a81001ea9 100644
--- a/assignment-client/src/avatars/MixerAvatar.h
+++ b/assignment-client/src/avatars/MixerAvatar.h
@@ -25,7 +25,15 @@ public:
void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; }
void fetchAvatarFST();
- virtual bool isCertifyFailed() const override { return _verifyState == kVerificationFailed || _verifyState == kVerificationFailedPending; }
+ virtual bool isCertifyFailed() const override { return _verifyState == kVerificationFailed; }
+ bool needsIdentityUpdate() const { return _needsIdentityUpdate; }
+ void clearIdentityUpdate() { _needsIdentityUpdate = false; }
+
+
+ //bool isPendingCertifyFailed() const { return _verifyState == kVerificationFailedPending; }
+ //void advanceCertifyFailed() {
+ // if (isPendingCertifyFailed()) { _verifyState = kVerificationFailed; }
+ //}
void processCertifyEvents();
void handleChallengeResponse(ReceivedMessage * response);
@@ -34,10 +42,11 @@ private:
// Avatar certification/verification:
enum VerifyState { kNoncertified, kRequestingFST, kReceivedFST, kStaticValidation, kRequestingOwner, kOwnerResponse,
- kChallengeClient, kChallengeResponse, kVerified, kVerificationFailedPending, kVerificationFailed,
+ kChallengeClient, kChallengeResponse, kVerified, kVerificationFailed,
kVerificationSucceeded, kError };
Q_ENUM(VerifyState);
VerifyState _verifyState { kNoncertified };
+ std::atomic _pendingEvent { false };
QMutex _avatarCertifyLock;
ResourceRequest* _avatarRequest { nullptr };
QString _marketplaceIdFromURL;
@@ -51,6 +60,7 @@ private:
QByteArray _challengeNonceHash;
QByteArray _challengeResponse;
QTimer _challengeTimeout;
+ bool _needsIdentityUpdate { false };
bool generateFSTHash();
bool validateFSTHash(const QString& publicKey);
@@ -59,6 +69,7 @@ private:
private slots:
void fstRequestComplete();
+ void ownerRequestComplete();
};
using MixerAvatarSharedPointer = std::shared_ptr;
diff --git a/interface/resources/images/AvatarTheftBanner.png b/interface/resources/images/AvatarTheftBanner.png
new file mode 100644
index 0000000000000000000000000000000000000000..3dc76999e084f73842d4d9d53c2600599bda2bbe
GIT binary patch
literal 49298
zcmeFYS6EY97dDE$0vl0Lstu4PDuPmDM?^q{5NZMl1nEc(C9$I-pdh_OMQVUZZ-Gb+
zJtBk>N~A^-LV%D2QqQyZ*;~Ko+??D0;=jnVk~QWW^BwPa=Ugjut?~MAfN%
zBBCa@uHP3C**-5Kvi0Hats){KK1c}@5fPCsf%lECij?-tE{ceVY=>SkyCNb|7Qc_@
zyhB7pWRL$X+dvT!vDUvoTX22_t|B7elufQ*u?hn(O(Jdq7pH{6P{9OciZW$ivq1k5
zVr!R{ocu67UHYN4;MgI{pzqBmO!h0T?s{{&`{S(xZ%!R|zIFY;x$FBS0*<)6I{tFn
z{m9GK>2K5imnJ4AB83|~c4KK{O*Q}CU?w*+5FLml&lWr9p&D6s)cVq@{%~5=a8(;l
z*}?4Z2t*z`|N3|v{5MdMBciZFe;=K$&jJ52NxTPb`TNVw>;E>8_`ePQA?p8h9#-Q2
za*2(zxMTE__k5d*ib+Jt6L)fZ2H%hgO^~%tgf1Wqo0?|m^i4X#`O`gMyI0H*PHjhE(u(!7SJ3GopF&ul
zq!-$nK4%>MMgSa((}{yK3AaKYzBxX7^{%)3jceR03a_UCxl)Pu^G
z!6A9etNvSHheQN|PLzmXPfKEq11xyy(OR<+ib{2~wXIUJ&COoz7TsJ|X@K0l6}B#zC3kztWT3SjRQhyfz4nQQ
z03NyrELrFWtc7{ruMDQ9Y;2FVePCIEUyzLz7hPdUBxV3T+
zg0QtEM^b#jVeyDvq=2>QZU1n(4pWiF^(hq{yfb_RI$Rf{4U&jk@
z?vQY6R@s+VB9?b;tbPn+woF_T3;a4_9ldPme}OSq_H0kwcOU%vj?~r@331t7eIa^x
z1Wge^sWEr-l0kNkB!6aiZs5^=n+B*{)klA04C6YhaD6+s8t3g}vN#4UzdtKG?8J;7
zGIca;bQFb|d{V8r(V>_Rb<0&I0;}wE664*aGDC>l_>;ig!zu4V@@mUjp4WWbR=ZBV
zj$4V({($P<9cv0mj99yysd1r=tK0qc*Yj&fm9^r(T=#2BdHh$5p}^mFY@U<1@KQ~R
z5nt%*q*Hg%=-Xjr_ysjB4c!jky}M&>`^)zjyia#xXcZDnyHD4K&3js21Fo)y*}r;d
zpMUm+v0`qPwU<4}9yT2mUf$|beX}Ds+aR%udT2-_V&?tZL}f8$9|M@j-k4W_R9l;X
z+@?>T7F_NPH;={}dCNJ)2IslS0?H9+g6lnMQuqnR+`x4dl^)64YUVt!tNe|cr;LnD
zQ4c$#GtX@*g
ztl5|%nB@J6H>&6%NTZ#j^}EHl11K_8>aPP?;d;XRq#T7Nhz!*OV`b&9y8B_B
zMjXgMNpK0O{#{1)UuBB;oj3Db-H`^Fb6?Y+wd5qm6~GHv2SfEYcfKIHh{@td4DErp
zreE{LDmLh0S9Ln26C(}Kl||Ldx9jXsmonyOVT6NenE^##yq4Qh%KNU3pbtuA8wR*S
zf>NpU$Y8T&KssXe_O%BOXt|7|CSstL%pJ
zAv1|0F|9$%q0%M4>UO>OhB$rglr<*V0J-@MBBy%3KgqFujQ<#k5y{ZDh;6NxTmO|SxTu_5w8)e7-j?@V`V
z^hqi4f(taeJyLN)*9(GF=F-gvzB4Kf(qIo+c@I~?aG!d!yi5`j7x^nH8nL!
zScI^-`x%#=$Fb`74s-+so*kWwXYa
z0W7C;x?=M{l5Ezm(%xIx?|K&+)}3ImuNRJwy56r_z3e<&^1g5^ZLx55b@g>2+Px{g
zmNB1HVa&1%*(Xt?J~NlXFqMq0Q;iBooH)@~TZITJs8BU?i`=t2xJ^NWek46LTUJi`
z$ProTwAA#p>|cJ^cgaUSZN2J$^@=^|S5|3v@5Z~21?vf%<>u-BAG5eK4%Cd)x_lit
zb1^1GQ=UXa8OVBR`wfBawmqo;X0=wZLDmXJHefru+KU&*NYYTfK>(GdcU9
z`_Lg46Ajo)+CP#dOSDo97dGapIa-0C(0rwijt*sPdRCS^&DS6$-HCK(^~rQuVZK*A
zj;?_=3g7rCm}!}5ne1v}?Te1LikrfY+kS<(4gKW18O|m)_U$M<=gC~^O3NJy4^jw4
zXM4tXtv=~b?>%zpRAy3*alV3JSsW{5Ew0Th4Y`sCE$d;Zjpz>9u99Wx)bzBhwDh|^
zmvjxav~-DsHeX3a_j}|Um5%qTovfJkaT|lpi`LwvpV7omd8!+DjErf_U$;dSJ*whJ
z&OUfh^nSrTZ|C@JaboM!ZIKQcpD1T%oUYLNRyTK{;;n&In!Thf=|$h7l2Ur=q&7O8
zo#oe{{Yco(RhWV^T<4e9hnynJ1kJlwMaz_B&v?`uY9kPCZkvK1Ki0Y_-7TPP8tuD)
z2~(_w=NT#n#5CLCABLCojHahH^V}Dt4nsN87VSiJB@i%rNv%QKVDz5l2Z(>!e6n7V
z`Wb*jM7dz8t9!i!l|Q5Nc}yMxNMv3%F%{17%86T+HS0o3H@l|16me|hf~=l7fA^G7
z^C`sI+N35OLOJ>Emp>Fd0g!R+eLr)lAhsk
zz~c4Kefkl^`{idK3S7?>JNM_DM2LT?58?{-Bc=U8>$7LiuC+c$w6v6b<_H`sJU3lh
zwFYw5q27-W-jqb0$ra{cb1$`f>TK4fUplpUU)bgW4Zd>4#4JV6KpoGlNy%T_A@YqR
z^Y?95GkT7oKvI7zW>FN_(>ztm@*1(z=jMYT7!F5ZGibt2eDlP4z+{XWwJ#odCZ1hQ
zNJip&G}TcL7v&voqQYv>$Jv+fY{a3g4*XR;k_O9#2CXbenGtd~q2}a6j`c)s!7oD3
zydb@Id-T{y!rf6_hQC{B{+U*)$z|d9f^C~43r+B-n&pq$2A{q}gcY!F+DEwsz`ftPcg=xo3RaE-xFwcIDk9_0BXd4hWVtQE?s5!y%BQul(GL
z3-U&(bpeW#)4;$h!?h0-Yd}~>dQZGgv{I%|MmTR~^V!(6y_{KaRj`@Q#hp<3;t~yu
z$)fJtKNd$1M0{TBn27;ZfLIZThJ?HN+=d!)-z3)#KN%O5(|S4Of$sc1Vi-Fs?Zsx{
z`|%Nd)gLFe@vqI|!7_*y4>$9w>yx=Nj2DV2p5x&iWGSiqUFmD$v`6$Dz0f%IBSIEG
z+z;ki+rp&jQQ=D&NNk_(7~JFT^<$B<
zT-ZK8HeLc$B==|P+S^!1m7qdhc_9yCdtx4MslrwcPz|v$=ZaN1QI$j-a>6BO@XGY?4PI_I}8DZKLxO@6$;^&;`NTjFq2pRQrpT
zmd|U%Q+mU!D6%;%bRotZG(=rNEe^PD4xmpb=q7UvA9^&xN1i(5lj!f9@}hCYgizB
zEEuX#1npA5Y%XUQYhB9%F?0K$owK&TN_^2@HlL4I=o6dt@G5S+KlnW)%84`G1G2~)
zkIB4O{Z0;hD7QUlG7V!IcjoRAs@w|ufTon|ME)fj=Q
zx$zcv_7~2Yo0@uogdjCM31g>#GWtb11XnV(nW@usABGq~vX9np-gr{M)2z5}(5
z(Ah%atX&tb^iGbwZeAXK$uQ^3;15gaHhO6)#8+nr-VECXi?ExP8jQ!{oS=svOTX3iI7T*GN54tLC3=H<_M53HKoIOl)yzAdRGCk95!u&&P&v^yz
zMn3*p`;6SM+~|O;|KPFb!dPa0d8SyDZ8L+NoH{6%7(c9FUU`Tjsxod$Bi0
zcR6T$Zf0%bZfmsmho7%D-cp|6G)H{1gHj)uN0#vxxa5kg_Qf)5w$B?ua$_NRyhGj~
zDiSU9w0=A9*N1Xspb?x&%91}QIHcUpaBNm{OPr+7K0L%w_F=DCteAL!pZ#TCFx7)<*#<=a?t7aD?6pA8P~1Q<+va0CUrqo$z|V47_tW0pR=V63lt
ze#YVAxAZ&CtNC0okJpp9JaIj$WQ3G$r(Idu)TBqaSL%r)h;Qv>)vkX=Z8MFBG{LZN
z258MQ>^^*aqlGeaE6GyW`Ehf5dh1D+Tjr?L;Uiy9mQpapiw!KwI5{TgA{C$CJ_h`_
zraeA$zME@3g|Vx>mnwVoz|S2%SSy{h3?s{|`g1ovuY~i!38PxPiSa?$^@h9Zf-NV#2tQ#pkC^87WnGBKvpwkr0N1VE;>c6H(e1k6l>sHzCha
zI7A1FzSoUK=6vs602YrTPKN;O<>~cb;xahjVY*27zhP_Auu1Jpc97!jA`gLKqzb9x?b%
z)RWH?UwAA={Ngww_HFVR)Li-3xtFce&JX^mdv=O&ML?HjqE)ZN1E#UQJM3m_6$}!rn
zAPFxhih$F)T>RY1$8fTN1z!Ufs41vP-GT9$WEItlDDaN#((=mG`2F*gS*gE{$V&TG
zjQdP~TKM=ab#2ve%J67W?-+XPEvLEFm-=jZ_1LPph^-ka21Yp>9W61++Z4+b4(FMV
zwiN3$^hwK0A2^sa$RZGlGnW}%rwPK}2V44tcSU_>rH%5Xu@?GaP+u#RObh2K&v8~JAonZd-d)b7(LmlF=G7rde+2cYJbIVNUA(h>*!m8LS#Jk+5Ip
zi+)$}_%+3UA$UY)o&I~8Um~WB=rH=mq0giSB~M&6gBwLu=dx)qCaZg#t?8~`0Wf$<
zftHUll%{yA<6V29Pc`lqjR#O$U{5@l_TRhhl7r978-O;%;IL=unX3Le0;
zLwdqYn?AiB14SQ38aiUK8(`6;3o^%sTg_tVO$Z_+O`cm;lfl`@cvLxN=S`IRU`Sc8lv%#Rdxr8SVWSV&YXMl7pOo^(kPnAjAo!
ziKR)uYm#ev?$+7r7FFbq498gw?WJrG!m5ES1&huB?EmZVa`CTpM|*^SzTOSFvCraj
zRc6>Qel2@F?qtTv9Ji3*;5GO5GMkEil%yA$Wbg~EONm0Z>mc8Q%&JdA+ehhen7(76
zs^aY(coB$&doV*ZG}!@Z3vSf6$*Qk<{jAqhN&&_c!2Iqd7#iH2C(QdzHHG^(oL6gR
z9MKlxJ$nb*!jt56^S=Qa8zVH-2Bw4KIFft4-EG;cMjU772=0rdjxxn?Q
zM(hQ6*hGEbrSX8~7ywE|}+@Ai-vLTx%NFhtg#RBKEcaGj2m;mrsx)R%E$PX|Uv?P2F
zs>zu$*XX=7&B$lx`-WZs7piM^_vvL;j(Cc(9O5RjK@n2Jklq@f((KQ}-!rEeOX4K_
zNIorQ_jyG}dDt4IE6Coq$j6I(_QsdY!i@0MSu{!9+rnu4yFS~W*4(=io6PB%SKPEU
z7H_)|8RQDi0+Fp1&a8tCYFCA*E!9yElX5f>LO6|eMsDK~xtG|Z95^YZb^npf&Eyee
z??h)TvA!DqljFqLd%Jy
z)r`2gZ}`e2Tih=zwBv;@ZKJ5^Y03N5=<^IywIVMnWe98!46ZB3tZ~-j5%4ht>vB*5
zRt;T>Xf%k_R_^jLyK`@Ghg<~&!TrkqLcq_wG9ob6&{*}6<@mi9ea7ot`a{f%i%Z^W
zyWZGUFJ}6gIVnl
zS$`#ga4945aYM43LtRAZ98e)JZ0P-3b5e??HFQ_K9R;PCGzV2MdmX<&lyt@AjhaUW
zIAYZWHWMQ^nS&|M^W*0Oilfqw&YrcuZ%@ZYmiafFf53geO++yD-{ovM5^3giCtD9?
zZ-4ZmdCkrnx#UpwChlOopZp}LeT|aH%oWrR7TWg@tc$*?uC1(K8cH%^OilNNV$+WV
ziVj@RG88WH=@2slxnZpcT$3O(8xvD8si@*CxFXTNRNtHgjd4`=o`If!(f%blM69n@
zQ(dDUZ*F$yjyb-!zqc2_S!nRHzNx=vlmLn%Z2Av1&r}Nv8$3HADl8MjZ@R&m?KLEW
zbXGSCPcm9EXfkzOJDi?MIv45punBv<&v&h+E$?tJyh;$17mjT|AOQ=Rqam6y)L+Mo
zjc0!>epuDWmM@v4slJ))LGv&O=aP#!e}>9I3u5gjjHC{6<@0Cdq$|5
zubUozTIqG-qu+}iCztW1c
zpo`ZWbo-K2X=daAk@GPpv4owfXkrK+)jT6r&A!|}ANOgD8_u)_CA%vx2WdO@nuQul
zpq@@vV?2%g=lh>Yk)nj0v1|CXMs-a<*!t3@gQaJ!whfu!2kEaQ>t00C#7tBlBH?Sx
zfT<=MX7AVb@^m&BxA=VHfe!->)?fp}vIltI)-%>T{UhP6D{B?Xi6i$zUthncIEY3#
zDAb3YdT4)I=Y?^uYf25%f@JUDkc!O6{@%J{5~wd|iGc_>p2z&O&_<5f5rm@Fl-eDR
zv24Frnw@YmM=)6Z`0KCQR$Artge{)pIv5@L@hBe+wZH>j{vW6~4yQGop01MBp#%
zCn0zqpP8wIxl57t%&7)|koW%Nc&oBS%1&uU-1T%vSTM8s`}{NH-sokF9;*;jG~DZ2
zy=12OP$4^FLup*jK?jRxDNk-KZxV4rPu(hxA^U%e>Tt3W8!?U;1DuUP*^hWOHDjeaGQsas*0@C>OmKEvAXaYv;Y7O2KWiv>}Z!
ztkEM~E%*bcd%+zKP0=zD8@~jdxV0jS%l^0S&$so|SSVBQO8PB2@=>B+<(nq38~*KF
z4uw1ic5mBzoXssZcs?i
zcYYONad@zliW-rEV6w54iEwhj$`dd46rBB)%vkgMIF?CdZlow!hpU>JL*2tvN#g^p
z{cVl*Ckj6gtDQhBjVok}TjjOG;sd?kba_cftjxVvMu%_g+@9_;pkhn*YIsREgfRt^
z92?qEK5xmdA9clqaMA)?9CuMlzh<)h$iaYPY|45Mz6&KUxN_LOx9PzPAT6{he1lm-
zClyb1z82M{)ou$-!ALuq-bTyvIr)Bc?L{$+ePgb|owp&U=5*rJ0q_f(H-?j^iwNar
z$0ck-rYv%Hkaj-4!AB
ziLH|gRxm+D
zMO2`;QrV^W;H-&9UPWr6>Ff~_f(x(8|8*DBq&c(A^1A00>aQQO^RM%YYh$_oQCl?@
zQWIW>BErXnbua>HW;BAsx`^Pff`KkpqaFQpiAwez+(tL?Sy_jr7#6FG-btaNW+B2A
z&(hUd@*450Y
zsx=;MoLrHWgnQt#y2Hm(99i7AZk5TqC7Dl)FHbf5dsD;UpPdb14n}n$b%>D_s7W$d
zG?b~BKWY{{$+A|l$HX678XVafpTVU23763znuCTSb>Xxt1ed1^ZuF76QFlWc>k}bKPb-%Ym80E-{v
z?67?h51fcxp&@t_2I{zyqDNq_di%vu=|r&3Do2wPFLfg$bV3Sa3QqdV_(xaX;s^;V
ztNuz<)W7je=(TqQ2}x9u#)RzYPhC4`iQ>Z`s9l8%rfCERINi2foEag+)#%fASySEj
zo`<2quME$j!YKqgX+L?aZH}A;a#V9QE5TH(djzvW+=@c83NFjbN=sxM!HyiB$=8&j
zCx8^ybAQHk%`1!NNyqDB$V}0dePuW$rM6LxW=}V7W#V#UWS!oGwg0geYLZqFQPO;P
z-6)K|%vdXWPV4cJZxr&nA|tFTLtkv{+-@sAJQ2keY~E#-ZS-Ait$iVP+Dirkub;4&
zCTp91Y(EgOiee++FmfQHD7D3hd&V5YWsEO&uc!J~jEvglec#BeF5G324Ao;uUK^75
z>DP;z35N$S5XPZ=SkZ$pqv-{s@*86gXlDW*4ZjJllBYUsE@ZSDMa3^KfZYSWBv+fG
zh0A^0vOw&O&BEnMS}sArT2F4EY9_xdT{tzCCFR>+VgQycF(v(IgFkt4IKumW3phjf
z7NETZXHU!qMMXwN5&+qNE#FA{{(0^pR7cIdD0auLlRD^B
zy||GO3lgICQUD72h&k<)MvWVO{QcFht%r6(L=V$qs)`aXFh!pncV>;gCF+ZV8^bRQ
z!_F9ug2;r5K1quygQIF@yTwICD~R6H&efUIAOKdj@KS>7h~KVzT05hOE%UE$HnNYfnW)fDf5IYX81AqG^q@)BJK16^J;r97H?iez4lS1S?zw;p}gh
z-^3~BUJ`G+X{S+&^N8YYecIxZ39Uy81e4cSr|>-}4*#05=`JK6$0cOsOMWgcGA+3N
zi*kp%*Upzs@;H4f9!PbLGbDVkS?6UK=k(dGhQBWD*dKaI_H->w$i)iC>_lKRE1XOS
zqfR~NtMt#M?na?h98gfROLi0N4ZzfXWX!u`NgzoIy^#yAVqbMnFjSxL6s=H9aX^Nz
zd;8>}479toSE{mmykji7N)@tY>XHxT4*Rh&MZ;$sKKZSV#UwQo*Bf}2z4Z|MJv|9iSmQ^mv
zR`89&OkJ+bexFGsyysXow9hr*aCp+e1JV+Rk6}N@6a<{`f^T&f)xVRq)`|+Iq>z*B
znG_`RFQ=}7dc&c(2W2;vTx;GIV>ZHJA2)}Z~}+7Y*~k#P)?(Z^jy^YA;28BA#OQ{zP6T
z69%HG@qh#nVnPG0gTipWWr2cSVgYbBe?C_jRuP8UaPt1pI{nJrAfzy2l~4n*K-}~9
zr&sTqKWN(9+|oamLTaoZPfj^d;{J-5@!{TBLv09Rag2>#4t6w}4&U=8gSPTQQ`t(4
z>&_JL?dt}XuSPM;JytRR7{mlZQBBv`&&n#OWx8c}x`EMypE9t|u6Kl8kC!;qBy{at
zNo3O@eWA(Zc`3(-rMeQ%k1}Zytl0+TYq7BC1ef)S
z(=XN8W3G=i6e~h_?RWq-BCJAPLpK28Y3+t@L_~fFlI$Y{w_aQz8w>EKF6FLHjgy4y?xhXql{MPavLPfb
z?$73yNv^oHwKbXBoglkaUQLSc%v<6Bl+_lo}I(ST*|7$aGY)VZx
zdWLCt1D7kS))+wVS{dL+n
z=xX>w@)84tr%x4KTByVLyv7&QH?DB_Y|Uk+W0%P!@{4s%aUf;2XP4zZl3p>QH0
zz&x#-YW6a*g3y(bVxc!XM7ePTr_fzL(Xc_G^$g6|9QrmCdNU;s%bAas(wYp;Z9l|J
z*%z0ql$Ysq6tU7rg)(Y9%ULu+psrR=LsK}5#jTRbU*Z%kxr2Si6F|U#iPi4x2F<1@
zzJKlJokwp^s;Vk2eji&Xg93Q#GyN~3LhvQ*;h~~w0;s6w3=5*Bw=j{C5W>1m-k70O
zK-qLd9*a6)I*vRY5-U6PIjsqQln%sWJP+6OS4v5caVu&MOI-bYG`r{sE7
z8=6)Y#1Wg*1I!jz+!?x~m}z&NUXZXo#`t7aKI;Z5N>fQiqsG~TMooGF1;F(|$SjYV
z?UCx}yzqHo)p+W&&*r2^-mE6T2-yHvxoAdr&;|M80VCU#Nrk!+v)LEb7u)8WadZT4
z@+t@hp4v?J3Qp4re5c6osED5f@_Vb*(3CdmL~9t7&$l2=(KU2Gd>jS^b93e|EezJ9E3{p*|uiGzE`
zm9nFRgN&)bbDv66-0K*WOb|Hs7B(_4%-6TT1sjUk@>r<*Pc7b1l`=TrX5>l0Jhn
zK&ARY;s~;_q;ycAjPYTGGom{6j3Sr4@uMl@7nnRgUC2uT2&csn90n3D1ZRZ+Wn*Jj
z^k&x&yT({(P;EIZHe!V-f9ZlPkySCO5h-^+`qPzGwp3-TQG`&?
z?vOI_xB{N{mzU?{hE`)QmbYku;PK;C^CnN@yGxo38>++6Xi5QCKQPEMfI8_X+#m`0
zBq5VbLST(7XDhdJ?8SlNB~k1_z;Xi+N|EW*%VVB
zWD-d6*m?e=k(P1D)Zc$$eX;4@rX@#gV9Z&hfI|`@*B}V~{CM;=4RYo^1(TN3dQ
zEO(uTM{Eo#2n(v0#Q{+@MuV0~X)A@Id(28@5j;Lpz`zQr03id75NwX)W=C#|Dd$3#
zvcot0S6MH&VFy>-j=wr?=Kkx3uw5SXw}Ozs1tV~6v`-D+yapPx#aO0UOm5STL>FyNlb=k1xb
z=c@(FDF9R-H)r`9pupEA)Dy|=`z*guFsCINYTd-2*F^BqLJk!I7Ydl&S(KBjW81*9
z8U(z7T}C4icH+#XwrPVGG2T4Q{dBHC)pY
zaf*vZ@SaWm<#-a%2-6#Qcl6LqFau3F#eIa1MPoJ>Ef7G#T0W9w)DTgjr+*R^vfh;e
zwVXef9q;38Rw56EGkPFEOA02d(wDpp7m4mNV*>Dl{%vaw?0
z<(D)U3Wg@~%8coPl@x5zwRSo}IGaIcjNp}TAeUf*R(4E@@(dwej|v^mE^vGl0q$$m
zS)F4RDWg@J1TOjx+Lf$@d_Y7B(;p!HQthp=40{
zK)PU_n1RNj-FrLgl=m)O~n0JKEzX#%Wsfk)Oc1Th|f9DAaIs9=k^hGT=eG*Wf)N?Pu=O
z#9r?T0zKq(G^CvPq3X9E-!GN`+QMsm|Mzo|4Ij+zfdiHEi%L-!?e2zMcYUsX;q=*W
zRCMGz*PJF1;x)R^=f-Jjax$&eIH6~xt8Ga8*|gF4J_H5NDZ;*KYzUZKIN~1rDsD0*
ze9r&2%Ugj_;5(H|L(&q?;x*4}OHbC-fxmi&UOM4DeO%$uxI6@aJr*N)vmNSL6o`nd
zyry~nk_}?ix8mhas^5y}*%vm&?+40OU#~{woG6}lkIE^d$`Ib}63f$Lw%b<4hIiZ@
z@p8EKj32diU13Q=$8ennU8zy18#-4Zpmcedy%`y-^c9p6;@_A)k~gu8v7|oSXE{!0
z_GY2Zx19VulVCX*Cp#e}lUqIXnR4drsop%w4Z?$O$yAilmgC81$BnHm=EJJw-k;mk
z6@6d4&~uDhnrzXOH3Ta8tf6{b7ufgy3(;HyOj4%64Si}AzZ;i)@^q>?`u<)3S3IvS
z{gU0qfLC24LK}yFy?)I~hC?Cj1EI(dpNd9AMm}w&-4l!O_}YcY0c`H5ZyJ`6OnH}j
zL_sn){7TsRCtltdh(4nCBNUtl+PAlAaYv!_IuqE~FDvb9*_RRa^ZWG4!md5%JiQ2d
z-mlpi>$mM_o-qzt-!kIS`GA}4H2n`DbMFM7cYCEOjy9wXN&WIIC7x1MIuii-T(pJ1
zJoUX`@T4CCrfma1MwI>tvS1sHP!^l=SsFMnfmIl7DZX4DF1T_SWvHif{J73dLjE#a
zU{t!f3%fQL}3ML^usP0^6%cy
zjM%mCSrE!URgat-YTL5&U|#s>9T);#t-Kab}Ki0r=?>gk1O1c{XI#|wyIuuxBIh`}ozCqjI2ldtJX~G1)zG+lcgD#3KHw)xXC9|f
zuUw9Z9eAE$tV6o2od}O%BtttwXHhID>Kfq#u_recMclrP$ovcsNTje)QdYZK;uqTwumUX
zEk7@wKyTfEj?T_f%g3E$RV6uv*s%)jGldlqeWBDkygc)U;7Q)kcYg){SVWW8c`9?FKyx~nsZ~8tC=DwF
zRb_JgM!faYz2bfMOuk(n7JVD!o`(wawk$I)@%`9U<+>+s0y%hr#iOChKL2vmsusBL
zCx`S+Dm71Rr3kOe!1tEX|_oKY2cnzo&NSqjhi~C~yEPyV$-6miU?PIbbOW
zDBU?iWjl9;2d^vDErj??viinnDGZKQ_ER$>cy>DHSAVYikg*85d*#9)S$=Fn?pi39
zCU?!RFJF&fojv>5h~n-0%B!{d%nwK8GHbl)k`$1f4X*N%j8I)^eJisIhF93~ll}s;
zS46nAC81wC&WOhui}btV-3iUAy4nVqv1$D^(b9YQ!Sbim_(O(>3PZtlw-FwLJ~e?2
z=q{syM}%hZ;kiJK8feM_7oGV=JP?q&DB~d(Zx|8~#?J>}P19&3OB$z
zL>^Qu={rUQy3hz2Bh;R4Ne?sP-DW_WFGnTMYdRtwB6&-jiAlK)gM)(=j29lw0u;u$cfjDONcoaXX#xJCLC^@U=
zw%Izou=L7CM#+Fl;aK}2W(g#OJ+(}y(;ElItbpl=t5uQyA4z9IeYT3MAf#c3MBert
zJS>s;RZ~~Rh$vq_%T(Er-E!Qyy1<5Cbf3J%5cAedT+G!bJm`Exki-48E%}3Wfa6N{
zu>l(=EGBz9!zso|2}?u6&YE)
zjPYb1{O7cj$TG%L5&cg^@BY81n23nT+0T6#Psab)PyT=GC)WOTH9?2`+xP7YD(WAH
zCr|vpjo)^Hb^!lv=P?0GV*h)(9gi#3LdBK*)A-0sB-+u4*_r
zED0p%x%OW;r5FnNyYL_5x3iu9VqRN~`4{$n98Qy%1
z7j;?wGxXyv8{6q4FaqwwA1qH3+b^Cr`5%|3Yd+pUKG4pf4Q~zbvy}V33UA}
zJtODBQjDj`|NM@=@d60wasEAoWVRCglQHk=;GcYGJMvoNIWOe?^s1hn@#|X=2Q2?)
z`owu4xX&sv|DW(hwygxXzQ$_wkZ%54jsD7S$Yiw!TKI>bn{K9;uP6Tz5hN(C8>PJM
zMEP&h^OM+E{E
zRdh$vj*g_?D!^`&Qasyq=TDaLu-E^&LqKy=51jcQi6~&y^V;@Wy5#=H+U{E=hb`|X
zsT30blhJnBd$3?q(nmh%cRCQ^CEICe(c7iJ(;-+4KJdXoD${}dUpjbk1XKUpjepn`
zuI}8U>ks^c3y^bfp78Q7XwH9QuK)&4yF;I){fqWMDA?!zpLF8A55!b-J^DKKAFvIm
zcFep}%%2euAu%>$Rez%Uh4C~RayR$+y{ZXxOI%ERDrA4lH3<|SWD^(kn>*p4&w=z|
ziFEV-G^2*j3;iF(kSQ4A(eMxd0(|!vBeqAMcqnA)<$odU?xcTMnVodTHT>2P!a3zR
zbp1K<*Xn->7m(30+7+PROdR_eDfREg_RA9ov-A?03QGX0l|Ti@p4uRCRczZ0^%xc=I|HuKAb$JgiLZ_dU4UL%E+
zD@h>S$^O`g|5hgz)VJ!XEC2DJht}JhFZ&|#Ph{@5KKEM~A2NgO6>*YkaycK<
zt~#x=r|Ol;cgv*(uY`A}O>Q5G2Hx3Hf9ldzmqbOq+v-r-&$XO@H)Xd#s(ebFMw(>Z
zQr;m(o0eowFL;>foqjKaD?n@ww&46!`EYcbe8P}ym-_pq`(3?>t+-Q0;FiAZ`0X}r@Y{B{Iji#L
zJE;e#0fm_3FQAHgz0B8;2R#Y(;|UQCNGx2*KJEg`wBNbRr}*XZjn^txF838xtel#mDA6+ccGWgvp-M{3LOJ`k~e%SY+c{$n8=AF=U5vchmX0
zVYP*$H^)-jv22YwfROm=8K3J?LO#LEL*1@EBT}1
zv?2RT1UNi-j%E08F+sA>B{(a3$Mf5++4~I>8HESrKbsaGeH3id{YFvALAVMUdz2et
z(*3Aif}*ksWJT6Bo@h_Q9-k3tZND9s>CmL?80@l-k5l?9(d@vPuFeNt3*Jl!s95G+gT?m{1#<955(!#RUOk+5kAI9
ztFPvN3ArzQyFJ3V5}B0kR`*WhJ+c!eW>WY;#`4jUUnk1?wMNQ7{v$mTb=fTVf$3Gn?gPfO{i;4K5btd0ZaTJk)kxo9g
zt3uHYX#=3okcEX9X-z`Uqlzou&T~Ls`d!^YHJV|Bh(NID-jo6>-Eg}Z@9YG;F?h!cy*UXK1N~>lUc@??
zcqL9L7S^3E3$gfQ{HiT0A>CxL{>-@r$F(9o{rzRP{J)L_CK=86B{;=Aw^O{}pq{K@
z@t&3rirUt8@PvEJ_Oy_?vSEGn&Vu$A6u;`{xBb7zeW*2#AIqDIyfMP+qN|P=sBB7}$kc1itks1N%Jv0f;3L?@10qIggNg$yk5J07O
zDIs*ELqd@fkU-!L&$0Z*^M9WEzvI5Rd6k{L#@chu`CW7EjJ-a4ZrlJy8Qr@l_(f;$
z5G@Gz3VSfH<)m0mO_aN-8B}+m&UXO{W+{Cgkk+3|9KOn0?RevGV|UG#GXH>nK&1T4
z1|NV%@<@=IzaI2N*V)AU)UgX5?{Mt>_q0sJEh4}Fy|7F#Ja2;Q?=yGk^Y?PjwSlA=
zbY@+cugjza>9aLDVQTwRdQf(W5Tvj^JR3*B7Pj_&FRx#X)`*oW{@*xs_t{6(nqESg_m6Ks1C>V
zjt10^J;q*_tFr|==Y}8OFD$CEt{7!yqxbh#?6n(S=VJX)-`B0REiIZ6G6QY}5*Xv{omx3;^<*caN)jDHbao#sbA
z(5AmWBMh{~SQsWs1MPBg3Ok|Qsgl0XiZaInPjNrq%+0&?0kI>dRgTq);uo@ga8Dx*
zmg0z9S#`}*iD2HpHf@kT?PFDaHYx_n^T)pCsnTZg3~)svOv!cPgu&lPn*Ufz`3f2-
zI>*NQ{j53@)4|pu?k1>tN|;o_#yO^^CJPq!Ca7}Zp$eaNRU;RQfhZzvDUx(hbAu4P
z8e`Rnn2DPIB!z66cwl^Ry6|(ITx$p|5Evdd)mjP4jhF??luH0(fNl$z0EichdLfde~&@bYB{)Q_=aZz8o9*w_xZJHxBh+w-gKAufe!Pv
z6!3}W#}N_0H{J!2;^X&uQTbez{$^5gV8b}R)HVhArK~tUySN1dc_^-81rhdmUZb@0
z2HD2EGn6CDh(}$pLJwO#^W4}j|K*V;FBx5TUpS)>=52tiDzo)+<$V$n+{plQ-)C?gHs(O0dwoL}{+R#84d+)u#k=ohai
zy2bb<^AIear+Ics2>5oCe#iB7Lt4~X-(qQVpR6bipc@T$(F@=b^IbyT132}0bK9>^
z>w-l25qbZ+t`D^0V@**rPjvcWj#C=(qaa}Dc%b`8v&35S%y*-j+cDL1-5Sx=bA&1f
z^(N&_A%EGdpC?8;NZzJE5eeo9!V8;M2VJptm1?8EuQo0|Q8h%PEw$uZ0
z8@e4-h6-7q83ek2*iv>~xy^+Z8}fp9>B|{uN8mf|1nF#lDT|)X3y*Re^3yzSvdP{q
zE;zoH&LN6~AB%bCDc^S$ioGS)n2^iwG684g5$TJSiey*{qS%yGxGI8=@^Erk|2mFV
zbkFN^$qqZmES<$Ba$o7{6|FAw*dx`f;5B(fEfhN}B&Tj^%8j9PrV1{`ZxYerh<;HU
z*tYK7_Be_Ip$5OTPO7nXt$Iub%{HZC>h4;L(d@~0>n-DF4vFfW^3iW|6p}UiOo~`N
zo-uAPuhTZr92fET;u4AolGnc?bAd7POoTA^TEKLPS+E2o4dz#cDDz}K&oAEokSv83
z+;)8~mXAHG9XBHjBzfi8#lE#d8dNfoPMgHzp3IX9uZ4ET?%4z!xmtO(l?CvrY9oFA
z*w?6l93@6!@!g2r>BrwfR?d~{L|mWtaurS<9L9EYLBnL5PK^%hOy%5TZXUQ$rMeY_le
zuc%a0W1{Nh7LEZ8DWn9BiM)NLw7r5Y{PN@sRw1wGWpgzk=Z^GJqBgGZ7*
zb->Qmv85V}4=0u4r16v+4}4DLZdGMttL|ybX^apzr9sDEF0#gw=$mNAD4HO%{aV#qAK7RqeeJu$OR?*>>=WbCq4v44HCtwZ
z$ZPU5b}6Ni;6QB7WBoh%{S)Y6L}+-_*M$PFTnAOsS{aV+X@oesI(o@K3#w8_Goy-CdSHV97DP%ptVZ^s!$0vUw{aC8Z+HxS-rh^c&F
zS&_PeD|FC8i=XZF(=m1d8pRlFfwqf1*)+WAYTLSHJrZX1sAy3Rk;irq2nECGO?7b!
zFRIc)oJ^0G3TljdE&ApAVDK!9$>?!kd#3^C?NB0i4lqLNsZ{JHq>^~^zGqR#192`X
z5%N13I&IuuWfhvO1)P&XayL&k-iZ}ci%=2#cfbbe5H
zKO0o$uwsgT&DUk}5#f3kZ34#ASvQCItB+
zY8J-boSSp~rnt3ls#PrD7Qivf_3rgduZQSkqMYdQ>fSG+LZ^@wl*D-IVN}fJI9^h9
z0<7B_xcI)7Biqqbg+=k(L$$b>-oHb=B?ScA`q*5xs4E7e5?)r<^Q-hT_n)(k6qcW(0wcs`mEZc>X%n!R~9mA^|k4N~ZQCWe;r3DUmO90@sXKCLXGmS+dy9
zCISvgfUM$U%RqgxC-I(KB`37NgurYrIETk`rsPy4_
z_#65D6(}0exg4(wWG{99OqZ#iAxYRLp|M~9;pm{(L+IkY6djS~Fv)vPtf9(+^K|Q(
zXXki&Iq2oHP_(9Y`63Z!o+Zt{Oca8;&y&jC?H6OuHVcY@mM9&oDAS~vF6z|V{eQXh9=%Lnj$4t>tC9e^
z;Tc!ne0oF))Sb?F=pp9>6}0m8P=0;T5Gc3T
z0zTY*F(tN-c{4<75~VA0mTk5nf|xiYTyPj6r5~aMCO$3$9R%2a5a`5?Z-HM@k@?%!
zkA>_AJYCTO>O$8cpP@BF4U@6S+PoF(DRi!4Q3HgUXD)agcc_bbtq~HWF4o?_C(1J7
z5J<@CZ+Va1kx=4^4~z$vj-LV0XFRi4BrWjm#Amlc!SVXOcur?}92lpHk^)Z%R3?i=
z<0qcxH)AXeYLk0Z+vDQF5TbcQdCT&&^kpET{60IoDcl9^R^2cMV!SN%0%56h$5LPf
z-~{R>D`-P@h$AP-oaAhrbTS`=F=i%D9pXierPl+k`kKsn3Z(TpCI&d|zY6%$VDsxU
zB*jIuIFvrUcIf-{%3BigQlNZQLLt`MBwrBI-s8!#zBWUOK5u_9PK}J`nv8mVMBc|_
zJ7z90&k4O}Iwrr2#dd=?lmbqN$yKjA6rEIwQ-qmoRb5H3LMX#C1QfwV&EPf}pi)-6
zrB;-_T$YapYr{e|jKM6|SP>*bJCvsu!MGQbg=6;eebU}TdGlAtb#$gH`7n)${uMA+Kq
z4Y})M&TB#r)7|w$pjHs{g+!_NU|J+EM^`BHm4CG$PfY%e)RnB-0WQf_m}frJVtfTK
zgB7^5@y@eL157w~9vQ*po&dQe5pV-F3plhu#9lurz8MbO^^#AWlcxy7V+Z8zsi$pY
z_us*D1!q9@Z%cg^Y*BKNOuM8rUsI`z?Bq}KidtCaO`W5i6`1wnC4Do`W>+<)ua*v6
z@ijs||NJNIltTMp+Rw{-S349p=4?>L`ye;OoXrr|Y^Kffl=<6qu@|
zV2;*s@XWpF`$j^KDhja+M7n_sTbn!iS|hzx4jv$r^WFr&EYzH*Vx<+kLOx
zVFB@%a9QT)2#Zz~q4G;GbKWoHYtryoJG6lrj4HtjT#fqVhpNATSGR5sYGMZOliz~w)XUeG87(kOyLrA8W5Ew^ugx(BJ;e#>EX8wF<3|waO`m9
z5VkLq?&4KGjMO=rO%s2;c|we2lo@$MUa-KhY6Udpt^_NDvX-|l2#9ukng6iDtR8Tv
zfn8Lc8{=q{R){4!=%AnT1w7Dbyu1p4mE=^khU>Kg@}=LoPG(Bmq&oF+&e^@q(IlrB
zt*c>hsuN(Ipu6zT5U&r?NJri|U;(yX$}FVrGQoR+TGJU0I2Yiw$y@h`v5?ZZp1ug3
z1GYqG``Y7rIYbzmQ&(e0*s{KTsEr)1&pvBBeX11!`syO<8cI^J9d+vSNuw+eIunI-
zVqOKk>uJ_uPQv;jGF92PDFt7Az0~O0n5*!KbG$P(HQbxd2@dKWjAa`x*w~ywPT(ns
zMO?&?7y&zc(L83$FyL@uPOHDrW>l#Z*rQC!!1DN}P;nfqd~PDCEMEz`uPQY6j><(-
zU#Aed%9IGfKx16#L^M%C#`Qi2)Sh1HL$(_Mq?`2z)b3U(-%K!*W7$0datbZ21h~1^
z?!N-7G*hJ~fv7Q|01t%0E6r7+L)=+83{K}dM8#Bsv+jnJpZ0hW^WI^ll1NN|31>!?`l9<*`*S
z#ODW7<~8JwE4z`K2Emo%!l5!%BGo2F0Xm*>{Sj0HU%|j_fjcecd6k4+i)1Vh`OMd0
zGtPHC=X2qi1&I*ezpe8TK+DGPP;Lf&`P)R
z&Y77&C8;8AyX>C1RPX>F*_P)SaO8`xmnnTdi})~OKOCNU>g$aow_;#_Eq#_c@6k)z
zQt85-K^aOHWFqqH96@VdN`?*t7I#uCeb*^Qp+MBAwmo+&Y6Ac`x`J?7NZ+6c1d@Siyr-?$vmkVq2;XC5dzqTwi55d<@37EA26|mMXy(1_f#hj#qL#*XCohJe9?Kx~aA~<~@X#4orR|8nUm|}x6aD;~MGwel}xih%}n+hI7
zh`fTw_K)^O5^zg1j?vT3biSot4@=s>YjIbULKTwN
zR++DS6*;&C9OvasjNj8_fE)1j63%O(M7l1;H0188Rez|kJvVv-fBM5L-64vn62IzQ
z>v^}i1s?9ULd9iLA<4oPmrb6Rwo2syX2LAwgCN@%oE=qez5;!UT?qkjS8q`AOG`s<
z3{a1q7TfIxQ;Ge_Br0LUAv$m_gPvU5vRbir;x)mn%M)l)mn@&5A8-%fuSJ0ayyXw^
zM9*>NeYjL~3uew6y_E#ei-G>-5BXZ=9$I;g@Z;&2osGPll+w!XGo=Uov^*kLfliXa
zeun~m@~yLok9h&{3WqCb9<`~ZVG%*STO8>z!RG&o(J_*JO@840s)$4vdr+9*9B@}P
zAjm8rvfnw0PJ7{BAW7fToF^&USBrLann2Am7fY2<&+^9Rmiy#J1Km|4gptbopSp-#
zqSn*BWi>&63X}0`7uX{XojEV-aLiO8%GvH*#Z!)Qk|ephl?}%KL$bk%HnR
zS=XrhSsI2f`nRdv!c>=5lCu3m^p-Uz*JhS*3e1d*P|1>b@G9gGPbU2qQ)y0igUQ4TT
zyj!IJ<)uE#qznvUT`65|RnJT5Rt5Nxy%;>4)1Xr%sB|tyIU1&A2}X5Pg>zQZ-ojjg
zyQq__i|SWNQh6Jcm)fSGX;0YR7^{Y5nMSoM(f#mS8UX~;Wm0$Cwsvb{MmTX;xWc>k
zpie5^29x<n(_YcFcqzA+E7L<}_Mg
z_oZZNY`N$T;+`>m^Q)#hE5)SQN9mZ;K$FyJ3ClU#veLI8>rg%x;C4OEgr2^ljSu{w
zVk1D%1}~!v)R{^IQ_}J$*`)w+bn&=v8D5*+8>iXXjNWmi70`q@W}ie*{falY
z&y9YO=};he>n!Jwquj0*NoB2a~-G$t@?les_#qEVKrF9;v`}Q4O*#D*-0qs7@JNt
zeR6*Uc-$;Vi(Q
zYtWj+)1VtOP$qs0aJ=J2FG5R&yK;ja7nFM&GGiBU`?GQaY#HSZonSLHf8W)o44NmM
zuXI=MdJHsptK9B%vEMeNh+hT0LnCr!Nu&BZ+Tsh?&q|)D*1DAxSlJ@w3r~ZlCpC
zT<%qkbV_Ph8d|1HULRTRz@;O~;Z=06OG6j-B#RKl2gnuBR+lc9SfvAA0p)c+Eqj&X
zvF`8`3Xuj+7sk#9JZXZcsM?`R&R%~qU$Wuw^krik#;-R>f1;CnA%L2m$jDWuZH}^;
zUOLr>C@|kz$(7m8q9{JG4T5MQbqA&t00Iw!!3L?L#=YI&U*#F=m-WnJ>+7rgCyQ)!
zcoRPh)*7>|`XlRVvrB=EW24-?@_XFt*tA3pSfnXAGgYeK`;phvh5Njp6bSTJ)HupQ
zzsz-E9r$N4dFeAYYx=Tq(QpAs>MC@j7I174@pSnA6ewyq&(q-FOWqK9sbDfylyU07kaX*r|BqGBMpoX~$yH^q1`&9#&$5>!|
zT&aAcqD+?Z^?Fgufh08K0M#(2oUsvn0hd=+<6v^{BrL+9Z95FH>PC)801h1HH*@C)cl{Ar1(m+68qdqqk6)%x)vMowt
z7zFQG-Ul6@Z4t8PL5IzkZ`R)lOXiGMI2^P})!Jg9FuZdp4UA~
z@ajLCCtN7^_w2$vwW{%A_ZQDHv;u8HK`|R-%xsnN$QNH(cx1g^=?#Lh(A-;7&S*og
zd;A-d*TW)#5`MIH5&i8&i+f9WDo%D5FoErU+0>e67(gA~=`%_5-&m*ml3@8jM3sq)
z4ydX8jvea5NuXBzRr1JP?^ax`WoX>A*>z7HZ6Q})l!&fnga*Z(F|N028QDo3{tyjg
z&@fqDoI1#ftm}7{M3tsm!udic+=a1oAZ$M&TbJ{?)Koo96Ye#fa;3Rzaen2wiU>hX
zLoKuPB-K_{E^qWayAa1JCS;qYbavV=;#%v?g
zzK>*i(&bFyG!|6U;~h&ahjpRPI3vdAneVcT$$OH%B=}TVmOVtp5c$zjdP{auUFpP8
zYkxA4{?F@RN#Jfsbj>?$`0icMHO?kTreU*^x4ea(V
zjL{e^%;5su&|*CPiVvlcH?UNHsmnTPu3nx%<2Q~s_4vtVneq^lv^55~r0+FX3O0v|
z*iw$jsp7}cR3*ez;mhjaOSfJv`Y6Wwv|}8A5ksg+r9*;>hGOxX7_cqszR&_J^!{zk
zLn!$*wMM`ld9qhGWJy6Fo<2j*3S6GS!*nG>5w$RY`fRTdqa3A6HVqwuAoc4!rtGHX
zXGyy5=CNv|=V*G9u?jq(H!m$bAKg@aT~SQ5h(5+$mkNU>9ihhi4s?UJ-*?%_u-052
zcqsoccfuT(NPx~llaAn`bcH+nkV|p>0m!Gi{N&lufE`HAu+W585W)7Zk)1>#F*B?%
zE&Pkt2}pn;tXjd~rSM|>`&e<7?Xj%3PbCcCX?{^-&LR4Nxtgq^5JAb;7>PLdiPT
zxbwDrpJ<9cWa4TFq=8HLLUjWldenw-ZsDtHt4|rN>9itWbvL>;0N+W(mMhh0BBV7y
zsF#(&`s-{x7)M=*z=5(a>4HE46p^j#Be*S)hqceR;RSbqGC#8m1(7PQ*ydS*
zKPbOlsP?RS@Uv)2_94nk4iTiJ^WQMHs#h&<^F4R#y2PMhJ
zXtysqF>V}ZdcxwA6!oF_?;m@_J|;1ulA-fgta@)$yV890twpeo4*qxMq73xyJ)x<
z1YV^1zc82>a4qUqymAW$>Gv&z853MvaTF(++ooG+N;=%w1!!M$&Hy|Kt@HQi*yFbWD+cZ;VjKuBK1Oml{OA*~(~qt&u#>GEMm{Q)nv+
z0^>CwGYYZ5cYmh>TC4CAGMQ6VEA|DD097fHMj&pb6ncK@n%_F5GgN05$`U)sleZA#
z7=XyTE61qh5`~P6WCRr~q%^>63U(m~$UMo4gqV#}ejD5GYxsev&qdE7
z-mg<`OZlS4w~B%}HqAKnMG%od7YY9soT_6g5m~I~;FksRGV>|x2R5EkI%jB+I#@tw
zV1(qd*5YE}=+EM*FVvQ!0(18tO%1G7jhQ-|DPBDs*84>WodUQT%uFQ?7nD0v1G*>|
zy|RoGgE5=2%4&H;NZ7b*c6A=b5~dGI5c2qwxmKSNr5&obj0L3O30J>ljU8|ln@Ul87483p6E
zDW4&Il5JHf^)v7En1zzeK3fSf-cXOZG3w8JeFHe;38eqkt#n(AV0emhx%k!w)~hbC
z&fT_}f@JzCSr~9{s-5G{^!uH`8^h;Kp~gcbjC+Ik!A&Rv|o*jxiMCJnvILf1bnct@|cvHrJ!QFoxcyntsl(
z?O6)J&pjm_dlOY2)ZS-DKSIq)&WkF2Oa_)`Smr?4wW(oFUJf*5)=Ds+S6{*m@>*{r
zkBEA^AVI^D*q1m3n1;#Z*;$QHUD_IvK!x0i?C&yS{SUIhmBYOw5S%u-pRD$#)l1C}
zYl!vB>X99H^1>wpcA~#jw|e0dydGd(oI6OuIXd82@zlSADdHc|ymYm@-QvJ8i7AP=
z5+US$eJIPmO+d|pXH5Ti?~CWyIq`ztcxXW$WL$_-l)fo*(+IBHrdTAq$GphtL7C}4
zL$=Q?R%fn!rwMztNpZzNOrkNan+05`Io6?7<-W~6Y3s2xX=^in3NhbU<(Tt+%h*`b
zp#pZeL;w|0V^qgQE-{sZ@uu!Q=A|E9;8xfenZeTU5c>1z($Oz3nuQOM5cK>m+$UCG
zSNs$uQbZ~K>9ra&v0>5}itdu<62Er^s2TO$Nz@f>uu8;!HcQ0EoZ#4eqL@U@cifMf
zSu);8K}{lGVA$gXjlA#yIVMsAEfzWNR0Y?cCPO&wt(w-2-J3!sp;Dz(g%GGYG$}z}
zQbfmv*Uwv=@h$_(3e%YN(piPxU1*RQO3LRy-U)Mb-0lwsm4uEs2pB|Q8{UcZBvoWG
zFN9R-7*NM^>E*+H(gVP%VOy)&$1$;~r0{Abf|$ZQb~{mAryH|njV|uh_dI@GVdhTl
zxiXMeg=0#!7q&wQ7gEv-w~2l|4Iznkb;25}a{B{{jS*cMrDQ3d(^4|cm=8b*)ak?k
zfeWJ0p8k;6v$F?foG4=85(+60%i8D3r4v4~ZuJa6S?P``;hCZClbogN@_t6^((Q}A1wpjvZroxl9mo3$NgtHJ
z`jd^MS`ymn@%uMcWJhmR&JCM?kT;9!qwLj!5bY{EGOKw8+Hlm8b5b7V8)^m$G{?;N
z&uxFmP7cFYig|a;VqZ!Mt=P}cnSKcyNYdR(|EBtPV193ZDHVxzt-fk|3!v%}=GoES
zsV6Eru`WYnRAReuo`i2w2MwNB(>GO*za`Xm6z|fV27=42O-?>#gN$e0_8p=(yoI8hT;IB_a~Y4s$hW>2gpdC-V-xnkg{
z?N%gVXN#RYlXn0%iL2d}d!m(=2rl>-aa^xD5?g9!yd&VaYvEC@&96+PLc~nLLzT9b
z!cbRRoO0loL7BX6YR@aVhp`~s>~QV>SOhoG+&w5H!|vb||>*sMCKny^y*rLO(V
z*BdJ3@@Qm4RW;vVn=t2;b~A)T_ePNFk4K&SKC`5-87Pzauo{g~rKy?J7vI;*xRY((
z@-@SBTdc0~szCQ+e=X#^^11!+zS8~jB980j5^$GYabe-?pvb(%i1~wHrjL#NgVLe#
z7yjXa9*OAZHk=2~=NrJIOQ%sYu}5+QG1T-Vyw-xdKC25i>!#e=+S|{V+GRSb|2*Yx
zd3o(kW+tXR?(1bI!H~x1|7`GI`YbyR@h`fm1KLjJTUZ!=9YLVfQMqYXGZlhg>`3ly
zmJ|!A_Lf{Vs}OV_dC|bc^mQUO&sqZ|_Rku?^!4l^33k8xy(ef^PU<=un*9UYeftIZ
zGKj)E+K=1Eby!~5@
z`ndQ%7?lLMdB%;_8!i45%6j$?9|=Oz7@J%_`KddfU9?(y5_kVxW0K4~`=m4AH_^c4
z*!|&g|2XV7z)+Y+Of>R?|F-huxSV8y*Y=+=EmepJ;#O*ZgB4F}d`z`T6DmeJznF-Z
zZMY#?`_NCLqVI2NX=pRn7wOi&{PcB!JN^bN{!gD7^yXQo`r_Yx-r4+^H6ttWhvsNF
z*{u;c{T~fFKE6zLG&THf_G`}MsOL@AzsL1-@R^?c#E1Of{LpC?fIwq7+nc1H+|uDt
z{*+i$6SI&h{9mFG__^g}>@6kT-(6}ze9Q6|?{aP*KKuiodi(fb;bp9xWW2k|wI9bH
zHBimMUgm$$4~^7!=rFBzj-+4V0`jHBfjis(IA4^$Lz?>!8xPk{Bk)
zC-D&udVaBkrev7F)Sqa2>XxZB{E_j84Keb+HBg)G{gg;FD|YhCe^7gq-e}nJe;ixj
ziChFL@qd`L;4?O1u!DwIxS)S9$43_}25!@Ii2EG>p1u3COFg^WKL_;Vg^@qZJ@X3j
zGg41Sy9rIdNYSY-T+yG90CIzeek#IB&xnMEk?Q{G_u9!{2%@L%sj{a3)&~lNmEXAN
ze?>lD|D>UQ^P#`^%3n;V1~W7)Fs;-4*PM7?jNNax4Bz^N0j&_Zw0;ilP2+`MUn5wt
zKS$T|@H5o>ROG;~7<5?l(D@nC{}_K#m6;Uv3%0G4UGw)MV!6pX}*x`*;mH(^);<
z)&El9J1_H7VS+!Rtq#54)`2)AFf&|K-c+(U;TD{DPD2SVL(#M}8>@kc;|R>~^{h
zcj2c4AA0JZD!>0fIB@u | |