Merge branch 'master' into 21255

This commit is contained in:
NeetBhagat 2017-06-01 12:46:14 +05:30
commit d27ad061cb
17 changed files with 224 additions and 212 deletions

View file

@ -31,15 +31,18 @@ These are not placed in your normal build tree when doing an out of source build
If you would like to use a specific install of a dependency instead of the version that would be grabbed as a CMake ExternalProject, you can pass -DUSE_LOCAL_$NAME=0 (where $NAME is the name of the subfolder in [cmake/externals](cmake/externals)) when you run CMake to tell it not to get that dependency as an external project.
### OS Specific Build Guides
* [BUILD_OSX.md](BUILD_OSX.md) - additional instructions for OS X.
* [BUILD_LINUX.md](BUILD_LINUX.md) - additional instructions for Linux.
* [BUILD_WIN.md](BUILD_WIN.md) - additional instructions for Windows.
* [BUILD_ANDROID.md](BUILD_ANDROID.md) - additional instructions for Android
### CMake
Hifi uses CMake to generate build files and project files for your platform.
#### Qt
In order for CMake to find the Qt5 find modules, you will need to set a QT_CMAKE_PREFIX_PATH environment variable pointing to your Qt installation.
This can either be entered directly into your shell session before you build or in your shell profile (e.g.: ~/.bash_profile, ~/.bashrc, ~/.zshrc - this depends on your shell and environment).
@ -51,6 +54,7 @@ The path it needs to be set to will depend on where and how Qt5 was installed. e
export QT_CMAKE_PREFIX_PATH=/usr/local/opt/qt5/lib/cmake
#### Generating build files
Create a build directory in the root of your checkout and then run the CMake build from there. This will keep the rest of the directory clean.
mkdir build
@ -60,6 +64,7 @@ Create a build directory in the root of your checkout and then run the CMake bui
If cmake gives you the same error message repeatedly after the build fails (e.g. you had a typo in the QT_CMAKE_PREFIX_PATH that you fixed but the `.cmake` files still cannot be found), try removing `CMakeCache.txt`.
#### Variables
Any variables that need to be set for CMake to find dependencies can be set as ENV variables in your shell profile, or passed directly to CMake with a `-D` flag appended to the `cmake ..` command.
For example, to pass the QT_CMAKE_PREFIX_PATH variable during build file generation:

View file

@ -1,6 +1,7 @@
Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Linux specific instructions are found in this file.
### Qt5 Dependencies
Should you choose not to install Qt5 via a package manager that handles dependencies for you, you may be missing some Qt5 dependencies. On Ubuntu, for example, the following additional packages are required:
libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev

View file

@ -1,6 +1,7 @@
Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only OS X specific instructions are found in this file.
### Homebrew
[Homebrew](https://brew.sh/) is an excellent package manager for OS X. It makes install of some High Fidelity dependencies very simple.
brew tap homebrew/versions
@ -16,6 +17,7 @@ For OpenSSL installed via homebrew, set OPENSSL_ROOT_DIR:
Note that this uses the version from the homebrew formula at the time of this writing, and the version in the path will likely change.
### Qt
Download and install the [Qt 5.6.2 for macOS](http://download.qt.io/official_releases/qt/5.6/5.6.2/qt-opensource-mac-x64-clang-5.6.2.dmg).
Keep the default components checked when going through the installer.
@ -24,6 +26,7 @@ Once Qt is installed, you need to manually configure the following:
* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt5.6.2/5.6/clang_64/lib/cmake/` directory.
### Xcode
If Xcode is your editor of choice, you can ask CMake to generate Xcode project files instead of Unix Makefiles.
cmake .. -GXcode

View file

@ -24,7 +24,7 @@ Go to "Control Panel > System > Advanced System Settings > Environment Variables
### Step 5. Installing OpenSSL
Download and install the [Win64 OpenSSL v1.0.2k Installer](https://slproweb.com/download/Win64OpenSSL-1_0_2k.exe).
Download and install the [Win64 OpenSSL v1.0.2L Installer](https://slproweb.com/download/Win64OpenSSL-1_0_2L.exe).
### Step 6. Running CMake to Generate Build Files

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -438,6 +438,10 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
const char* portStr = getCmdOption(argc, constArgv, "--listenPort");
const int listenPort = portStr ? atoi(portStr) : INVALID_PORT;
static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset";
bool suppressPrompt = cmdOptionExists(argc, const_cast<const char**>(argv), SUPPRESS_SETTINGS_RESET);
bool previousSessionCrashed = CrashHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt);
Setting::init();
if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) {
@ -458,10 +462,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
QCoreApplication::addLibraryPath(audioDLLPath);
#endif
static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset";
bool suppressPrompt = cmdOptionExists(argc, const_cast<const char**>(argv), SUPPRESS_SETTINGS_RESET);
bool previousSessionCrashed = CrashHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt);
DependencyManager::registerInheritance<LimitedNodeList, NodeList>();
DependencyManager::registerInheritance<AvatarHashMap, AvatarManager>();
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, InterfaceDynamicFactory>();

View file

@ -24,12 +24,15 @@
#include "Application.h"
#include "Menu.h"
#include <SettingHandle.h>
#include <RunningMarker.h>
#include <SettingHandle.h>
#include <SettingHelpers.h>
bool CrashHandler::checkForResetSettings(bool wasLikelyCrash, bool suppressPrompt) {
Settings settings;
QSettings::setDefaultFormat(JSON_FORMAT);
QSettings settings;
settings.beginGroup("Developer");
QVariant displayCrashOptions = settings.value(MenuOption::DisplayCrashOptions);
QVariant askToResetSettingsOption = settings.value(MenuOption::AskToResetSettings);
@ -106,7 +109,7 @@ void CrashHandler::handleCrash(CrashHandler::Action action) {
return;
}
Settings settings;
QSettings settings;
const QString ADDRESS_MANAGER_GROUP = "AddressManager";
const QString ADDRESS_KEY = "address";
const QString AVATAR_GROUP = "Avatar";

View file

@ -25,7 +25,7 @@
#include <QThread>
const Discoverability::Mode DEFAULT_DISCOVERABILITY_MODE = Discoverability::Friends;
const Discoverability::Mode DEFAULT_DISCOVERABILITY_MODE = Discoverability::Connections;
DiscoverabilityManager::DiscoverabilityManager() :
_mode("discoverabilityMode", DEFAULT_DISCOVERABILITY_MODE)

View file

@ -13,6 +13,8 @@
#include <glm/glm.hpp>
#include <QRunnable>
#include <QThreadPool>
#include <QDataStream>
#include <QtCore/QDebug>
#include <QtNetwork/QNetworkRequest>
@ -49,13 +51,43 @@ Sound::Sound(const QUrl& url, bool isStereo, bool isAmbisonic) :
_isAmbisonic(isAmbisonic),
_isReady(false)
{
}
void Sound::downloadFinished(const QByteArray& data) {
// this is a QRunnable, will delete itself after it has finished running
SoundProcessor* soundProcessor = new SoundProcessor(_url, data, _isStereo, _isAmbisonic);
connect(soundProcessor, &SoundProcessor::onSuccess, this, &Sound::soundProcessSuccess);
connect(soundProcessor, &SoundProcessor::onError, this, &Sound::soundProcessError);
QThreadPool::globalInstance()->start(soundProcessor);
}
void Sound::soundProcessSuccess(QByteArray data, bool stereo, bool ambisonic, float duration) {
qCDebug(audio) << "Setting ready state for sound file" << _url.toDisplayString();
_byteArray = data;
_isStereo = stereo;
_isAmbisonic = ambisonic;
_duration = duration;
_isReady = true;
finishedLoading(true);
emit ready();
}
void Sound::soundProcessError(int error, QString str) {
qCCritical(audio) << "Failed to process sound file" << _url.toDisplayString() << "code =" << error << str;
emit failed(QNetworkReply::UnknownContentError);
finishedLoading(false);
}
void SoundProcessor::run() {
qCDebug(audio) << "Processing sound file" << _url.toDisplayString();
// replace our byte array with the downloaded data
QByteArray rawAudioByteArray = QByteArray(data);
QString fileName = getURL().fileName().toLower();
QByteArray rawAudioByteArray = QByteArray(_data);
QString fileName = _url.fileName().toLower();
static const QString WAV_EXTENSION = ".wav";
static const QString RAW_EXTENSION = ".raw";
@ -72,31 +104,28 @@ void Sound::downloadFinished(const QByteArray& data) {
// since it's raw the only way for us to know that is if the file was called .stereo.raw
if (fileName.toLower().endsWith("stereo.raw")) {
_isStereo = true;
qCDebug(audio) << "Processing sound of" << rawAudioByteArray.size() << "bytes from" << getURL() << "as stereo audio file.";
qCDebug(audio) << "Processing sound of" << rawAudioByteArray.size() << "bytes from" << _url << "as stereo audio file.";
}
// Process as 48khz RAW file
downSample(rawAudioByteArray, 48000);
} else {
qCDebug(audio) << "Unknown sound file type";
emit onError(300, "Failed to load sound file, reason: unknown sound file type");
return;
}
finishedLoading(true);
_isReady = true;
emit ready();
emit onSuccess(_data, _isStereo, _isAmbisonic, _duration);
}
void Sound::downSample(const QByteArray& rawAudioByteArray, int sampleRate) {
void SoundProcessor::downSample(const QByteArray& rawAudioByteArray, int sampleRate) {
// we want to convert it to the format that the audio-mixer wants
// which is signed, 16-bit, 24Khz
if (sampleRate == AudioConstants::SAMPLE_RATE) {
// no resampling needed
_byteArray = rawAudioByteArray;
_data = rawAudioByteArray;
} else {
int numChannels = _isAmbisonic ? AudioConstants::AMBISONIC : (_isStereo ? AudioConstants::STEREO : AudioConstants::MONO);
@ -106,15 +135,15 @@ void Sound::downSample(const QByteArray& rawAudioByteArray, int sampleRate) {
int numSourceFrames = rawAudioByteArray.size() / (numChannels * sizeof(AudioConstants::AudioSample));
int maxDestinationFrames = resampler.getMaxOutput(numSourceFrames);
int maxDestinationBytes = maxDestinationFrames * numChannels * sizeof(AudioConstants::AudioSample);
_byteArray.resize(maxDestinationBytes);
_data.resize(maxDestinationBytes);
int numDestinationFrames = resampler.render((int16_t*)rawAudioByteArray.data(),
(int16_t*)_byteArray.data(),
(int16_t*)_data.data(),
numSourceFrames);
// truncate to actual output
int numDestinationBytes = numDestinationFrames * numChannels * sizeof(AudioConstants::AudioSample);
_byteArray.resize(numDestinationBytes);
_data.resize(numDestinationBytes);
}
}
@ -163,7 +192,7 @@ struct WAVEFormat {
};
// returns wavfile sample rate, used for resampling
int Sound::interpretAsWav(const QByteArray& inputAudioByteArray, QByteArray& outputAudioByteArray) {
int SoundProcessor::interpretAsWav(const QByteArray& inputAudioByteArray, QByteArray& outputAudioByteArray) {
// Create a data stream to analyze the data
QDataStream waveStream(const_cast<QByteArray *>(&inputAudioByteArray), QIODevice::ReadOnly);

View file

@ -12,6 +12,7 @@
#ifndef hifi_Sound_h
#define hifi_Sound_h
#include <QRunnable>
#include <QtCore/QObject>
#include <QtNetwork/QNetworkReply>
#include <QtScript/qscriptengine.h>
@ -29,12 +30,15 @@ public:
bool isReady() const { return _isReady; }
float getDuration() const { return _duration; }
const QByteArray& getByteArray() const { return _byteArray; }
signals:
void ready();
protected slots:
void soundProcessSuccess(QByteArray data, bool stereo, bool ambisonic, float duration);
void soundProcessError(int error, QString str);
private:
QByteArray _byteArray;
bool _isStereo;
@ -42,10 +46,33 @@ private:
bool _isReady;
float _duration; // In seconds
virtual void downloadFinished(const QByteArray& data) override;
};
class SoundProcessor : public QObject, public QRunnable {
Q_OBJECT
public:
SoundProcessor(const QUrl& url, const QByteArray& data, bool stereo, bool ambisonic)
: _url(url), _data(data), _isStereo(stereo), _isAmbisonic(ambisonic)
{
}
virtual void run() override;
void downSample(const QByteArray& rawAudioByteArray, int sampleRate);
int interpretAsWav(const QByteArray& inputAudioByteArray, QByteArray& outputAudioByteArray);
virtual void downloadFinished(const QByteArray& data) override;
signals:
void onSuccess(QByteArray data, bool stereo, bool ambisonic, float duration);
void onError(int error, QString str);
private:
QUrl _url;
QByteArray _data;
bool _isStereo;
bool _isAmbisonic;
float _duration;
};
typedef QSharedPointer<Sound> SharedSoundPointer;

View file

@ -241,6 +241,9 @@ void SendQueue::handshakeACK(SequenceNumber initialSequenceNumber) {
std::lock_guard<std::mutex> locker { _handshakeMutex };
_hasReceivedHandshakeACK = true;
}
_lastReceiverResponse = QDateTime::currentMSecsSinceEpoch();
// Notify on the handshake ACK condition
_handshakeACKCondition.notify_one();
}

View file

@ -293,6 +293,7 @@ function addImage(image_data, isLoggedIn, canShare, isGifLoading, isShowingPrevi
isGif = img.src.split('.').pop().toLowerCase() === "gif";
imageContainer.appendChild(img);
document.getElementById("snapshot-images").appendChild(imageContainer);
img.onload = function () {
paths.push(image_data.localPath);
if (isGif) {
imageContainer.innerHTML += '<span class="gifLabel">GIF</span>';
@ -306,6 +307,11 @@ function addImage(image_data, isLoggedIn, canShare, isGifLoading, isShowingPrevi
if (isShowingPreviousImages && isLoggedIn && image_data.story_id) {
updateShareInfo(id, image_data.story_id);
}
};
img.onerror = function () {
img.onload = null;
img.src = image_data.errorPath;
};
}
function showConfirmationMessage(selectedID, destination) {
if (selectedID.id) {

View file

@ -82,11 +82,23 @@ function showElements(els, show) {
}
}
function updateProperty(propertyName, propertyValue) {
var properties = {};
properties[propertyName] = propertyValue;
updateProperties(properties);
}
function updateProperties(properties) {
EventBridge.emitWebEvent(JSON.stringify({
id: lastEntityID,
type: "update",
properties: properties
}));
}
function createEmitCheckedPropertyUpdateFunction(propertyName) {
return function() {
EventBridge.emitWebEvent(
'{"id":' + lastEntityID + ', "type":"update", "properties":{"' + propertyName + '":' + this.checked + '}}'
);
updateProperty(propertyName, this.checked);
};
}
@ -105,13 +117,7 @@ function createEmitGroupCheckedPropertyUpdateFunction(group, propertyName) {
var properties = {};
properties[group] = {};
properties[group][propertyName] = this.checked;
EventBridge.emitWebEvent(
JSON.stringify({
id: lastEntityID,
type: "update",
properties: properties
})
);
updateProperties(properties);
};
}
@ -119,10 +125,7 @@ function createEmitNumberPropertyUpdateFunction(propertyName, decimals) {
decimals = decimals == undefined ? 4 : decimals;
return function() {
var value = parseFloat(this.value).toFixed(decimals);
EventBridge.emitWebEvent(
'{"id":' + lastEntityID + ', "type":"update", "properties":{"' + propertyName + '":' + value + '}}'
);
updateProperty(propertyName, value);
};
}
@ -131,28 +134,14 @@ function createEmitGroupNumberPropertyUpdateFunction(group, propertyName) {
var properties = {};
properties[group] = {};
properties[group][propertyName] = this.value;
EventBridge.emitWebEvent(
JSON.stringify({
id: lastEntityID,
type: "update",
properties: properties,
})
);
updateProperties(properties);
};
}
function createEmitTextPropertyUpdateFunction(propertyName) {
return function() {
var properties = {};
properties[propertyName] = this.value;
EventBridge.emitWebEvent(
JSON.stringify({
id: lastEntityID,
type: "update",
properties: properties,
})
);
updateProperty(propertyName, this.value);
};
}
@ -161,62 +150,44 @@ function createEmitGroupTextPropertyUpdateFunction(group, propertyName) {
var properties = {};
properties[group] = {};
properties[group][propertyName] = this.value;
EventBridge.emitWebEvent(
JSON.stringify({
id: lastEntityID,
type: "update",
properties: properties,
})
);
updateProperties(properties);
};
}
function createEmitVec3PropertyUpdateFunction(property, elX, elY, elZ) {
return function() {
var data = {
id: lastEntityID,
type: "update",
properties: {}
};
data.properties[property] = {
var properties = {};
properties[property] = {
x: elX.value,
y: elY.value,
z: elZ.value,
};
EventBridge.emitWebEvent(JSON.stringify(data));
updateProperties(properties);
}
};
function createEmitGroupVec3PropertyUpdateFunction(group, property, elX, elY, elZ) {
return function() {
var data = {
id: lastEntityID,
type: "update",
properties: {}
};
data.properties[group] = {};
data.properties[group][property] = {
var properties = {};
properties[group] = {};
properties[group][property] = {
x: elX.value,
y: elY.value,
z: elZ ? elZ.value : 0,
};
EventBridge.emitWebEvent(JSON.stringify(data));
updateProperties(properties);
}
};
function createEmitVec3PropertyUpdateFunctionWithMultiplier(property, elX, elY, elZ, multiplier) {
return function() {
var data = {
id: lastEntityID,
type: "update",
properties: {}
};
data.properties[property] = {
var properties = {};
properties[property] = {
x: elX.value * multiplier,
y: elY.value * multiplier,
z: elZ.value * multiplier,
};
EventBridge.emitWebEvent(JSON.stringify(data));
updateProperties(properties);
}
};
@ -227,44 +198,35 @@ function createEmitColorPropertyUpdateFunction(property, elRed, elGreen, elBlue)
};
function emitColorPropertyUpdate(property, red, green, blue, group) {
var data = {
id: lastEntityID,
type: "update",
properties: {}
};
var properties = {};
if (group) {
data.properties[group] = {};
data.properties[group][property] = {
properties[group] = {};
properties[group][property] = {
red: red,
green: green,
blue: blue,
};
} else {
data.properties[property] = {
properties[property] = {
red: red,
green: green,
blue: blue,
};
}
EventBridge.emitWebEvent(JSON.stringify(data));
updateProperties(properties);
};
function createEmitGroupColorPropertyUpdateFunction(group, property, elRed, elGreen, elBlue) {
return function() {
var data = {
id: lastEntityID,
type: "update",
properties: {}
};
data.properties[group] = {};
data.properties[group][property] = {
var properties = {};
properties[group] = {};
properties[group][property] = {
red: elRed.value,
green: elGreen.value,
blue: elBlue.value,
};
EventBridge.emitWebEvent(JSON.stringify(data));
updateProperties(properties);
}
};
@ -277,18 +239,7 @@ function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElemen
// We've unchecked, so remove
propertyValue = propertyValue.replace(subPropertyString + ",", "");
}
var _properties = {}
_properties[propertyName] = propertyValue;
EventBridge.emitWebEvent(
JSON.stringify({
id: lastEntityID,
type: "update",
properties: _properties
})
);
updateProperty(propertyName, propertyValue);
}
function setUserDataFromEditor(noUpdate) {
@ -314,18 +265,11 @@ function setUserDataFromEditor(noUpdate) {
);
return;
} else {
EventBridge.emitWebEvent(
JSON.stringify({
id: lastEntityID,
type: "update",
properties: {
userData: text
},
})
);
updateProperty('userData', text);
}
}
}
function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults) {
var properties = {};
var parsedData = {};
@ -372,13 +316,7 @@ function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults) {
userDataElement.value = properties['userData'];
EventBridge.emitWebEvent(
JSON.stringify({
id: lastEntityID,
type: "update",
properties: properties,
})
);
updateProperties(properties);
}
function userDataChanger(groupName, keyName, values, userDataElement, defaultValue) {
var val = {}, def = {};
@ -900,7 +838,6 @@ function loaded() {
elCloneable.checked = parsedUserData["grabbableKey"].cloneable;
elCloneableGroup.style.display = elCloneable.checked ? "block": "none";
elCloneableDynamic.checked = parsedUserData["grabbableKey"].cloneDynamic ? parsedUserData["grabbableKey"].cloneDynamic : properties.dynamic;
elDynamic.checked = elCloneable.checked ? false: properties.dynamic;
if (elCloneable.checked) {
if ("cloneLifetime" in parsedUserData["grabbableKey"]) {
elCloneableLifetime.value = parsedUserData["grabbableKey"].cloneLifetime ? parsedUserData["grabbableKey"].cloneLifetime : 300;
@ -1213,17 +1150,22 @@ function loaded() {
elCloneable.addEventListener('change', function (event) {
var checked = event.target.checked;
if (checked) {
multiDataUpdater("grabbableKey",
{cloneLifetime: elCloneableLifetime, cloneLimit: elCloneableLimit, cloneDynamic: elCloneableDynamic, cloneable: event.target},
elUserData, {});
multiDataUpdater("grabbableKey", {
cloneLifetime: elCloneableLifetime,
cloneLimit: elCloneableLimit,
cloneDynamic: elCloneableDynamic,
cloneable: event.target,
grabbable: null
}, elUserData, {});
elCloneableGroup.style.display = "block";
EventBridge.emitWebEvent(
'{"id":' + lastEntityID + ', "type":"update", "properties":{"dynamic":false, "grabbable": false}}'
);
updateProperty('dynamic', false);
} else {
multiDataUpdater("grabbableKey",
{cloneLifetime: null, cloneLimit: null, cloneDynamic: null, cloneable: false},
elUserData, {});
multiDataUpdater("grabbableKey", {
cloneLifetime: null,
cloneLimit: null,
cloneDynamic: null,
cloneable: false
}, elUserData, {});
elCloneableGroup.style.display = "none";
}
});
@ -1258,15 +1200,7 @@ function loaded() {
showUserDataTextArea();
showNewJSONEditorButton();
hideSaveUserDataButton();
var properties = {};
properties['userData'] = elUserData.value;
EventBridge.emitWebEvent(
JSON.stringify({
id: lastEntityID,
type: "update",
properties: properties,
})
);
updateProperty('userData', elUserData.value)
});
elSaveUserData.addEventListener("click", function() {

View file

@ -613,7 +613,6 @@
error = "All participants must be logged in to connect.";
}
result = error ? {status: 'error', connection: error} : response;
UserActivityLogger.makeUserConnection(connectingId, false, error || response);
connectionRequestCompleted();
} else {
result = response;

View file

@ -273,7 +273,8 @@ function fillImageDataFromPrevious() {
localPath: previousStillSnapPath,
story_id: previousStillSnapStoryID,
blastButtonDisabled: previousStillSnapBlastingDisabled,
hifiButtonDisabled: previousStillSnapHifiSharingDisabled
hifiButtonDisabled: previousStillSnapHifiSharingDisabled,
errorPath: Script.resolvePath(Script.resourcesPath() + 'snapshot/img/no-image.jpg')
});
}
if (previousAnimatedSnapPath !== "") {
@ -281,7 +282,8 @@ function fillImageDataFromPrevious() {
localPath: previousAnimatedSnapPath,
story_id: previousAnimatedSnapStoryID,
blastButtonDisabled: previousAnimatedSnapBlastingDisabled,
hifiButtonDisabled: previousAnimatedSnapHifiSharingDisabled
hifiButtonDisabled: previousAnimatedSnapHifiSharingDisabled,
errorPath: Script.resolvePath(Script.resourcesPath() + 'snapshot/img/no-image.jpg')
});
}
}