diff --git a/.clang-format b/.clang-format index f000a27017..505366dd9c 100644 --- a/.clang-format +++ b/.clang-format @@ -1,12 +1,12 @@ Language: Cpp Standard: Cpp11 -BasedOnStyle: "Chromium" +BasedOnStyle: "Chromium" ColumnLimit: 128 IndentWidth: 4 UseTab: Never BreakBeforeBraces: Custom -BraceWrapping: +BraceWrapping: AfterEnum: true AfterClass: false AfterControlStatement: false @@ -21,11 +21,11 @@ BraceWrapping: AccessModifierOffset: -4 -AllowShortFunctionsOnASingleLine: InlineOnly -BreakConstructorInitializers: BeforeColon -BreakConstructorInitializersBeforeComma: true +AllowShortFunctionsOnASingleLine: InlineOnly +BreakConstructorInitializers: AfterColon +BreakConstructorInitializersBeforeComma: false IndentCaseLabels: true -ReflowComments: false +ReflowComments: false Cpp11BracedListStyle: false ContinuationIndentWidth: 4 ConstructorInitializerAllOnOneLineOrOnePerLine: false diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index a42b78a6fa..10b8d44545 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -340,7 +340,6 @@ void Agent::scriptRequestFinished() { request->deleteLater(); } - void Agent::executeScript() { _scriptEngine = scriptEngineFactory(ScriptEngine::AGENT_SCRIPT, _scriptContents, _payload); diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 1ae65290ff..87827a27d9 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -257,12 +257,10 @@ AssetServer::AssetServer(ReceivedMessage& message) : _transferTaskPool.setMaxThreadCount(TASK_POOL_THREAD_COUNT); _bakingTaskPool.setMaxThreadCount(1); + // Queue all requests until the Asset Server is fully setup auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); - packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet"); - packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo"); - packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload"); - packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation"); - + packetReceiver.registerListenerForTypes({ PacketType::AssetGet, PacketType::AssetGetInfo, PacketType::AssetUpload, PacketType::AssetMappingOperation }, this, "queueRequests"); + #ifdef Q_OS_WIN updateConsumedCores(); QTimer* timer = new QTimer(this); @@ -291,6 +289,7 @@ void AssetServer::aboutToFinish() { if (pendingRunnable) { it = _pendingBakes.erase(it); } else { + qDebug() << "Aborting bake for" << it.key(); it.value()->abort(); ++it; } @@ -396,6 +395,7 @@ void AssetServer::completeSetup() { if (_fileMappings.size() > 0) { cleanupUnmappedFiles(); + cleanupBakedFilesForDeletedAssets(); } nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer }); @@ -417,10 +417,65 @@ void AssetServer::completeSetup() { PathUtils::removeTemporaryApplicationDirs(); PathUtils::removeTemporaryApplicationDirs("Oven"); + + qCDebug(asset_server) << "Overriding temporary queuing packet handler."; + // We're fully setup, override the request queueing handler and replay all requests + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); + packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet"); + packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo"); + packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload"); + packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation"); + + replayRequests(); +} + +void AssetServer::queueRequests(QSharedPointer packet, SharedNodePointer senderNode) { + qCDebug(asset_server) << "Queuing requests until fully setup"; + + QMutexLocker lock { &_queuedRequestsMutex }; + _queuedRequests.push_back({ packet, senderNode }); + + // If we've stopped queueing but the callback was already in flight, + // then replay it immediately. + if (!_isQueueingRequests) { + lock.unlock(); + replayRequests(); + } +} + +void AssetServer::replayRequests() { + RequestQueue queue; + { + QMutexLocker lock { &_queuedRequestsMutex }; + std::swap(queue, _queuedRequests); + _isQueueingRequests = false; + } + + qCDebug(asset_server) << "Replaying" << queue.size() << "requests."; + + for (const auto& request : queue) { + switch (request.first->getType()) { + case PacketType::AssetGet: + handleAssetGet(request.first, request.second); + break; + case PacketType::AssetGetInfo: + handleAssetGetInfo(request.first, request.second); + break; + case PacketType::AssetUpload: + handleAssetUpload(request.first, request.second); + break; + case PacketType::AssetMappingOperation: + handleAssetMappingOperation(request.first, request.second); + break; + default: + qCWarning(asset_server) << "Unknown queued request type:" << request.first->getType(); + break; + } + } } void AssetServer::cleanupUnmappedFiles() { - QRegExp hashFileRegex { "^[a-f0-9]{" + QString::number(AssetUtils::SHA256_HASH_HEX_LENGTH) + "}" }; + QRegExp hashFileRegex { AssetUtils::ASSET_HASH_REGEX_STRING }; auto files = _filesDirectory.entryInfoList(QDir::Files); @@ -452,6 +507,38 @@ void AssetServer::cleanupUnmappedFiles() { } } +void AssetServer::cleanupBakedFilesForDeletedAssets() { + qCInfo(asset_server) << "Performing baked asset cleanup for deleted assets"; + + std::set bakedHashes; + + for (const auto& it : _fileMappings) { + // check if this is a mapping to baked content + if (it.first.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { + // extract the hash from the baked mapping + AssetUtils::AssetHash hash = it.first.mid(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER.length(), + AssetUtils::SHA256_HASH_HEX_LENGTH); + + // add the hash to our set of hashes for which we have baked content + bakedHashes.insert(hash); + } + } + + // enumerate the hashes for which we have baked content + for (const auto& hash : bakedHashes) { + // check if we have a mapping that points to this hash + auto matchingMapping = std::find_if(std::begin(_fileMappings), std::end(_fileMappings), + [&hash](const std::pair mappingPair) { + return mappingPair.second == hash; + }); + + if (matchingMapping == std::end(_fileMappings)) { + // we didn't find a mapping for this hash, remove any baked content we still have for it + removeBakedPathsForDeletedAsset(hash); + } + } +} + void AssetServer::handleAssetMappingOperation(QSharedPointer message, SharedNodePointer senderNode) { using AssetMappingOperationType = AssetUtils::AssetMappingOperationType; @@ -1301,6 +1388,8 @@ void AssetServer::handleCompletedBake(QString originalAssetHash, QString origina } void AssetServer::handleAbortedBake(QString originalAssetHash, QString assetPath) { + qDebug() << "Aborted bake:" << originalAssetHash; + // for an aborted bake we don't do anything but remove the BakeAssetTask from our pending bakes _pendingBakes.remove(originalAssetHash); } diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h index 6cbfa76414..00ab27c74d 100644 --- a/assignment-client/src/assets/AssetServer.h +++ b/assignment-client/src/assets/AssetServer.h @@ -44,6 +44,7 @@ public slots: private slots: void completeSetup(); + void queueRequests(QSharedPointer packet, SharedNodePointer senderNode); void handleAssetGetInfo(QSharedPointer packet, SharedNodePointer senderNode); void handleAssetGet(QSharedPointer packet, SharedNodePointer senderNode); void handleAssetUpload(QSharedPointer packetList, SharedNodePointer senderNode); @@ -52,6 +53,8 @@ private slots: void sendStatsPacket() override; private: + void replayRequests(); + void handleGetMappingOperation(ReceivedMessage& message, NLPacketList& replyPacket); void handleGetAllMappingOperation(NLPacketList& replyPacket); void handleSetMappingOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket); @@ -80,6 +83,9 @@ private: /// Delete any unmapped files from the local asset directory void cleanupUnmappedFiles(); + /// Delete any baked files for assets removed from the local asset directory + void cleanupBakedFilesForDeletedAssets(); + QString getPathToAssetHash(const AssetUtils::AssetHash& assetHash); std::pair getAssetStatus(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash); @@ -115,6 +121,11 @@ private: QHash> _pendingBakes; QThreadPool _bakingTaskPool; + QMutex _queuedRequestsMutex; + bool _isQueueingRequests { true }; + using RequestQueue = QVector, SharedNodePointer>>; + RequestQueue _queuedRequests; + bool _wasColorTextureCompressionEnabled { false }; bool _wasGrayscaleTextureCompressionEnabled { false }; bool _wasNormalTextureCompressionEnabled { false }; diff --git a/assignment-client/src/assets/BakeAssetTask.cpp b/assignment-client/src/assets/BakeAssetTask.cpp index 5b9add7467..ecb4ede5d8 100644 --- a/assignment-client/src/assets/BakeAssetTask.cpp +++ b/assignment-client/src/assets/BakeAssetTask.cpp @@ -69,8 +69,10 @@ void BakeAssetTask::run() { _ovenProcess.reset(new QProcess()); + QEventLoop loop; + connect(_ovenProcess.get(), static_cast(&QProcess::finished), - this, [this, tempOutputDir](int exitCode, QProcess::ExitStatus exitStatus) { + this, [&loop, this, tempOutputDir](int exitCode, QProcess::ExitStatus exitStatus) { qDebug() << "Baking process finished: " << exitCode << exitStatus; if (exitStatus == QProcess::CrashExit) { @@ -108,6 +110,7 @@ void BakeAssetTask::run() { emit bakeFailed(_assetHash, _assetPath, errors); } + loop.quit(); }); qDebug() << "Starting oven for " << _assetPath; @@ -117,11 +120,21 @@ void BakeAssetTask::run() { emit bakeFailed(_assetHash, _assetPath, errors); return; } - _ovenProcess->waitForFinished(); + + _isBaking = true; + + loop.exec(); } void BakeAssetTask::abort() { - if (!_wasAborted.exchange(true)) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "abort"); + return; + } + qDebug() << "Aborting BakeAssetTask for" << _assetHash; + if (_ovenProcess->state() != QProcess::NotRunning) { + qDebug() << "Teminating oven process for" << _assetHash; + _wasAborted = true; _ovenProcess->terminate(); } } diff --git a/assignment-client/src/assets/BakeAssetTask.h b/assignment-client/src/assets/BakeAssetTask.h index 7b003fa1bd..24b070d08a 100644 --- a/assignment-client/src/assets/BakeAssetTask.h +++ b/assignment-client/src/assets/BakeAssetTask.h @@ -27,12 +27,14 @@ class BakeAssetTask : public QObject, public QRunnable { public: BakeAssetTask(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath); + // Thread-safe inspection methods bool isBaking() { return _isBaking.load(); } + bool wasAborted() const { return _wasAborted.load(); } void run() override; +public slots: void abort(); - bool wasAborted() const { return _wasAborted.load(); } signals: void bakeComplete(QString assetHash, QString assetPath, QString tempOutputDir, QVector outputFiles); diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 7f088d8183..8af4eec934 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "AudioMixer.h" + #include #include @@ -36,8 +38,6 @@ #include "AvatarAudioStream.h" #include "InjectedAudioStream.h" -#include "AudioMixer.h" - static const float DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE = 0.5f; // attenuation = -6dB * log2(distance) static const int DISABLE_STATIC_JITTER_FRAMES = -1; static const float DEFAULT_NOISE_MUTING_THRESHOLD = 1.0f; diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index ed3a5b0541..8c47893aa3 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -18,6 +18,8 @@ #include #include +#include + #include "AudioMixerStats.h" #include "AudioMixerSlavePool.h" diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 2560f43337..d4d4f847ee 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "AudioMixerClientData.h" + #include #include @@ -22,8 +24,6 @@ #include "AudioLogging.h" #include "AudioHelpers.h" #include "AudioMixer.h" -#include "AudioMixerClientData.h" - AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) : NodeData(nodeID), diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index c3a31715ea..514e1c9756 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -21,6 +21,7 @@ #include #include +#include #include #include "PositionalAudioStream.h" diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index abc2f7220d..ccdd2fca4f 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "AudioMixerSlave.h" + #include #include @@ -34,8 +36,6 @@ #include "InjectedAudioStream.h" #include "AudioHelpers.h" -#include "AudioMixerSlave.h" - using AudioStreamMap = AudioMixerClientData::AudioStreamMap; // packet helpers @@ -49,7 +49,7 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); inline float computeGain(const AudioMixerClientData& listenerNodeData, const AvatarAudioStream& listeningNodeStream, - const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, bool isEcho); + const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho); inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); @@ -276,7 +276,7 @@ void AudioMixerSlave::addStream(AudioMixerClientData& listenerNodeData, const QU glm::vec3 relativePosition = streamToAdd.getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = computeGain(listenerNodeData, listeningNodeStream, streamToAdd, relativePosition, isEcho); + float gain = computeGain(listenerNodeData, listeningNodeStream, streamToAdd, relativePosition, distance, isEcho); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); const int HRTF_DATASET_INDEX = 1; @@ -489,9 +489,6 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi // avatar: skip attenuation - it is too costly to approximate // distance attenuation: approximate, ignore zone-specific attenuations - // this is a good approximation for streams further than ATTENUATION_START_DISTANCE - // those streams closer will be amplified; amplifying close streams is acceptable - // when throttling, as close streams are expected to be heard by a user float distance = glm::length(relativePosition); return gain / distance; @@ -499,7 +496,7 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi } float computeGain(const AudioMixerClientData& listenerNodeData, const AvatarAudioStream& listeningNodeStream, - const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, bool isEcho) { + const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho) { float gain = 1.0f; // injector: apply attenuation @@ -536,23 +533,13 @@ float computeGain(const AudioMixerClientData& listenerNodeData, const AvatarAudi break; } } + // translate the zone setting to gain per log2(distance) + float g = glm::clamp(1.0f - attenuationPerDoublingInDistance, EPSILON, 1.0f); - // distance attenuation - const float ATTENUATION_START_DISTANCE = 1.0f; - float distance = glm::length(relativePosition); - assert(ATTENUATION_START_DISTANCE > EPSILON); - if (distance >= ATTENUATION_START_DISTANCE) { - - // translate the zone setting to gain per log2(distance) - float g = 1.0f - attenuationPerDoublingInDistance; - g = glm::clamp(g, EPSILON, 1.0f); - - // calculate the distance coefficient using the distance to this node - float distanceCoefficient = fastExp2f(fastLog2f(g) * fastLog2f(distance/ATTENUATION_START_DISTANCE)); - - // multiply the current attenuation coefficient by the distance coefficient - gain *= distanceCoefficient; - } + // calculate the attenuation using the distance to this node + // reference attenuation of 0dB at distance = 1.0m + gain *= fastExp2f(fastLog2f(g) * fastLog2f(std::max(distance, HRTF_NEARFIELD_MIN))); + gain = std::min(gain, 1.0f / HRTF_NEARFIELD_MIN); return gain; } diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index f72832f902..e394884dc2 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -116,7 +116,6 @@ void EntityServer::beforeRun() { void EntityServer::entityCreated(const EntityItem& newEntity, const SharedNodePointer& senderNode) { } - // EntityServer will use the "special packets" to send list of recently deleted entities bool EntityServer::hasSpecialPacketsToSend(const SharedNodePointer& node) { bool shouldSendDeletedEntities = false; @@ -277,7 +276,6 @@ int EntityServer::sendSpecialPackets(const SharedNodePointer& node, OctreeQueryN return totalBytes; } - void EntityServer::pruneDeletedEntities() { EntityTreePointer tree = std::static_pointer_cast(_tree); if (tree->hasAnyDeletedEntities()) { diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 05404b28c8..4d3f1ee89f 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -30,7 +30,6 @@ struct ViewerSendingStats { class SimpleEntitySimulation; using SimpleEntitySimulationPointer = std::shared_ptr; - class EntityServer : public OctreeServer, public NewlyCreatedEntityHook { Q_OBJECT public: @@ -38,7 +37,7 @@ public: ~EntityServer(); // Subclasses must implement these methods - virtual std::unique_ptr createOctreeQueryNode() override ; + virtual std::unique_ptr createOctreeQueryNode() override; virtual char getMyNodeType() const override { return NodeType::EntityServer; } virtual PacketType getMyQueryMessageType() const override { return PacketType::EntityQuery; } virtual const char* getMyServerName() const override { return MODEL_SERVER_NAME; } @@ -82,12 +81,12 @@ private: QReadWriteLock _viewerSendingStatsLock; QMap> _viewerSendingStats; - static const int DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 45 * 60 * 1000; // 45m - static const int DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 60 * 60 * 1000; // 1h - int _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 45m - int _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 1h + static const int DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 45 * 60 * 1000; // 45m + static const int DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 60 * 60 * 1000; // 1h + int _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 45m + int _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 1h QTimer _dynamicDomainVerificationTimer; void startDynamicDomainVerification(); }; -#endif // hifi_EntityServer_h +#endif // hifi_EntityServer_h diff --git a/assignment-client/src/main.cpp b/assignment-client/src/main.cpp index 4f64bf8f7f..971e9ed272 100644 --- a/assignment-client/src/main.cpp +++ b/assignment-client/src/main.cpp @@ -9,22 +9,13 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include +#include #include #include "AssignmentClientApp.h" -#include int main(int argc, char* argv[]) { - disableQtBearerPoll(); // Fixes wifi ping spikes - - QCoreApplication::setApplicationName(BuildInfo::ASSIGNMENT_CLIENT_NAME); - QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); - QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); - QCoreApplication::setApplicationVersion(BuildInfo::VERSION); - - qInstallMessageHandler(LogHandler::verboseMessageHandler); - qInfo() << "Starting."; + setupHifiApplication(BuildInfo::ASSIGNMENT_CLIENT_NAME); AssignmentClientApp app(argc, argv); diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 42494ea7ee..0dbd24fe9c 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -33,6 +33,10 @@ #include #include +#include + +Q_LOGGING_CATEGORY(octree_server, "hifi.octree-server") + int OctreeServer::_clientCount = 0; const int MOVING_AVERAGE_SAMPLE_COUNTS = 1000; @@ -84,6 +88,8 @@ int OctreeServer::_longProcessWait = 0; int OctreeServer::_shortProcessWait = 0; int OctreeServer::_noProcessWait = 0; +static const QString PERSIST_FILE_DOWNLOAD_PATH = "/models.json.gz"; + void OctreeServer::resetSendingStats() { _averageLoopTime.reset(); @@ -202,7 +208,6 @@ void OctreeServer::trackPacketSendingTime(float time) { } } - void OctreeServer::trackProcessWaitTime(float time) { const float MAX_SHORT_TIME = 10.0f; const float MAX_LONG_TIME = 100.0f; @@ -283,8 +288,6 @@ void OctreeServer::initHTTPManager(int port) { _httpManager = new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this, this); } -const QString PERSIST_FILE_DOWNLOAD_PATH = "/models.json.gz"; - bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) { #ifdef FORCE_CRASH @@ -922,87 +925,6 @@ void OctreeServer::handleOctreeDataNackPacket(QSharedPointer me } } -void OctreeServer::handleOctreeFileReplacement(QSharedPointer message) { - if (!_isFinished && !_isShuttingDown) { - // these messages are only allowed to come from the domain server, so make sure that is the case - auto nodeList = DependencyManager::get(); - if (message->getSenderSockAddr() == nodeList->getDomainHandler().getSockAddr()) { - // it's far cleaner to load up the new content upon server startup - // so here we just store a special file at our persist path - // and then force a stop of the server so that it can pick it up when it relaunches - if (!_persistAbsoluteFilePath.isEmpty()) { - replaceContentFromMessageData(message->getMessage()); - } else { - qDebug() << "Cannot perform octree file replacement since current persist file path is not yet known"; - } - } else { - qDebug() << "Received an octree file replacement that was not from our domain server - refusing to process"; - } - } -} - -// Message->getMessage() contains a QByteArray representation of the URL to download from -void OctreeServer::handleOctreeFileReplacementFromURL(QSharedPointer message) { - qInfo() << "Received request to replace content from a url"; - if (!_isFinished && !_isShuttingDown) { - // This call comes from Interface, so we skip our domain server check - // but confirm that we have permissions to replace content sets - if (DependencyManager::get()->getThisNodeCanReplaceContent()) { - if (!_persistAbsoluteFilePath.isEmpty()) { - // Convert message data into our URL - QString url(message->getMessage()); - QUrl modelsURL = QUrl(url, QUrl::StrictMode); - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest request(modelsURL); - QNetworkReply* reply = networkAccessManager.get(request); - connect(reply, &QNetworkReply::finished, [this, reply, modelsURL]() { - QNetworkReply::NetworkError networkError = reply->error(); - if (networkError == QNetworkReply::NoError) { - QByteArray contents = reply->readAll(); - replaceContentFromMessageData(contents); - } else { - qDebug() << "Error downloading JSON from specified file"; - } - }); - } else { - qDebug() << "Cannot perform octree file replacement since current persist file path is not yet known"; - } - } - } -} - -void OctreeServer::replaceContentFromMessageData(QByteArray content) { - //Assume we have compressed data - auto compressedOctree = content; - QByteArray jsonOctree; - - bool wasCompressed = gunzip(compressedOctree, jsonOctree); - if (!wasCompressed) { - // the source was not compressed, assume we were sent regular JSON data - jsonOctree = compressedOctree; - } - // check the JSON data to verify it is an object - if (QJsonDocument::fromJson(jsonOctree).isObject()) { - if (!wasCompressed) { - // source was not compressed, we compress it before we write it locally - gzip(jsonOctree, compressedOctree); - } - // write the compressed octree data to a special file - auto replacementFilePath = _persistAbsoluteFilePath.append(OctreePersistThread::REPLACEMENT_FILE_EXTENSION); - QFile replacementFile(replacementFilePath); - if (replacementFile.open(QIODevice::WriteOnly) && replacementFile.write(compressedOctree) != -1) { - // we've now written our replacement file, time to take the server down so it can - // process it when it comes back up - qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server"; - setFinished(true); - } else { - qWarning() << "Could not write replacement octree data to file - refusing to process"; - } - } else { - qDebug() << "Received replacement octree file that is invalid - refusing to process"; - } -} - bool OctreeServer::readOptionBool(const QString& optionName, const QJsonObject& settingsSectionObject, bool& result) { result = false; // assume it doesn't exist bool optionAvailable = false; @@ -1119,7 +1041,18 @@ void OctreeServer::readConfiguration() { _persistFilePath = getMyDefaultPersistFilename(); } + QDir persistPath { _persistFilePath }; + + if (persistPath.isRelative()) { + // if the domain settings passed us a relative path, make an absolute path that is relative to the + // default data directory + _persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); + } else { + _persistAbsoluteFilePath = persistPath.absolutePath(); + } + qDebug() << "persistFilePath=" << _persistFilePath; + qDebug() << "persisAbsoluteFilePath=" << _persistAbsoluteFilePath; _persistAsFileType = "json.gz"; @@ -1200,20 +1133,94 @@ void OctreeServer::run() { } void OctreeServer::domainSettingsRequestComplete() { + if (_state != OctreeServerState::WaitingForDomainSettings) { + qCWarning(octree_server) << "Received domain settings after they have already been received"; + return; + } + + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); + packetReceiver.registerListener(getMyQueryMessageType(), this, "handleOctreeQueryPacket"); + packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket"); + + packetReceiver.registerListener(PacketType::OctreeDataFileReply, this, "handleOctreeDataFileReply"); + + qDebug(octree_server) << "Received domain settings"; + + readConfiguration(); + + _state = OctreeServerState::WaitingForOctreeDataNegotation; + + auto nodeList = DependencyManager::get(); + const DomainHandler& domainHandler = nodeList->getDomainHandler(); + + auto packet = NLPacket::create(PacketType::OctreeDataFileRequest, -1, true, false); + + OctreeUtils::RawOctreeData data; + qCDebug(octree_server) << "Reading octree data from" << _persistAbsoluteFilePath; + if (data.readOctreeDataInfoFromFile(_persistAbsoluteFilePath)) { + qCDebug(octree_server) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.version << ")"; + packet->writePrimitive(true); + auto id = data.id.toRfc4122(); + packet->write(id); + packet->writePrimitive(data.version); + } else { + qCWarning(octree_server) << "No octree data found"; + packet->writePrimitive(false); + } + + qCDebug(octree_server) << "Sending request for octree data to DS"; + nodeList->sendPacket(std::move(packet), domainHandler.getSockAddr()); +} + +void OctreeServer::handleOctreeDataFileReply(QSharedPointer message) { + if (_state != OctreeServerState::WaitingForOctreeDataNegotation) { + qCWarning(octree_server) << "Server received ocree data file reply but is not currently negotiating."; + return; + } + + bool includesNewData; + message->readPrimitive(&includesNewData); + QByteArray replaceData; + if (includesNewData) { + replaceData = message->readAll(); + qDebug() << "Got reply to octree data file request, new data sent"; + } else { + qDebug() << "Got reply to octree data file request, current entity data is sufficient"; + + OctreeUtils::RawEntityData data; + qCDebug(octree_server) << "Reading octree data from" << _persistAbsoluteFilePath; + if (data.readOctreeDataInfoFromFile(_persistAbsoluteFilePath)) { + if (data.id.isNull()) { + qCDebug(octree_server) << "Current octree data has a null id, updating"; + data.resetIdAndVersion(); + + QFile file(_persistAbsoluteFilePath); + if (file.open(QIODevice::WriteOnly)) { + auto entityData = data.toGzippedByteArray(); + file.write(entityData); + file.close(); + } else { + qCDebug(octree_server) << "Failed to update octree data"; + } + } + } + } + + _state = OctreeServerState::Running; + beginRunning(replaceData); +} + +void OctreeServer::beginRunning(QByteArray replaceData) { + if (_state != OctreeServerState::Running) { + qCWarning(octree_server) << "Server is not running"; + return; + } auto nodeList = DependencyManager::get(); // we need to ask the DS about agents so we can ping/reply with them nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer }); - auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); - packetReceiver.registerListener(getMyQueryMessageType(), this, "handleOctreeQueryPacket"); - packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket"); - packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacement"); - packetReceiver.registerListener(PacketType::OctreeFileReplacementFromUrl, this, "handleOctreeFileReplacementFromURL"); - - readConfiguration(); - beforeRun(); // after payload has been processed connect(nodeList.data(), SIGNAL(nodeAdded(SharedNodePointer)), SLOT(nodeAdded(SharedNodePointer))); @@ -1233,17 +1240,6 @@ void OctreeServer::domainSettingsRequestComplete() { // if we want Persistence, set up the local file and persist thread if (_wantPersist) { - // If persist filename does not exist, let's see if there is one beside the application binary - // If there is, let's copy it over to our target persist directory - QDir persistPath { _persistFilePath }; - _persistAbsoluteFilePath = persistPath.absolutePath(); - - if (persistPath.isRelative()) { - // if the domain settings passed us a relative path, make an absolute path that is relative to the - // default data directory - _persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); - } - static const QString ENTITY_PERSIST_EXTENSION = ".json.gz"; // force the persist file to end with .json.gz @@ -1328,7 +1324,7 @@ void OctreeServer::domainSettingsRequestComplete() { // now set up PersistThread _persistThread = new OctreePersistThread(_tree, _persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval, - _wantBackup, _settings, _debugTimestampNow, _persistAsFileType); + _wantBackup, _settings, _debugTimestampNow, _persistAsFileType, replaceData); _persistThread->initialize(true); } diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 0eba914064..e7efc731f2 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -27,8 +27,18 @@ #include "OctreeServerConsts.h" #include "OctreeInboundPacketProcessor.h" +#include + +Q_DECLARE_LOGGING_CATEGORY(octree_server) + const int DEFAULT_PACKETS_PER_INTERVAL = 2000; // some 120,000 packets per second total +enum class OctreeServerState { + WaitingForDomainSettings, + WaitingForOctreeDataNegotation, + Running +}; + /// Handles assignments of type OctreeServer - sending octrees to various clients. class OctreeServer : public ThreadedAssignment, public HTTPRequestHandler { Q_OBJECT @@ -36,6 +46,8 @@ public: OctreeServer(ReceivedMessage& message); ~OctreeServer(); + OctreeServerState _state { OctreeServerState::WaitingForDomainSettings }; + /// allows setting of run arguments void setArguments(int argc, char** argv); @@ -137,8 +149,7 @@ private slots: void domainSettingsRequestComplete(); void handleOctreeQueryPacket(QSharedPointer message, SharedNodePointer senderNode); void handleOctreeDataNackPacket(QSharedPointer message, SharedNodePointer senderNode); - void handleOctreeFileReplacement(QSharedPointer message); - void handleOctreeFileReplacementFromURL(QSharedPointer message); + void handleOctreeDataFileReply(QSharedPointer message); void removeSendThread(); protected: @@ -159,12 +170,12 @@ protected: QString getFileLoadTime(); QString getConfiguration(); QString getStatusLink(); + + void beginRunning(QByteArray replaceData); UniqueSendThread createSendThread(const SharedNodePointer& node); virtual UniqueSendThread newSendThread(const SharedNodePointer& node); - void replaceContentFromMessageData(QByteArray content); - int _argc; const char** _argv; char** _parsedArgV; diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index b4a6b3af93..60cb1e349b 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -178,7 +178,7 @@ void EntityScriptServer::updateEntityPPS() { int numRunningScripts = _entitiesScriptEngine->getNumRunningEntityScripts(); int pps; if (std::numeric_limits::max() / _entityPPSPerScript < numRunningScripts) { - qWarning() << QString("Integer multiplaction would overflow, clamping to maxint: %1 * %2").arg(numRunningScripts).arg(_entityPPSPerScript); + qWarning() << QString("Integer multiplication would overflow, clamping to maxint: %1 * %2").arg(numRunningScripts).arg(_entityPPSPerScript); pps = std::numeric_limits::max(); pps = std::min(_maxEntityPPS, pps); } else { diff --git a/domain-server/CMakeLists.txt b/domain-server/CMakeLists.txt index f67be55b92..458dcb5ab8 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -24,7 +24,19 @@ symlink_or_copy_directory_beside_target(${_SHOULD_SYMLINK_RESOURCES} "${CMAKE_CU # link the shared hifi libraries include_hifi_library_headers(gpu) include_hifi_library_headers(graphics) -link_hifi_libraries(embedded-webserver networking shared avatars) +link_hifi_libraries(embedded-webserver networking shared avatars octree) + +target_zlib() + +add_dependency_external_projects(quazip) + +find_package(QuaZip REQUIRED) +target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${QUAZIP_INCLUDE_DIRS}) +target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) + +if (WIN32) + add_paths_to_fixup_libs(${QUAZIP_DLL_PATH}) +endif () # find OpenSSL find_package(OpenSSL REQUIRED) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 93d703c8b3..83dd633d22 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 2.1, + "version": 2.2, "settings": [ { "name": "metaverse", @@ -306,7 +306,37 @@ } ], "non-deletable-row-key": "permissions_id", - "non-deletable-row-values": [ "localhost", "anonymous", "logged-in" ] + "non-deletable-row-values": [ "localhost", "anonymous", "logged-in" ], + "default": [ + { + "id_can_connect": true, + "id_can_rez_tmp_certified": true, + "permissions_id": "anonymous" + }, + { + "id_can_connect": true, + "id_can_rez_tmp_certified": true, + "permissions_id": "friends" + }, + { + "id_can_adjust_locks": true, + "id_can_connect": true, + "id_can_connect_past_max_capacity": true, + "id_can_kick": true, + "id_can_replace_content": true, + "id_can_rez": true, + "id_can_rez_certified": true, + "id_can_rez_tmp": true, + "id_can_rez_tmp_certified": true, + "id_can_write_to_asset_server": true, + "permissions_id": "localhost" + }, + { + "id_can_connect": true, + "id_can_rez_tmp_certified": true, + "permissions_id": "logged-in" + } + ] }, { "name": "group_permissions", @@ -1321,73 +1351,6 @@ "default": "30000", "advanced": true }, - { - "name": "backups", - "type": "table", - "label": "Backup Rules", - "help": "In this table you can define a set of rules for how frequently to backup copies of your entites content file.", - "numbered": false, - "can_add_new_rows": true, - "default": [ - { - "Name": "Half Hourly Rolling", - "backupInterval": 1800, - "format": ".backup.halfhourly.%N", - "maxBackupVersions": 5 - }, - { - "Name": "Daily Rolling", - "backupInterval": 86400, - "format": ".backup.daily.%N", - "maxBackupVersions": 7 - }, - { - "Name": "Weekly Rolling", - "backupInterval": 604800, - "format": ".backup.weekly.%N", - "maxBackupVersions": 4 - }, - { - "Name": "Thirty Day Rolling", - "backupInterval": 2592000, - "format": ".backup.thirtyday.%N", - "maxBackupVersions": 12 - } - ], - "columns": [ - { - "name": "Name", - "label": "Name", - "can_set": true, - "placeholder": "Example", - "default": "Example" - }, - { - "name": "format", - "label": "Rule Format", - "can_set": true, - "help": "Format used to create the extension for the backup of your persisted entities. Use a format with %N to get rolling. Or use date formatting like %Y-%m-%d.%H:%M:%S.%z", - "placeholder": ".backup.example.%N", - "default": ".backup.example.%N" - }, - { - "name": "backupInterval", - "label": "Backup Interval in Seconds", - "help": "Interval between backup checks in seconds.", - "placeholder": 1800, - "default": 1800, - "can_set": true - }, - { - "name": "maxBackupVersions", - "label": "Max Rolled Backup Versions", - "help": "If your backup extension format uses 'rolling', how many versions do you want us to keep?", - "placeholder": 5, - "default": 5, - "can_set": true - } - ] - }, { "name": "NoPersist", "type": "checkbox", @@ -1649,6 +1612,67 @@ } ] }, + { + "name": "automatic_content_archives", + "label": "Automatic Content Archives", + "settings": [ + { + "name": "backup_rules", + "type": "table", + "label": "Rolling Backup Rules", + "help": "Define how frequently to create automatic content archives", + "numbered": false, + "can_add_new_rows": true, + "default": [ + { + "Name": "Half Hourly Rolling", + "backupInterval": 1800, + "maxBackupVersions": 5 + }, + { + "Name": "Daily Rolling", + "backupInterval": 86400, + "maxBackupVersions": 7 + }, + { + "Name": "Weekly Rolling", + "backupInterval": 604800, + "maxBackupVersions": 4 + }, + { + "Name": "Thirty Day Rolling", + "backupInterval": 2592000, + "maxBackupVersions": 12 + } + ], + "columns": [ + { + "name": "Name", + "label": "Name", + "can_set": true, + "placeholder": "Example", + "default": "Example" + }, + { + "name": "backupInterval", + "label": "Backup Interval in Seconds", + "help": "Interval between backup checks in seconds.", + "placeholder": 1800, + "default": 1800, + "can_set": true + }, + { + "name": "maxBackupVersions", + "label": "Max Rolled Backup Versions", + "help": "If your backup extension format uses 'rolling', how many versions do you want us to keep?", + "placeholder": 5, + "default": 5, + "can_set": true + } + ] + } + ] + }, { "name": "wizard", "label": "Setup Wizard", diff --git a/domain-server/resources/web/base-settings-scripts.html b/domain-server/resources/web/base-settings-scripts.html index fe370c4675..877b0a6125 100644 --- a/domain-server/resources/web/base-settings-scripts.html +++ b/domain-server/resources/web/base-settings-scripts.html @@ -3,5 +3,4 @@ - diff --git a/domain-server/resources/web/content/index.shtml b/domain-server/resources/web/content/index.shtml index 9b507f7826..f934faa976 100644 --- a/domain-server/resources/web/content/index.shtml +++ b/domain-server/resources/web/content/index.shtml @@ -14,6 +14,8 @@ + + diff --git a/domain-server/resources/web/content/js/bootstrap-sortable.min.js b/domain-server/resources/web/content/js/bootstrap-sortable.min.js new file mode 100755 index 0000000000..ac21ebe969 --- /dev/null +++ b/domain-server/resources/web/content/js/bootstrap-sortable.min.js @@ -0,0 +1 @@ +!function(t,e){"use strict";"function"==typeof define&&define.amd?define("tinysort",function(){return e}):t.tinysort=e}(this,function(){"use strict";function t(t,e){for(var r,a=t.length,o=a;o--;)e(t[r=a-o-1],r)}function e(t,e,r){for(var o in e)(r||t[o]===a)&&(t[o]=e[o]);return t}function r(t,e,r){f.push({prepare:t,sort:e,sortBy:r})}var a,o,n=!1,s=null,i=window,d=i.document,l=parseFloat,c=/(-?\d+\.?\d*)\s*$/g,u=/(\d+\.?\d*)\s*$/g,f=[],h=0,p=0,v=String.fromCharCode(4095),m={selector:s,order:"asc",attr:s,data:s,useVal:n,place:"org",returns:n,cases:n,natural:n,forceStrings:n,ignoreDashes:n,sortFunction:s,useFlex:n,emptyEnd:n};return i.Element&&((o=Element.prototype).matchesSelector=o.matchesSelector||o.mozMatchesSelector||o.msMatchesSelector||o.oMatchesSelector||o.webkitMatchesSelector||function(t){for(var e=(this.parentNode||this.document).querySelectorAll(t),r=-1;e[++r]&&e[r]!=this;);return!!e[r]}),e(r,{loop:t}),e(function(r,o){function i(t){var r=!!t.selector,a=r&&":"===t.selector[0],o=e(t||{},m);k.push(e({hasSelector:r,hasAttr:!(o.attr===s||""===o.attr),hasData:o.data!==s,hasFilter:a,sortReturnNumber:"asc"===o.order?1:-1},o))}function g(t,e,r){for(var a=r(t.toString()),o=r(e.toString()),n=0;a[n]&&o[n];n++)if(a[n]!==o[n]){var s=Number(a[n]),i=Number(o[n]);return s==a[n]&&i==o[n]?s-i:a[n]>o[n]?1:-1}return a.length-o.length}function b(t){for(var e,r,a=[],o=0,n=-1,s=0;e=(r=t.charAt(o++)).charCodeAt(0);){var i=46==e||e>=48&&57>=e;i!==s&&(a[++n]="",s=i),a[n]+=r}return a}function w(){return q.forEach(function(t){E.appendChild(t.elm)}),E}function y(t){var e=t.elm,r=d.createElement("div");return t.ghost=r,e.parentNode.insertBefore(r,e),t}function S(t,e){var r=t.ghost,a=r.parentNode;a.insertBefore(e,r),a.removeChild(r),delete t.ghost}function x(t,e){var r,a=t.elm;return e.selector&&(e.hasFilter?a.matchesSelector(e.selector)||(a=s):a=a.querySelector(e.selector)),e.hasAttr?r=a.getAttribute(e.attr):e.useVal?r=a.value||a.getAttribute("value"):e.hasData?r=a.getAttribute("data-"+e.data):a&&(r=a.textContent),C(r)&&(e.cases||(r=r.toLowerCase()),r=r.replace(/\s+/g," ")),null===r&&(r=v),r}function C(t){return"string"==typeof t}C(r)&&(r=d.querySelectorAll(r)),0===r.length&&console.warn("No elements to sort");var F,N,E=d.createDocumentFragment(),A=[],q=[],M=[],k=[],D=!0,z=r.length&&r[0].parentNode,Y=z.rootNode!==document,H=r.length&&(o===a||!1!==o.useFlex)&&!Y&&-1!==getComputedStyle(z,null).display.indexOf("flex");return function(){0===arguments.length?i({}):t(arguments,function(t){i(C(t)?{selector:t}:t)}),h=k.length}.apply(s,Array.prototype.slice.call(arguments,1)),t(r,function(t,e){N?N!==t.parentNode&&(D=!1):N=t.parentNode;var r=k[0],a=r.hasFilter,o=r.selector,n=!o||a&&t.matchesSelector(o)||o&&t.querySelector(o)?q:M,s={elm:t,pos:e,posn:n.length};A.push(s),n.push(s)}),F=q.slice(0),q.sort(function(e,r){var o=0;for(0!==p&&(p=0);0===o&&h>p;){var s=k[p],i=s.ignoreDashes?u:c;if(t(f,function(t){var e=t.prepare;e&&e(s)}),s.sortFunction)o=s.sortFunction(e,r);else if("rand"==s.order)o=Math.random()<.5?1:-1;else{var d=n,v=x(e,s),m=x(r,s),w=""===v||v===a,y=""===m||m===a;if(v===m)o=0;else if(s.emptyEnd&&(w||y))o=w&&y?0:w?1:-1;else{if(!s.forceStrings){var S=C(v)?v&&v.match(i):n,F=C(m)?m&&m.match(i):n;S&&F&&v.substr(0,v.length-S[0].length)==m.substr(0,m.length-F[0].length)&&(d=!n,v=l(S[0]),m=l(F[0]))}o=v===a||m===a?0:s.natural&&(isNaN(v)||isNaN(m))?g(v,m,b):m>v?-1:v>m?1:0}}t(f,function(t){var e=t.sort;e&&(o=e(s,d,v,m,o))}),0==(o*=s.sortReturnNumber)&&p++}return 0===o&&(o=e.pos>r.pos?1:-1),o}),function(){var t=q.length===A.length;if(D&&t)H?q.forEach(function(t,e){t.elm.style.order=e}):N?N.appendChild(w()):console.warn("parentNode has been removed");else{var e=k[0].place,r="start"===e,a="end"===e,o="first"===e,n="last"===e;if("org"===e)q.forEach(y),q.forEach(function(t,e){S(F[e],t.elm)});else if(r||a){var s=F[r?0:F.length-1],i=s&&s.elm.parentNode,d=i&&(r&&i.firstChild||i.lastChild);d&&(d!==s.elm&&(s={elm:d}),y(s),a&&i.appendChild(s.ghost),S(s,w()))}else(o||n)&&S(y(F[o?0:F.length-1]),w())}}(),q.map(function(t){return t.elm})},{plugin:r,defaults:m})}()),function(t,e){"function"==typeof define&&define.amd?define(["jquery","tinysort","moment"],e):e(t.jQuery,t.tinysort,t.moment||void 0)}(this,function(t,e,r){var a,o,n,s=t(document);function i(e){var s=void 0!==r;a=e.sign?e.sign:"arrow","default"==e.customSort&&(e.customSort=u),o=e.customSort||o||u,n=e.emptyEnd,t("table.sortable").each(function(){var a=t(this),o=!0===e.applyLast;a.find("span.sign").remove(),a.find("> thead [colspan]").each(function(){for(var e=parseFloat(t(this).attr("colspan")),r=1;r')}),a.find("> thead [rowspan]").each(function(){for(var e=t(this),r=parseFloat(e.attr("rowspan")),a=1;a')}}),a.find("> thead tr").each(function(e){t(this).find("th").each(function(r){var a=t(this);a.addClass("nosort").removeClass("up down"),a.attr("data-sortcolumn",r),a.attr("data-sortkey",r+"-"+e)})}),a.find("> thead .rowspan-compensate, .colspan-compensate").remove(),a.find("th").each(function(){var e=t(this);if(void 0!==e.attr("data-dateformat")&&s){var o=parseFloat(e.attr("data-sortcolumn"));a.find("td:nth-child("+(o+1)+")").each(function(){var a=t(this);a.attr("data-value",r(a.text(),e.attr("data-dateformat")).format("YYYY/MM/DD/HH/mm/ss"))})}else if(void 0!==e.attr("data-valueprovider")){o=parseFloat(e.attr("data-sortcolumn"));a.find("td:nth-child("+(o+1)+")").each(function(){var r=t(this);r.attr("data-value",new RegExp(e.attr("data-valueprovider")).exec(r.text())[0])})}}),a.find("td").each(function(){var e=t(this);void 0!==e.attr("data-dateformat")&&s?e.attr("data-value",r(e.text(),e.attr("data-dateformat")).format("YYYY/MM/DD/HH/mm/ss")):void 0!==e.attr("data-valueprovider")?e.attr("data-value",new RegExp(e.attr("data-valueprovider")).exec(e.text())[0]):void 0===e.attr("data-value")&&e.attr("data-value",e.text())});var n=c(a),i=n.bsSort;a.find('> thead th[data-defaultsort!="disabled"]').each(function(e){var r=t(this),a=r.closest("table.sortable");r.data("sortTable",a);var s=r.attr("data-sortkey"),d=o?n.lastSort:-1;i[s]=o?i[s]:r.attr("data-defaultsort"),void 0!==i[s]&&o===(s===d)&&(i[s]="asc"===i[s]?"desc":"asc",f(r,a))})})}function d(e){e.find("> tbody [rowspan]").each(function(){var e=t(this),r=parseFloat(e.attr("rowspan"));e.removeAttr("rowspan");var a=e.attr("rowspan-group")||function(){function t(){return Math.floor(65536*(1+Math.random())).toString(16).substring(1)}return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()}();e.attr("rowspan-group",a),e.attr("rowspan-value",r);for(var o=e.parent("tr"),n=o.children().index(e),s=1;s thead th[data-defaultsort!="disabled"]').each(function(e){var a=t(this),o=a.attr("data-sortkey");r.bsSort[o]=a.attr("data-defaultsort"),void 0!==r.bsSort[o]&&(r.lastSort=o)}),e.data("bootstrap-sortable-context",r)),r}function u(t,r){e(t,r)}function f(e,r){r.trigger("before-sort"),d(r);var s=parseFloat(e.attr("data-sortcolumn")),i=c(r),l=i.bsSort;if(e.attr("colspan")){var u=parseFloat(e.data("mainsort"))||0,h=parseFloat(e.data("sortkey").split("-").pop());if(r.find("> thead tr").length-1>h)return void f(r.find('[data-sortkey="'+(s+u)+"-"+(h+1)+'"]'),r);s+=u}var p=e.attr("data-defaultsign")||a;if(r.find("> thead th").each(function(){t(this).removeClass("up").removeClass("down").addClass("nosort")}),t.browser.mozilla){var v=r.find("> thead div.mozilla");void 0!==v&&(v.find(".sign").remove(),v.parent().html(v.html())),e.wrapInner('
'),e.children().eq(0).append('')}else r.find("> thead span.sign").remove(),e.append('');var m=e.attr("data-sortkey"),g="desc"!==e.attr("data-firstsort")?"desc":"asc",b=l[m]||g;i.lastSort!==m&&void 0!==l[m]||(b="asc"===b?"desc":"asc"),l[m]=b,i.lastSort=m,"desc"===l[m]?(e.find("span.sign").addClass("up"),e.addClass("up").removeClass("down nosort")):e.addClass("down").removeClass("up nosort");var w=r.children("tbody").children("tr"),y=[];t(w.filter('[data-disablesort="true"]').get().reverse()).each(function(e,r){var a=t(r);y.push({index:w.index(a),row:a}),a.remove()});var S=w.not('[data-disablesort="true"]');if(0!=S.length){var x="asc"===l[m]&&n;o(S,{emptyEnd:x,selector:"td:nth-child("+(s+1)+")",order:l[m],data:"value"})}t(y.reverse()).each(function(t,e){0===e.index?r.children("tbody").prepend(e.row):r.children("tbody").children("tr").eq(e.index-1).after(e.row)}),r.find("> tbody > tr > td.sorted,> thead th.sorted").removeClass("sorted"),S.find("td:eq("+s+")").addClass("sorted"),e.addClass("sorted"),r.find("> tbody [rowspan-group]").each(function(){for(var e=t(this),r=e.attr("rowspan-group"),a=e.parent("tr"),o=a.children().index(e);;){var n=a.next("tr");if(!n.is("tr"))break;var s=n.children().eq(o);if(s.attr("rowspan-group")!==r)break;var i=parseFloat(e.attr("rowspan"))||1;e.attr("rowspan",i+1),s.remove(),a=n}}),r.trigger("sorted")}if(t.bootstrapSortable=function(t){null==t?i({}):t.constructor===Boolean?i({applyLast:t}):void 0!==t.sortingHeader?l(t.sortingHeader):i(t)},s.on("click",'table.sortable>thead th[data-defaultsort!="disabled"]',function(t){l(this)}),!t.browser){t.browser={chrome:!1,mozilla:!1,opera:!1,msie:!1,safari:!1};var h=navigator.userAgent;t.each(t.browser,function(e){t.browser[e]=!!new RegExp(e,"i").test(h),t.browser.mozilla&&"mozilla"===e&&(t.browser.mozilla=!!new RegExp("firefox","i").test(h)),t.browser.chrome&&"safari"===e&&(t.browser.safari=!1)})}t(t.bootstrapSortable)}); \ No newline at end of file diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index e448952c65..3fec5a9000 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -1,37 +1,437 @@ $(document).ready(function(){ - Settings.afterReloadActions = function() {}; + var RESTORE_SETTINGS_UPLOAD_ID = 'restore-settings-button'; + var RESTORE_SETTINGS_FILE_ID = 'restore-settings-file'; + var UPLOAD_CONTENT_ALLOWED_DIV_ID = 'upload-content-allowed'; + var UPLOAD_CONTENT_RECOVERING_DIV_ID = 'upload-content-recovering'; - var frm = $('#upload-form'); - frm.submit(function (ev) { - $.ajax({ - type: frm.attr('method'), - url: frm.attr('action'), - data: new FormData($(this)[0]), - cache: false, - contentType: false, - processData: false, - success: function (data) { - swal({ - title: 'Uploaded', - type: 'success', - text: 'Your Entity Server is restarting to replace its local content with the uploaded file.', - confirmButtonText: 'OK' - }) - }, - error: function (data) { - swal({ - title: '', - type: 'error', - text: 'Your entities file could not be transferred to the Entity Server.
Verify that the file is a .json or .json.gz entities file and try again.', - html: true, - confirmButtonText: 'OK', + var isRestoring = false; + + function progressBarHTML(extraClass, label) { + var html = "
"; + html += "
"; + html += label + "
"; + return html; + } + + function setupBackupUpload() { + // construct the HTML needed for the settings backup panel + var html = "
"; + + html += "Upload a content archive (.zip) or entity file (.json, .json.gz) to replace the content of this domain."; + html += "
Note: Your domain content will be replaced by the content you upload, but the existing backup files of your domain's content will not immediately be changed.
"; + + html += ""; + html += ""; + + html += "
"; + html += "Restore in progress"; + html += progressBarHTML('recovery', 'Restoring'); + html += "
"; + + $('#' + Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID + ' .panel-body').html(html); + } + + // handle content archive or entity file upload + + // when the selected file is changed, enable the button if there's a selected file + $('body').on('change', '#' + RESTORE_SETTINGS_FILE_ID, function() { + $('#' + RESTORE_SETTINGS_UPLOAD_ID).attr('disabled', $(this).val().length == 0); + }); + + // when the upload button is clicked, send the file to the DS + // and reload the page if restore was successful or + // show an error if not + $('body').on('click', '#' + RESTORE_SETTINGS_UPLOAD_ID, function(e){ + e.preventDefault(); + + swalAreYouSure( + "Your domain content will be replaced by the uploaded content archive or entity file", + "Restore content", + function() { + var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); + + var fileFormData = new FormData(); + fileFormData.append('restore-file', files[0]); + + showSpinnerAlert("Uploading content to restore"); + + $.ajax({ + url: '/content/upload', + type: 'POST', + cache: false, + processData: false, + contentType: false, + data: fileFormData + }).done(function(data, textStatus, jqXHR) { + isRestoring = true; + + // immediately reload backup information since one should be restoring now + reloadBackupInformation(); + + swal.close(); + }).fail(function(jqXHR, textStatus, errorThrown) { + showErrorMessage( + "Error", + "There was a problem restoring domain content.\n" + + "Please ensure that the content archive or entity file is valid and try again." + ); }); } - }); - - ev.preventDefault(); - - showSpinnerAlert("Uploading Entities File"); + ); }); + + var GENERATE_ARCHIVE_BUTTON_ID = 'generate-archive-button'; + var CONTENT_ARCHIVES_NORMAL_ID = 'content-archives-success'; + var CONTENT_ARCHIVES_ERROR_ID = 'content-archives-error'; + var AUTOMATIC_ARCHIVES_TABLE_ID = 'automatic-archives-table'; + var AUTOMATIC_ARCHIVES_TBODY_ID = 'automatic-archives-tbody'; + var MANUAL_ARCHIVES_TABLE_ID = 'manual-archives-table'; + var MANUAL_ARCHIVES_TBODY_ID = 'manual-archives-tbody'; + var AUTO_ARCHIVES_SETTINGS_LINK_ID = 'auto-archives-settings-link'; + var ACTION_MENU_CLASS = 'action-menu'; + + var automaticBackups = []; + var manualBackups = []; + + function setupContentArchives() { + // construct the HTML needed for the content archives panel + var html = "
"; + html += ""; + html += "Your domain server makes regular archives of the content in your domain. In the list below, you can see and download all of your backups of domain content and content settings." + html += "Click here to manage automatic content archive intervals."; + html += "
"; + html += ""; + + var backups_table_head = "" + + "" + + ""; + + html += backups_table_head; + html += "
Archive NameArchive DateActions
"; + html += "
"; + html += ""; + html += "You can generate and download an archive of your domain content right now. You can also download, delete and restore any archive listed."; + html += ""; + html += "
"; + html += ""; + html += backups_table_head; + html += "
"; + + html += ""; + + // put the base HTML in the content archives panel + $('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html(html); + } + + var BACKUP_RESTORE_LINK_CLASS = 'restore-backup'; + var BACKUP_DOWNLOAD_LINK_CLASS = 'download-backup'; + var BACKUP_DELETE_LINK_CLASS = 'delete-backup'; + var ACTIVE_BACKUP_ROW_CLASS = 'active-backup'; + var CORRUPTED_ROW_CLASS = 'danger'; + + function reloadBackupInformation() { + // make a GET request to get backup information to populate the table + $.ajax({ + url: '/api/backups', + cache: false + }).done(function(data) { + + // split the returned data into manual and automatic manual backups + var splitBackups = _.partition(data.backups, function(value, index) { + return value.isManualBackup; + }); + + if (isRestoring && !data.status.isRecovering) { + // we were recovering and we finished - the DS is going to restart so show the restart modal + showRestartModal(); + return; + } + + isRestoring = data.status.isRecovering; + + manualBackups = splitBackups[0]; + automaticBackups = splitBackups[1]; + + // populate the backups tables with the backups + function createBackupTableRow(backup) { + return "" + + "" + backup.name + "" + + moment(backup.createdAtMillis).format('lll') + + "" + + ""; + } + + function updateProgressBars($progressBar, value) { + $progressBar.attr('aria-valuenow', value).attr('style', 'width: ' + value + '%'); + $progressBar.find('.sr-only').html(value + "% Complete"); + } + + // before we add any new rows and update existing ones + // remove our flag for active rows + $('.' + ACTIVE_BACKUP_ROW_CLASS).removeClass(ACTIVE_BACKUP_ROW_CLASS); + + function updateOrAddTableRow(backup, tableBodyID) { + // check for a backup with this ID + var $backupRow = $("tr[data-backup-id='" + backup.id + "']"); + + if ($backupRow.length == 0) { + // create a new row and then add it to the table + $backupRow = $(createBackupTableRow(backup)); + $('#' + tableBodyID).append($backupRow); + } + + // update the row status column depending on if it is available or recovering + if (!backup.isAvailable) { + // add a progress bar to the status row for availability + $backupRow.find('td.backup-status').html(progressBarHTML('availability', 'Archiving')); + + // set the value of the progress bar based on availability progress + updateProgressBars($backupRow.find('.progress-bar'), backup.availabilityProgress * 100); + } else if (backup.id == data.status.recoveringBackupId) { + // add a progress bar to the status row for recovery + $backupRow.find('td.backup-status').html(progressBarHTML('recovery', 'Restoring')); + } else if (backup.isCorrupted) { + // add text for corrupted status to row + $backupRow.find('td.backup-status').html('Corrupted'); + } else { + // no special status for this row, use an empty status column + $backupRow.find('td.backup-status').html(''); + } + + // color the row red if it is corrupted + $backupRow.toggleClass(CORRUPTED_ROW_CLASS, backup.isCorrupted); + + // disable restore if the backup is corrupted + $backupRow.find('a.' + BACKUP_RESTORE_LINK_CLASS).parent().toggleClass('disabled', backup.isCorrupted); + + // toggle the dropdown menu depending on if the row is available + $backupRow.find('td.' + ACTION_MENU_CLASS + ' .dropdown').toggle(backup.isAvailable); + + $backupRow.addClass(ACTIVE_BACKUP_ROW_CLASS); + } + + if (automaticBackups.length > 0) { + for (var backupIndex in automaticBackups) { + updateOrAddTableRow(automaticBackups[backupIndex], AUTOMATIC_ARCHIVES_TBODY_ID); + } + } + + if (manualBackups.length > 0) { + for (var backupIndex in manualBackups) { + updateOrAddTableRow(manualBackups[backupIndex], MANUAL_ARCHIVES_TBODY_ID); + } + } + + // at this point, any rows that no longer have the ACTIVE_BACKUP_ROW_CLASS + // are deleted backups, so we remove them from the table + $('#' + CONTENT_ARCHIVES_NORMAL_ID + ' tbody tr:not(.' + ACTIVE_BACKUP_ROW_CLASS + ')').remove(); + + // check if the restore action on all rows should be enabled or disabled + $('tr:not(.' + CORRUPTED_ROW_CLASS + ') .' + BACKUP_RESTORE_LINK_CLASS).parent().toggleClass('disabled', data.status.isRecovering); + + // hide or show the manual content upload file and button depending on our recovering status + $('#' + UPLOAD_CONTENT_ALLOWED_DIV_ID).toggle(!data.status.isRecovering); + $('#' + UPLOAD_CONTENT_RECOVERING_DIV_ID).toggle(data.status.isRecovering); + + // update the progress bars for current restore status + if (data.status.isRecovering) { + updateProgressBars($('.recovery.progress-bar'), data.status.recoveryProgress * 100); + } + + // tell bootstrap sortable to update for the new rows + $.bootstrapSortable({ applyLast: true }); + + $('#' + CONTENT_ARCHIVES_NORMAL_ID).toggle(true); + $('#' + CONTENT_ARCHIVES_ERROR_ID).toggle(false); + + }).fail(function(){ + // we've hit the very rare case where we couldn't load the list of backups from the domain server + + // set our backups to empty + automaticBackups = []; + manualBackups = []; + + // replace the content archives panel with a simple error message + // stating that the user should reload the page + $('#' + CONTENT_ARCHIVES_NORMAL_ID).toggle(false); + $('#' + CONTENT_ARCHIVES_ERROR_ID).toggle(true); + + }).always(function(){ + // toggle showing or hiding the tables depending on if they have entries + $('#' + AUTOMATIC_ARCHIVES_TABLE_ID).toggle(automaticBackups.length > 0); + $('#' + MANUAL_ARCHIVES_TABLE_ID).toggle(manualBackups.length > 0); + }); + } + + // handle click in table to restore a given content backup + $('body').on('click', '.' + BACKUP_RESTORE_LINK_CLASS, function(e) { + // stop the default behaviour + e.preventDefault(); + + // if this is a disabled link, don't proceed with the restore + if ($(this).parent().hasClass('disabled')) { + return false; + } + + // grab the name of this backup so we can show it in alerts + var backupName = $(this).closest('tr').attr('data-backup-name'); + + // grab the ID of this backup in case we need to send a POST + var backupID = $(this).closest('tr').attr('data-backup-id'); + + // make sure the user knows what is about to happen + swalAreYouSure( + "Your domain content will be replaced by the content archive " + backupName, + "Restore content", + function() { + // show a spinner while we send off our request + showSpinnerAlert("Starting restore of " + backupName); + + // setup an AJAX POST to request content restore + $.post('/api/backups/recover/' + backupID).done(function(data, textStatus, jqXHR) { + isRestoring = true; + + // immediately reload our backup information since one should be restoring now + reloadBackupInformation(); + + swal.close(); + }).fail(function(jqXHR, textStatus, errorThrown) { + showErrorMessage( + "Error", + "There was a problem restoring domain content.\n" + + "If the problem persists, the content archive may be corrupted." + ); + }); + } + ) + }); + + // handle click in table to delete a given content backup + $('body').on('click', '.' + BACKUP_DELETE_LINK_CLASS, function(e){ + // stop the default behaviour + e.preventDefault(); + + // grab the name of this backup so we can show it in alerts + var backupName = $(this).closest('tr').attr('data-backup-name'); + + // grab the ID of this backup in case we need to send the DELETE request + var backupID = $(this).closest('tr').attr('data-backup-id'); + + // make sure the user knows what is about to happen + swalAreYouSure( + "The content archive " + backupName + " will be deleted and will no longer be available for restore or download from this page.", + "Delete content archive", + function() { + // show a spinner while we send off our request + showSpinnerAlert("Deleting content archive " + backupName); + + // setup an AJAX DELETE to request content archive delete + $.ajax({ + url: '/api/backups/' + backupID, + type: 'DELETE' + }).done(function(data, textStatus, jqXHR) { + swal.close(); + }).fail(function(jqXHR, textStatus, errorThrown) { + showErrorMessage( + "Error", + "There was an unexpected error deleting the content archive" + ); + }).always(function(){ + // reload the list of content archives in case we deleted a backup + // or it's no longer an available backup for some other reason + reloadBackupInformation(); + }); + } + ) + }); + + // handle click on automatic content archive settings link + $('body').on('click', '#' + AUTO_ARCHIVES_SETTINGS_LINK_ID, function(e) { + if (Settings.pendingChanges > 0) { + // don't follow the link right away, make sure the user knows they are about to leave + // the page and lose changes + e.preventDefault(); + + var settingsLink = $(this).attr('href'); + + swalAreYouSure( + "You have pending changes to content settings that have not been saved. They will be lost if you leave the page to manage automatic content archive intervals.", + "Proceed without Saving", + function() { + // user wants to drop their changes, switch pages + window.location = settingsLink; + } + ); + } + }); + + // handle click on manual archive creation button + $('body').on('click', '#' + GENERATE_ARCHIVE_BUTTON_ID, function(e) { + e.preventDefault(); + + // show a sweet alert to ask the user to provide a name for their content archive + swal({ + title: "Generate a content archive", + type: "input", + text: "This will capture the state of all the content in your domain right now, which you can save as a backup and restore from later.", + confirmButtonText: "Generate Archive", + showCancelButton: true, + closeOnConfirm: false, + inputPlaceholder: 'Archive Name' + }, function(inputValue){ + if (inputValue === false) { + return false; + } + + if (inputValue === "") { + swal.showInputError("Please give the content archive a name.") + return false; + } + + var MANUAL_ARCHIVE_NAME_REGEX = /^[a-zA-Z0-9\-_ ]+$/; + if (!MANUAL_ARCHIVE_NAME_REGEX.test(inputValue)) { + swal.showInputError("Valid characters include A-z, 0-9, ' ', '_', and '-'."); + return false; + } + + // post the provided archive name to ask the server to kick off a manual backup + $.ajax({ + type: 'POST', + url: '/api/backups', + data: { + 'name': inputValue + } + }).done(function(data) { + // since we successfully setup a new content archive, reload the table of archives + // which should show that this archive is pending creation + swal.close(); + reloadBackupInformation(); + }).fail(function(jqXHR, textStatus, errorThrown) { + showErrorMessage( + "Error", + "There was an unexpected error creating the manual content archive" + ) + }); + }); + }); + + Settings.extraGroupsAtIndex = Settings.extraContentGroupsAtIndex; + + Settings.afterReloadActions = function() { + setupBackupUpload(); + setupContentArchives(); + + // load the latest backups immediately + reloadBackupInformation(); + + // setup a timer to reload them every 5 seconds + setInterval(reloadBackupInformation, 5000); + }; }); diff --git a/domain-server/resources/web/content/js/moment-locale.min.js b/domain-server/resources/web/content/js/moment-locale.min.js new file mode 100644 index 0000000000..fabea0c841 --- /dev/null +++ b/domain-server/resources/web/content/js/moment-locale.min.js @@ -0,0 +1 @@ +!function(e,a){"object"==typeof exports&&"undefined"!=typeof module?module.exports=a():"function"==typeof define&&define.amd?define(a):e.moment=a()}(this,function(){"use strict";function e(){return Ea.apply(null,arguments)}function a(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function t(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function s(e){return void 0===e}function n(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function d(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function r(e,a){var t,s=[];for(t=0;t0)for(t=0;t=0?t?"+":"":"-")+Math.pow(10,Math.max(0,n)).toString().substr(1)+s}function j(e,a,t,s){var n=s;"string"==typeof s&&(n=function(){return this[s]()}),e&&(Va[e]=n),a&&(Va[a[0]]=function(){return b(n.apply(this,arguments),a[1],a[2])}),t&&(Va[t]=function(){return this.localeData().ordinal(n.apply(this,arguments),e)})}function x(e){return e.match(/\[[\s\S]/)?e.replace(/^\[|\]$/g,""):e.replace(/\\/g,"")}function P(e,a){return e.isValid()?(a=O(a,e.localeData()),Ua[a]=Ua[a]||function(e){var a,t,s=e.match(Ca);for(a=0,t=s.length;a=0&&Ga.test(e);)e=e.replace(Ga,t),Ga.lastIndex=0,s-=1;return e}function W(e,a,t){ot[e]=D(a)?a:function(e,s){return e&&t?t:a}}function E(e,a){return _(ot,e)?ot[e](a._strict,a._locale):new RegExp(function(e){return A(e.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(e,a,t,s,n){return a||t||s||n}))}(e))}function A(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function F(e,a){var t,s=a;for("string"==typeof e&&(e=[e]),n(a)&&(s=function(e,t){t[a]=Y(e)}),t=0;t=0&&isFinite(a.getUTCFullYear())&&a.setUTCFullYear(e),a}function B(e,a,t){var s=7+a-t;return-((7+$(e,0,s).getUTCDay()-a)%7)+s-1}function q(e,a,t,s,n){var d,r,_=1+7*(a-1)+(7+t-s)%7+B(e,s,n);return _<=0?r=N(d=e-1)+_:_>N(e)?(d=e+1,r=_-N(e)):(d=e,r=_),{year:d,dayOfYear:r}}function Q(e,a,t){var s,n,d=B(e.year(),a,t),r=Math.floor((e.dayOfYear()-d-1)/7)+1;return r<1?s=r+X(n=e.year()-1,a,t):r>X(e.year(),a,t)?(s=r-X(e.year(),a,t),n=e.year()+1):(n=e.year(),s=r),{week:s,year:n}}function X(e,a,t){var s=B(e,a,t),n=B(e+1,a,t);return(N(e)-s+n)/7}function ee(){function e(e,a){return a.length-e.length}var a,t,s,n,d,r=[],_=[],i=[],m=[];for(a=0;a<7;a++)t=o([2e3,1]).day(a),s=this.weekdaysMin(t,""),n=this.weekdaysShort(t,""),d=this.weekdays(t,""),r.push(s),_.push(n),i.push(d),m.push(s),m.push(n),m.push(d);for(r.sort(e),_.sort(e),i.sort(e),m.sort(e),a=0;a<7;a++)_[a]=A(_[a]),i[a]=A(i[a]),m[a]=A(m[a]);this._weekdaysRegex=new RegExp("^("+m.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+_.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+r.join("|")+")","i")}function ae(){return this.hours()%12||12}function te(e,a){j(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),a)})}function se(e,a){return a._meridiemParse}function ne(e){return e?e.toLowerCase().replace("_","-"):e}function de(e){var a=null;if(!At[e]&&"undefined"!=typeof module&&module&&module.exports)try{a=Ot._abbr;require("./locale/"+e),re(a)}catch(e){}return At[e]}function re(e,a){var t;return e&&(t=s(a)?ie(e):_e(e,a))&&(Ot=t),Ot._abbr}function _e(e,a){if(null!==a){var t=Et;if(a.abbr=e,null!=At[e])p("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),t=At[e]._config;else if(null!=a.parentLocale){if(null==At[a.parentLocale])return Ft[a.parentLocale]||(Ft[a.parentLocale]=[]),Ft[a.parentLocale].push({name:e,config:a}),null;t=At[a.parentLocale]._config}return At[e]=new g(T(t,a)),Ft[e]&&Ft[e].forEach(function(e){_e(e.name,e.config)}),re(e),At[e]}return delete At[e],null}function ie(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return Ot;if(!a(e)){if(t=de(e))return t;e=[e]}return function(e){for(var a,t,s,n,d=0;d0;){if(s=de(n.slice(0,a).join("-")))return s;if(t&&t.length>=a&&y(n,t,!0)>=a-1)break;a--}d++}return null}(e)}function oe(e){var a,t=e._a;return t&&-2===m(e).overflow&&(a=t[lt]<0||t[lt]>11?lt:t[Mt]<1||t[Mt]>U(t[ut],t[lt])?Mt:t[ht]<0||t[ht]>24||24===t[ht]&&(0!==t[Lt]||0!==t[ct]||0!==t[Yt])?ht:t[Lt]<0||t[Lt]>59?Lt:t[ct]<0||t[ct]>59?ct:t[Yt]<0||t[Yt]>999?Yt:-1,m(e)._overflowDayOfYear&&(aMt)&&(a=Mt),m(e)._overflowWeeks&&-1===a&&(a=yt),m(e)._overflowWeekday&&-1===a&&(a=ft),m(e).overflow=a),e}function me(e,a,t){return null!=e?e:null!=a?a:t}function ue(a){var t,s,n,d,r,_=[];if(!a._d){for(n=function(a){var t=new Date(e.now());return a._useUTC?[t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate()]:[t.getFullYear(),t.getMonth(),t.getDate()]}(a),a._w&&null==a._a[Mt]&&null==a._a[lt]&&function(e){var a,t,s,n,d,r,_,i;if(null!=(a=e._w).GG||null!=a.W||null!=a.E)d=1,r=4,t=me(a.GG,e._a[ut],Q(ye(),1,4).year),s=me(a.W,1),((n=me(a.E,1))<1||n>7)&&(i=!0);else{d=e._locale._week.dow,r=e._locale._week.doy;var o=Q(ye(),d,r);t=me(a.gg,e._a[ut],o.year),s=me(a.w,o.week),null!=a.d?((n=a.d)<0||n>6)&&(i=!0):null!=a.e?(n=a.e+d,(a.e<0||a.e>6)&&(i=!0)):n=d}s<1||s>X(t,d,r)?m(e)._overflowWeeks=!0:null!=i?m(e)._overflowWeekday=!0:(_=q(t,s,n,d,r),e._a[ut]=_.year,e._dayOfYear=_.dayOfYear)}(a),null!=a._dayOfYear&&(r=me(a._a[ut],n[ut]),(a._dayOfYear>N(r)||0===a._dayOfYear)&&(m(a)._overflowDayOfYear=!0),s=$(r,0,a._dayOfYear),a._a[lt]=s.getUTCMonth(),a._a[Mt]=s.getUTCDate()),t=0;t<3&&null==a._a[t];++t)a._a[t]=_[t]=n[t];for(;t<7;t++)a._a[t]=_[t]=null==a._a[t]?2===t?1:0:a._a[t];24===a._a[ht]&&0===a._a[Lt]&&0===a._a[ct]&&0===a._a[Yt]&&(a._nextDay=!0,a._a[ht]=0),a._d=(a._useUTC?$:function(e,a,t,s,n,d,r){var _=new Date(e,a,t,s,n,d,r);return e<100&&e>=0&&isFinite(_.getFullYear())&&_.setFullYear(e),_}).apply(null,_),d=a._useUTC?a._d.getUTCDay():a._d.getDay(),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[ht]=24),a._w&&void 0!==a._w.d&&a._w.d!==d&&(m(a).weekdayMismatch=!0)}}function le(e){var a,t,s,n,d,r,_=e._i,i=zt.exec(_)||Jt.exec(_);if(i){for(m(e).iso=!0,a=0,t=Rt.length;a0&&m(a).unusedInput.push(r),_=_.slice(_.indexOf(s)+s.length),o+=s.length),Va[d]?(s?m(a).empty=!1:m(a).unusedTokens.push(d),J(d,s,a)):a._strict&&!s&&m(a).unusedTokens.push(d);m(a).charsLeftOver=i-o,_.length>0&&m(a).unusedInput.push(_),a._a[ht]<=12&&!0===m(a).bigHour&&a._a[ht]>0&&(m(a).bigHour=void 0),m(a).parsedDateParts=a._a.slice(0),m(a).meridiem=a._meridiem,a._a[ht]=function(e,a,t){var s;if(null==t)return a;return null!=e.meridiemHour?e.meridiemHour(a,t):null!=e.isPM?((s=e.isPM(t))&&a<12&&(a+=12),s||12!==a||(a=0),a):a}(a._locale,a._a[ht],a._meridiem),ue(a),oe(a)}else he(a);else le(a)}function ce(_){var o=_._i,c=_._f;return _._locale=_._locale||ie(_._l),null===o||void 0===c&&""===o?l({nullInput:!0}):("string"==typeof o&&(_._i=o=_._locale.preparse(o)),L(o)?new h(oe(o)):(d(o)?_._d=o:a(c)?function(e){var a,t,s,n,d;if(0===e._f.length)return m(e).invalidFormat=!0,void(e._d=new Date(NaN));for(n=0;nd&&(a=d),function(e,a,t,s,n){var d=q(e,a,t,s,n),r=$(d.year,0,d.dayOfYear);return this.year(r.getUTCFullYear()),this.month(r.getUTCMonth()),this.date(r.getUTCDate()),this}.call(this,e,a,t,s,n))}function ze(e,a){a[Yt]=Y(1e3*("0."+e))}function Je(e){return e}function Ne(e,a,t,s){var n=ie(),d=o().set(s,a);return n[t](d,e)}function Re(e,a,t){if(n(e)&&(a=e,e=void 0),e=e||"",null!=a)return Ne(e,a,t,"month");var s,d=[];for(s=0;s<12;s++)d[s]=Ne(e,s,t,"month");return d}function Ie(e,a,t,s){"boolean"==typeof e?(n(a)&&(t=a,a=void 0),a=a||""):(t=a=e,e=!1,n(a)&&(t=a,a=void 0),a=a||"");var d=ie(),r=e?d._week.dow:0;if(null!=t)return Ne(a,(t+r)%7,s,"day");var _,i=[];for(_=0;_<7;_++)i[_]=Ne(a,(_+r)%7,s,"day");return i}function Ce(e,a,t,s){var n=He(a,t);return e._milliseconds+=s*n._milliseconds,e._days+=s*n._days,e._months+=s*n._months,e._bubble()}function Ge(e){return e<0?Math.floor(e):Math.ceil(e)}function Ue(e){return 4800*e/146097}function Ve(e){return 146097*e/4800}function Ke(e){return function(){return this.as(e)}}function Ze(e){return function(){return this.isValid()?this._data[e]:NaN}}function $e(e){return(e>0)-(e<0)||+e}function Be(){if(!this.isValid())return this.localeData().invalidDate();var e,a,t=vs(this._milliseconds)/1e3,s=vs(this._days),n=vs(this._months);a=c((e=c(t/60))/60),t%=60,e%=60;var d=c(n/12),r=n%=12,_=s,i=a,o=e,m=t?t.toFixed(3).replace(/\.?0+$/,""):"",u=this.asSeconds();if(!u)return"P0D";var l=u<0?"-":"",M=$e(this._months)!==$e(u)?"-":"",h=$e(this._days)!==$e(u)?"-":"",L=$e(this._milliseconds)!==$e(u)?"-":"";return l+"P"+(d?M+d+"Y":"")+(r?M+r+"M":"")+(_?h+_+"D":"")+(i||o||m?"T":"")+(i?L+i+"H":"")+(o?L+o+"M":"")+(m?L+m+"S":"")}function qe(e,a,t){return"m"===t?a?"\u0445\u0432\u0456\u043b\u0456\u043d\u0430":"\u0445\u0432\u0456\u043b\u0456\u043d\u0443":"h"===t?a?"\u0433\u0430\u0434\u0437\u0456\u043d\u0430":"\u0433\u0430\u0434\u0437\u0456\u043d\u0443":e+" "+function(e,a){var t=e.split("_");return a%10==1&&a%100!=11?t[0]:a%10>=2&&a%10<=4&&(a%100<10||a%100>=20)?t[1]:t[2]}({ss:a?"\u0441\u0435\u043a\u0443\u043d\u0434\u0430_\u0441\u0435\u043a\u0443\u043d\u0434\u044b_\u0441\u0435\u043a\u0443\u043d\u0434":"\u0441\u0435\u043a\u0443\u043d\u0434\u0443_\u0441\u0435\u043a\u0443\u043d\u0434\u044b_\u0441\u0435\u043a\u0443\u043d\u0434",mm:a?"\u0445\u0432\u0456\u043b\u0456\u043d\u0430_\u0445\u0432\u0456\u043b\u0456\u043d\u044b_\u0445\u0432\u0456\u043b\u0456\u043d":"\u0445\u0432\u0456\u043b\u0456\u043d\u0443_\u0445\u0432\u0456\u043b\u0456\u043d\u044b_\u0445\u0432\u0456\u043b\u0456\u043d",hh:a?"\u0433\u0430\u0434\u0437\u0456\u043d\u0430_\u0433\u0430\u0434\u0437\u0456\u043d\u044b_\u0433\u0430\u0434\u0437\u0456\u043d":"\u0433\u0430\u0434\u0437\u0456\u043d\u0443_\u0433\u0430\u0434\u0437\u0456\u043d\u044b_\u0433\u0430\u0434\u0437\u0456\u043d",dd:"\u0434\u0437\u0435\u043d\u044c_\u0434\u043d\u0456_\u0434\u0437\u0451\u043d",MM:"\u043c\u0435\u0441\u044f\u0446_\u043c\u0435\u0441\u044f\u0446\u044b_\u043c\u0435\u0441\u044f\u0446\u0430\u045e",yy:"\u0433\u043e\u0434_\u0433\u0430\u0434\u044b_\u0433\u0430\u0434\u043e\u045e"}[t],+e)}function Qe(e,a,t){return e+" "+function(e,a){if(2===a)return function(e){var a={m:"v",b:"v",d:"z"};if(void 0===a[e.charAt(0)])return e;return a[e.charAt(0)]+e.substring(1)}(e);return e}({mm:"munutenn",MM:"miz",dd:"devezh"}[t],e)}function Xe(e){return e>9?Xe(e%10):e}function ea(e,a,t){var s=e+" ";switch(t){case"ss":return s+=1===e?"sekunda":2===e||3===e||4===e?"sekunde":"sekundi";case"m":return a?"jedna minuta":"jedne minute";case"mm":return s+=1===e?"minuta":2===e||3===e||4===e?"minute":"minuta";case"h":return a?"jedan sat":"jednog sata";case"hh":return s+=1===e?"sat":2===e||3===e||4===e?"sata":"sati";case"dd":return s+=1===e?"dan":"dana";case"MM":return s+=1===e?"mjesec":2===e||3===e||4===e?"mjeseca":"mjeseci";case"yy":return s+=1===e?"godina":2===e||3===e||4===e?"godine":"godina"}}function aa(e){return e>1&&e<5&&1!=~~(e/10)}function ta(e,a,t,s){var n=e+" ";switch(t){case"s":return a||s?"p\xe1r sekund":"p\xe1r sekundami";case"ss":return a||s?n+(aa(e)?"sekundy":"sekund"):n+"sekundami";break;case"m":return a?"minuta":s?"minutu":"minutou";case"mm":return a||s?n+(aa(e)?"minuty":"minut"):n+"minutami";break;case"h":return a?"hodina":s?"hodinu":"hodinou";case"hh":return a||s?n+(aa(e)?"hodiny":"hodin"):n+"hodinami";break;case"d":return a||s?"den":"dnem";case"dd":return a||s?n+(aa(e)?"dny":"dn\xed"):n+"dny";break;case"M":return a||s?"m\u011bs\xedc":"m\u011bs\xedcem";case"MM":return a||s?n+(aa(e)?"m\u011bs\xedce":"m\u011bs\xedc\u016f"):n+"m\u011bs\xedci";break;case"y":return a||s?"rok":"rokem";case"yy":return a||s?n+(aa(e)?"roky":"let"):n+"lety";break}}function sa(e,a,t,s){var n={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[e+" Tage",e+" Tagen"],M:["ein Monat","einem Monat"],MM:[e+" Monate",e+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[e+" Jahre",e+" Jahren"]};return a?n[t][0]:n[t][1]}function na(e,a,t,s){var n={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[e+" Tage",e+" Tagen"],M:["ein Monat","einem Monat"],MM:[e+" Monate",e+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[e+" Jahre",e+" Jahren"]};return a?n[t][0]:n[t][1]}function da(e,a,t,s){var n={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[e+" Tage",e+" Tagen"],M:["ein Monat","einem Monat"],MM:[e+" Monate",e+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[e+" Jahre",e+" Jahren"]};return a?n[t][0]:n[t][1]}function ra(e,a,t,s){var n={s:["m\xf5ne sekundi","m\xf5ni sekund","paar sekundit"],ss:[e+"sekundi",e+"sekundit"],m:["\xfche minuti","\xfcks minut"],mm:[e+" minuti",e+" minutit"],h:["\xfche tunni","tund aega","\xfcks tund"],hh:[e+" tunni",e+" tundi"],d:["\xfche p\xe4eva","\xfcks p\xe4ev"],M:["kuu aja","kuu aega","\xfcks kuu"],MM:[e+" kuu",e+" kuud"],y:["\xfche aasta","aasta","\xfcks aasta"],yy:[e+" aasta",e+" aastat"]};return a?n[t][2]?n[t][2]:n[t][1]:s?n[t][0]:n[t][1]}function _a(e,a,t,s){var n="";switch(t){case"s":return s?"muutaman sekunnin":"muutama sekunti";case"ss":return s?"sekunnin":"sekuntia";case"m":return s?"minuutin":"minuutti";case"mm":n=s?"minuutin":"minuuttia";break;case"h":return s?"tunnin":"tunti";case"hh":n=s?"tunnin":"tuntia";break;case"d":return s?"p\xe4iv\xe4n":"p\xe4iv\xe4";case"dd":n=s?"p\xe4iv\xe4n":"p\xe4iv\xe4\xe4";break;case"M":return s?"kuukauden":"kuukausi";case"MM":n=s?"kuukauden":"kuukautta";break;case"y":return s?"vuoden":"vuosi";case"yy":n=s?"vuoden":"vuotta";break}return n=function(e,a){return e<10?a?mn[e]:on[e]:e}(e,s)+" "+n}function ia(e,a,t,s){var n={s:["thodde secondanim","thodde second"],ss:[e+" secondanim",e+" second"],m:["eka mintan","ek minute"],mm:[e+" mintanim",e+" mintam"],h:["eka horan","ek hor"],hh:[e+" horanim",e+" hor"],d:["eka disan","ek dis"],dd:[e+" disanim",e+" dis"],M:["eka mhoinean","ek mhoino"],MM:[e+" mhoineanim",e+" mhoine"],y:["eka vorsan","ek voros"],yy:[e+" vorsanim",e+" vorsam"]};return a?n[t][0]:n[t][1]}function oa(e,a,t){var s=e+" ";switch(t){case"ss":return s+=1===e?"sekunda":2===e||3===e||4===e?"sekunde":"sekundi";case"m":return a?"jedna minuta":"jedne minute";case"mm":return s+=1===e?"minuta":2===e||3===e||4===e?"minute":"minuta";case"h":return a?"jedan sat":"jednog sata";case"hh":return s+=1===e?"sat":2===e||3===e||4===e?"sata":"sati";case"dd":return s+=1===e?"dan":"dana";case"MM":return s+=1===e?"mjesec":2===e||3===e||4===e?"mjeseca":"mjeseci";case"yy":return s+=1===e?"godina":2===e||3===e||4===e?"godine":"godina"}}function ma(e,a,t,s){var n=e;switch(t){case"s":return s||a?"n\xe9h\xe1ny m\xe1sodperc":"n\xe9h\xe1ny m\xe1sodperce";case"ss":return n+(s||a)?" m\xe1sodperc":" m\xe1sodperce";case"m":return"egy"+(s||a?" perc":" perce");case"mm":return n+(s||a?" perc":" perce");case"h":return"egy"+(s||a?" \xf3ra":" \xf3r\xe1ja");case"hh":return n+(s||a?" \xf3ra":" \xf3r\xe1ja");case"d":return"egy"+(s||a?" nap":" napja");case"dd":return n+(s||a?" nap":" napja");case"M":return"egy"+(s||a?" h\xf3nap":" h\xf3napja");case"MM":return n+(s||a?" h\xf3nap":" h\xf3napja");case"y":return"egy"+(s||a?" \xe9v":" \xe9ve");case"yy":return n+(s||a?" \xe9v":" \xe9ve")}return""}function ua(e){return(e?"":"[m\xfalt] ")+"["+Yn[this.day()]+"] LT[-kor]"}function la(e){return e%100==11||e%10!=1}function Ma(e,a,t,s){var n=e+" ";switch(t){case"s":return a||s?"nokkrar sek\xfandur":"nokkrum sek\xfandum";case"ss":return la(e)?n+(a||s?"sek\xfandur":"sek\xfandum"):n+"sek\xfanda";case"m":return a?"m\xedn\xfata":"m\xedn\xfatu";case"mm":return la(e)?n+(a||s?"m\xedn\xfatur":"m\xedn\xfatum"):a?n+"m\xedn\xfata":n+"m\xedn\xfatu";case"hh":return la(e)?n+(a||s?"klukkustundir":"klukkustundum"):n+"klukkustund";case"d":return a?"dagur":s?"dag":"degi";case"dd":return la(e)?a?n+"dagar":n+(s?"daga":"d\xf6gum"):a?n+"dagur":n+(s?"dag":"degi");case"M":return a?"m\xe1nu\xf0ur":s?"m\xe1nu\xf0":"m\xe1nu\xf0i";case"MM":return la(e)?a?n+"m\xe1nu\xf0ir":n+(s?"m\xe1nu\xf0i":"m\xe1nu\xf0um"):a?n+"m\xe1nu\xf0ur":n+(s?"m\xe1nu\xf0":"m\xe1nu\xf0i");case"y":return a||s?"\xe1r":"\xe1ri";case"yy":return la(e)?n+(a||s?"\xe1r":"\xe1rum"):n+(a||s?"\xe1r":"\xe1ri")}}function ha(e,a,t,s){var n={m:["eng Minutt","enger Minutt"],h:["eng Stonn","enger Stonn"],d:["een Dag","engem Dag"],M:["ee Mount","engem Mount"],y:["ee Joer","engem Joer"]};return a?n[t][0]:n[t][1]}function La(e){if(e=parseInt(e,10),isNaN(e))return!1;if(e<0)return!0;if(e<10)return 4<=e&&e<=7;if(e<100){var a=e%10;return La(0===a?e/10:a)}if(e<1e4){for(;e>=10;)e/=10;return La(e)}return e/=1e3,La(e)}function ca(e,a,t,s){return a?ya(t)[0]:s?ya(t)[1]:ya(t)[2]}function Ya(e){return e%10==0||e>10&&e<20}function ya(e){return Dn[e].split("_")}function fa(e,a,t,s){var n=e+" ";return 1===e?n+ca(0,a,t[0],s):a?n+(Ya(e)?ya(t)[1]:ya(t)[0]):s?n+ya(t)[1]:n+(Ya(e)?ya(t)[1]:ya(t)[2])}function ka(e,a,t){return t?a%10==1&&a%100!=11?e[2]:e[3]:a%10==1&&a%100!=11?e[0]:e[1]}function pa(e,a,t){return e+" "+ka(Tn[t],e,a)}function Da(e,a,t){return ka(Tn[t],e,a)}function Ta(e,a,t,s){var n="";if(a)switch(t){case"s":n="\u0915\u093e\u0939\u0940 \u0938\u0947\u0915\u0902\u0926";break;case"ss":n="%d \u0938\u0947\u0915\u0902\u0926";break;case"m":n="\u090f\u0915 \u092e\u093f\u0928\u093f\u091f";break;case"mm":n="%d \u092e\u093f\u0928\u093f\u091f\u0947";break;case"h":n="\u090f\u0915 \u0924\u093e\u0938";break;case"hh":n="%d \u0924\u093e\u0938";break;case"d":n="\u090f\u0915 \u0926\u093f\u0935\u0938";break;case"dd":n="%d \u0926\u093f\u0935\u0938";break;case"M":n="\u090f\u0915 \u092e\u0939\u093f\u0928\u093e";break;case"MM":n="%d \u092e\u0939\u093f\u0928\u0947";break;case"y":n="\u090f\u0915 \u0935\u0930\u094d\u0937";break;case"yy":n="%d \u0935\u0930\u094d\u0937\u0947";break}else switch(t){case"s":n="\u0915\u093e\u0939\u0940 \u0938\u0947\u0915\u0902\u0926\u093e\u0902";break;case"ss":n="%d \u0938\u0947\u0915\u0902\u0926\u093e\u0902";break;case"m":n="\u090f\u0915\u093e \u092e\u093f\u0928\u093f\u091f\u093e";break;case"mm":n="%d \u092e\u093f\u0928\u093f\u091f\u093e\u0902";break;case"h":n="\u090f\u0915\u093e \u0924\u093e\u0938\u093e";break;case"hh":n="%d \u0924\u093e\u0938\u093e\u0902";break;case"d":n="\u090f\u0915\u093e \u0926\u093f\u0935\u0938\u093e";break;case"dd":n="%d \u0926\u093f\u0935\u0938\u093e\u0902";break;case"M":n="\u090f\u0915\u093e \u092e\u0939\u093f\u0928\u094d\u092f\u093e";break;case"MM":n="%d \u092e\u0939\u093f\u0928\u094d\u092f\u093e\u0902";break;case"y":n="\u090f\u0915\u093e \u0935\u0930\u094d\u0937\u093e";break;case"yy":n="%d \u0935\u0930\u094d\u0937\u093e\u0902";break}return n.replace(/%d/i,e)}function ga(e){return e%10<5&&e%10>1&&~~(e/10)%10!=1}function wa(e,a,t){var s=e+" ";switch(t){case"ss":return s+(ga(e)?"sekundy":"sekund");case"m":return a?"minuta":"minut\u0119";case"mm":return s+(ga(e)?"minuty":"minut");case"h":return a?"godzina":"godzin\u0119";case"hh":return s+(ga(e)?"godziny":"godzin");case"MM":return s+(ga(e)?"miesi\u0105ce":"miesi\u0119cy");case"yy":return s+(ga(e)?"lata":"lat")}}function va(e,a,t){var s=" ";return(e%100>=20||e>=100&&e%100==0)&&(s=" de "),e+s+{ss:"secunde",mm:"minute",hh:"ore",dd:"zile",MM:"luni",yy:"ani"}[t]}function Sa(e,a,t){return"m"===t?a?"\u043c\u0438\u043d\u0443\u0442\u0430":"\u043c\u0438\u043d\u0443\u0442\u0443":e+" "+function(e,a){var t=e.split("_");return a%10==1&&a%100!=11?t[0]:a%10>=2&&a%10<=4&&(a%100<10||a%100>=20)?t[1]:t[2]}({ss:a?"\u0441\u0435\u043a\u0443\u043d\u0434\u0430_\u0441\u0435\u043a\u0443\u043d\u0434\u044b_\u0441\u0435\u043a\u0443\u043d\u0434":"\u0441\u0435\u043a\u0443\u043d\u0434\u0443_\u0441\u0435\u043a\u0443\u043d\u0434\u044b_\u0441\u0435\u043a\u0443\u043d\u0434",mm:a?"\u043c\u0438\u043d\u0443\u0442\u0430_\u043c\u0438\u043d\u0443\u0442\u044b_\u043c\u0438\u043d\u0443\u0442":"\u043c\u0438\u043d\u0443\u0442\u0443_\u043c\u0438\u043d\u0443\u0442\u044b_\u043c\u0438\u043d\u0443\u0442",hh:"\u0447\u0430\u0441_\u0447\u0430\u0441\u0430_\u0447\u0430\u0441\u043e\u0432",dd:"\u0434\u0435\u043d\u044c_\u0434\u043d\u044f_\u0434\u043d\u0435\u0439",MM:"\u043c\u0435\u0441\u044f\u0446_\u043c\u0435\u0441\u044f\u0446\u0430_\u043c\u0435\u0441\u044f\u0446\u0435\u0432",yy:"\u0433\u043e\u0434_\u0433\u043e\u0434\u0430_\u043b\u0435\u0442"}[t],+e)}function Ha(e){return e>1&&e<5}function ba(e,a,t,s){var n=e+" ";switch(t){case"s":return a||s?"p\xe1r sek\xfand":"p\xe1r sekundami";case"ss":return a||s?n+(Ha(e)?"sekundy":"sek\xfand"):n+"sekundami";break;case"m":return a?"min\xfata":s?"min\xfatu":"min\xfatou";case"mm":return a||s?n+(Ha(e)?"min\xfaty":"min\xfat"):n+"min\xfatami";break;case"h":return a?"hodina":s?"hodinu":"hodinou";case"hh":return a||s?n+(Ha(e)?"hodiny":"hod\xedn"):n+"hodinami";break;case"d":return a||s?"de\u0148":"d\u0148om";case"dd":return a||s?n+(Ha(e)?"dni":"dn\xed"):n+"d\u0148ami";break;case"M":return a||s?"mesiac":"mesiacom";case"MM":return a||s?n+(Ha(e)?"mesiace":"mesiacov"):n+"mesiacmi";break;case"y":return a||s?"rok":"rokom";case"yy":return a||s?n+(Ha(e)?"roky":"rokov"):n+"rokmi";break}}function ja(e,a,t,s){var n=e+" ";switch(t){case"s":return a||s?"nekaj sekund":"nekaj sekundami";case"ss":return n+=1===e?a?"sekundo":"sekundi":2===e?a||s?"sekundi":"sekundah":e<5?a||s?"sekunde":"sekundah":"sekund";case"m":return a?"ena minuta":"eno minuto";case"mm":return n+=1===e?a?"minuta":"minuto":2===e?a||s?"minuti":"minutama":e<5?a||s?"minute":"minutami":a||s?"minut":"minutami";case"h":return a?"ena ura":"eno uro";case"hh":return n+=1===e?a?"ura":"uro":2===e?a||s?"uri":"urama":e<5?a||s?"ure":"urami":a||s?"ur":"urami";case"d":return a||s?"en dan":"enim dnem";case"dd":return n+=1===e?a||s?"dan":"dnem":2===e?a||s?"dni":"dnevoma":a||s?"dni":"dnevi";case"M":return a||s?"en mesec":"enim mesecem";case"MM":return n+=1===e?a||s?"mesec":"mesecem":2===e?a||s?"meseca":"mesecema":e<5?a||s?"mesece":"meseci":a||s?"mesecev":"meseci";case"y":return a||s?"eno leto":"enim letom";case"yy":return n+=1===e?a||s?"leto":"letom":2===e?a||s?"leti":"letoma":e<5?a||s?"leta":"leti":a||s?"let":"leti"}}function xa(e,a,t,s){var n=function(e){var a=Math.floor(e%1e3/100),t=Math.floor(e%100/10),s=e%10,n="";a>0&&(n+=Qn[a]+"vatlh");t>0&&(n+=(""!==n?" ":"")+Qn[t]+"maH");s>0&&(n+=(""!==n?" ":"")+Qn[s]);return""===n?"pagh":n}(e);switch(t){case"ss":return n+" lup";case"mm":return n+" tup";case"hh":return n+" rep";case"dd":return n+" jaj";case"MM":return n+" jar";case"yy":return n+" DIS"}}function Pa(e,a,t,s){var n={s:["viensas secunds","'iensas secunds"],ss:[e+" secunds",e+" secunds"],m:["'n m\xedut","'iens m\xedut"],mm:[e+" m\xeduts",e+" m\xeduts"],h:["'n \xfeora","'iensa \xfeora"],hh:[e+" \xfeoras",e+" \xfeoras"],d:["'n ziua","'iensa ziua"],dd:[e+" ziuas",e+" ziuas"],M:["'n mes","'iens mes"],MM:[e+" mesen",e+" mesen"],y:["'n ar","'iens ar"],yy:[e+" ars",e+" ars"]};return s?n[t][0]:a?n[t][0]:n[t][1]}function Oa(e,a,t){return"m"===t?a?"\u0445\u0432\u0438\u043b\u0438\u043d\u0430":"\u0445\u0432\u0438\u043b\u0438\u043d\u0443":"h"===t?a?"\u0433\u043e\u0434\u0438\u043d\u0430":"\u0433\u043e\u0434\u0438\u043d\u0443":e+" "+function(e,a){var t=e.split("_");return a%10==1&&a%100!=11?t[0]:a%10>=2&&a%10<=4&&(a%100<10||a%100>=20)?t[1]:t[2]}({ss:a?"\u0441\u0435\u043a\u0443\u043d\u0434\u0430_\u0441\u0435\u043a\u0443\u043d\u0434\u0438_\u0441\u0435\u043a\u0443\u043d\u0434":"\u0441\u0435\u043a\u0443\u043d\u0434\u0443_\u0441\u0435\u043a\u0443\u043d\u0434\u0438_\u0441\u0435\u043a\u0443\u043d\u0434",mm:a?"\u0445\u0432\u0438\u043b\u0438\u043d\u0430_\u0445\u0432\u0438\u043b\u0438\u043d\u0438_\u0445\u0432\u0438\u043b\u0438\u043d":"\u0445\u0432\u0438\u043b\u0438\u043d\u0443_\u0445\u0432\u0438\u043b\u0438\u043d\u0438_\u0445\u0432\u0438\u043b\u0438\u043d",hh:a?"\u0433\u043e\u0434\u0438\u043d\u0430_\u0433\u043e\u0434\u0438\u043d\u0438_\u0433\u043e\u0434\u0438\u043d":"\u0433\u043e\u0434\u0438\u043d\u0443_\u0433\u043e\u0434\u0438\u043d\u0438_\u0433\u043e\u0434\u0438\u043d",dd:"\u0434\u0435\u043d\u044c_\u0434\u043d\u0456_\u0434\u043d\u0456\u0432",MM:"\u043c\u0456\u0441\u044f\u0446\u044c_\u043c\u0456\u0441\u044f\u0446\u0456_\u043c\u0456\u0441\u044f\u0446\u0456\u0432",yy:"\u0440\u0456\u043a_\u0440\u043e\u043a\u0438_\u0440\u043e\u043a\u0456\u0432"}[t],+e)}function Wa(e){return function(){return e+"\u043e"+(11===this.hours()?"\u0431":"")+"] LT"}}var Ea,Aa;Aa=Array.prototype.some?Array.prototype.some:function(e){for(var a=Object(this),t=a.length>>>0,s=0;s68?1900:2e3)};var kt,pt=I("FullYear",!0);kt=Array.prototype.indexOf?Array.prototype.indexOf:function(e){var a;for(a=0;athis?this:e:l()}),Zt=["year","quarter","month","week","day","hour","minute","second","millisecond"];Te("Z",":"),Te("ZZ",""),W("Z",_t),W("ZZ",_t),F(["Z","ZZ"],function(e,a,t){t._useUTC=!0,t._tzm=ge(_t,e)});var $t=/([\+\-]|\d\d)/gi;e.updateOffset=function(){};var Bt=/^(\-|\+)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,qt=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;He.fn=ke.prototype,He.invalid=function(){return He(NaN)};var Qt=xe(1,"add"),Xt=xe(-1,"subtract");e.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",e.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var es=k("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(e){return void 0===e?this.localeData():this.locale(e)});j(0,["gg",2],0,function(){return this.weekYear()%100}),j(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Ae("gggg","weekYear"),Ae("ggggg","weekYear"),Ae("GGGG","isoWeekYear"),Ae("GGGGG","isoWeekYear"),w("weekYear","gg"),w("isoWeekYear","GG"),H("weekYear",1),H("isoWeekYear",1),W("G",dt),W("g",dt),W("GG",Qa,Za),W("gg",Qa,Za),W("GGGG",tt,Ba),W("gggg",tt,Ba),W("GGGGG",st,qa),W("ggggg",st,qa),z(["gggg","ggggg","GGGG","GGGGG"],function(e,a,t,s){a[s.substr(0,2)]=Y(e)}),z(["gg","GG"],function(a,t,s,n){t[n]=e.parseTwoDigitYear(a)}),j("Q",0,"Qo","quarter"),w("quarter","Q"),H("quarter",7),W("Q",Ka),F("Q",function(e,a){a[lt]=3*(Y(e)-1)}),j("D",["DD",2],"Do","date"),w("date","D"),H("date",9),W("D",Qa),W("DD",Qa,Za),W("Do",function(e,a){return e?a._dayOfMonthOrdinalParse||a._ordinalParse:a._dayOfMonthOrdinalParseLenient}),F(["D","DD"],Mt),F("Do",function(e,a){a[Mt]=Y(e.match(Qa)[0])});var as=I("Date",!0);j("DDD",["DDDD",3],"DDDo","dayOfYear"),w("dayOfYear","DDD"),H("dayOfYear",4),W("DDD",at),W("DDDD",$a),F(["DDD","DDDD"],function(e,a,t){t._dayOfYear=Y(e)}),j("m",["mm",2],0,"minute"),w("minute","m"),H("minute",14),W("m",Qa),W("mm",Qa,Za),F(["m","mm"],Lt);var ts=I("Minutes",!1);j("s",["ss",2],0,"second"),w("second","s"),H("second",15),W("s",Qa),W("ss",Qa,Za),F(["s","ss"],ct);var ss=I("Seconds",!1);j("S",0,0,function(){return~~(this.millisecond()/100)}),j(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),j(0,["SSS",3],0,"millisecond"),j(0,["SSSS",4],0,function(){return 10*this.millisecond()}),j(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),j(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),j(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),j(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),j(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),w("millisecond","ms"),H("millisecond",16),W("S",at,Ka),W("SS",at,Za),W("SSS",at,$a);var ns;for(ns="SSSS";ns.length<=9;ns+="S")W(ns,nt);for(ns="S";ns.length<=9;ns+="S")F(ns,ze);var ds=I("Milliseconds",!1);j("z",0,0,"zoneAbbr"),j("zz",0,0,"zoneName");var rs=h.prototype;rs.add=Qt,rs.calendar=function(a,t){var s=a||ye(),n=we(s,this).startOf("day"),d=e.calendarFormat(this,n)||"sameElse",r=t&&(D(t[d])?t[d].call(this,s):t[d]);return this.format(r||this.localeData().calendar(d,this,ye(s)))},rs.clone=function(){return new h(this)},rs.diff=function(e,a,t){var s,n,d;if(!this.isValid())return NaN;if(!(s=we(e,this)).isValid())return NaN;switch(n=6e4*(s.utcOffset()-this.utcOffset()),a=v(a)){case"year":d=Oe(this,s)/12;break;case"month":d=Oe(this,s);break;case"quarter":d=Oe(this,s)/3;break;case"second":d=(this-s)/1e3;break;case"minute":d=(this-s)/6e4;break;case"hour":d=(this-s)/36e5;break;case"day":d=(this-s-n)/864e5;break;case"week":d=(this-s-n)/6048e5;break;default:d=this-s}return t?d:c(d)},rs.endOf=function(e){return void 0===(e=v(e))||"millisecond"===e?this:("date"===e&&(e="day"),this.startOf(e).add(1,"isoWeek"===e?"week":e).subtract(1,"ms"))},rs.format=function(a){a||(a=this.isUtc()?e.defaultFormatUtc:e.defaultFormat);var t=P(this,a);return this.localeData().postformat(t)},rs.from=function(e,a){return this.isValid()&&(L(e)&&e.isValid()||ye(e).isValid())?He({to:this,from:e}).locale(this.locale()).humanize(!a):this.localeData().invalidDate()},rs.fromNow=function(e){return this.from(ye(),e)},rs.to=function(e,a){return this.isValid()&&(L(e)&&e.isValid()||ye(e).isValid())?He({from:this,to:e}).locale(this.locale()).humanize(!a):this.localeData().invalidDate()},rs.toNow=function(e){return this.to(ye(),e)},rs.get=function(e){return e=v(e),D(this[e])?this[e]():this},rs.invalidAt=function(){return m(this).overflow},rs.isAfter=function(e,a){var t=L(e)?e:ye(e);return!(!this.isValid()||!t.isValid())&&("millisecond"===(a=v(s(a)?"millisecond":a))?this.valueOf()>t.valueOf():t.valueOf()9999?P(t,a?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):D(Date.prototype.toISOString)?a?this.toDate().toISOString():new Date(this._d.valueOf()).toISOString().replace("Z",P(t,"Z")):P(t,a?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},rs.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var e="moment",a="";this.isLocal()||(e=0===this.utcOffset()?"moment.utc":"moment.parseZone",a="Z");var t="["+e+'("]',s=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",n=a+'[")]';return this.format(t+s+"-MM-DD[T]HH:mm:ss.SSS"+n)},rs.toJSON=function(){return this.isValid()?this.toISOString():null},rs.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},rs.unix=function(){return Math.floor(this.valueOf()/1e3)},rs.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},rs.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},rs.year=pt,rs.isLeapYear=function(){return R(this.year())},rs.weekYear=function(e){return Fe.call(this,e,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},rs.isoWeekYear=function(e){return Fe.call(this,e,this.isoWeek(),this.isoWeekday(),1,4)},rs.quarter=rs.quarters=function(e){return null==e?Math.ceil((this.month()+1)/3):this.month(3*(e-1)+this.month()%3)},rs.month=K,rs.daysInMonth=function(){return U(this.year(),this.month())},rs.week=rs.weeks=function(e){var a=this.localeData().week(this);return null==e?a:this.add(7*(e-a),"d")},rs.isoWeek=rs.isoWeeks=function(e){var a=Q(this,1,4).week;return null==e?a:this.add(7*(e-a),"d")},rs.weeksInYear=function(){var e=this.localeData()._week;return X(this.year(),e.dow,e.doy)},rs.isoWeeksInYear=function(){return X(this.year(),1,4)},rs.date=as,rs.day=rs.days=function(e){if(!this.isValid())return null!=e?this:NaN;var a=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=e?(e=function(e,a){return"string"!=typeof e?e:isNaN(e)?"number"==typeof(e=a.weekdaysParse(e))?e:null:parseInt(e,10)}(e,this.localeData()),this.add(e-a,"d")):a},rs.weekday=function(e){if(!this.isValid())return null!=e?this:NaN;var a=(this.day()+7-this.localeData()._week.dow)%7;return null==e?a:this.add(e-a,"d")},rs.isoWeekday=function(e){if(!this.isValid())return null!=e?this:NaN;if(null!=e){var a=function(e,a){return"string"==typeof e?a.weekdaysParse(e)%7||7:isNaN(e)?null:e}(e,this.localeData());return this.day(this.day()%7?a:a-7)}return this.day()||7},rs.dayOfYear=function(e){var a=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==e?a:this.add(e-a,"d")},rs.hour=rs.hours=Wt,rs.minute=rs.minutes=ts,rs.second=rs.seconds=ss,rs.millisecond=rs.milliseconds=ds,rs.utcOffset=function(a,t,s){var n,d=this._offset||0;if(!this.isValid())return null!=a?this:NaN;if(null!=a){if("string"==typeof a){if(null===(a=ge(_t,a)))return this}else Math.abs(a)<16&&!s&&(a*=60);return!this._isUTC&&t&&(n=ve(this)),this._offset=a,this._isUTC=!0,null!=n&&this.add(n,"m"),d!==a&&(!t||this._changeInProgress?Pe(this,He(a-d,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,e.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?d:ve(this)},rs.utc=function(e){return this.utcOffset(0,e)},rs.local=function(e){return this._isUTC&&(this.utcOffset(0,e),this._isUTC=!1,e&&this.subtract(ve(this),"m")),this},rs.parseZone=function(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var e=ge(rt,this._i);null!=e?this.utcOffset(e):this.utcOffset(0,!0)}return this},rs.hasAlignedHourOffset=function(e){return!!this.isValid()&&(e=e?ye(e).utcOffset():0,(this.utcOffset()-e)%60==0)},rs.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},rs.isLocal=function(){return!!this.isValid()&&!this._isUTC},rs.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},rs.isUtc=Se,rs.isUTC=Se,rs.zoneAbbr=function(){return this._isUTC?"UTC":""},rs.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},rs.dates=k("dates accessor is deprecated. Use date instead.",as),rs.months=k("months accessor is deprecated. Use month instead",K),rs.years=k("years accessor is deprecated. Use year instead",pt),rs.zone=k("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,a){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,a),this):-this.utcOffset()}),rs.isDSTShifted=k("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!s(this._isDSTShifted))return this._isDSTShifted;var e={};if(M(e,this),(e=ce(e))._a){var a=e._isUTC?o(e._a):ye(e._a);this._isDSTShifted=this.isValid()&&y(e._a,a.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted});var _s=g.prototype;_s.calendar=function(e,a,t){var s=this._calendar[e]||this._calendar.sameElse;return D(s)?s.call(a,t):s},_s.longDateFormat=function(e){var a=this._longDateFormat[e],t=this._longDateFormat[e.toUpperCase()];return a||!t?a:(this._longDateFormat[e]=t.replace(/MMMM|MM|DD|dddd/g,function(e){return e.slice(1)}),this._longDateFormat[e])},_s.invalidDate=function(){return this._invalidDate},_s.ordinal=function(e){return this._ordinal.replace("%d",e)},_s.preparse=Je,_s.postformat=Je,_s.relativeTime=function(e,a,t,s){var n=this._relativeTime[t];return D(n)?n(e,a,t,s):n.replace(/%d/i,e)},_s.pastFuture=function(e,a){var t=this._relativeTime[e>0?"future":"past"];return D(t)?t(a):t.replace(/%s/i,a)},_s.set=function(e){var a,t;for(t in e)D(a=e[t])?this[t]=a:this["_"+t]=a;this._config=e,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},_s.months=function(e,t){return e?a(this._months)?this._months[e.month()]:this._months[(this._months.isFormat||Dt).test(t)?"format":"standalone"][e.month()]:a(this._months)?this._months:this._months.standalone},_s.monthsShort=function(e,t){return e?a(this._monthsShort)?this._monthsShort[e.month()]:this._monthsShort[Dt.test(t)?"format":"standalone"][e.month()]:a(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},_s.monthsParse=function(e,a,t){var s,n,d;if(this._monthsParseExact)return function(e,a,t){var s,n,d,r=e.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],s=0;s<12;++s)d=o([2e3,s]),this._shortMonthsParse[s]=this.monthsShort(d,"").toLocaleLowerCase(),this._longMonthsParse[s]=this.months(d,"").toLocaleLowerCase();return t?"MMM"===a?-1!==(n=kt.call(this._shortMonthsParse,r))?n:null:-1!==(n=kt.call(this._longMonthsParse,r))?n:null:"MMM"===a?-1!==(n=kt.call(this._shortMonthsParse,r))?n:-1!==(n=kt.call(this._longMonthsParse,r))?n:null:-1!==(n=kt.call(this._longMonthsParse,r))?n:-1!==(n=kt.call(this._shortMonthsParse,r))?n:null}.call(this,e,a,t);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),s=0;s<12;s++){if(n=o([2e3,s]),t&&!this._longMonthsParse[s]&&(this._longMonthsParse[s]=new RegExp("^"+this.months(n,"").replace(".","")+"$","i"),this._shortMonthsParse[s]=new RegExp("^"+this.monthsShort(n,"").replace(".","")+"$","i")),t||this._monthsParse[s]||(d="^"+this.months(n,"")+"|^"+this.monthsShort(n,""),this._monthsParse[s]=new RegExp(d.replace(".",""),"i")),t&&"MMMM"===a&&this._longMonthsParse[s].test(e))return s;if(t&&"MMM"===a&&this._shortMonthsParse[s].test(e))return s;if(!t&&this._monthsParse[s].test(e))return s}},_s.monthsRegex=function(e){return this._monthsParseExact?(_(this,"_monthsRegex")||Z.call(this),e?this._monthsStrictRegex:this._monthsRegex):(_(this,"_monthsRegex")||(this._monthsRegex=vt),this._monthsStrictRegex&&e?this._monthsStrictRegex:this._monthsRegex)},_s.monthsShortRegex=function(e){return this._monthsParseExact?(_(this,"_monthsRegex")||Z.call(this),e?this._monthsShortStrictRegex:this._monthsShortRegex):(_(this,"_monthsShortRegex")||(this._monthsShortRegex=wt),this._monthsShortStrictRegex&&e?this._monthsShortStrictRegex:this._monthsShortRegex)},_s.week=function(e){return Q(e,this._week.dow,this._week.doy).week},_s.firstDayOfYear=function(){return this._week.doy},_s.firstDayOfWeek=function(){return this._week.dow},_s.weekdays=function(e,t){return e?a(this._weekdays)?this._weekdays[e.day()]:this._weekdays[this._weekdays.isFormat.test(t)?"format":"standalone"][e.day()]:a(this._weekdays)?this._weekdays:this._weekdays.standalone},_s.weekdaysMin=function(e){return e?this._weekdaysMin[e.day()]:this._weekdaysMin},_s.weekdaysShort=function(e){return e?this._weekdaysShort[e.day()]:this._weekdaysShort},_s.weekdaysParse=function(e,a,t){var s,n,d;if(this._weekdaysParseExact)return function(e,a,t){var s,n,d,r=e.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],s=0;s<7;++s)d=o([2e3,1]).day(s),this._minWeekdaysParse[s]=this.weekdaysMin(d,"").toLocaleLowerCase(),this._shortWeekdaysParse[s]=this.weekdaysShort(d,"").toLocaleLowerCase(),this._weekdaysParse[s]=this.weekdays(d,"").toLocaleLowerCase();return t?"dddd"===a?-1!==(n=kt.call(this._weekdaysParse,r))?n:null:"ddd"===a?-1!==(n=kt.call(this._shortWeekdaysParse,r))?n:null:-1!==(n=kt.call(this._minWeekdaysParse,r))?n:null:"dddd"===a?-1!==(n=kt.call(this._weekdaysParse,r))?n:-1!==(n=kt.call(this._shortWeekdaysParse,r))?n:-1!==(n=kt.call(this._minWeekdaysParse,r))?n:null:"ddd"===a?-1!==(n=kt.call(this._shortWeekdaysParse,r))?n:-1!==(n=kt.call(this._weekdaysParse,r))?n:-1!==(n=kt.call(this._minWeekdaysParse,r))?n:null:-1!==(n=kt.call(this._minWeekdaysParse,r))?n:-1!==(n=kt.call(this._weekdaysParse,r))?n:-1!==(n=kt.call(this._shortWeekdaysParse,r))?n:null}.call(this,e,a,t);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),s=0;s<7;s++){if(n=o([2e3,1]).day(s),t&&!this._fullWeekdaysParse[s]&&(this._fullWeekdaysParse[s]=new RegExp("^"+this.weekdays(n,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[s]=new RegExp("^"+this.weekdaysShort(n,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[s]=new RegExp("^"+this.weekdaysMin(n,"").replace(".",".?")+"$","i")),this._weekdaysParse[s]||(d="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[s]=new RegExp(d.replace(".",""),"i")),t&&"dddd"===a&&this._fullWeekdaysParse[s].test(e))return s;if(t&&"ddd"===a&&this._shortWeekdaysParse[s].test(e))return s;if(t&&"dd"===a&&this._minWeekdaysParse[s].test(e))return s;if(!t&&this._weekdaysParse[s].test(e))return s}},_s.weekdaysRegex=function(e){return this._weekdaysParseExact?(_(this,"_weekdaysRegex")||ee.call(this),e?this._weekdaysStrictRegex:this._weekdaysRegex):(_(this,"_weekdaysRegex")||(this._weekdaysRegex=jt),this._weekdaysStrictRegex&&e?this._weekdaysStrictRegex:this._weekdaysRegex)},_s.weekdaysShortRegex=function(e){return this._weekdaysParseExact?(_(this,"_weekdaysRegex")||ee.call(this),e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(_(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=xt),this._weekdaysShortStrictRegex&&e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},_s.weekdaysMinRegex=function(e){return this._weekdaysParseExact?(_(this,"_weekdaysRegex")||ee.call(this),e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(_(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Pt),this._weekdaysMinStrictRegex&&e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},_s.isPM=function(e){return"p"===(e+"").toLowerCase().charAt(0)},_s.meridiem=function(e,a,t){return e>11?t?"pm":"PM":t?"am":"AM"},re("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var a=e%10;return e+(1===Y(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")}}),e.lang=k("moment.lang is deprecated. Use moment.locale instead.",re),e.langData=k("moment.langData is deprecated. Use moment.localeData instead.",ie);var is=Math.abs,os=Ke("ms"),ms=Ke("s"),us=Ke("m"),ls=Ke("h"),Ms=Ke("d"),hs=Ke("w"),Ls=Ke("M"),cs=Ke("y"),Ys=Ze("milliseconds"),ys=Ze("seconds"),fs=Ze("minutes"),ks=Ze("hours"),ps=Ze("days"),Ds=Ze("months"),Ts=Ze("years"),gs=Math.round,ws={ss:44,s:45,m:45,h:22,d:26,M:11},vs=Math.abs,Ss=ke.prototype;Ss.isValid=function(){return this._isValid},Ss.abs=function(){var e=this._data;return this._milliseconds=is(this._milliseconds),this._days=is(this._days),this._months=is(this._months),e.milliseconds=is(e.milliseconds),e.seconds=is(e.seconds),e.minutes=is(e.minutes),e.hours=is(e.hours),e.months=is(e.months),e.years=is(e.years),this},Ss.add=function(e,a){return Ce(this,e,a,1)},Ss.subtract=function(e,a){return Ce(this,e,a,-1)},Ss.as=function(e){if(!this.isValid())return NaN;var a,t,s=this._milliseconds;if("month"===(e=v(e))||"year"===e)return a=this._days+s/864e5,t=this._months+Ue(a),"month"===e?t:t/12;switch(a=this._days+Math.round(Ve(this._months)),e){case"week":return a/7+s/6048e5;case"day":return a+s/864e5;case"hour":return 24*a+s/36e5;case"minute":return 1440*a+s/6e4;case"second":return 86400*a+s/1e3;case"millisecond":return Math.floor(864e5*a)+s;default:throw new Error("Unknown unit "+e)}},Ss.asMilliseconds=os,Ss.asSeconds=ms,Ss.asMinutes=us,Ss.asHours=ls,Ss.asDays=Ms,Ss.asWeeks=hs,Ss.asMonths=Ls,Ss.asYears=cs,Ss.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*Y(this._months/12):NaN},Ss._bubble=function(){var e,a,t,s,n,d=this._milliseconds,r=this._days,_=this._months,i=this._data;return d>=0&&r>=0&&_>=0||d<=0&&r<=0&&_<=0||(d+=864e5*Ge(Ve(_)+r),r=0,_=0),i.milliseconds=d%1e3,e=c(d/1e3),i.seconds=e%60,a=c(e/60),i.minutes=a%60,t=c(a/60),i.hours=t%24,r+=c(t/24),n=c(Ue(r)),_+=n,r-=Ge(Ve(n)),s=c(_/12),_%=12,i.days=r,i.months=_,i.years=s,this},Ss.clone=function(){return He(this)},Ss.get=function(e){return e=v(e),this.isValid()?this[e+"s"]():NaN},Ss.milliseconds=Ys,Ss.seconds=ys,Ss.minutes=fs,Ss.hours=ks,Ss.days=ps,Ss.weeks=function(){return c(this.days()/7)},Ss.months=Ds,Ss.years=Ts,Ss.humanize=function(e){if(!this.isValid())return this.localeData().invalidDate();var a=this.localeData(),t=function(e,a,t){var s=He(e).abs(),n=gs(s.as("s")),d=gs(s.as("m")),r=gs(s.as("h")),_=gs(s.as("d")),i=gs(s.as("M")),o=gs(s.as("y")),m=n<=ws.ss&&["s",n]||n0,m[4]=t,function(e,a,t,s,n){return n.relativeTime(a||1,!!t,e,s)}.apply(null,m)}(this,!e,a);return e&&(t=a.pastFuture(+this,t)),a.postformat(t)},Ss.toISOString=Be,Ss.toString=Be,Ss.toJSON=Be,Ss.locale=We,Ss.localeData=Ee,Ss.toIsoString=k("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Be),Ss.lang=es,j("X",0,0,"unix"),j("x",0,0,"valueOf"),W("x",dt),W("X",/[+-]?\d+(\.\d{1,3})?/),F("X",function(e,a,t){t._d=new Date(1e3*parseFloat(e,10))}),F("x",function(e,a,t){t._d=new Date(Y(e))}),e.version="2.20.1",function(e){Ea=e}(ye),e.fn=rs,e.min=function(){return fe("isBefore",[].slice.call(arguments,0))},e.max=function(){return fe("isAfter",[].slice.call(arguments,0))},e.now=function(){return Date.now?Date.now():+new Date},e.utc=o,e.unix=function(e){return ye(1e3*e)},e.months=function(e,a){return Re(e,a,"months")},e.isDate=d,e.locale=re,e.invalid=l,e.duration=He,e.isMoment=L,e.weekdays=function(e,a,t){return Ie(e,a,t,"weekdays")},e.parseZone=function(){return ye.apply(null,arguments).parseZone()},e.localeData=ie,e.isDuration=pe,e.monthsShort=function(e,a){return Re(e,a,"monthsShort")},e.weekdaysMin=function(e,a,t){return Ie(e,a,t,"weekdaysMin")},e.defineLocale=_e,e.updateLocale=function(e,a){if(null!=a){var t,s,n=Et;null!=(s=de(e))&&(n=s._config),(t=new g(a=T(n,a))).parentLocale=At[e],At[e]=t,re(e)}else null!=At[e]&&(null!=At[e].parentLocale?At[e]=At[e].parentLocale:null!=At[e]&&delete At[e]);return At[e]},e.locales=function(){return Na(At)},e.weekdaysShort=function(e,a,t){return Ie(e,a,t,"weekdaysShort")},e.normalizeUnits=v,e.relativeTimeRounding=function(e){return void 0===e?gs:"function"==typeof e&&(gs=e,!0)},e.relativeTimeThreshold=function(e,a){return void 0!==ws[e]&&(void 0===a?ws[e]:(ws[e]=a,"s"===e&&(ws.ss=a-1),!0))},e.calendarFormat=function(e,a){var t=e.diff(a,"days",!0);return t<-6?"sameElse":t<-1?"lastWeek":t<0?"lastDay":t<1?"sameDay":t<2?"nextDay":t<7?"nextWeek":"sameElse"},e.prototype=rs,e.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"YYYY-[W]WW",MONTH:"YYYY-MM"},e.defineLocale("af",{months:"Januarie_Februarie_Maart_April_Mei_Junie_Julie_Augustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mrt_Apr_Mei_Jun_Jul_Aug_Sep_Okt_Nov_Des".split("_"),weekdays:"Sondag_Maandag_Dinsdag_Woensdag_Donderdag_Vrydag_Saterdag".split("_"),weekdaysShort:"Son_Maa_Din_Woe_Don_Vry_Sat".split("_"),weekdaysMin:"So_Ma_Di_Wo_Do_Vr_Sa".split("_"),meridiemParse:/vm|nm/i,isPM:function(e){return/^nm$/i.test(e)},meridiem:function(e,a,t){return e<12?t?"vm":"VM":t?"nm":"NM"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Vandag om] LT",nextDay:"[M\xf4re om] LT",nextWeek:"dddd [om] LT",lastDay:"[Gister om] LT",lastWeek:"[Laas] dddd [om] LT",sameElse:"L"},relativeTime:{future:"oor %s",past:"%s gelede",s:"'n paar sekondes",ss:"%d sekondes",m:"'n minuut",mm:"%d minute",h:"'n uur",hh:"%d ure",d:"'n dag",dd:"%d dae",M:"'n maand",MM:"%d maande",y:"'n jaar",yy:"%d jaar"},dayOfMonthOrdinalParse:/\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||e>=20?"ste":"de")},week:{dow:1,doy:4}}),e.defineLocale("ar-dz",{months:"\u062c\u0627\u0646\u0641\u064a_\u0641\u064a\u0641\u0631\u064a_\u0645\u0627\u0631\u0633_\u0623\u0641\u0631\u064a\u0644_\u0645\u0627\u064a_\u062c\u0648\u0627\u0646_\u062c\u0648\u064a\u0644\u064a\u0629_\u0623\u0648\u062a_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),monthsShort:"\u062c\u0627\u0646\u0641\u064a_\u0641\u064a\u0641\u0631\u064a_\u0645\u0627\u0631\u0633_\u0623\u0641\u0631\u064a\u0644_\u0645\u0627\u064a_\u062c\u0648\u0627\u0646_\u062c\u0648\u064a\u0644\u064a\u0629_\u0623\u0648\u062a_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0627\u062d\u062f_\u0627\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u0623\u062d_\u0625\u062b_\u062b\u0644\u0627_\u0623\u0631_\u062e\u0645_\u062c\u0645_\u0633\u0628".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},week:{dow:0,doy:4}}),e.defineLocale("ar-kw",{months:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648\u0632_\u063a\u0634\u062a_\u0634\u062a\u0646\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0646\u0628\u0631_\u062f\u062c\u0646\u0628\u0631".split("_"),monthsShort:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648\u0632_\u063a\u0634\u062a_\u0634\u062a\u0646\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0646\u0628\u0631_\u062f\u062c\u0646\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062a\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0627\u062d\u062f_\u0627\u062a\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},week:{dow:0,doy:12}});var Hs={1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",0:"0"},bs=function(e){return 0===e?0:1===e?1:2===e?2:e%100>=3&&e%100<=10?3:e%100>=11?4:5},js={s:["\u0623\u0642\u0644 \u0645\u0646 \u062b\u0627\u0646\u064a\u0629","\u062b\u0627\u0646\u064a\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062b\u0627\u0646\u064a\u062a\u0627\u0646","\u062b\u0627\u0646\u064a\u062a\u064a\u0646"],"%d \u062b\u0648\u0627\u0646","%d \u062b\u0627\u0646\u064a\u0629","%d \u062b\u0627\u0646\u064a\u0629"],m:["\u0623\u0642\u0644 \u0645\u0646 \u062f\u0642\u064a\u0642\u0629","\u062f\u0642\u064a\u0642\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062f\u0642\u064a\u0642\u062a\u0627\u0646","\u062f\u0642\u064a\u0642\u062a\u064a\u0646"],"%d \u062f\u0642\u0627\u0626\u0642","%d \u062f\u0642\u064a\u0642\u0629","%d \u062f\u0642\u064a\u0642\u0629"],h:["\u0623\u0642\u0644 \u0645\u0646 \u0633\u0627\u0639\u0629","\u0633\u0627\u0639\u0629 \u0648\u0627\u062d\u062f\u0629",["\u0633\u0627\u0639\u062a\u0627\u0646","\u0633\u0627\u0639\u062a\u064a\u0646"],"%d \u0633\u0627\u0639\u0627\u062a","%d \u0633\u0627\u0639\u0629","%d \u0633\u0627\u0639\u0629"],d:["\u0623\u0642\u0644 \u0645\u0646 \u064a\u0648\u0645","\u064a\u0648\u0645 \u0648\u0627\u062d\u062f",["\u064a\u0648\u0645\u0627\u0646","\u064a\u0648\u0645\u064a\u0646"],"%d \u0623\u064a\u0627\u0645","%d \u064a\u0648\u0645\u064b\u0627","%d \u064a\u0648\u0645"],M:["\u0623\u0642\u0644 \u0645\u0646 \u0634\u0647\u0631","\u0634\u0647\u0631 \u0648\u0627\u062d\u062f",["\u0634\u0647\u0631\u0627\u0646","\u0634\u0647\u0631\u064a\u0646"],"%d \u0623\u0634\u0647\u0631","%d \u0634\u0647\u0631\u0627","%d \u0634\u0647\u0631"],y:["\u0623\u0642\u0644 \u0645\u0646 \u0639\u0627\u0645","\u0639\u0627\u0645 \u0648\u0627\u062d\u062f",["\u0639\u0627\u0645\u0627\u0646","\u0639\u0627\u0645\u064a\u0646"],"%d \u0623\u0639\u0648\u0627\u0645","%d \u0639\u0627\u0645\u064b\u0627","%d \u0639\u0627\u0645"]},xs=function(e){return function(a,t,s,n){var d=bs(a),r=js[e][bs(a)];return 2===d&&(r=r[t?0:1]),r.replace(/%d/i,a)}},Ps=["\u064a\u0646\u0627\u064a\u0631","\u0641\u0628\u0631\u0627\u064a\u0631","\u0645\u0627\u0631\u0633","\u0623\u0628\u0631\u064a\u0644","\u0645\u0627\u064a\u0648","\u064a\u0648\u0646\u064a\u0648","\u064a\u0648\u0644\u064a\u0648","\u0623\u063a\u0633\u0637\u0633","\u0633\u0628\u062a\u0645\u0628\u0631","\u0623\u0643\u062a\u0648\u0628\u0631","\u0646\u0648\u0641\u0645\u0628\u0631","\u062f\u064a\u0633\u0645\u0628\u0631"];e.defineLocale("ar-ly",{months:Ps,monthsShort:Ps,weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/\u200fM/\u200fYYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0635|\u0645/,isPM:function(e){return"\u0645"===e},meridiem:function(e,a,t){return e<12?"\u0635":"\u0645"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u064b\u0627 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0628\u0639\u062f %s",past:"\u0645\u0646\u0630 %s",s:xs("s"),ss:xs("s"),m:xs("m"),mm:xs("m"),h:xs("h"),hh:xs("h"),d:xs("d"),dd:xs("d"),M:xs("M"),MM:xs("M"),y:xs("y"),yy:xs("y")},preparse:function(e){return e.replace(/\u060c/g,",")},postformat:function(e){return e.replace(/\d/g,function(e){return Hs[e]}).replace(/,/g,"\u060c")},week:{dow:6,doy:12}}),e.defineLocale("ar-ma",{months:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648\u0632_\u063a\u0634\u062a_\u0634\u062a\u0646\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0646\u0628\u0631_\u062f\u062c\u0646\u0628\u0631".split("_"),monthsShort:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648\u0632_\u063a\u0634\u062a_\u0634\u062a\u0646\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0646\u0628\u0631_\u062f\u062c\u0646\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062a\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0627\u062d\u062f_\u0627\u062a\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},week:{dow:6,doy:12}});var Os={1:"\u0661",2:"\u0662",3:"\u0663",4:"\u0664",5:"\u0665",6:"\u0666",7:"\u0667",8:"\u0668",9:"\u0669",0:"\u0660"},Ws={"\u0661":"1","\u0662":"2","\u0663":"3","\u0664":"4","\u0665":"5","\u0666":"6","\u0667":"7","\u0668":"8","\u0669":"9","\u0660":"0"};e.defineLocale("ar-sa",{months:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a\u0648_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648_\u0623\u063a\u0633\u0637\u0633_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),monthsShort:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a\u0648_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648_\u0623\u063a\u0633\u0637\u0633_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0635|\u0645/,isPM:function(e){return"\u0645"===e},meridiem:function(e,a,t){return e<12?"\u0635":"\u0645"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},preparse:function(e){return e.replace(/[\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\u0660]/g,function(e){return Ws[e]}).replace(/\u060c/g,",")},postformat:function(e){return e.replace(/\d/g,function(e){return Os[e]}).replace(/,/g,"\u060c")},week:{dow:0,doy:6}}),e.defineLocale("ar-tn",{months:"\u062c\u0627\u0646\u0641\u064a_\u0641\u064a\u0641\u0631\u064a_\u0645\u0627\u0631\u0633_\u0623\u0641\u0631\u064a\u0644_\u0645\u0627\u064a_\u062c\u0648\u0627\u0646_\u062c\u0648\u064a\u0644\u064a\u0629_\u0623\u0648\u062a_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),monthsShort:"\u062c\u0627\u0646\u0641\u064a_\u0641\u064a\u0641\u0631\u064a_\u0645\u0627\u0631\u0633_\u0623\u0641\u0631\u064a\u0644_\u0645\u0627\u064a_\u062c\u0648\u0627\u0646_\u062c\u0648\u064a\u0644\u064a\u0629_\u0623\u0648\u062a_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},week:{dow:1,doy:4}});var Es={1:"\u0661",2:"\u0662",3:"\u0663",4:"\u0664",5:"\u0665",6:"\u0666",7:"\u0667",8:"\u0668",9:"\u0669",0:"\u0660"},As={"\u0661":"1","\u0662":"2","\u0663":"3","\u0664":"4","\u0665":"5","\u0666":"6","\u0667":"7","\u0668":"8","\u0669":"9","\u0660":"0"},Fs=function(e){return 0===e?0:1===e?1:2===e?2:e%100>=3&&e%100<=10?3:e%100>=11?4:5},zs={s:["\u0623\u0642\u0644 \u0645\u0646 \u062b\u0627\u0646\u064a\u0629","\u062b\u0627\u0646\u064a\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062b\u0627\u0646\u064a\u062a\u0627\u0646","\u062b\u0627\u0646\u064a\u062a\u064a\u0646"],"%d \u062b\u0648\u0627\u0646","%d \u062b\u0627\u0646\u064a\u0629","%d \u062b\u0627\u0646\u064a\u0629"],m:["\u0623\u0642\u0644 \u0645\u0646 \u062f\u0642\u064a\u0642\u0629","\u062f\u0642\u064a\u0642\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062f\u0642\u064a\u0642\u062a\u0627\u0646","\u062f\u0642\u064a\u0642\u062a\u064a\u0646"],"%d \u062f\u0642\u0627\u0626\u0642","%d \u062f\u0642\u064a\u0642\u0629","%d \u062f\u0642\u064a\u0642\u0629"],h:["\u0623\u0642\u0644 \u0645\u0646 \u0633\u0627\u0639\u0629","\u0633\u0627\u0639\u0629 \u0648\u0627\u062d\u062f\u0629",["\u0633\u0627\u0639\u062a\u0627\u0646","\u0633\u0627\u0639\u062a\u064a\u0646"],"%d \u0633\u0627\u0639\u0627\u062a","%d \u0633\u0627\u0639\u0629","%d \u0633\u0627\u0639\u0629"],d:["\u0623\u0642\u0644 \u0645\u0646 \u064a\u0648\u0645","\u064a\u0648\u0645 \u0648\u0627\u062d\u062f",["\u064a\u0648\u0645\u0627\u0646","\u064a\u0648\u0645\u064a\u0646"],"%d \u0623\u064a\u0627\u0645","%d \u064a\u0648\u0645\u064b\u0627","%d \u064a\u0648\u0645"],M:["\u0623\u0642\u0644 \u0645\u0646 \u0634\u0647\u0631","\u0634\u0647\u0631 \u0648\u0627\u062d\u062f",["\u0634\u0647\u0631\u0627\u0646","\u0634\u0647\u0631\u064a\u0646"],"%d \u0623\u0634\u0647\u0631","%d \u0634\u0647\u0631\u0627","%d \u0634\u0647\u0631"],y:["\u0623\u0642\u0644 \u0645\u0646 \u0639\u0627\u0645","\u0639\u0627\u0645 \u0648\u0627\u062d\u062f",["\u0639\u0627\u0645\u0627\u0646","\u0639\u0627\u0645\u064a\u0646"],"%d \u0623\u0639\u0648\u0627\u0645","%d \u0639\u0627\u0645\u064b\u0627","%d \u0639\u0627\u0645"]},Js=function(e){return function(a,t,s,n){var d=Fs(a),r=zs[e][Fs(a)];return 2===d&&(r=r[t?0:1]),r.replace(/%d/i,a)}},Ns=["\u064a\u0646\u0627\u064a\u0631","\u0641\u0628\u0631\u0627\u064a\u0631","\u0645\u0627\u0631\u0633","\u0623\u0628\u0631\u064a\u0644","\u0645\u0627\u064a\u0648","\u064a\u0648\u0646\u064a\u0648","\u064a\u0648\u0644\u064a\u0648","\u0623\u063a\u0633\u0637\u0633","\u0633\u0628\u062a\u0645\u0628\u0631","\u0623\u0643\u062a\u0648\u0628\u0631","\u0646\u0648\u0641\u0645\u0628\u0631","\u062f\u064a\u0633\u0645\u0628\u0631"];e.defineLocale("ar",{months:Ns,monthsShort:Ns,weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/\u200fM/\u200fYYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0635|\u0645/,isPM:function(e){return"\u0645"===e},meridiem:function(e,a,t){return e<12?"\u0635":"\u0645"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u064b\u0627 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0628\u0639\u062f %s",past:"\u0645\u0646\u0630 %s",s:Js("s"),ss:Js("s"),m:Js("m"),mm:Js("m"),h:Js("h"),hh:Js("h"),d:Js("d"),dd:Js("d"),M:Js("M"),MM:Js("M"),y:Js("y"),yy:Js("y")},preparse:function(e){return e.replace(/[\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\u0660]/g,function(e){return As[e]}).replace(/\u060c/g,",")},postformat:function(e){return e.replace(/\d/g,function(e){return Es[e]}).replace(/,/g,"\u060c")},week:{dow:6,doy:12}});var Rs={1:"-inci",5:"-inci",8:"-inci",70:"-inci",80:"-inci",2:"-nci",7:"-nci",20:"-nci",50:"-nci",3:"-\xfcnc\xfc",4:"-\xfcnc\xfc",100:"-\xfcnc\xfc",6:"-nc\u0131",9:"-uncu",10:"-uncu",30:"-uncu",60:"-\u0131nc\u0131",90:"-\u0131nc\u0131"};e.defineLocale("az",{months:"yanvar_fevral_mart_aprel_may_iyun_iyul_avqust_sentyabr_oktyabr_noyabr_dekabr".split("_"),monthsShort:"yan_fev_mar_apr_may_iyn_iyl_avq_sen_okt_noy_dek".split("_"),weekdays:"Bazar_Bazar ert\u0259si_\xc7\u0259r\u015f\u0259nb\u0259 ax\u015fam\u0131_\xc7\u0259r\u015f\u0259nb\u0259_C\xfcm\u0259 ax\u015fam\u0131_C\xfcm\u0259_\u015e\u0259nb\u0259".split("_"),weekdaysShort:"Baz_BzE_\xc7Ax_\xc7\u0259r_CAx_C\xfcm_\u015e\u0259n".split("_"),weekdaysMin:"Bz_BE_\xc7A_\xc7\u0259_CA_C\xfc_\u015e\u0259".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[bug\xfcn saat] LT",nextDay:"[sabah saat] LT",nextWeek:"[g\u0259l\u0259n h\u0259ft\u0259] dddd [saat] LT",lastDay:"[d\xfcn\u0259n] LT",lastWeek:"[ke\xe7\u0259n h\u0259ft\u0259] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s \u0259vv\u0259l",s:"birne\xe7\u0259 saniyy\u0259",ss:"%d saniy\u0259",m:"bir d\u0259qiq\u0259",mm:"%d d\u0259qiq\u0259",h:"bir saat",hh:"%d saat",d:"bir g\xfcn",dd:"%d g\xfcn",M:"bir ay",MM:"%d ay",y:"bir il",yy:"%d il"},meridiemParse:/gec\u0259|s\u0259h\u0259r|g\xfcnd\xfcz|ax\u015fam/,isPM:function(e){return/^(g\xfcnd\xfcz|ax\u015fam)$/.test(e)},meridiem:function(e,a,t){return e<4?"gec\u0259":e<12?"s\u0259h\u0259r":e<17?"g\xfcnd\xfcz":"ax\u015fam"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0131nc\u0131|inci|nci|\xfcnc\xfc|nc\u0131|uncu)/,ordinal:function(e){if(0===e)return e+"-\u0131nc\u0131";var a=e%10;return e+(Rs[a]||Rs[e%100-a]||Rs[e>=100?100:null])},week:{dow:1,doy:7}}),e.defineLocale("be",{months:{format:"\u0441\u0442\u0443\u0434\u0437\u0435\u043d\u044f_\u043b\u044e\u0442\u0430\u0433\u0430_\u0441\u0430\u043a\u0430\u0432\u0456\u043a\u0430_\u043a\u0440\u0430\u0441\u0430\u0432\u0456\u043a\u0430_\u0442\u0440\u0430\u045e\u043d\u044f_\u0447\u044d\u0440\u0432\u0435\u043d\u044f_\u043b\u0456\u043f\u0435\u043d\u044f_\u0436\u043d\u0456\u045e\u043d\u044f_\u0432\u0435\u0440\u0430\u0441\u043d\u044f_\u043a\u0430\u0441\u0442\u0440\u044b\u0447\u043d\u0456\u043a\u0430_\u043b\u0456\u0441\u0442\u0430\u043f\u0430\u0434\u0430_\u0441\u043d\u0435\u0436\u043d\u044f".split("_"),standalone:"\u0441\u0442\u0443\u0434\u0437\u0435\u043d\u044c_\u043b\u044e\u0442\u044b_\u0441\u0430\u043a\u0430\u0432\u0456\u043a_\u043a\u0440\u0430\u0441\u0430\u0432\u0456\u043a_\u0442\u0440\u0430\u0432\u0435\u043d\u044c_\u0447\u044d\u0440\u0432\u0435\u043d\u044c_\u043b\u0456\u043f\u0435\u043d\u044c_\u0436\u043d\u0456\u0432\u0435\u043d\u044c_\u0432\u0435\u0440\u0430\u0441\u0435\u043d\u044c_\u043a\u0430\u0441\u0442\u0440\u044b\u0447\u043d\u0456\u043a_\u043b\u0456\u0441\u0442\u0430\u043f\u0430\u0434_\u0441\u043d\u0435\u0436\u0430\u043d\u044c".split("_")},monthsShort:"\u0441\u0442\u0443\u0434_\u043b\u044e\u0442_\u0441\u0430\u043a_\u043a\u0440\u0430\u0441_\u0442\u0440\u0430\u0432_\u0447\u044d\u0440\u0432_\u043b\u0456\u043f_\u0436\u043d\u0456\u0432_\u0432\u0435\u0440_\u043a\u0430\u0441\u0442_\u043b\u0456\u0441\u0442_\u0441\u043d\u0435\u0436".split("_"),weekdays:{format:"\u043d\u044f\u0434\u0437\u0435\u043b\u044e_\u043f\u0430\u043d\u044f\u0434\u0437\u0435\u043b\u0430\u043a_\u0430\u045e\u0442\u043e\u0440\u0430\u043a_\u0441\u0435\u0440\u0430\u0434\u0443_\u0447\u0430\u0446\u0432\u0435\u0440_\u043f\u044f\u0442\u043d\u0456\u0446\u0443_\u0441\u0443\u0431\u043e\u0442\u0443".split("_"),standalone:"\u043d\u044f\u0434\u0437\u0435\u043b\u044f_\u043f\u0430\u043d\u044f\u0434\u0437\u0435\u043b\u0430\u043a_\u0430\u045e\u0442\u043e\u0440\u0430\u043a_\u0441\u0435\u0440\u0430\u0434\u0430_\u0447\u0430\u0446\u0432\u0435\u0440_\u043f\u044f\u0442\u043d\u0456\u0446\u0430_\u0441\u0443\u0431\u043e\u0442\u0430".split("_"),isFormat:/\[ ?[\u0412\u0432] ?(?:\u043c\u0456\u043d\u0443\u043b\u0443\u044e|\u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443\u044e)? ?\] ?dddd/},weekdaysShort:"\u043d\u0434_\u043f\u043d_\u0430\u0442_\u0441\u0440_\u0447\u0446_\u043f\u0442_\u0441\u0431".split("_"),weekdaysMin:"\u043d\u0434_\u043f\u043d_\u0430\u0442_\u0441\u0440_\u0447\u0446_\u043f\u0442_\u0441\u0431".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY \u0433.",LLL:"D MMMM YYYY \u0433., HH:mm",LLLL:"dddd, D MMMM YYYY \u0433., HH:mm"},calendar:{sameDay:"[\u0421\u0451\u043d\u043d\u044f \u045e] LT",nextDay:"[\u0417\u0430\u045e\u0442\u0440\u0430 \u045e] LT",lastDay:"[\u0423\u0447\u043e\u0440\u0430 \u045e] LT",nextWeek:function(){return"[\u0423] dddd [\u045e] LT"},lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return"[\u0423 \u043c\u0456\u043d\u0443\u043b\u0443\u044e] dddd [\u045e] LT";case 1:case 2:case 4:return"[\u0423 \u043c\u0456\u043d\u0443\u043b\u044b] dddd [\u045e] LT"}},sameElse:"L"},relativeTime:{future:"\u043f\u0440\u0430\u0437 %s",past:"%s \u0442\u0430\u043c\u0443",s:"\u043d\u0435\u043a\u0430\u043b\u044c\u043a\u0456 \u0441\u0435\u043a\u0443\u043d\u0434",m:qe,mm:qe,h:qe,hh:qe,d:"\u0434\u0437\u0435\u043d\u044c",dd:qe,M:"\u043c\u0435\u0441\u044f\u0446",MM:qe,y:"\u0433\u043e\u0434",yy:qe},meridiemParse:/\u043d\u043e\u0447\u044b|\u0440\u0430\u043d\u0456\u0446\u044b|\u0434\u043d\u044f|\u0432\u0435\u0447\u0430\u0440\u0430/,isPM:function(e){return/^(\u0434\u043d\u044f|\u0432\u0435\u0447\u0430\u0440\u0430)$/.test(e)},meridiem:function(e,a,t){return e<4?"\u043d\u043e\u0447\u044b":e<12?"\u0440\u0430\u043d\u0456\u0446\u044b":e<17?"\u0434\u043d\u044f":"\u0432\u0435\u0447\u0430\u0440\u0430"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0456|\u044b|\u0433\u0430)/,ordinal:function(e,a){switch(a){case"M":case"d":case"DDD":case"w":case"W":return e%10!=2&&e%10!=3||e%100==12||e%100==13?e+"-\u044b":e+"-\u0456";case"D":return e+"-\u0433\u0430";default:return e}},week:{dow:1,doy:7}}),e.defineLocale("bg",{months:"\u044f\u043d\u0443\u0430\u0440\u0438_\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0438\u043b_\u043c\u0430\u0439_\u044e\u043d\u0438_\u044e\u043b\u0438_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438_\u043e\u043a\u0442\u043e\u043c\u0432\u0440\u0438_\u043d\u043e\u0435\u043c\u0432\u0440\u0438_\u0434\u0435\u043a\u0435\u043c\u0432\u0440\u0438".split("_"),monthsShort:"\u044f\u043d\u0440_\u0444\u0435\u0432_\u043c\u0430\u0440_\u0430\u043f\u0440_\u043c\u0430\u0439_\u044e\u043d\u0438_\u044e\u043b\u0438_\u0430\u0432\u0433_\u0441\u0435\u043f_\u043e\u043a\u0442_\u043d\u043e\u0435_\u0434\u0435\u043a".split("_"),weekdays:"\u043d\u0435\u0434\u0435\u043b\u044f_\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a_\u0432\u0442\u043e\u0440\u043d\u0438\u043a_\u0441\u0440\u044f\u0434\u0430_\u0447\u0435\u0442\u0432\u044a\u0440\u0442\u044a\u043a_\u043f\u0435\u0442\u044a\u043a_\u0441\u044a\u0431\u043e\u0442\u0430".split("_"),weekdaysShort:"\u043d\u0435\u0434_\u043f\u043e\u043d_\u0432\u0442\u043e_\u0441\u0440\u044f_\u0447\u0435\u0442_\u043f\u0435\u0442_\u0441\u044a\u0431".split("_"),weekdaysMin:"\u043d\u0434_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[\u0414\u043d\u0435\u0441 \u0432] LT",nextDay:"[\u0423\u0442\u0440\u0435 \u0432] LT",nextWeek:"dddd [\u0432] LT",lastDay:"[\u0412\u0447\u0435\u0440\u0430 \u0432] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[\u0412 \u0438\u0437\u043c\u0438\u043d\u0430\u043b\u0430\u0442\u0430] dddd [\u0432] LT";case 1:case 2:case 4:case 5:return"[\u0412 \u0438\u0437\u043c\u0438\u043d\u0430\u043b\u0438\u044f] dddd [\u0432] LT"}},sameElse:"L"},relativeTime:{future:"\u0441\u043b\u0435\u0434 %s",past:"\u043f\u0440\u0435\u0434\u0438 %s",s:"\u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434\u0438",ss:"%d \u0441\u0435\u043a\u0443\u043d\u0434\u0438",m:"\u043c\u0438\u043d\u0443\u0442\u0430",mm:"%d \u043c\u0438\u043d\u0443\u0442\u0438",h:"\u0447\u0430\u0441",hh:"%d \u0447\u0430\u0441\u0430",d:"\u0434\u0435\u043d",dd:"%d \u0434\u043d\u0438",M:"\u043c\u0435\u0441\u0435\u0446",MM:"%d \u043c\u0435\u0441\u0435\u0446\u0430",y:"\u0433\u043e\u0434\u0438\u043d\u0430",yy:"%d \u0433\u043e\u0434\u0438\u043d\u0438"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0435\u0432|\u0435\u043d|\u0442\u0438|\u0432\u0438|\u0440\u0438|\u043c\u0438)/,ordinal:function(e){var a=e%10,t=e%100;return 0===e?e+"-\u0435\u0432":0===t?e+"-\u0435\u043d":t>10&&t<20?e+"-\u0442\u0438":1===a?e+"-\u0432\u0438":2===a?e+"-\u0440\u0438":7===a||8===a?e+"-\u043c\u0438":e+"-\u0442\u0438"},week:{dow:1,doy:7}}),e.defineLocale("bm",{months:"Zanwuyekalo_Fewuruyekalo_Marisikalo_Awirilikalo_M\u025bkalo_Zuw\u025bnkalo_Zuluyekalo_Utikalo_S\u025btanburukalo_\u0254kut\u0254burukalo_Nowanburukalo_Desanburukalo".split("_"),monthsShort:"Zan_Few_Mar_Awi_M\u025b_Zuw_Zul_Uti_S\u025bt_\u0254ku_Now_Des".split("_"),weekdays:"Kari_Nt\u025bn\u025bn_Tarata_Araba_Alamisa_Juma_Sibiri".split("_"),weekdaysShort:"Kar_Nt\u025b_Tar_Ara_Ala_Jum_Sib".split("_"),weekdaysMin:"Ka_Nt_Ta_Ar_Al_Ju_Si".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"MMMM [tile] D [san] YYYY",LLL:"MMMM [tile] D [san] YYYY [l\u025br\u025b] HH:mm",LLLL:"dddd MMMM [tile] D [san] YYYY [l\u025br\u025b] HH:mm"},calendar:{sameDay:"[Bi l\u025br\u025b] LT",nextDay:"[Sini l\u025br\u025b] LT",nextWeek:"dddd [don l\u025br\u025b] LT",lastDay:"[Kunu l\u025br\u025b] LT",lastWeek:"dddd [t\u025bm\u025bnen l\u025br\u025b] LT",sameElse:"L"},relativeTime:{future:"%s k\u0254n\u0254",past:"a b\u025b %s b\u0254",s:"sanga dama dama",ss:"sekondi %d",m:"miniti kelen",mm:"miniti %d",h:"l\u025br\u025b kelen",hh:"l\u025br\u025b %d",d:"tile kelen",dd:"tile %d",M:"kalo kelen",MM:"kalo %d",y:"san kelen",yy:"san %d"},week:{dow:1,doy:4}});var Is={1:"\u09e7",2:"\u09e8",3:"\u09e9",4:"\u09ea",5:"\u09eb",6:"\u09ec",7:"\u09ed",8:"\u09ee",9:"\u09ef",0:"\u09e6"},Cs={"\u09e7":"1","\u09e8":"2","\u09e9":"3","\u09ea":"4","\u09eb":"5","\u09ec":"6","\u09ed":"7","\u09ee":"8","\u09ef":"9","\u09e6":"0"};e.defineLocale("bn",{months:"\u099c\u09be\u09a8\u09c1\u09df\u09be\u09b0\u09c0_\u09ab\u09c7\u09ac\u09cd\u09b0\u09c1\u09df\u09be\u09b0\u09bf_\u09ae\u09be\u09b0\u09cd\u099a_\u098f\u09aa\u09cd\u09b0\u09bf\u09b2_\u09ae\u09c7_\u099c\u09c1\u09a8_\u099c\u09c1\u09b2\u09be\u0987_\u0986\u0997\u09b8\u09cd\u099f_\u09b8\u09c7\u09aa\u09cd\u099f\u09c7\u09ae\u09cd\u09ac\u09b0_\u0985\u0995\u09cd\u099f\u09cb\u09ac\u09b0_\u09a8\u09ad\u09c7\u09ae\u09cd\u09ac\u09b0_\u09a1\u09bf\u09b8\u09c7\u09ae\u09cd\u09ac\u09b0".split("_"),monthsShort:"\u099c\u09be\u09a8\u09c1_\u09ab\u09c7\u09ac_\u09ae\u09be\u09b0\u09cd\u099a_\u098f\u09aa\u09cd\u09b0_\u09ae\u09c7_\u099c\u09c1\u09a8_\u099c\u09c1\u09b2_\u0986\u0997_\u09b8\u09c7\u09aa\u09cd\u099f_\u0985\u0995\u09cd\u099f\u09cb_\u09a8\u09ad\u09c7_\u09a1\u09bf\u09b8\u09c7".split("_"),weekdays:"\u09b0\u09ac\u09bf\u09ac\u09be\u09b0_\u09b8\u09cb\u09ae\u09ac\u09be\u09b0_\u09ae\u0999\u09cd\u0997\u09b2\u09ac\u09be\u09b0_\u09ac\u09c1\u09a7\u09ac\u09be\u09b0_\u09ac\u09c3\u09b9\u09b8\u09cd\u09aa\u09a4\u09bf\u09ac\u09be\u09b0_\u09b6\u09c1\u0995\u09cd\u09b0\u09ac\u09be\u09b0_\u09b6\u09a8\u09bf\u09ac\u09be\u09b0".split("_"),weekdaysShort:"\u09b0\u09ac\u09bf_\u09b8\u09cb\u09ae_\u09ae\u0999\u09cd\u0997\u09b2_\u09ac\u09c1\u09a7_\u09ac\u09c3\u09b9\u09b8\u09cd\u09aa\u09a4\u09bf_\u09b6\u09c1\u0995\u09cd\u09b0_\u09b6\u09a8\u09bf".split("_"),weekdaysMin:"\u09b0\u09ac\u09bf_\u09b8\u09cb\u09ae_\u09ae\u0999\u09cd\u0997_\u09ac\u09c1\u09a7_\u09ac\u09c3\u09b9\u0983_\u09b6\u09c1\u0995\u09cd\u09b0_\u09b6\u09a8\u09bf".split("_"),longDateFormat:{LT:"A h:mm \u09b8\u09ae\u09df",LTS:"A h:mm:ss \u09b8\u09ae\u09df",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u09b8\u09ae\u09df",LLLL:"dddd, D MMMM YYYY, A h:mm \u09b8\u09ae\u09df"},calendar:{sameDay:"[\u0986\u099c] LT",nextDay:"[\u0986\u0997\u09be\u09ae\u09c0\u0995\u09be\u09b2] LT",nextWeek:"dddd, LT",lastDay:"[\u0997\u09a4\u0995\u09be\u09b2] LT",lastWeek:"[\u0997\u09a4] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u09aa\u09b0\u09c7",past:"%s \u0986\u0997\u09c7",s:"\u0995\u09df\u09c7\u0995 \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1",ss:"%d \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1",m:"\u098f\u0995 \u09ae\u09bf\u09a8\u09bf\u099f",mm:"%d \u09ae\u09bf\u09a8\u09bf\u099f",h:"\u098f\u0995 \u0998\u09a8\u09cd\u099f\u09be",hh:"%d \u0998\u09a8\u09cd\u099f\u09be",d:"\u098f\u0995 \u09a6\u09bf\u09a8",dd:"%d \u09a6\u09bf\u09a8",M:"\u098f\u0995 \u09ae\u09be\u09b8",MM:"%d \u09ae\u09be\u09b8",y:"\u098f\u0995 \u09ac\u099b\u09b0",yy:"%d \u09ac\u099b\u09b0"},preparse:function(e){return e.replace(/[\u09e7\u09e8\u09e9\u09ea\u09eb\u09ec\u09ed\u09ee\u09ef\u09e6]/g,function(e){return Cs[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Is[e]})},meridiemParse:/\u09b0\u09be\u09a4|\u09b8\u0995\u09be\u09b2|\u09a6\u09c1\u09aa\u09c1\u09b0|\u09ac\u09bf\u0995\u09be\u09b2|\u09b0\u09be\u09a4/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u09b0\u09be\u09a4"===a&&e>=4||"\u09a6\u09c1\u09aa\u09c1\u09b0"===a&&e<5||"\u09ac\u09bf\u0995\u09be\u09b2"===a?e+12:e},meridiem:function(e,a,t){return e<4?"\u09b0\u09be\u09a4":e<10?"\u09b8\u0995\u09be\u09b2":e<17?"\u09a6\u09c1\u09aa\u09c1\u09b0":e<20?"\u09ac\u09bf\u0995\u09be\u09b2":"\u09b0\u09be\u09a4"},week:{dow:0,doy:6}});var Gs={1:"\u0f21",2:"\u0f22",3:"\u0f23",4:"\u0f24",5:"\u0f25",6:"\u0f26",7:"\u0f27",8:"\u0f28",9:"\u0f29",0:"\u0f20"},Us={"\u0f21":"1","\u0f22":"2","\u0f23":"3","\u0f24":"4","\u0f25":"5","\u0f26":"6","\u0f27":"7","\u0f28":"8","\u0f29":"9","\u0f20":"0"};e.defineLocale("bo",{months:"\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54".split("_"),monthsShort:"\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54".split("_"),weekdays:"\u0f42\u0f5f\u0f60\u0f0b\u0f49\u0f72\u0f0b\u0f58\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f63\u0fb7\u0f42\u0f0b\u0f54\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74_\u0f42\u0f5f\u0f60\u0f0b\u0f54\u0f0b\u0f66\u0f44\u0f66\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b".split("_"),weekdaysShort:"\u0f49\u0f72\u0f0b\u0f58\u0f0b_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b_\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b_\u0f63\u0fb7\u0f42\u0f0b\u0f54\u0f0b_\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74_\u0f54\u0f0b\u0f66\u0f44\u0f66\u0f0b_\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b".split("_"),weekdaysMin:"\u0f49\u0f72\u0f0b\u0f58\u0f0b_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b_\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b_\u0f63\u0fb7\u0f42\u0f0b\u0f54\u0f0b_\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74_\u0f54\u0f0b\u0f66\u0f44\u0f66\u0f0b_\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b".split("_"),longDateFormat:{LT:"A h:mm",LTS:"A h:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm",LLLL:"dddd, D MMMM YYYY, A h:mm"},calendar:{sameDay:"[\u0f51\u0f72\u0f0b\u0f62\u0f72\u0f44] LT",nextDay:"[\u0f66\u0f44\u0f0b\u0f49\u0f72\u0f53] LT",nextWeek:"[\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f55\u0fb2\u0f42\u0f0b\u0f62\u0f97\u0f7a\u0f66\u0f0b\u0f58], LT",lastDay:"[\u0f41\u0f0b\u0f66\u0f44] LT",lastWeek:"[\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f55\u0fb2\u0f42\u0f0b\u0f58\u0f50\u0f60\u0f0b\u0f58] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0f63\u0f0b",past:"%s \u0f66\u0f94\u0f53\u0f0b\u0f63",s:"\u0f63\u0f58\u0f0b\u0f66\u0f44",ss:"%d \u0f66\u0f90\u0f62\u0f0b\u0f46\u0f0d",m:"\u0f66\u0f90\u0f62\u0f0b\u0f58\u0f0b\u0f42\u0f45\u0f72\u0f42",mm:"%d \u0f66\u0f90\u0f62\u0f0b\u0f58",h:"\u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51\u0f0b\u0f42\u0f45\u0f72\u0f42",hh:"%d \u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51",d:"\u0f49\u0f72\u0f53\u0f0b\u0f42\u0f45\u0f72\u0f42",dd:"%d \u0f49\u0f72\u0f53\u0f0b",M:"\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f45\u0f72\u0f42",MM:"%d \u0f5f\u0fb3\u0f0b\u0f56",y:"\u0f63\u0f7c\u0f0b\u0f42\u0f45\u0f72\u0f42",yy:"%d \u0f63\u0f7c"},preparse:function(e){return e.replace(/[\u0f21\u0f22\u0f23\u0f24\u0f25\u0f26\u0f27\u0f28\u0f29\u0f20]/g,function(e){return Us[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Gs[e]})},meridiemParse:/\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c|\u0f5e\u0f7c\u0f42\u0f66\u0f0b\u0f40\u0f66|\u0f49\u0f72\u0f53\u0f0b\u0f42\u0f74\u0f44|\u0f51\u0f42\u0f7c\u0f44\u0f0b\u0f51\u0f42|\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c"===a&&e>=4||"\u0f49\u0f72\u0f53\u0f0b\u0f42\u0f74\u0f44"===a&&e<5||"\u0f51\u0f42\u0f7c\u0f44\u0f0b\u0f51\u0f42"===a?e+12:e},meridiem:function(e,a,t){return e<4?"\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c":e<10?"\u0f5e\u0f7c\u0f42\u0f66\u0f0b\u0f40\u0f66":e<17?"\u0f49\u0f72\u0f53\u0f0b\u0f42\u0f74\u0f44":e<20?"\u0f51\u0f42\u0f7c\u0f44\u0f0b\u0f51\u0f42":"\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c"},week:{dow:0,doy:6}}),e.defineLocale("br",{months:"Genver_C'hwevrer_Meurzh_Ebrel_Mae_Mezheven_Gouere_Eost_Gwengolo_Here_Du_Kerzu".split("_"),monthsShort:"Gen_C'hwe_Meu_Ebr_Mae_Eve_Gou_Eos_Gwe_Her_Du_Ker".split("_"),weekdays:"Sul_Lun_Meurzh_Merc'her_Yaou_Gwener_Sadorn".split("_"),weekdaysShort:"Sul_Lun_Meu_Mer_Yao_Gwe_Sad".split("_"),weekdaysMin:"Su_Lu_Me_Mer_Ya_Gw_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"h[e]mm A",LTS:"h[e]mm:ss A",L:"DD/MM/YYYY",LL:"D [a viz] MMMM YYYY",LLL:"D [a viz] MMMM YYYY h[e]mm A",LLLL:"dddd, D [a viz] MMMM YYYY h[e]mm A"},calendar:{sameDay:"[Hiziv da] LT",nextDay:"[Warc'hoazh da] LT",nextWeek:"dddd [da] LT",lastDay:"[Dec'h da] LT",lastWeek:"dddd [paset da] LT",sameElse:"L"},relativeTime:{future:"a-benn %s",past:"%s 'zo",s:"un nebeud segondenno\xf9",ss:"%d eilenn",m:"ur vunutenn",mm:Qe,h:"un eur",hh:"%d eur",d:"un devezh",dd:Qe,M:"ur miz",MM:Qe,y:"ur bloaz",yy:function(e){switch(Xe(e)){case 1:case 3:case 4:case 5:case 9:return e+" bloaz";default:return e+" vloaz"}}},dayOfMonthOrdinalParse:/\d{1,2}(a\xf1|vet)/,ordinal:function(e){return e+(1===e?"a\xf1":"vet")},week:{dow:1,doy:4}}),e.defineLocale("bs",{months:"januar_februar_mart_april_maj_juni_juli_august_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj._jun._jul._aug._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedjelja_ponedjeljak_utorak_srijeda_\u010detvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._\u010det._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_\u010de_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[ju\u010der u] LT",lastWeek:function(){switch(this.day()){case 0:case 3:return"[pro\u0161lu] dddd [u] LT";case 6:return"[pro\u0161le] [subote] [u] LT";case 1:case 2:case 4:case 5:return"[pro\u0161li] dddd [u] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"par sekundi",ss:ea,m:ea,mm:ea,h:ea,hh:ea,d:"dan",dd:ea,M:"mjesec",MM:ea,y:"godinu",yy:ea},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),e.defineLocale("ca",{months:{standalone:"gener_febrer_mar\xe7_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre".split("_"),format:"de gener_de febrer_de mar\xe7_d'abril_de maig_de juny_de juliol_d'agost_de setembre_d'octubre_de novembre_de desembre".split("_"),isFormat:/D[oD]?(\s)+MMMM/},monthsShort:"gen._febr._mar\xe7_abr._maig_juny_jul._ag._set._oct._nov._des.".split("_"),monthsParseExact:!0,weekdays:"diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte".split("_"),weekdaysShort:"dg._dl._dt._dc._dj._dv._ds.".split("_"),weekdaysMin:"dg_dl_dt_dc_dj_dv_ds".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM [de] YYYY",ll:"D MMM YYYY",LLL:"D MMMM [de] YYYY [a les] H:mm",lll:"D MMM YYYY, H:mm",LLLL:"dddd D MMMM [de] YYYY [a les] H:mm",llll:"ddd D MMM YYYY, H:mm"},calendar:{sameDay:function(){return"[avui a "+(1!==this.hours()?"les":"la")+"] LT"},nextDay:function(){return"[dem\xe0 a "+(1!==this.hours()?"les":"la")+"] LT"},nextWeek:function(){return"dddd [a "+(1!==this.hours()?"les":"la")+"] LT"},lastDay:function(){return"[ahir a "+(1!==this.hours()?"les":"la")+"] LT"},lastWeek:function(){return"[el] dddd [passat a "+(1!==this.hours()?"les":"la")+"] LT"},sameElse:"L"},relativeTime:{future:"d'aqu\xed %s",past:"fa %s",s:"uns segons",ss:"%d segons",m:"un minut",mm:"%d minuts",h:"una hora",hh:"%d hores",d:"un dia",dd:"%d dies",M:"un mes",MM:"%d mesos",y:"un any",yy:"%d anys"},dayOfMonthOrdinalParse:/\d{1,2}(r|n|t|\xe8|a)/,ordinal:function(e,a){var t=1===e?"r":2===e?"n":3===e?"r":4===e?"t":"\xe8";return"w"!==a&&"W"!==a||(t="a"),e+t},week:{dow:1,doy:4}});var Vs="leden_\xfanor_b\u0159ezen_duben_kv\u011bten_\u010derven_\u010dervenec_srpen_z\xe1\u0159\xed_\u0159\xedjen_listopad_prosinec".split("_"),Ks="led_\xfano_b\u0159e_dub_kv\u011b_\u010dvn_\u010dvc_srp_z\xe1\u0159_\u0159\xedj_lis_pro".split("_");e.defineLocale("cs",{months:Vs,monthsShort:Ks,monthsParse:function(e,a){var t,s=[];for(t=0;t<12;t++)s[t]=new RegExp("^"+e[t]+"$|^"+a[t]+"$","i");return s}(Vs,Ks),shortMonthsParse:function(e){var a,t=[];for(a=0;a<12;a++)t[a]=new RegExp("^"+e[a]+"$","i");return t}(Ks),longMonthsParse:function(e){var a,t=[];for(a=0;a<12;a++)t[a]=new RegExp("^"+e[a]+"$","i");return t}(Vs),weekdays:"ned\u011ble_pond\u011bl\xed_\xfater\xfd_st\u0159eda_\u010dtvrtek_p\xe1tek_sobota".split("_"),weekdaysShort:"ne_po_\xfat_st_\u010dt_p\xe1_so".split("_"),weekdaysMin:"ne_po_\xfat_st_\u010dt_p\xe1_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd D. MMMM YYYY H:mm",l:"D. M. YYYY"},calendar:{sameDay:"[dnes v] LT",nextDay:"[z\xedtra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v ned\u011bli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve st\u0159edu v] LT";case 4:return"[ve \u010dtvrtek v] LT";case 5:return"[v p\xe1tek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[v\u010dera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou ned\u011bli v] LT";case 1:case 2:return"[minul\xe9] dddd [v] LT";case 3:return"[minulou st\u0159edu v] LT";case 4:case 5:return"[minul\xfd] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"p\u0159ed %s",s:ta,ss:ta,m:ta,mm:ta,h:ta,hh:ta,d:ta,dd:ta,M:ta,MM:ta,y:ta,yy:ta},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("cv",{months:"\u043a\u04d1\u0440\u043b\u0430\u0447_\u043d\u0430\u0440\u04d1\u0441_\u043f\u0443\u0448_\u0430\u043a\u0430_\u043c\u0430\u0439_\u04ab\u04d7\u0440\u0442\u043c\u0435_\u0443\u0442\u04d1_\u04ab\u0443\u0440\u043b\u0430_\u0430\u0432\u04d1\u043d_\u044e\u043f\u0430_\u0447\u04f3\u043a_\u0440\u0430\u0448\u0442\u0430\u0432".split("_"),monthsShort:"\u043a\u04d1\u0440_\u043d\u0430\u0440_\u043f\u0443\u0448_\u0430\u043a\u0430_\u043c\u0430\u0439_\u04ab\u04d7\u0440_\u0443\u0442\u04d1_\u04ab\u0443\u0440_\u0430\u0432\u043d_\u044e\u043f\u0430_\u0447\u04f3\u043a_\u0440\u0430\u0448".split("_"),weekdays:"\u0432\u044b\u0440\u0441\u0430\u0440\u043d\u0438\u043a\u0443\u043d_\u0442\u0443\u043d\u0442\u0438\u043a\u0443\u043d_\u044b\u0442\u043b\u0430\u0440\u0438\u043a\u0443\u043d_\u044e\u043d\u043a\u0443\u043d_\u043a\u04d7\u04ab\u043d\u0435\u0440\u043d\u0438\u043a\u0443\u043d_\u044d\u0440\u043d\u0435\u043a\u0443\u043d_\u0448\u04d1\u043c\u0430\u0442\u043a\u0443\u043d".split("_"),weekdaysShort:"\u0432\u044b\u0440_\u0442\u0443\u043d_\u044b\u0442\u043b_\u044e\u043d_\u043a\u04d7\u04ab_\u044d\u0440\u043d_\u0448\u04d1\u043c".split("_"),weekdaysMin:"\u0432\u0440_\u0442\u043d_\u044b\u0442_\u044e\u043d_\u043a\u04ab_\u044d\u0440_\u0448\u043c".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"YYYY [\u04ab\u0443\u043b\u0445\u0438] MMMM [\u0443\u0439\u04d1\u0445\u04d7\u043d] D[-\u043c\u04d7\u0448\u04d7]",LLL:"YYYY [\u04ab\u0443\u043b\u0445\u0438] MMMM [\u0443\u0439\u04d1\u0445\u04d7\u043d] D[-\u043c\u04d7\u0448\u04d7], HH:mm",LLLL:"dddd, YYYY [\u04ab\u0443\u043b\u0445\u0438] MMMM [\u0443\u0439\u04d1\u0445\u04d7\u043d] D[-\u043c\u04d7\u0448\u04d7], HH:mm"},calendar:{sameDay:"[\u041f\u0430\u044f\u043d] LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",nextDay:"[\u042b\u0440\u0430\u043d] LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",lastDay:"[\u04d6\u043d\u0435\u0440] LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",nextWeek:"[\u04aa\u0438\u0442\u0435\u0441] dddd LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",lastWeek:"[\u0418\u0440\u0442\u043d\u04d7] dddd LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",sameElse:"L"},relativeTime:{future:function(e){return e+(/\u0441\u0435\u0445\u0435\u0442$/i.exec(e)?"\u0440\u0435\u043d":/\u04ab\u0443\u043b$/i.exec(e)?"\u0442\u0430\u043d":"\u0440\u0430\u043d")},past:"%s \u043a\u0430\u044f\u043b\u043b\u0430",s:"\u043f\u04d7\u0440-\u0438\u043a \u04ab\u0435\u043a\u043a\u0443\u043d\u0442",ss:"%d \u04ab\u0435\u043a\u043a\u0443\u043d\u0442",m:"\u043f\u04d7\u0440 \u043c\u0438\u043d\u0443\u0442",mm:"%d \u043c\u0438\u043d\u0443\u0442",h:"\u043f\u04d7\u0440 \u0441\u0435\u0445\u0435\u0442",hh:"%d \u0441\u0435\u0445\u0435\u0442",d:"\u043f\u04d7\u0440 \u043a\u0443\u043d",dd:"%d \u043a\u0443\u043d",M:"\u043f\u04d7\u0440 \u0443\u0439\u04d1\u0445",MM:"%d \u0443\u0439\u04d1\u0445",y:"\u043f\u04d7\u0440 \u04ab\u0443\u043b",yy:"%d \u04ab\u0443\u043b"},dayOfMonthOrdinalParse:/\d{1,2}-\u043c\u04d7\u0448/,ordinal:"%d-\u043c\u04d7\u0448",week:{dow:1,doy:7}}),e.defineLocale("cy",{months:"Ionawr_Chwefror_Mawrth_Ebrill_Mai_Mehefin_Gorffennaf_Awst_Medi_Hydref_Tachwedd_Rhagfyr".split("_"),monthsShort:"Ion_Chwe_Maw_Ebr_Mai_Meh_Gor_Aws_Med_Hyd_Tach_Rhag".split("_"),weekdays:"Dydd Sul_Dydd Llun_Dydd Mawrth_Dydd Mercher_Dydd Iau_Dydd Gwener_Dydd Sadwrn".split("_"),weekdaysShort:"Sul_Llun_Maw_Mer_Iau_Gwe_Sad".split("_"),weekdaysMin:"Su_Ll_Ma_Me_Ia_Gw_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Heddiw am] LT",nextDay:"[Yfory am] LT",nextWeek:"dddd [am] LT",lastDay:"[Ddoe am] LT",lastWeek:"dddd [diwethaf am] LT",sameElse:"L"},relativeTime:{future:"mewn %s",past:"%s yn \xf4l",s:"ychydig eiliadau",ss:"%d eiliad",m:"munud",mm:"%d munud",h:"awr",hh:"%d awr",d:"diwrnod",dd:"%d diwrnod",M:"mis",MM:"%d mis",y:"blwyddyn",yy:"%d flynedd"},dayOfMonthOrdinalParse:/\d{1,2}(fed|ain|af|il|ydd|ed|eg)/,ordinal:function(e){var a="";return e>20?a=40===e||50===e||60===e||80===e||100===e?"fed":"ain":e>0&&(a=["","af","il","ydd","ydd","ed","ed","ed","fed","fed","fed","eg","fed","eg","eg","fed","eg","eg","fed","eg","fed"][e]),e+a},week:{dow:1,doy:4}}),e.defineLocale("da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"s\xf8ndag_mandag_tirsdag_onsdag_torsdag_fredag_l\xf8rdag".split("_"),weekdaysShort:"s\xf8n_man_tir_ons_tor_fre_l\xf8r".split("_"),weekdaysMin:"s\xf8_ma_ti_on_to_fr_l\xf8".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd [d.] D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"p\xe5 dddd [kl.] LT",lastDay:"[i g\xe5r kl.] LT",lastWeek:"[i] dddd[s kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"f\xe5 sekunder",ss:"%d sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en m\xe5ned",MM:"%d m\xe5neder",y:"et \xe5r",yy:"%d \xe5r"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("de-at",{months:"J\xe4nner_Februar_M\xe4rz_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"J\xe4n._Feb._M\xe4rz_Apr._Mai_Juni_Juli_Aug._Sep._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",ss:"%d Sekunden",m:sa,mm:"%d Minuten",h:sa,hh:"%d Stunden",d:sa,dd:sa,M:sa,MM:sa,y:sa,yy:sa},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("de-ch",{months:"Januar_Februar_M\xe4rz_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Feb._M\xe4rz_Apr._Mai_Juni_Juli_Aug._Sep._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",ss:"%d Sekunden",m:na,mm:"%d Minuten",h:na,hh:"%d Stunden",d:na,dd:na,M:na,MM:na,y:na,yy:na},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("de",{months:"Januar_Februar_M\xe4rz_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Feb._M\xe4rz_Apr._Mai_Juni_Juli_Aug._Sep._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",ss:"%d Sekunden",m:da,mm:"%d Minuten",h:da,hh:"%d Stunden",d:da,dd:da,M:da,MM:da,y:da,yy:da},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});var Zs=["\u0796\u07ac\u0782\u07aa\u0787\u07a6\u0783\u07a9","\u078a\u07ac\u0784\u07b0\u0783\u07aa\u0787\u07a6\u0783\u07a9","\u0789\u07a7\u0783\u07a8\u0797\u07aa","\u0787\u07ad\u0795\u07b0\u0783\u07a9\u078d\u07aa","\u0789\u07ad","\u0796\u07ab\u0782\u07b0","\u0796\u07aa\u078d\u07a6\u0787\u07a8","\u0787\u07af\u078e\u07a6\u0790\u07b0\u0793\u07aa","\u0790\u07ac\u0795\u07b0\u0793\u07ac\u0789\u07b0\u0784\u07a6\u0783\u07aa","\u0787\u07ae\u0786\u07b0\u0793\u07af\u0784\u07a6\u0783\u07aa","\u0782\u07ae\u0788\u07ac\u0789\u07b0\u0784\u07a6\u0783\u07aa","\u0791\u07a8\u0790\u07ac\u0789\u07b0\u0784\u07a6\u0783\u07aa"],$s=["\u0787\u07a7\u078b\u07a8\u0787\u07b0\u078c\u07a6","\u0780\u07af\u0789\u07a6","\u0787\u07a6\u0782\u07b0\u078e\u07a7\u0783\u07a6","\u0784\u07aa\u078b\u07a6","\u0784\u07aa\u0783\u07a7\u0790\u07b0\u078a\u07a6\u078c\u07a8","\u0780\u07aa\u0786\u07aa\u0783\u07aa","\u0780\u07ae\u0782\u07a8\u0780\u07a8\u0783\u07aa"];e.defineLocale("dv",{months:Zs,monthsShort:Zs,weekdays:$s,weekdaysShort:$s,weekdaysMin:"\u0787\u07a7\u078b\u07a8_\u0780\u07af\u0789\u07a6_\u0787\u07a6\u0782\u07b0_\u0784\u07aa\u078b\u07a6_\u0784\u07aa\u0783\u07a7_\u0780\u07aa\u0786\u07aa_\u0780\u07ae\u0782\u07a8".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/M/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0789\u0786|\u0789\u078a/,isPM:function(e){return"\u0789\u078a"===e},meridiem:function(e,a,t){return e<12?"\u0789\u0786":"\u0789\u078a"},calendar:{sameDay:"[\u0789\u07a8\u0787\u07a6\u078b\u07aa] LT",nextDay:"[\u0789\u07a7\u078b\u07a6\u0789\u07a7] LT",nextWeek:"dddd LT",lastDay:"[\u0787\u07a8\u0787\u07b0\u0794\u07ac] LT",lastWeek:"[\u078a\u07a7\u0787\u07a8\u078c\u07aa\u0788\u07a8] dddd LT",sameElse:"L"},relativeTime:{future:"\u078c\u07ac\u0783\u07ad\u078e\u07a6\u0787\u07a8 %s",past:"\u0786\u07aa\u0783\u07a8\u0782\u07b0 %s",s:"\u0790\u07a8\u0786\u07aa\u0782\u07b0\u078c\u07aa\u0786\u07ae\u0785\u07ac\u0787\u07b0",ss:"d% \u0790\u07a8\u0786\u07aa\u0782\u07b0\u078c\u07aa",m:"\u0789\u07a8\u0782\u07a8\u0793\u07ac\u0787\u07b0",mm:"\u0789\u07a8\u0782\u07a8\u0793\u07aa %d",h:"\u078e\u07a6\u0791\u07a8\u0787\u07a8\u0783\u07ac\u0787\u07b0",hh:"\u078e\u07a6\u0791\u07a8\u0787\u07a8\u0783\u07aa %d",d:"\u078b\u07aa\u0788\u07a6\u0780\u07ac\u0787\u07b0",dd:"\u078b\u07aa\u0788\u07a6\u0790\u07b0 %d",M:"\u0789\u07a6\u0780\u07ac\u0787\u07b0",MM:"\u0789\u07a6\u0790\u07b0 %d",y:"\u0787\u07a6\u0780\u07a6\u0783\u07ac\u0787\u07b0",yy:"\u0787\u07a6\u0780\u07a6\u0783\u07aa %d"},preparse:function(e){return e.replace(/\u060c/g,",")},postformat:function(e){return e.replace(/,/g,"\u060c")},week:{dow:7,doy:12}}),e.defineLocale("el",{monthsNominativeEl:"\u0399\u03b1\u03bd\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2_\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2_\u039c\u03ac\u03c1\u03c4\u03b9\u03bf\u03c2_\u0391\u03c0\u03c1\u03af\u03bb\u03b9\u03bf\u03c2_\u039c\u03ac\u03b9\u03bf\u03c2_\u0399\u03bf\u03cd\u03bd\u03b9\u03bf\u03c2_\u0399\u03bf\u03cd\u03bb\u03b9\u03bf\u03c2_\u0391\u03cd\u03b3\u03bf\u03c5\u03c3\u03c4\u03bf\u03c2_\u03a3\u03b5\u03c0\u03c4\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2_\u039f\u03ba\u03c4\u03ce\u03b2\u03c1\u03b9\u03bf\u03c2_\u039d\u03bf\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2_\u0394\u03b5\u03ba\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2".split("_"),monthsGenitiveEl:"\u0399\u03b1\u03bd\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5_\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5_\u039c\u03b1\u03c1\u03c4\u03af\u03bf\u03c5_\u0391\u03c0\u03c1\u03b9\u03bb\u03af\u03bf\u03c5_\u039c\u03b1\u0390\u03bf\u03c5_\u0399\u03bf\u03c5\u03bd\u03af\u03bf\u03c5_\u0399\u03bf\u03c5\u03bb\u03af\u03bf\u03c5_\u0391\u03c5\u03b3\u03bf\u03cd\u03c3\u03c4\u03bf\u03c5_\u03a3\u03b5\u03c0\u03c4\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5_\u039f\u03ba\u03c4\u03c9\u03b2\u03c1\u03af\u03bf\u03c5_\u039d\u03bf\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5_\u0394\u03b5\u03ba\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5".split("_"),months:function(e,a){return e?"string"==typeof a&&/D/.test(a.substring(0,a.indexOf("MMMM")))?this._monthsGenitiveEl[e.month()]:this._monthsNominativeEl[e.month()]:this._monthsNominativeEl},monthsShort:"\u0399\u03b1\u03bd_\u03a6\u03b5\u03b2_\u039c\u03b1\u03c1_\u0391\u03c0\u03c1_\u039c\u03b1\u03ca_\u0399\u03bf\u03c5\u03bd_\u0399\u03bf\u03c5\u03bb_\u0391\u03c5\u03b3_\u03a3\u03b5\u03c0_\u039f\u03ba\u03c4_\u039d\u03bf\u03b5_\u0394\u03b5\u03ba".split("_"),weekdays:"\u039a\u03c5\u03c1\u03b9\u03b1\u03ba\u03ae_\u0394\u03b5\u03c5\u03c4\u03ad\u03c1\u03b1_\u03a4\u03c1\u03af\u03c4\u03b7_\u03a4\u03b5\u03c4\u03ac\u03c1\u03c4\u03b7_\u03a0\u03ad\u03bc\u03c0\u03c4\u03b7_\u03a0\u03b1\u03c1\u03b1\u03c3\u03ba\u03b5\u03c5\u03ae_\u03a3\u03ac\u03b2\u03b2\u03b1\u03c4\u03bf".split("_"),weekdaysShort:"\u039a\u03c5\u03c1_\u0394\u03b5\u03c5_\u03a4\u03c1\u03b9_\u03a4\u03b5\u03c4_\u03a0\u03b5\u03bc_\u03a0\u03b1\u03c1_\u03a3\u03b1\u03b2".split("_"),weekdaysMin:"\u039a\u03c5_\u0394\u03b5_\u03a4\u03c1_\u03a4\u03b5_\u03a0\u03b5_\u03a0\u03b1_\u03a3\u03b1".split("_"),meridiem:function(e,a,t){return e>11?t?"\u03bc\u03bc":"\u039c\u039c":t?"\u03c0\u03bc":"\u03a0\u039c"},isPM:function(e){return"\u03bc"===(e+"").toLowerCase()[0]},meridiemParse:/[\u03a0\u039c]\.?\u039c?\.?/i,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendarEl:{sameDay:"[\u03a3\u03ae\u03bc\u03b5\u03c1\u03b1 {}] LT",nextDay:"[\u0391\u03cd\u03c1\u03b9\u03bf {}] LT",nextWeek:"dddd [{}] LT",lastDay:"[\u03a7\u03b8\u03b5\u03c2 {}] LT",lastWeek:function(){switch(this.day()){case 6:return"[\u03c4\u03bf \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf] dddd [{}] LT";default:return"[\u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7] dddd [{}] LT"}},sameElse:"L"},calendar:function(e,a){var t=this._calendarEl[e],s=a&&a.hours();return D(t)&&(t=t.apply(a)),t.replace("{}",s%12==1?"\u03c3\u03c4\u03b7":"\u03c3\u03c4\u03b9\u03c2")},relativeTime:{future:"\u03c3\u03b5 %s",past:"%s \u03c0\u03c1\u03b9\u03bd",s:"\u03bb\u03af\u03b3\u03b1 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1",ss:"%d \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1",m:"\u03ad\u03bd\u03b1 \u03bb\u03b5\u03c0\u03c4\u03cc",mm:"%d \u03bb\u03b5\u03c0\u03c4\u03ac",h:"\u03bc\u03af\u03b1 \u03ce\u03c1\u03b1",hh:"%d \u03ce\u03c1\u03b5\u03c2",d:"\u03bc\u03af\u03b1 \u03bc\u03ad\u03c1\u03b1",dd:"%d \u03bc\u03ad\u03c1\u03b5\u03c2",M:"\u03ad\u03bd\u03b1\u03c2 \u03bc\u03ae\u03bd\u03b1\u03c2",MM:"%d \u03bc\u03ae\u03bd\u03b5\u03c2",y:"\u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2",yy:"%d \u03c7\u03c1\u03cc\u03bd\u03b9\u03b1"},dayOfMonthOrdinalParse:/\d{1,2}\u03b7/,ordinal:"%d\u03b7",week:{dow:1,doy:4}}),e.defineLocale("en-au",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")},week:{dow:1,doy:4}}),e.defineLocale("en-ca",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"YYYY-MM-DD",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")}}),e.defineLocale("en-gb",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")},week:{dow:1,doy:4}}),e.defineLocale("en-ie",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")},week:{dow:1,doy:4}}),e.defineLocale("en-nz",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")},week:{dow:1,doy:4}}),e.defineLocale("eo",{months:"januaro_februaro_marto_aprilo_majo_junio_julio_a\u016dgusto_septembro_oktobro_novembro_decembro".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_a\u016dg_sep_okt_nov_dec".split("_"),weekdays:"diman\u0109o_lundo_mardo_merkredo_\u0135a\u016ddo_vendredo_sabato".split("_"),weekdaysShort:"dim_lun_mard_merk_\u0135a\u016d_ven_sab".split("_"),weekdaysMin:"di_lu_ma_me_\u0135a_ve_sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D[-a de] MMMM, YYYY",LLL:"D[-a de] MMMM, YYYY HH:mm",LLLL:"dddd, [la] D[-a de] MMMM, YYYY HH:mm"},meridiemParse:/[ap]\.t\.m/i,isPM:function(e){return"p"===e.charAt(0).toLowerCase()},meridiem:function(e,a,t){return e>11?t?"p.t.m.":"P.T.M.":t?"a.t.m.":"A.T.M."},calendar:{sameDay:"[Hodia\u016d je] LT",nextDay:"[Morga\u016d je] LT",nextWeek:"dddd [je] LT",lastDay:"[Hiera\u016d je] LT",lastWeek:"[pasinta] dddd [je] LT",sameElse:"L"},relativeTime:{future:"post %s",past:"anta\u016d %s",s:"sekundoj",ss:"%d sekundoj",m:"minuto",mm:"%d minutoj",h:"horo",hh:"%d horoj",d:"tago",dd:"%d tagoj",M:"monato",MM:"%d monatoj",y:"jaro",yy:"%d jaroj"},dayOfMonthOrdinalParse:/\d{1,2}a/,ordinal:"%da",week:{dow:1,doy:7}});var Bs="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),qs="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"),Qs=[/^ene/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i],Xs=/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i;e.defineLocale("es-do",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(e,a){return e?/-MMM-/.test(a)?qs[e.month()]:Bs[e.month()]:Bs},monthsRegex:Xs,monthsShortRegex:Xs,monthsStrictRegex:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i,monthsShortStrictRegex:/^(ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i,monthsParse:Qs,longMonthsParse:Qs,shortMonthsParse:Qs,weekdays:"domingo_lunes_martes_mi\xe9rcoles_jueves_viernes_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._mi\xe9._jue._vie._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY h:mm A",LLLL:"dddd, D [de] MMMM [de] YYYY h:mm A"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[ma\xf1ana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",M:"un mes",MM:"%d meses",y:"un a\xf1o",yy:"%d a\xf1os"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}});var en="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),an="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_");e.defineLocale("es-us",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(e,a){return e?/-MMM-/.test(a)?an[e.month()]:en[e.month()]:en},monthsParseExact:!0,weekdays:"domingo_lunes_martes_mi\xe9rcoles_jueves_viernes_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._mi\xe9._jue._vie._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"MM/DD/YYYY",LL:"MMMM [de] D [de] YYYY",LLL:"MMMM [de] D [de] YYYY h:mm A",LLLL:"dddd, MMMM [de] D [de] YYYY h:mm A"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[ma\xf1ana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",M:"un mes",MM:"%d meses",y:"un a\xf1o",yy:"%d a\xf1os"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:0,doy:6}});var tn="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),sn="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"),nn=[/^ene/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i],dn=/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i;e.defineLocale("es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(e,a){return e?/-MMM-/.test(a)?sn[e.month()]:tn[e.month()]:tn},monthsRegex:dn,monthsShortRegex:dn,monthsStrictRegex:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i,monthsShortStrictRegex:/^(ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i,monthsParse:nn,longMonthsParse:nn,shortMonthsParse:nn,weekdays:"domingo_lunes_martes_mi\xe9rcoles_jueves_viernes_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._mi\xe9._jue._vie._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY H:mm",LLLL:"dddd, D [de] MMMM [de] YYYY H:mm"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[ma\xf1ana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",M:"un mes",MM:"%d meses",y:"un a\xf1o",yy:"%d a\xf1os"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}}),e.defineLocale("et",{months:"jaanuar_veebruar_m\xe4rts_aprill_mai_juuni_juuli_august_september_oktoober_november_detsember".split("_"),monthsShort:"jaan_veebr_m\xe4rts_apr_mai_juuni_juuli_aug_sept_okt_nov_dets".split("_"),weekdays:"p\xfchap\xe4ev_esmasp\xe4ev_teisip\xe4ev_kolmap\xe4ev_neljap\xe4ev_reede_laup\xe4ev".split("_"),weekdaysShort:"P_E_T_K_N_R_L".split("_"),weekdaysMin:"P_E_T_K_N_R_L".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[T\xe4na,] LT",nextDay:"[Homme,] LT",nextWeek:"[J\xe4rgmine] dddd LT",lastDay:"[Eile,] LT",lastWeek:"[Eelmine] dddd LT",sameElse:"L"},relativeTime:{future:"%s p\xe4rast",past:"%s tagasi",s:ra,ss:ra,m:ra,mm:ra,h:ra,hh:ra,d:ra,dd:"%d p\xe4eva",M:ra,MM:ra,y:ra,yy:ra},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("eu",{months:"urtarrila_otsaila_martxoa_apirila_maiatza_ekaina_uztaila_abuztua_iraila_urria_azaroa_abendua".split("_"),monthsShort:"urt._ots._mar._api._mai._eka._uzt._abu._ira._urr._aza._abe.".split("_"),monthsParseExact:!0,weekdays:"igandea_astelehena_asteartea_asteazkena_osteguna_ostirala_larunbata".split("_"),weekdaysShort:"ig._al._ar._az._og._ol._lr.".split("_"),weekdaysMin:"ig_al_ar_az_og_ol_lr".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY[ko] MMMM[ren] D[a]",LLL:"YYYY[ko] MMMM[ren] D[a] HH:mm",LLLL:"dddd, YYYY[ko] MMMM[ren] D[a] HH:mm",l:"YYYY-M-D",ll:"YYYY[ko] MMM D[a]",lll:"YYYY[ko] MMM D[a] HH:mm",llll:"ddd, YYYY[ko] MMM D[a] HH:mm"},calendar:{sameDay:"[gaur] LT[etan]",nextDay:"[bihar] LT[etan]",nextWeek:"dddd LT[etan]",lastDay:"[atzo] LT[etan]",lastWeek:"[aurreko] dddd LT[etan]",sameElse:"L"},relativeTime:{future:"%s barru",past:"duela %s",s:"segundo batzuk",ss:"%d segundo",m:"minutu bat",mm:"%d minutu",h:"ordu bat",hh:"%d ordu",d:"egun bat",dd:"%d egun",M:"hilabete bat",MM:"%d hilabete",y:"urte bat",yy:"%d urte"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});var rn={1:"\u06f1",2:"\u06f2",3:"\u06f3",4:"\u06f4",5:"\u06f5",6:"\u06f6",7:"\u06f7",8:"\u06f8",9:"\u06f9",0:"\u06f0"},_n={"\u06f1":"1","\u06f2":"2","\u06f3":"3","\u06f4":"4","\u06f5":"5","\u06f6":"6","\u06f7":"7","\u06f8":"8","\u06f9":"9","\u06f0":"0"};e.defineLocale("fa",{months:"\u0698\u0627\u0646\u0648\u06cc\u0647_\u0641\u0648\u0631\u06cc\u0647_\u0645\u0627\u0631\u0633_\u0622\u0648\u0631\u06cc\u0644_\u0645\u0647_\u0698\u0648\u0626\u0646_\u0698\u0648\u0626\u06cc\u0647_\u0627\u0648\u062a_\u0633\u067e\u062a\u0627\u0645\u0628\u0631_\u0627\u06a9\u062a\u0628\u0631_\u0646\u0648\u0627\u0645\u0628\u0631_\u062f\u0633\u0627\u0645\u0628\u0631".split("_"),monthsShort:"\u0698\u0627\u0646\u0648\u06cc\u0647_\u0641\u0648\u0631\u06cc\u0647_\u0645\u0627\u0631\u0633_\u0622\u0648\u0631\u06cc\u0644_\u0645\u0647_\u0698\u0648\u0626\u0646_\u0698\u0648\u0626\u06cc\u0647_\u0627\u0648\u062a_\u0633\u067e\u062a\u0627\u0645\u0628\u0631_\u0627\u06a9\u062a\u0628\u0631_\u0646\u0648\u0627\u0645\u0628\u0631_\u062f\u0633\u0627\u0645\u0628\u0631".split("_"),weekdays:"\u06cc\u06a9\u200c\u0634\u0646\u0628\u0647_\u062f\u0648\u0634\u0646\u0628\u0647_\u0633\u0647\u200c\u0634\u0646\u0628\u0647_\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647_\u067e\u0646\u062c\u200c\u0634\u0646\u0628\u0647_\u062c\u0645\u0639\u0647_\u0634\u0646\u0628\u0647".split("_"),weekdaysShort:"\u06cc\u06a9\u200c\u0634\u0646\u0628\u0647_\u062f\u0648\u0634\u0646\u0628\u0647_\u0633\u0647\u200c\u0634\u0646\u0628\u0647_\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647_\u067e\u0646\u062c\u200c\u0634\u0646\u0628\u0647_\u062c\u0645\u0639\u0647_\u0634\u0646\u0628\u0647".split("_"),weekdaysMin:"\u06cc_\u062f_\u0633_\u0686_\u067e_\u062c_\u0634".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},meridiemParse:/\u0642\u0628\u0644 \u0627\u0632 \u0638\u0647\u0631|\u0628\u0639\u062f \u0627\u0632 \u0638\u0647\u0631/,isPM:function(e){return/\u0628\u0639\u062f \u0627\u0632 \u0638\u0647\u0631/.test(e)},meridiem:function(e,a,t){return e<12?"\u0642\u0628\u0644 \u0627\u0632 \u0638\u0647\u0631":"\u0628\u0639\u062f \u0627\u0632 \u0638\u0647\u0631"},calendar:{sameDay:"[\u0627\u0645\u0631\u0648\u0632 \u0633\u0627\u0639\u062a] LT",nextDay:"[\u0641\u0631\u062f\u0627 \u0633\u0627\u0639\u062a] LT",nextWeek:"dddd [\u0633\u0627\u0639\u062a] LT",lastDay:"[\u062f\u06cc\u0631\u0648\u0632 \u0633\u0627\u0639\u062a] LT",lastWeek:"dddd [\u067e\u06cc\u0634] [\u0633\u0627\u0639\u062a] LT",sameElse:"L"},relativeTime:{future:"\u062f\u0631 %s",past:"%s \u067e\u06cc\u0634",s:"\u0686\u0646\u062f \u062b\u0627\u0646\u06cc\u0647",ss:"\u062b\u0627\u0646\u06cc\u0647 d%",m:"\u06cc\u06a9 \u062f\u0642\u06cc\u0642\u0647",mm:"%d \u062f\u0642\u06cc\u0642\u0647",h:"\u06cc\u06a9 \u0633\u0627\u0639\u062a",hh:"%d \u0633\u0627\u0639\u062a",d:"\u06cc\u06a9 \u0631\u0648\u0632",dd:"%d \u0631\u0648\u0632",M:"\u06cc\u06a9 \u0645\u0627\u0647",MM:"%d \u0645\u0627\u0647",y:"\u06cc\u06a9 \u0633\u0627\u0644",yy:"%d \u0633\u0627\u0644"},preparse:function(e){return e.replace(/[\u06f0-\u06f9]/g,function(e){return _n[e]}).replace(/\u060c/g,",")},postformat:function(e){return e.replace(/\d/g,function(e){return rn[e]}).replace(/,/g,"\u060c")},dayOfMonthOrdinalParse:/\d{1,2}\u0645/,ordinal:"%d\u0645",week:{dow:6,doy:12}});var on="nolla yksi kaksi kolme nelj\xe4 viisi kuusi seitsem\xe4n kahdeksan yhdeks\xe4n".split(" "),mn=["nolla","yhden","kahden","kolmen","nelj\xe4n","viiden","kuuden",on[7],on[8],on[9]];e.defineLocale("fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kes\xe4kuu_hein\xe4kuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kes\xe4_hein\xe4_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] HH.mm",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] HH.mm",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] HH.mm",llll:"ddd, Do MMM YYYY, [klo] HH.mm"},calendar:{sameDay:"[t\xe4n\xe4\xe4n] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s p\xe4\xe4st\xe4",past:"%s sitten",s:_a,ss:_a,m:_a,mm:_a,h:_a,hh:_a,d:_a,dd:_a,M:_a,MM:_a,y:_a,yy:_a},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("fo",{months:"januar_februar_mars_apr\xedl_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"sunnudagur_m\xe1nadagur_t\xfdsdagur_mikudagur_h\xf3sdagur_fr\xedggjadagur_leygardagur".split("_"),weekdaysShort:"sun_m\xe1n_t\xfds_mik_h\xf3s_fr\xed_ley".split("_"),weekdaysMin:"su_m\xe1_t\xfd_mi_h\xf3_fr_le".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D. MMMM, YYYY HH:mm"},calendar:{sameDay:"[\xcd dag kl.] LT",nextDay:"[\xcd morgin kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[\xcd gj\xe1r kl.] LT",lastWeek:"[s\xed\xf0stu] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"um %s",past:"%s s\xed\xf0ani",s:"f\xe1 sekund",ss:"%d sekundir",m:"ein minutt",mm:"%d minuttir",h:"ein t\xedmi",hh:"%d t\xedmar",d:"ein dagur",dd:"%d dagar",M:"ein m\xe1na\xf0i",MM:"%d m\xe1na\xf0ir",y:"eitt \xe1r",yy:"%d \xe1r"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("fr-ca",{months:"janvier_f\xe9vrier_mars_avril_mai_juin_juillet_ao\xfbt_septembre_octobre_novembre_d\xe9cembre".split("_"),monthsShort:"janv._f\xe9vr._mars_avr._mai_juin_juil._ao\xfbt_sept._oct._nov._d\xe9c.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"di_lu_ma_me_je_ve_sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd\u2019hui \xe0] LT",nextDay:"[Demain \xe0] LT",nextWeek:"dddd [\xe0] LT",lastDay:"[Hier \xe0] LT",lastWeek:"dddd [dernier \xe0] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",ss:"%d secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},dayOfMonthOrdinalParse:/\d{1,2}(er|e)/,ordinal:function(e,a){switch(a){default:case"M":case"Q":case"D":case"DDD":case"d":return e+(1===e?"er":"e");case"w":case"W":return e+(1===e?"re":"e")}}}),e.defineLocale("fr-ch",{months:"janvier_f\xe9vrier_mars_avril_mai_juin_juillet_ao\xfbt_septembre_octobre_novembre_d\xe9cembre".split("_"),monthsShort:"janv._f\xe9vr._mars_avr._mai_juin_juil._ao\xfbt_sept._oct._nov._d\xe9c.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"di_lu_ma_me_je_ve_sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd\u2019hui \xe0] LT",nextDay:"[Demain \xe0] LT",nextWeek:"dddd [\xe0] LT",lastDay:"[Hier \xe0] LT",lastWeek:"dddd [dernier \xe0] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",ss:"%d secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},dayOfMonthOrdinalParse:/\d{1,2}(er|e)/,ordinal:function(e,a){switch(a){default:case"M":case"Q":case"D":case"DDD":case"d":return e+(1===e?"er":"e");case"w":case"W":return e+(1===e?"re":"e")}},week:{dow:1,doy:4}}),e.defineLocale("fr",{months:"janvier_f\xe9vrier_mars_avril_mai_juin_juillet_ao\xfbt_septembre_octobre_novembre_d\xe9cembre".split("_"),monthsShort:"janv._f\xe9vr._mars_avr._mai_juin_juil._ao\xfbt_sept._oct._nov._d\xe9c.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"di_lu_ma_me_je_ve_sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd\u2019hui \xe0] LT",nextDay:"[Demain \xe0] LT",nextWeek:"dddd [\xe0] LT",lastDay:"[Hier \xe0] LT",lastWeek:"dddd [dernier \xe0] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",ss:"%d secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},dayOfMonthOrdinalParse:/\d{1,2}(er|)/,ordinal:function(e,a){switch(a){case"D":return e+(1===e?"er":"");default:case"M":case"Q":case"DDD":case"d":return e+(1===e?"er":"e");case"w":case"W":return e+(1===e?"re":"e")}},week:{dow:1,doy:4}});var un="jan._feb._mrt._apr._mai_jun._jul._aug._sep._okt._nov._des.".split("_"),ln="jan_feb_mrt_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_");e.defineLocale("fy",{months:"jannewaris_febrewaris_maart_april_maaie_juny_july_augustus_septimber_oktober_novimber_desimber".split("_"),monthsShort:function(e,a){return e?/-MMM-/.test(a)?ln[e.month()]:un[e.month()]:un},monthsParseExact:!0,weekdays:"snein_moandei_tiisdei_woansdei_tongersdei_freed_sneon".split("_"),weekdaysShort:"si._mo._ti._wo._to._fr._so.".split("_"),weekdaysMin:"Si_Mo_Ti_Wo_To_Fr_So".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[hjoed om] LT",nextDay:"[moarn om] LT",nextWeek:"dddd [om] LT",lastDay:"[juster om] LT",lastWeek:"[\xf4fr\xfbne] dddd [om] LT",sameElse:"L"},relativeTime:{future:"oer %s",past:"%s lyn",s:"in pear sekonden",ss:"%d sekonden",m:"ien min\xfat",mm:"%d minuten",h:"ien oere",hh:"%d oeren",d:"ien dei",dd:"%d dagen",M:"ien moanne",MM:"%d moannen",y:"ien jier",yy:"%d jierren"},dayOfMonthOrdinalParse:/\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||e>=20?"ste":"de")},week:{dow:1,doy:4}});e.defineLocale("gd",{months:["Am Faoilleach","An Gearran","Am M\xe0rt","An Giblean","An C\xe8itean","An t-\xd2gmhios","An t-Iuchar","An L\xf9nastal","An t-Sultain","An D\xe0mhair","An t-Samhain","An D\xf9bhlachd"],monthsShort:["Faoi","Gear","M\xe0rt","Gibl","C\xe8it","\xd2gmh","Iuch","L\xf9n","Sult","D\xe0mh","Samh","D\xf9bh"],monthsParseExact:!0,weekdays:["Did\xf2mhnaich","Diluain","Dim\xe0irt","Diciadain","Diardaoin","Dihaoine","Disathairne"],weekdaysShort:["Did","Dil","Dim","Dic","Dia","Dih","Dis"],weekdaysMin:["D\xf2","Lu","M\xe0","Ci","Ar","Ha","Sa"],longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[An-diugh aig] LT",nextDay:"[A-m\xe0ireach aig] LT",nextWeek:"dddd [aig] LT",lastDay:"[An-d\xe8 aig] LT",lastWeek:"dddd [seo chaidh] [aig] LT",sameElse:"L"},relativeTime:{future:"ann an %s",past:"bho chionn %s",s:"beagan diogan",ss:"%d diogan",m:"mionaid",mm:"%d mionaidean",h:"uair",hh:"%d uairean",d:"latha",dd:"%d latha",M:"m\xecos",MM:"%d m\xecosan",y:"bliadhna",yy:"%d bliadhna"},dayOfMonthOrdinalParse:/\d{1,2}(d|na|mh)/,ordinal:function(e){return e+(1===e?"d":e%10==2?"na":"mh")},week:{dow:1,doy:4}}),e.defineLocale("gl",{months:"xaneiro_febreiro_marzo_abril_maio_xu\xf1o_xullo_agosto_setembro_outubro_novembro_decembro".split("_"),monthsShort:"xan._feb._mar._abr._mai._xu\xf1._xul._ago._set._out._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"domingo_luns_martes_m\xe9rcores_xoves_venres_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._m\xe9r._xov._ven._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_m\xe9_xo_ve_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY H:mm",LLLL:"dddd, D [de] MMMM [de] YYYY H:mm"},calendar:{sameDay:function(){return"[hoxe "+(1!==this.hours()?"\xe1s":"\xe1")+"] LT"},nextDay:function(){return"[ma\xf1\xe1 "+(1!==this.hours()?"\xe1s":"\xe1")+"] LT"},nextWeek:function(){return"dddd ["+(1!==this.hours()?"\xe1s":"a")+"] LT"},lastDay:function(){return"[onte "+(1!==this.hours()?"\xe1":"a")+"] LT"},lastWeek:function(){return"[o] dddd [pasado "+(1!==this.hours()?"\xe1s":"a")+"] LT"},sameElse:"L"},relativeTime:{future:function(e){return 0===e.indexOf("un")?"n"+e:"en "+e},past:"hai %s",s:"uns segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"unha hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",M:"un mes",MM:"%d meses",y:"un ano",yy:"%d anos"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}}),e.defineLocale("gom-latn",{months:"Janer_Febrer_Mars_Abril_Mai_Jun_Julai_Agost_Setembr_Otubr_Novembr_Dezembr".split("_"),monthsShort:"Jan._Feb._Mars_Abr._Mai_Jun_Jul._Ago._Set._Otu._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Aitar_Somar_Mongllar_Budvar_Brestar_Sukrar_Son'var".split("_"),weekdaysShort:"Ait._Som._Mon._Bud._Bre._Suk._Son.".split("_"),weekdaysMin:"Ai_Sm_Mo_Bu_Br_Su_Sn".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"A h:mm [vazta]",LTS:"A h:mm:ss [vazta]",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY A h:mm [vazta]",LLLL:"dddd, MMMM[achea] Do, YYYY, A h:mm [vazta]",llll:"ddd, D MMM YYYY, A h:mm [vazta]"},calendar:{sameDay:"[Aiz] LT",nextDay:"[Faleam] LT",nextWeek:"[Ieta to] dddd[,] LT",lastDay:"[Kal] LT",lastWeek:"[Fatlo] dddd[,] LT",sameElse:"L"},relativeTime:{future:"%s",past:"%s adim",s:ia,ss:ia,m:ia,mm:ia,h:ia,hh:ia,d:ia,dd:ia,M:ia,MM:ia,y:ia,yy:ia},dayOfMonthOrdinalParse:/\d{1,2}(er)/,ordinal:function(e,a){switch(a){case"D":return e+"er";default:case"M":case"Q":case"DDD":case"d":case"w":case"W":return e}},week:{dow:1,doy:4},meridiemParse:/rati|sokalli|donparam|sanje/,meridiemHour:function(e,a){return 12===e&&(e=0),"rati"===a?e<4?e:e+12:"sokalli"===a?e:"donparam"===a?e>12?e:e+12:"sanje"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"rati":e<12?"sokalli":e<16?"donparam":e<20?"sanje":"rati"}});var Mn={1:"\u0ae7",2:"\u0ae8",3:"\u0ae9",4:"\u0aea",5:"\u0aeb",6:"\u0aec",7:"\u0aed",8:"\u0aee",9:"\u0aef",0:"\u0ae6"},hn={"\u0ae7":"1","\u0ae8":"2","\u0ae9":"3","\u0aea":"4","\u0aeb":"5","\u0aec":"6","\u0aed":"7","\u0aee":"8","\u0aef":"9","\u0ae6":"0"};e.defineLocale("gu",{months:"\u0a9c\u0abe\u0aa8\u0acd\u0aaf\u0ac1\u0a86\u0ab0\u0ac0_\u0aab\u0ac7\u0aac\u0acd\u0ab0\u0ac1\u0a86\u0ab0\u0ac0_\u0aae\u0abe\u0ab0\u0acd\u0a9a_\u0a8f\u0aaa\u0acd\u0ab0\u0abf\u0ab2_\u0aae\u0ac7_\u0a9c\u0ac2\u0aa8_\u0a9c\u0ac1\u0ab2\u0abe\u0a88_\u0a91\u0a97\u0ab8\u0acd\u0a9f_\u0ab8\u0aaa\u0acd\u0a9f\u0ac7\u0aae\u0acd\u0aac\u0ab0_\u0a91\u0a95\u0acd\u0a9f\u0acd\u0aac\u0ab0_\u0aa8\u0ab5\u0ac7\u0aae\u0acd\u0aac\u0ab0_\u0aa1\u0abf\u0ab8\u0ac7\u0aae\u0acd\u0aac\u0ab0".split("_"),monthsShort:"\u0a9c\u0abe\u0aa8\u0acd\u0aaf\u0ac1._\u0aab\u0ac7\u0aac\u0acd\u0ab0\u0ac1._\u0aae\u0abe\u0ab0\u0acd\u0a9a_\u0a8f\u0aaa\u0acd\u0ab0\u0abf._\u0aae\u0ac7_\u0a9c\u0ac2\u0aa8_\u0a9c\u0ac1\u0ab2\u0abe._\u0a91\u0a97._\u0ab8\u0aaa\u0acd\u0a9f\u0ac7._\u0a91\u0a95\u0acd\u0a9f\u0acd._\u0aa8\u0ab5\u0ac7._\u0aa1\u0abf\u0ab8\u0ac7.".split("_"),monthsParseExact:!0,weekdays:"\u0ab0\u0ab5\u0abf\u0ab5\u0abe\u0ab0_\u0ab8\u0acb\u0aae\u0ab5\u0abe\u0ab0_\u0aae\u0a82\u0a97\u0ab3\u0ab5\u0abe\u0ab0_\u0aac\u0ac1\u0aa7\u0acd\u0ab5\u0abe\u0ab0_\u0a97\u0ac1\u0ab0\u0ac1\u0ab5\u0abe\u0ab0_\u0ab6\u0ac1\u0a95\u0acd\u0ab0\u0ab5\u0abe\u0ab0_\u0ab6\u0aa8\u0abf\u0ab5\u0abe\u0ab0".split("_"),weekdaysShort:"\u0ab0\u0ab5\u0abf_\u0ab8\u0acb\u0aae_\u0aae\u0a82\u0a97\u0ab3_\u0aac\u0ac1\u0aa7\u0acd_\u0a97\u0ac1\u0ab0\u0ac1_\u0ab6\u0ac1\u0a95\u0acd\u0ab0_\u0ab6\u0aa8\u0abf".split("_"),weekdaysMin:"\u0ab0_\u0ab8\u0acb_\u0aae\u0a82_\u0aac\u0ac1_\u0a97\u0ac1_\u0ab6\u0ac1_\u0ab6".split("_"),longDateFormat:{LT:"A h:mm \u0ab5\u0abe\u0a97\u0acd\u0aaf\u0ac7",LTS:"A h:mm:ss \u0ab5\u0abe\u0a97\u0acd\u0aaf\u0ac7",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u0ab5\u0abe\u0a97\u0acd\u0aaf\u0ac7",LLLL:"dddd, D MMMM YYYY, A h:mm \u0ab5\u0abe\u0a97\u0acd\u0aaf\u0ac7"},calendar:{sameDay:"[\u0a86\u0a9c] LT",nextDay:"[\u0a95\u0abe\u0ab2\u0ac7] LT",nextWeek:"dddd, LT",lastDay:"[\u0a97\u0a87\u0a95\u0abe\u0ab2\u0ac7] LT",lastWeek:"[\u0aaa\u0abe\u0a9b\u0ab2\u0abe] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0aae\u0abe",past:"%s \u0aaa\u0ac7\u0ab9\u0ab2\u0abe",s:"\u0a85\u0aae\u0ac1\u0a95 \u0aaa\u0ab3\u0acb",ss:"%d \u0ab8\u0ac7\u0a95\u0a82\u0aa1",m:"\u0a8f\u0a95 \u0aae\u0abf\u0aa8\u0abf\u0a9f",mm:"%d \u0aae\u0abf\u0aa8\u0abf\u0a9f",h:"\u0a8f\u0a95 \u0a95\u0ab2\u0abe\u0a95",hh:"%d \u0a95\u0ab2\u0abe\u0a95",d:"\u0a8f\u0a95 \u0aa6\u0abf\u0ab5\u0ab8",dd:"%d \u0aa6\u0abf\u0ab5\u0ab8",M:"\u0a8f\u0a95 \u0aae\u0ab9\u0abf\u0aa8\u0acb",MM:"%d \u0aae\u0ab9\u0abf\u0aa8\u0acb",y:"\u0a8f\u0a95 \u0ab5\u0ab0\u0acd\u0ab7",yy:"%d \u0ab5\u0ab0\u0acd\u0ab7"},preparse:function(e){return e.replace(/[\u0ae7\u0ae8\u0ae9\u0aea\u0aeb\u0aec\u0aed\u0aee\u0aef\u0ae6]/g,function(e){return hn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Mn[e]})},meridiemParse:/\u0ab0\u0abe\u0aa4|\u0aac\u0aaa\u0acb\u0ab0|\u0ab8\u0ab5\u0abe\u0ab0|\u0ab8\u0abe\u0a82\u0a9c/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0ab0\u0abe\u0aa4"===a?e<4?e:e+12:"\u0ab8\u0ab5\u0abe\u0ab0"===a?e:"\u0aac\u0aaa\u0acb\u0ab0"===a?e>=10?e:e+12:"\u0ab8\u0abe\u0a82\u0a9c"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"\u0ab0\u0abe\u0aa4":e<10?"\u0ab8\u0ab5\u0abe\u0ab0":e<17?"\u0aac\u0aaa\u0acb\u0ab0":e<20?"\u0ab8\u0abe\u0a82\u0a9c":"\u0ab0\u0abe\u0aa4"},week:{dow:0,doy:6}}),e.defineLocale("he",{months:"\u05d9\u05e0\u05d5\u05d0\u05e8_\u05e4\u05d1\u05e8\u05d5\u05d0\u05e8_\u05de\u05e8\u05e5_\u05d0\u05e4\u05e8\u05d9\u05dc_\u05de\u05d0\u05d9_\u05d9\u05d5\u05e0\u05d9_\u05d9\u05d5\u05dc\u05d9_\u05d0\u05d5\u05d2\u05d5\u05e1\u05d8_\u05e1\u05e4\u05d8\u05de\u05d1\u05e8_\u05d0\u05d5\u05e7\u05d8\u05d5\u05d1\u05e8_\u05e0\u05d5\u05d1\u05de\u05d1\u05e8_\u05d3\u05e6\u05de\u05d1\u05e8".split("_"),monthsShort:"\u05d9\u05e0\u05d5\u05f3_\u05e4\u05d1\u05e8\u05f3_\u05de\u05e8\u05e5_\u05d0\u05e4\u05e8\u05f3_\u05de\u05d0\u05d9_\u05d9\u05d5\u05e0\u05d9_\u05d9\u05d5\u05dc\u05d9_\u05d0\u05d5\u05d2\u05f3_\u05e1\u05e4\u05d8\u05f3_\u05d0\u05d5\u05e7\u05f3_\u05e0\u05d5\u05d1\u05f3_\u05d3\u05e6\u05de\u05f3".split("_"),weekdays:"\u05e8\u05d0\u05e9\u05d5\u05df_\u05e9\u05e0\u05d9_\u05e9\u05dc\u05d9\u05e9\u05d9_\u05e8\u05d1\u05d9\u05e2\u05d9_\u05d7\u05de\u05d9\u05e9\u05d9_\u05e9\u05d9\u05e9\u05d9_\u05e9\u05d1\u05ea".split("_"),weekdaysShort:"\u05d0\u05f3_\u05d1\u05f3_\u05d2\u05f3_\u05d3\u05f3_\u05d4\u05f3_\u05d5\u05f3_\u05e9\u05f3".split("_"),weekdaysMin:"\u05d0_\u05d1_\u05d2_\u05d3_\u05d4_\u05d5_\u05e9".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [\u05d1]MMMM YYYY",LLL:"D [\u05d1]MMMM YYYY HH:mm",LLLL:"dddd, D [\u05d1]MMMM YYYY HH:mm",l:"D/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY HH:mm",llll:"ddd, D MMM YYYY HH:mm"},calendar:{sameDay:"[\u05d4\u05d9\u05d5\u05dd \u05d1\u05be]LT",nextDay:"[\u05de\u05d7\u05e8 \u05d1\u05be]LT",nextWeek:"dddd [\u05d1\u05e9\u05e2\u05d4] LT",lastDay:"[\u05d0\u05ea\u05de\u05d5\u05dc \u05d1\u05be]LT",lastWeek:"[\u05d1\u05d9\u05d5\u05dd] dddd [\u05d4\u05d0\u05d7\u05e8\u05d5\u05df \u05d1\u05e9\u05e2\u05d4] LT",sameElse:"L"},relativeTime:{future:"\u05d1\u05e2\u05d5\u05d3 %s",past:"\u05dc\u05e4\u05e0\u05d9 %s",s:"\u05de\u05e1\u05e4\u05e8 \u05e9\u05e0\u05d9\u05d5\u05ea",ss:"%d \u05e9\u05e0\u05d9\u05d5\u05ea",m:"\u05d3\u05e7\u05d4",mm:"%d \u05d3\u05e7\u05d5\u05ea",h:"\u05e9\u05e2\u05d4",hh:function(e){return 2===e?"\u05e9\u05e2\u05ea\u05d9\u05d9\u05dd":e+" \u05e9\u05e2\u05d5\u05ea"},d:"\u05d9\u05d5\u05dd",dd:function(e){return 2===e?"\u05d9\u05d5\u05de\u05d9\u05d9\u05dd":e+" \u05d9\u05de\u05d9\u05dd"},M:"\u05d7\u05d5\u05d3\u05e9",MM:function(e){return 2===e?"\u05d7\u05d5\u05d3\u05e9\u05d9\u05d9\u05dd":e+" \u05d7\u05d5\u05d3\u05e9\u05d9\u05dd"},y:"\u05e9\u05e0\u05d4",yy:function(e){return 2===e?"\u05e9\u05e0\u05ea\u05d9\u05d9\u05dd":e%10==0&&10!==e?e+" \u05e9\u05e0\u05d4":e+" \u05e9\u05e0\u05d9\u05dd"}},meridiemParse:/\u05d0\u05d7\u05d4"\u05e6|\u05dc\u05e4\u05e0\u05d4"\u05e6|\u05d0\u05d7\u05e8\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd|\u05dc\u05e4\u05e0\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd|\u05dc\u05e4\u05e0\u05d5\u05ea \u05d1\u05d5\u05e7\u05e8|\u05d1\u05d1\u05d5\u05e7\u05e8|\u05d1\u05e2\u05e8\u05d1/i,isPM:function(e){return/^(\u05d0\u05d7\u05d4"\u05e6|\u05d0\u05d7\u05e8\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd|\u05d1\u05e2\u05e8\u05d1)$/.test(e)},meridiem:function(e,a,t){return e<5?"\u05dc\u05e4\u05e0\u05d5\u05ea \u05d1\u05d5\u05e7\u05e8":e<10?"\u05d1\u05d1\u05d5\u05e7\u05e8":e<12?t?'\u05dc\u05e4\u05e0\u05d4"\u05e6':"\u05dc\u05e4\u05e0\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd":e<18?t?'\u05d0\u05d7\u05d4"\u05e6':"\u05d0\u05d7\u05e8\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd":"\u05d1\u05e2\u05e8\u05d1"}});var Ln={1:"\u0967",2:"\u0968",3:"\u0969",4:"\u096a",5:"\u096b",6:"\u096c",7:"\u096d",8:"\u096e",9:"\u096f",0:"\u0966"},cn={"\u0967":"1","\u0968":"2","\u0969":"3","\u096a":"4","\u096b":"5","\u096c":"6","\u096d":"7","\u096e":"8","\u096f":"9","\u0966":"0"};e.defineLocale("hi",{months:"\u091c\u0928\u0935\u0930\u0940_\u092b\u093c\u0930\u0935\u0930\u0940_\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u0948\u0932_\u092e\u0908_\u091c\u0942\u0928_\u091c\u0941\u0932\u093e\u0908_\u0905\u0917\u0938\u094d\u0924_\u0938\u093f\u0924\u092e\u094d\u092c\u0930_\u0905\u0915\u094d\u091f\u0942\u092c\u0930_\u0928\u0935\u092e\u094d\u092c\u0930_\u0926\u093f\u0938\u092e\u094d\u092c\u0930".split("_"),monthsShort:"\u091c\u0928._\u092b\u093c\u0930._\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u0948._\u092e\u0908_\u091c\u0942\u0928_\u091c\u0941\u0932._\u0905\u0917._\u0938\u093f\u0924._\u0905\u0915\u094d\u091f\u0942._\u0928\u0935._\u0926\u093f\u0938.".split("_"),monthsParseExact:!0,weekdays:"\u0930\u0935\u093f\u0935\u093e\u0930_\u0938\u094b\u092e\u0935\u093e\u0930_\u092e\u0902\u0917\u0932\u0935\u093e\u0930_\u092c\u0941\u0927\u0935\u093e\u0930_\u0917\u0941\u0930\u0942\u0935\u093e\u0930_\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930_\u0936\u0928\u093f\u0935\u093e\u0930".split("_"),weekdaysShort:"\u0930\u0935\u093f_\u0938\u094b\u092e_\u092e\u0902\u0917\u0932_\u092c\u0941\u0927_\u0917\u0941\u0930\u0942_\u0936\u0941\u0915\u094d\u0930_\u0936\u0928\u093f".split("_"),weekdaysMin:"\u0930_\u0938\u094b_\u092e\u0902_\u092c\u0941_\u0917\u0941_\u0936\u0941_\u0936".split("_"),longDateFormat:{LT:"A h:mm \u092c\u091c\u0947",LTS:"A h:mm:ss \u092c\u091c\u0947",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u092c\u091c\u0947",LLLL:"dddd, D MMMM YYYY, A h:mm \u092c\u091c\u0947"},calendar:{sameDay:"[\u0906\u091c] LT",nextDay:"[\u0915\u0932] LT",nextWeek:"dddd, LT",lastDay:"[\u0915\u0932] LT",lastWeek:"[\u092a\u093f\u091b\u0932\u0947] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u092e\u0947\u0902",past:"%s \u092a\u0939\u0932\u0947",s:"\u0915\u0941\u091b \u0939\u0940 \u0915\u094d\u0937\u0923",ss:"%d \u0938\u0947\u0915\u0902\u0921",m:"\u090f\u0915 \u092e\u093f\u0928\u091f",mm:"%d \u092e\u093f\u0928\u091f",h:"\u090f\u0915 \u0918\u0902\u091f\u093e",hh:"%d \u0918\u0902\u091f\u0947",d:"\u090f\u0915 \u0926\u093f\u0928",dd:"%d \u0926\u093f\u0928",M:"\u090f\u0915 \u092e\u0939\u0940\u0928\u0947",MM:"%d \u092e\u0939\u0940\u0928\u0947",y:"\u090f\u0915 \u0935\u0930\u094d\u0937",yy:"%d \u0935\u0930\u094d\u0937"},preparse:function(e){return e.replace(/[\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u0966]/g,function(e){return cn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Ln[e]})},meridiemParse:/\u0930\u093e\u0924|\u0938\u0941\u092c\u0939|\u0926\u094b\u092a\u0939\u0930|\u0936\u093e\u092e/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0930\u093e\u0924"===a?e<4?e:e+12:"\u0938\u0941\u092c\u0939"===a?e:"\u0926\u094b\u092a\u0939\u0930"===a?e>=10?e:e+12:"\u0936\u093e\u092e"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"\u0930\u093e\u0924":e<10?"\u0938\u0941\u092c\u0939":e<17?"\u0926\u094b\u092a\u0939\u0930":e<20?"\u0936\u093e\u092e":"\u0930\u093e\u0924"},week:{dow:0,doy:6}}),e.defineLocale("hr",{months:{format:"sije\u010dnja_velja\u010de_o\u017eujka_travnja_svibnja_lipnja_srpnja_kolovoza_rujna_listopada_studenoga_prosinca".split("_"),standalone:"sije\u010danj_velja\u010da_o\u017eujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac".split("_")},monthsShort:"sij._velj._o\u017eu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.".split("_"),monthsParseExact:!0,weekdays:"nedjelja_ponedjeljak_utorak_srijeda_\u010detvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._\u010det._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_\u010de_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[ju\u010der u] LT",lastWeek:function(){switch(this.day()){case 0:case 3:return"[pro\u0161lu] dddd [u] LT";case 6:return"[pro\u0161le] [subote] [u] LT";case 1:case 2:case 4:case 5:return"[pro\u0161li] dddd [u] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"par sekundi",ss:oa,m:oa,mm:oa,h:oa,hh:oa,d:"dan",dd:oa,M:"mjesec",MM:oa,y:"godinu",yy:oa},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});var Yn="vas\xe1rnap h\xe9tf\u0151n kedden szerd\xe1n cs\xfct\xf6rt\xf6k\xf6n p\xe9nteken szombaton".split(" ");e.defineLocale("hu",{months:"janu\xe1r_febru\xe1r_m\xe1rcius_\xe1prilis_m\xe1jus_j\xfanius_j\xfalius_augusztus_szeptember_okt\xf3ber_november_december".split("_"),monthsShort:"jan_feb_m\xe1rc_\xe1pr_m\xe1j_j\xfan_j\xfal_aug_szept_okt_nov_dec".split("_"),weekdays:"vas\xe1rnap_h\xe9tf\u0151_kedd_szerda_cs\xfct\xf6rt\xf6k_p\xe9ntek_szombat".split("_"),weekdaysShort:"vas_h\xe9t_kedd_sze_cs\xfct_p\xe9n_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D. H:mm",LLLL:"YYYY. MMMM D., dddd H:mm"},meridiemParse:/de|du/i,isPM:function(e){return"u"===e.charAt(1).toLowerCase()},meridiem:function(e,a,t){return e<12?!0===t?"de":"DE":!0===t?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return ua.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return ua.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s m\xfalva",past:"%s",s:ma,ss:ma,m:ma,mm:ma,h:ma,hh:ma,d:ma,dd:ma,M:ma,MM:ma,y:ma,yy:ma},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("hy-am",{months:{format:"\u0570\u0578\u0582\u0576\u057e\u0561\u0580\u056b_\u0583\u0565\u057f\u0580\u057e\u0561\u0580\u056b_\u0574\u0561\u0580\u057f\u056b_\u0561\u057a\u0580\u056b\u056c\u056b_\u0574\u0561\u0575\u056b\u057d\u056b_\u0570\u0578\u0582\u0576\u056b\u057d\u056b_\u0570\u0578\u0582\u056c\u056b\u057d\u056b_\u0585\u0563\u0578\u057d\u057f\u0578\u057d\u056b_\u057d\u0565\u057a\u057f\u0565\u0574\u0562\u0565\u0580\u056b_\u0570\u0578\u056f\u057f\u0565\u0574\u0562\u0565\u0580\u056b_\u0576\u0578\u0575\u0565\u0574\u0562\u0565\u0580\u056b_\u0564\u0565\u056f\u057f\u0565\u0574\u0562\u0565\u0580\u056b".split("_"),standalone:"\u0570\u0578\u0582\u0576\u057e\u0561\u0580_\u0583\u0565\u057f\u0580\u057e\u0561\u0580_\u0574\u0561\u0580\u057f_\u0561\u057a\u0580\u056b\u056c_\u0574\u0561\u0575\u056b\u057d_\u0570\u0578\u0582\u0576\u056b\u057d_\u0570\u0578\u0582\u056c\u056b\u057d_\u0585\u0563\u0578\u057d\u057f\u0578\u057d_\u057d\u0565\u057a\u057f\u0565\u0574\u0562\u0565\u0580_\u0570\u0578\u056f\u057f\u0565\u0574\u0562\u0565\u0580_\u0576\u0578\u0575\u0565\u0574\u0562\u0565\u0580_\u0564\u0565\u056f\u057f\u0565\u0574\u0562\u0565\u0580".split("_")},monthsShort:"\u0570\u0576\u057e_\u0583\u057f\u0580_\u0574\u0580\u057f_\u0561\u057a\u0580_\u0574\u0575\u057d_\u0570\u0576\u057d_\u0570\u056c\u057d_\u0585\u0563\u057d_\u057d\u057a\u057f_\u0570\u056f\u057f_\u0576\u0574\u0562_\u0564\u056f\u057f".split("_"),weekdays:"\u056f\u056b\u0580\u0561\u056f\u056b_\u0565\u0580\u056f\u0578\u0582\u0577\u0561\u0562\u0569\u056b_\u0565\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056b_\u0579\u0578\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056b_\u0570\u056b\u0576\u0563\u0577\u0561\u0562\u0569\u056b_\u0578\u0582\u0580\u0562\u0561\u0569_\u0577\u0561\u0562\u0561\u0569".split("_"),weekdaysShort:"\u056f\u0580\u056f_\u0565\u0580\u056f_\u0565\u0580\u0584_\u0579\u0580\u0584_\u0570\u0576\u0563_\u0578\u0582\u0580\u0562_\u0577\u0562\u0569".split("_"),weekdaysMin:"\u056f\u0580\u056f_\u0565\u0580\u056f_\u0565\u0580\u0584_\u0579\u0580\u0584_\u0570\u0576\u0563_\u0578\u0582\u0580\u0562_\u0577\u0562\u0569".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY \u0569.",LLL:"D MMMM YYYY \u0569., HH:mm",LLLL:"dddd, D MMMM YYYY \u0569., HH:mm"},calendar:{sameDay:"[\u0561\u0575\u057d\u0585\u0580] LT",nextDay:"[\u057e\u0561\u0572\u0568] LT",lastDay:"[\u0565\u0580\u0565\u056f] LT",nextWeek:function(){return"dddd [\u0585\u0580\u0568 \u056a\u0561\u0574\u0568] LT"},lastWeek:function(){return"[\u0561\u0576\u0581\u0561\u056e] dddd [\u0585\u0580\u0568 \u056a\u0561\u0574\u0568] LT"},sameElse:"L"},relativeTime:{future:"%s \u0570\u0565\u057f\u0578",past:"%s \u0561\u057c\u0561\u057b",s:"\u0574\u056b \u0584\u0561\u0576\u056b \u057e\u0561\u0575\u0580\u056f\u0575\u0561\u0576",ss:"%d \u057e\u0561\u0575\u0580\u056f\u0575\u0561\u0576",m:"\u0580\u0578\u057a\u0565",mm:"%d \u0580\u0578\u057a\u0565",h:"\u056a\u0561\u0574",hh:"%d \u056a\u0561\u0574",d:"\u0585\u0580",dd:"%d \u0585\u0580",M:"\u0561\u0574\u056b\u057d",MM:"%d \u0561\u0574\u056b\u057d",y:"\u057f\u0561\u0580\u056b",yy:"%d \u057f\u0561\u0580\u056b"},meridiemParse:/\u0563\u056b\u0577\u0565\u0580\u057e\u0561|\u0561\u057c\u0561\u057e\u0578\u057f\u057e\u0561|\u0581\u0565\u0580\u0565\u056f\u057e\u0561|\u0565\u0580\u0565\u056f\u0578\u0575\u0561\u0576/,isPM:function(e){return/^(\u0581\u0565\u0580\u0565\u056f\u057e\u0561|\u0565\u0580\u0565\u056f\u0578\u0575\u0561\u0576)$/.test(e)},meridiem:function(e){return e<4?"\u0563\u056b\u0577\u0565\u0580\u057e\u0561":e<12?"\u0561\u057c\u0561\u057e\u0578\u057f\u057e\u0561":e<17?"\u0581\u0565\u0580\u0565\u056f\u057e\u0561":"\u0565\u0580\u0565\u056f\u0578\u0575\u0561\u0576"},dayOfMonthOrdinalParse:/\d{1,2}|\d{1,2}-(\u056b\u0576|\u0580\u0564)/,ordinal:function(e,a){switch(a){case"DDD":case"w":case"W":case"DDDo":return 1===e?e+"-\u056b\u0576":e+"-\u0580\u0564";default:return e}},week:{dow:1,doy:7}}),e.defineLocale("id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(e,a){return 12===e&&(e=0),"pagi"===a?e:"siang"===a?e>=11?e:e+12:"sore"===a||"malam"===a?e+12:void 0},meridiem:function(e,a,t){return e<11?"pagi":e<15?"siang":e<19?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",ss:"%d detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),e.defineLocale("is",{months:"jan\xfaar_febr\xfaar_mars_apr\xedl_ma\xed_j\xfan\xed_j\xfal\xed_\xe1g\xfast_september_okt\xf3ber_n\xf3vember_desember".split("_"),monthsShort:"jan_feb_mar_apr_ma\xed_j\xfan_j\xfal_\xe1g\xfa_sep_okt_n\xf3v_des".split("_"),weekdays:"sunnudagur_m\xe1nudagur_\xferi\xf0judagur_mi\xf0vikudagur_fimmtudagur_f\xf6studagur_laugardagur".split("_"),weekdaysShort:"sun_m\xe1n_\xferi_mi\xf0_fim_f\xf6s_lau".split("_"),weekdaysMin:"Su_M\xe1_\xder_Mi_Fi_F\xf6_La".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] H:mm",LLLL:"dddd, D. MMMM YYYY [kl.] H:mm"},calendar:{sameDay:"[\xed dag kl.] LT",nextDay:"[\xe1 morgun kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[\xed g\xe6r kl.] LT",lastWeek:"[s\xed\xf0asta] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"eftir %s",past:"fyrir %s s\xed\xf0an",s:Ma,ss:Ma,m:Ma,mm:Ma,h:"klukkustund",hh:Ma,d:Ma,dd:Ma,M:Ma,MM:Ma,y:Ma,yy:Ma},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"domenica_luned\xec_marted\xec_mercoled\xec_gioved\xec_venerd\xec_sabato".split("_"),weekdaysShort:"dom_lun_mar_mer_gio_ven_sab".split("_"),weekdaysMin:"do_lu_ma_me_gi_ve_sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(e){return(/^[0-9].+$/.test(e)?"tra":"in")+" "+e},past:"%s fa",s:"alcuni secondi",ss:"%d secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}}),e.defineLocale("ja",{months:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u65e5\u66dc\u65e5_\u6708\u66dc\u65e5_\u706b\u66dc\u65e5_\u6c34\u66dc\u65e5_\u6728\u66dc\u65e5_\u91d1\u66dc\u65e5_\u571f\u66dc\u65e5".split("_"),weekdaysShort:"\u65e5_\u6708_\u706b_\u6c34_\u6728_\u91d1_\u571f".split("_"),weekdaysMin:"\u65e5_\u6708_\u706b_\u6c34_\u6728_\u91d1_\u571f".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5 HH:mm",LLLL:"YYYY\u5e74M\u6708D\u65e5 HH:mm dddd",l:"YYYY/MM/DD",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5 HH:mm dddd"},meridiemParse:/\u5348\u524d|\u5348\u5f8c/i,isPM:function(e){return"\u5348\u5f8c"===e},meridiem:function(e,a,t){return e<12?"\u5348\u524d":"\u5348\u5f8c"},calendar:{sameDay:"[\u4eca\u65e5] LT",nextDay:"[\u660e\u65e5] LT",nextWeek:"[\u6765\u9031]dddd LT",lastDay:"[\u6628\u65e5] LT",lastWeek:"[\u524d\u9031]dddd LT",sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}\u65e5/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"\u65e5";default:return e}},relativeTime:{future:"%s\u5f8c",past:"%s\u524d",s:"\u6570\u79d2",ss:"%d\u79d2",m:"1\u5206",mm:"%d\u5206",h:"1\u6642\u9593",hh:"%d\u6642\u9593",d:"1\u65e5",dd:"%d\u65e5",M:"1\u30f6\u6708",MM:"%d\u30f6\u6708",y:"1\u5e74",yy:"%d\u5e74"}}),e.defineLocale("jv",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_Nopember_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nop_Des".split("_"),weekdays:"Minggu_Senen_Seloso_Rebu_Kemis_Jemuwah_Septu".split("_"),weekdaysShort:"Min_Sen_Sel_Reb_Kem_Jem_Sep".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sp".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/enjing|siyang|sonten|ndalu/,meridiemHour:function(e,a){return 12===e&&(e=0),"enjing"===a?e:"siyang"===a?e>=11?e:e+12:"sonten"===a||"ndalu"===a?e+12:void 0},meridiem:function(e,a,t){return e<11?"enjing":e<15?"siyang":e<19?"sonten":"ndalu"},calendar:{sameDay:"[Dinten puniko pukul] LT",nextDay:"[Mbenjang pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kala wingi pukul] LT",lastWeek:"dddd [kepengker pukul] LT",sameElse:"L"},relativeTime:{future:"wonten ing %s",past:"%s ingkang kepengker",s:"sawetawis detik",ss:"%d detik",m:"setunggal menit",mm:"%d menit",h:"setunggal jam",hh:"%d jam",d:"sedinten",dd:"%d dinten",M:"sewulan",MM:"%d wulan",y:"setaun",yy:"%d taun"},week:{dow:1,doy:7}}),e.defineLocale("ka",{months:{standalone:"\u10d8\u10d0\u10dc\u10d5\u10d0\u10e0\u10d8_\u10d7\u10d4\u10d1\u10d4\u10e0\u10d5\u10d0\u10da\u10d8_\u10db\u10d0\u10e0\u10e2\u10d8_\u10d0\u10de\u10e0\u10d8\u10da\u10d8_\u10db\u10d0\u10d8\u10e1\u10d8_\u10d8\u10d5\u10dc\u10d8\u10e1\u10d8_\u10d8\u10d5\u10da\u10d8\u10e1\u10d8_\u10d0\u10d2\u10d5\u10d8\u10e1\u10e2\u10dd_\u10e1\u10d4\u10e5\u10e2\u10d4\u10db\u10d1\u10d4\u10e0\u10d8_\u10dd\u10e5\u10e2\u10dd\u10db\u10d1\u10d4\u10e0\u10d8_\u10dc\u10dd\u10d4\u10db\u10d1\u10d4\u10e0\u10d8_\u10d3\u10d4\u10d9\u10d4\u10db\u10d1\u10d4\u10e0\u10d8".split("_"),format:"\u10d8\u10d0\u10dc\u10d5\u10d0\u10e0\u10e1_\u10d7\u10d4\u10d1\u10d4\u10e0\u10d5\u10d0\u10da\u10e1_\u10db\u10d0\u10e0\u10e2\u10e1_\u10d0\u10de\u10e0\u10d8\u10da\u10d8\u10e1_\u10db\u10d0\u10d8\u10e1\u10e1_\u10d8\u10d5\u10dc\u10d8\u10e1\u10e1_\u10d8\u10d5\u10da\u10d8\u10e1\u10e1_\u10d0\u10d2\u10d5\u10d8\u10e1\u10e2\u10e1_\u10e1\u10d4\u10e5\u10e2\u10d4\u10db\u10d1\u10d4\u10e0\u10e1_\u10dd\u10e5\u10e2\u10dd\u10db\u10d1\u10d4\u10e0\u10e1_\u10dc\u10dd\u10d4\u10db\u10d1\u10d4\u10e0\u10e1_\u10d3\u10d4\u10d9\u10d4\u10db\u10d1\u10d4\u10e0\u10e1".split("_")},monthsShort:"\u10d8\u10d0\u10dc_\u10d7\u10d4\u10d1_\u10db\u10d0\u10e0_\u10d0\u10de\u10e0_\u10db\u10d0\u10d8_\u10d8\u10d5\u10dc_\u10d8\u10d5\u10da_\u10d0\u10d2\u10d5_\u10e1\u10d4\u10e5_\u10dd\u10e5\u10e2_\u10dc\u10dd\u10d4_\u10d3\u10d4\u10d9".split("_"),weekdays:{standalone:"\u10d9\u10d5\u10d8\u10e0\u10d0_\u10dd\u10e0\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8_\u10e1\u10d0\u10db\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8_\u10dd\u10d7\u10ee\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8_\u10ee\u10e3\u10d7\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8_\u10de\u10d0\u10e0\u10d0\u10e1\u10d9\u10d4\u10d5\u10d8_\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8".split("_"),format:"\u10d9\u10d5\u10d8\u10e0\u10d0\u10e1_\u10dd\u10e0\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1_\u10e1\u10d0\u10db\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1_\u10dd\u10d7\u10ee\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1_\u10ee\u10e3\u10d7\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1_\u10de\u10d0\u10e0\u10d0\u10e1\u10d9\u10d4\u10d5\u10e1_\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1".split("_"),isFormat:/(\u10ec\u10d8\u10dc\u10d0|\u10e8\u10d4\u10db\u10d3\u10d4\u10d2)/},weekdaysShort:"\u10d9\u10d5\u10d8_\u10dd\u10e0\u10e8_\u10e1\u10d0\u10db_\u10dd\u10d7\u10ee_\u10ee\u10e3\u10d7_\u10de\u10d0\u10e0_\u10e8\u10d0\u10d1".split("_"),weekdaysMin:"\u10d9\u10d5_\u10dd\u10e0_\u10e1\u10d0_\u10dd\u10d7_\u10ee\u10e3_\u10de\u10d0_\u10e8\u10d0".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[\u10d3\u10e6\u10d4\u10e1] LT[-\u10d6\u10d4]",nextDay:"[\u10ee\u10d5\u10d0\u10da] LT[-\u10d6\u10d4]",lastDay:"[\u10d2\u10e3\u10e8\u10d8\u10dc] LT[-\u10d6\u10d4]",nextWeek:"[\u10e8\u10d4\u10db\u10d3\u10d4\u10d2] dddd LT[-\u10d6\u10d4]",lastWeek:"[\u10ec\u10d8\u10dc\u10d0] dddd LT-\u10d6\u10d4",sameElse:"L"},relativeTime:{future:function(e){return/(\u10ec\u10d0\u10db\u10d8|\u10ec\u10e3\u10d7\u10d8|\u10e1\u10d0\u10d0\u10d7\u10d8|\u10ec\u10d4\u10da\u10d8)/.test(e)?e.replace(/\u10d8$/,"\u10e8\u10d8"):e+"\u10e8\u10d8"},past:function(e){return/(\u10ec\u10d0\u10db\u10d8|\u10ec\u10e3\u10d7\u10d8|\u10e1\u10d0\u10d0\u10d7\u10d8|\u10d3\u10e6\u10d4|\u10d7\u10d5\u10d4)/.test(e)?e.replace(/(\u10d8|\u10d4)$/,"\u10d8\u10e1 \u10e3\u10d9\u10d0\u10dc"):/\u10ec\u10d4\u10da\u10d8/.test(e)?e.replace(/\u10ec\u10d4\u10da\u10d8$/,"\u10ec\u10da\u10d8\u10e1 \u10e3\u10d9\u10d0\u10dc"):void 0},s:"\u10e0\u10d0\u10db\u10d3\u10d4\u10dc\u10d8\u10db\u10d4 \u10ec\u10d0\u10db\u10d8",ss:"%d \u10ec\u10d0\u10db\u10d8",m:"\u10ec\u10e3\u10d7\u10d8",mm:"%d \u10ec\u10e3\u10d7\u10d8",h:"\u10e1\u10d0\u10d0\u10d7\u10d8",hh:"%d \u10e1\u10d0\u10d0\u10d7\u10d8",d:"\u10d3\u10e6\u10d4",dd:"%d \u10d3\u10e6\u10d4",M:"\u10d7\u10d5\u10d4",MM:"%d \u10d7\u10d5\u10d4",y:"\u10ec\u10d4\u10da\u10d8",yy:"%d \u10ec\u10d4\u10da\u10d8"},dayOfMonthOrdinalParse:/0|1-\u10da\u10d8|\u10db\u10d4-\d{1,2}|\d{1,2}-\u10d4/,ordinal:function(e){return 0===e?e:1===e?e+"-\u10da\u10d8":e<20||e<=100&&e%20==0||e%100==0?"\u10db\u10d4-"+e:e+"-\u10d4"},week:{dow:1,doy:7}});var yn={0:"-\u0448\u0456",1:"-\u0448\u0456",2:"-\u0448\u0456",3:"-\u0448\u0456",4:"-\u0448\u0456",5:"-\u0448\u0456",6:"-\u0448\u044b",7:"-\u0448\u0456",8:"-\u0448\u0456",9:"-\u0448\u044b",10:"-\u0448\u044b",20:"-\u0448\u044b",30:"-\u0448\u044b",40:"-\u0448\u044b",50:"-\u0448\u0456",60:"-\u0448\u044b",70:"-\u0448\u0456",80:"-\u0448\u0456",90:"-\u0448\u044b",100:"-\u0448\u0456"};e.defineLocale("kk",{months:"\u049b\u0430\u04a3\u0442\u0430\u0440_\u0430\u049b\u043f\u0430\u043d_\u043d\u0430\u0443\u0440\u044b\u0437_\u0441\u04d9\u0443\u0456\u0440_\u043c\u0430\u043c\u044b\u0440_\u043c\u0430\u0443\u0441\u044b\u043c_\u0448\u0456\u043b\u0434\u0435_\u0442\u0430\u043c\u044b\u0437_\u049b\u044b\u0440\u043a\u04af\u0439\u0435\u043a_\u049b\u0430\u0437\u0430\u043d_\u049b\u0430\u0440\u0430\u0448\u0430_\u0436\u0435\u043b\u0442\u043e\u049b\u0441\u0430\u043d".split("_"),monthsShort:"\u049b\u0430\u04a3_\u0430\u049b\u043f_\u043d\u0430\u0443_\u0441\u04d9\u0443_\u043c\u0430\u043c_\u043c\u0430\u0443_\u0448\u0456\u043b_\u0442\u0430\u043c_\u049b\u044b\u0440_\u049b\u0430\u0437_\u049b\u0430\u0440_\u0436\u0435\u043b".split("_"),weekdays:"\u0436\u0435\u043a\u0441\u0435\u043d\u0431\u0456_\u0434\u04af\u0439\u0441\u0435\u043d\u0431\u0456_\u0441\u0435\u0439\u0441\u0435\u043d\u0431\u0456_\u0441\u04d9\u0440\u0441\u0435\u043d\u0431\u0456_\u0431\u0435\u0439\u0441\u0435\u043d\u0431\u0456_\u0436\u04b1\u043c\u0430_\u0441\u0435\u043d\u0431\u0456".split("_"),weekdaysShort:"\u0436\u0435\u043a_\u0434\u04af\u0439_\u0441\u0435\u0439_\u0441\u04d9\u0440_\u0431\u0435\u0439_\u0436\u04b1\u043c_\u0441\u0435\u043d".split("_"),weekdaysMin:"\u0436\u043a_\u0434\u0439_\u0441\u0439_\u0441\u0440_\u0431\u0439_\u0436\u043c_\u0441\u043d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0411\u04af\u0433\u0456\u043d \u0441\u0430\u0493\u0430\u0442] LT",nextDay:"[\u0415\u0440\u0442\u0435\u04a3 \u0441\u0430\u0493\u0430\u0442] LT",nextWeek:"dddd [\u0441\u0430\u0493\u0430\u0442] LT",lastDay:"[\u041a\u0435\u0448\u0435 \u0441\u0430\u0493\u0430\u0442] LT",lastWeek:"[\u04e8\u0442\u043a\u0435\u043d \u0430\u043f\u0442\u0430\u043d\u044b\u04a3] dddd [\u0441\u0430\u0493\u0430\u0442] LT",sameElse:"L"},relativeTime:{future:"%s \u0456\u0448\u0456\u043d\u0434\u0435",past:"%s \u0431\u04b1\u0440\u044b\u043d",s:"\u0431\u0456\u0440\u043d\u0435\u0448\u0435 \u0441\u0435\u043a\u0443\u043d\u0434",ss:"%d \u0441\u0435\u043a\u0443\u043d\u0434",m:"\u0431\u0456\u0440 \u043c\u0438\u043d\u0443\u0442",mm:"%d \u043c\u0438\u043d\u0443\u0442",h:"\u0431\u0456\u0440 \u0441\u0430\u0493\u0430\u0442",hh:"%d \u0441\u0430\u0493\u0430\u0442",d:"\u0431\u0456\u0440 \u043a\u04af\u043d",dd:"%d \u043a\u04af\u043d",M:"\u0431\u0456\u0440 \u0430\u0439",MM:"%d \u0430\u0439",y:"\u0431\u0456\u0440 \u0436\u044b\u043b",yy:"%d \u0436\u044b\u043b"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0448\u0456|\u0448\u044b)/,ordinal:function(e){return e+(yn[e]||yn[e%10]||yn[e>=100?100:null])},week:{dow:1,doy:7}}),e.defineLocale("km",{months:"\u1798\u1780\u179a\u17b6_\u1780\u17bb\u1798\u17d2\u1797\u17c8_\u1798\u17b8\u1793\u17b6_\u1798\u17c1\u179f\u17b6_\u17a7\u179f\u1797\u17b6_\u1798\u17b7\u1790\u17bb\u1793\u17b6_\u1780\u1780\u17d2\u1780\u178a\u17b6_\u179f\u17b8\u17a0\u17b6_\u1780\u1789\u17d2\u1789\u17b6_\u178f\u17bb\u179b\u17b6_\u179c\u17b7\u1785\u17d2\u1786\u17b7\u1780\u17b6_\u1792\u17d2\u1793\u17bc".split("_"),monthsShort:"\u1798\u1780\u179a\u17b6_\u1780\u17bb\u1798\u17d2\u1797\u17c8_\u1798\u17b8\u1793\u17b6_\u1798\u17c1\u179f\u17b6_\u17a7\u179f\u1797\u17b6_\u1798\u17b7\u1790\u17bb\u1793\u17b6_\u1780\u1780\u17d2\u1780\u178a\u17b6_\u179f\u17b8\u17a0\u17b6_\u1780\u1789\u17d2\u1789\u17b6_\u178f\u17bb\u179b\u17b6_\u179c\u17b7\u1785\u17d2\u1786\u17b7\u1780\u17b6_\u1792\u17d2\u1793\u17bc".split("_"),weekdays:"\u17a2\u17b6\u1791\u17b7\u178f\u17d2\u1799_\u1785\u17d0\u1793\u17d2\u1791_\u17a2\u1784\u17d2\u1782\u17b6\u179a_\u1796\u17bb\u1792_\u1796\u17d2\u179a\u17a0\u179f\u17d2\u1794\u178f\u17b7\u17cd_\u179f\u17bb\u1780\u17d2\u179a_\u179f\u17c5\u179a\u17cd".split("_"),weekdaysShort:"\u17a2\u17b6\u1791\u17b7\u178f\u17d2\u1799_\u1785\u17d0\u1793\u17d2\u1791_\u17a2\u1784\u17d2\u1782\u17b6\u179a_\u1796\u17bb\u1792_\u1796\u17d2\u179a\u17a0\u179f\u17d2\u1794\u178f\u17b7\u17cd_\u179f\u17bb\u1780\u17d2\u179a_\u179f\u17c5\u179a\u17cd".split("_"),weekdaysMin:"\u17a2\u17b6\u1791\u17b7\u178f\u17d2\u1799_\u1785\u17d0\u1793\u17d2\u1791_\u17a2\u1784\u17d2\u1782\u17b6\u179a_\u1796\u17bb\u1792_\u1796\u17d2\u179a\u17a0\u179f\u17d2\u1794\u178f\u17b7\u17cd_\u179f\u17bb\u1780\u17d2\u179a_\u179f\u17c5\u179a\u17cd".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u1790\u17d2\u1784\u17c3\u1793\u17c1\u17c7 \u1798\u17c9\u17c4\u1784] LT",nextDay:"[\u179f\u17d2\u17a2\u17c2\u1780 \u1798\u17c9\u17c4\u1784] LT",nextWeek:"dddd [\u1798\u17c9\u17c4\u1784] LT",lastDay:"[\u1798\u17d2\u179f\u17b7\u179b\u1798\u17b7\u1789 \u1798\u17c9\u17c4\u1784] LT",lastWeek:"dddd [\u179f\u1794\u17d2\u178f\u17b6\u17a0\u17cd\u1798\u17bb\u1793] [\u1798\u17c9\u17c4\u1784] LT",sameElse:"L"},relativeTime:{future:"%s\u1791\u17c0\u178f",past:"%s\u1798\u17bb\u1793",s:"\u1794\u17c9\u17bb\u1793\u17d2\u1798\u17b6\u1793\u179c\u17b7\u1793\u17b6\u1791\u17b8",ss:"%d \u179c\u17b7\u1793\u17b6\u1791\u17b8",m:"\u1798\u17bd\u1799\u1793\u17b6\u1791\u17b8",mm:"%d \u1793\u17b6\u1791\u17b8",h:"\u1798\u17bd\u1799\u1798\u17c9\u17c4\u1784",hh:"%d \u1798\u17c9\u17c4\u1784",d:"\u1798\u17bd\u1799\u1790\u17d2\u1784\u17c3",dd:"%d \u1790\u17d2\u1784\u17c3",M:"\u1798\u17bd\u1799\u1781\u17c2",MM:"%d \u1781\u17c2",y:"\u1798\u17bd\u1799\u1786\u17d2\u1793\u17b6\u17c6",yy:"%d \u1786\u17d2\u1793\u17b6\u17c6"},week:{dow:1,doy:4}});var fn={1:"\u0ce7",2:"\u0ce8",3:"\u0ce9",4:"\u0cea",5:"\u0ceb",6:"\u0cec",7:"\u0ced",8:"\u0cee",9:"\u0cef",0:"\u0ce6"},kn={"\u0ce7":"1","\u0ce8":"2","\u0ce9":"3","\u0cea":"4","\u0ceb":"5","\u0cec":"6","\u0ced":"7","\u0cee":"8","\u0cef":"9","\u0ce6":"0"};e.defineLocale("kn",{months:"\u0c9c\u0ca8\u0cb5\u0cb0\u0cbf_\u0cab\u0cc6\u0cac\u0ccd\u0cb0\u0cb5\u0cb0\u0cbf_\u0cae\u0cbe\u0cb0\u0ccd\u0c9a\u0ccd_\u0c8f\u0caa\u0ccd\u0cb0\u0cbf\u0cb2\u0ccd_\u0cae\u0cc6\u0cd5_\u0c9c\u0cc2\u0ca8\u0ccd_\u0c9c\u0cc1\u0cb2\u0cc6\u0cd6_\u0c86\u0c97\u0cb8\u0ccd\u0c9f\u0ccd_\u0cb8\u0cc6\u0caa\u0ccd\u0c9f\u0cc6\u0c82\u0cac\u0cb0\u0ccd_\u0c85\u0c95\u0ccd\u0c9f\u0cc6\u0cc2\u0cd5\u0cac\u0cb0\u0ccd_\u0ca8\u0cb5\u0cc6\u0c82\u0cac\u0cb0\u0ccd_\u0ca1\u0cbf\u0cb8\u0cc6\u0c82\u0cac\u0cb0\u0ccd".split("_"),monthsShort:"\u0c9c\u0ca8_\u0cab\u0cc6\u0cac\u0ccd\u0cb0_\u0cae\u0cbe\u0cb0\u0ccd\u0c9a\u0ccd_\u0c8f\u0caa\u0ccd\u0cb0\u0cbf\u0cb2\u0ccd_\u0cae\u0cc6\u0cd5_\u0c9c\u0cc2\u0ca8\u0ccd_\u0c9c\u0cc1\u0cb2\u0cc6\u0cd6_\u0c86\u0c97\u0cb8\u0ccd\u0c9f\u0ccd_\u0cb8\u0cc6\u0caa\u0ccd\u0c9f\u0cc6\u0c82\u0cac_\u0c85\u0c95\u0ccd\u0c9f\u0cc6\u0cc2\u0cd5\u0cac_\u0ca8\u0cb5\u0cc6\u0c82\u0cac_\u0ca1\u0cbf\u0cb8\u0cc6\u0c82\u0cac".split("_"),monthsParseExact:!0,weekdays:"\u0cad\u0cbe\u0ca8\u0cc1\u0cb5\u0cbe\u0cb0_\u0cb8\u0cc6\u0cc2\u0cd5\u0cae\u0cb5\u0cbe\u0cb0_\u0cae\u0c82\u0c97\u0cb3\u0cb5\u0cbe\u0cb0_\u0cac\u0cc1\u0ca7\u0cb5\u0cbe\u0cb0_\u0c97\u0cc1\u0cb0\u0cc1\u0cb5\u0cbe\u0cb0_\u0cb6\u0cc1\u0c95\u0ccd\u0cb0\u0cb5\u0cbe\u0cb0_\u0cb6\u0ca8\u0cbf\u0cb5\u0cbe\u0cb0".split("_"),weekdaysShort:"\u0cad\u0cbe\u0ca8\u0cc1_\u0cb8\u0cc6\u0cc2\u0cd5\u0cae_\u0cae\u0c82\u0c97\u0cb3_\u0cac\u0cc1\u0ca7_\u0c97\u0cc1\u0cb0\u0cc1_\u0cb6\u0cc1\u0c95\u0ccd\u0cb0_\u0cb6\u0ca8\u0cbf".split("_"),weekdaysMin:"\u0cad\u0cbe_\u0cb8\u0cc6\u0cc2\u0cd5_\u0cae\u0c82_\u0cac\u0cc1_\u0c97\u0cc1_\u0cb6\u0cc1_\u0cb6".split("_"),longDateFormat:{LT:"A h:mm",LTS:"A h:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm",LLLL:"dddd, D MMMM YYYY, A h:mm"},calendar:{sameDay:"[\u0c87\u0c82\u0ca6\u0cc1] LT",nextDay:"[\u0ca8\u0cbe\u0cb3\u0cc6] LT",nextWeek:"dddd, LT",lastDay:"[\u0ca8\u0cbf\u0ca8\u0ccd\u0ca8\u0cc6] LT",lastWeek:"[\u0c95\u0cc6\u0cc2\u0ca8\u0cc6\u0caf] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0ca8\u0c82\u0ca4\u0cb0",past:"%s \u0cb9\u0cbf\u0c82\u0ca6\u0cc6",s:"\u0c95\u0cc6\u0cb2\u0cb5\u0cc1 \u0c95\u0ccd\u0cb7\u0ca3\u0c97\u0cb3\u0cc1",ss:"%d \u0cb8\u0cc6\u0c95\u0cc6\u0c82\u0ca1\u0cc1\u0c97\u0cb3\u0cc1",m:"\u0c92\u0c82\u0ca6\u0cc1 \u0ca8\u0cbf\u0cae\u0cbf\u0cb7",mm:"%d \u0ca8\u0cbf\u0cae\u0cbf\u0cb7",h:"\u0c92\u0c82\u0ca6\u0cc1 \u0c97\u0c82\u0c9f\u0cc6",hh:"%d \u0c97\u0c82\u0c9f\u0cc6",d:"\u0c92\u0c82\u0ca6\u0cc1 \u0ca6\u0cbf\u0ca8",dd:"%d \u0ca6\u0cbf\u0ca8",M:"\u0c92\u0c82\u0ca6\u0cc1 \u0ca4\u0cbf\u0c82\u0c97\u0cb3\u0cc1",MM:"%d \u0ca4\u0cbf\u0c82\u0c97\u0cb3\u0cc1",y:"\u0c92\u0c82\u0ca6\u0cc1 \u0cb5\u0cb0\u0ccd\u0cb7",yy:"%d \u0cb5\u0cb0\u0ccd\u0cb7"},preparse:function(e){return e.replace(/[\u0ce7\u0ce8\u0ce9\u0cea\u0ceb\u0cec\u0ced\u0cee\u0cef\u0ce6]/g,function(e){return kn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return fn[e]})},meridiemParse:/\u0cb0\u0cbe\u0ca4\u0ccd\u0cb0\u0cbf|\u0cac\u0cc6\u0cb3\u0cbf\u0c97\u0ccd\u0c97\u0cc6|\u0cae\u0ca7\u0ccd\u0caf\u0cbe\u0cb9\u0ccd\u0ca8|\u0cb8\u0c82\u0c9c\u0cc6/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0cb0\u0cbe\u0ca4\u0ccd\u0cb0\u0cbf"===a?e<4?e:e+12:"\u0cac\u0cc6\u0cb3\u0cbf\u0c97\u0ccd\u0c97\u0cc6"===a?e:"\u0cae\u0ca7\u0ccd\u0caf\u0cbe\u0cb9\u0ccd\u0ca8"===a?e>=10?e:e+12:"\u0cb8\u0c82\u0c9c\u0cc6"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"\u0cb0\u0cbe\u0ca4\u0ccd\u0cb0\u0cbf":e<10?"\u0cac\u0cc6\u0cb3\u0cbf\u0c97\u0ccd\u0c97\u0cc6":e<17?"\u0cae\u0ca7\u0ccd\u0caf\u0cbe\u0cb9\u0ccd\u0ca8":e<20?"\u0cb8\u0c82\u0c9c\u0cc6":"\u0cb0\u0cbe\u0ca4\u0ccd\u0cb0\u0cbf"},dayOfMonthOrdinalParse:/\d{1,2}(\u0ca8\u0cc6\u0cd5)/,ordinal:function(e){return e+"\u0ca8\u0cc6\u0cd5"},week:{dow:0,doy:6}}),e.defineLocale("ko",{months:"1\uc6d4_2\uc6d4_3\uc6d4_4\uc6d4_5\uc6d4_6\uc6d4_7\uc6d4_8\uc6d4_9\uc6d4_10\uc6d4_11\uc6d4_12\uc6d4".split("_"),monthsShort:"1\uc6d4_2\uc6d4_3\uc6d4_4\uc6d4_5\uc6d4_6\uc6d4_7\uc6d4_8\uc6d4_9\uc6d4_10\uc6d4_11\uc6d4_12\uc6d4".split("_"),weekdays:"\uc77c\uc694\uc77c_\uc6d4\uc694\uc77c_\ud654\uc694\uc77c_\uc218\uc694\uc77c_\ubaa9\uc694\uc77c_\uae08\uc694\uc77c_\ud1a0\uc694\uc77c".split("_"),weekdaysShort:"\uc77c_\uc6d4_\ud654_\uc218_\ubaa9_\uae08_\ud1a0".split("_"),weekdaysMin:"\uc77c_\uc6d4_\ud654_\uc218_\ubaa9_\uae08_\ud1a0".split("_"),longDateFormat:{LT:"A h:mm",LTS:"A h:mm:ss",L:"YYYY.MM.DD",LL:"YYYY\ub144 MMMM D\uc77c",LLL:"YYYY\ub144 MMMM D\uc77c A h:mm",LLLL:"YYYY\ub144 MMMM D\uc77c dddd A h:mm",l:"YYYY.MM.DD",ll:"YYYY\ub144 MMMM D\uc77c",lll:"YYYY\ub144 MMMM D\uc77c A h:mm",llll:"YYYY\ub144 MMMM D\uc77c dddd A h:mm"},calendar:{sameDay:"\uc624\ub298 LT",nextDay:"\ub0b4\uc77c LT",nextWeek:"dddd LT",lastDay:"\uc5b4\uc81c LT",lastWeek:"\uc9c0\ub09c\uc8fc dddd LT",sameElse:"L"},relativeTime:{future:"%s \ud6c4",past:"%s \uc804",s:"\uba87 \ucd08",ss:"%d\ucd08",m:"1\ubd84",mm:"%d\ubd84",h:"\ud55c \uc2dc\uac04",hh:"%d\uc2dc\uac04",d:"\ud558\ub8e8",dd:"%d\uc77c",M:"\ud55c \ub2ec",MM:"%d\ub2ec",y:"\uc77c \ub144",yy:"%d\ub144"},dayOfMonthOrdinalParse:/\d{1,2}(\uc77c|\uc6d4|\uc8fc)/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"\uc77c";case"M":return e+"\uc6d4";case"w":case"W":return e+"\uc8fc";default:return e}},meridiemParse:/\uc624\uc804|\uc624\ud6c4/,isPM:function(e){return"\uc624\ud6c4"===e},meridiem:function(e,a,t){return e<12?"\uc624\uc804":"\uc624\ud6c4"}});var pn={0:"-\u0447\u04af",1:"-\u0447\u0438",2:"-\u0447\u0438",3:"-\u0447\u04af",4:"-\u0447\u04af",5:"-\u0447\u0438",6:"-\u0447\u044b",7:"-\u0447\u0438",8:"-\u0447\u0438",9:"-\u0447\u0443",10:"-\u0447\u0443",20:"-\u0447\u044b",30:"-\u0447\u0443",40:"-\u0447\u044b",50:"-\u0447\u04af",60:"-\u0447\u044b",70:"-\u0447\u0438",80:"-\u0447\u0438",90:"-\u0447\u0443",100:"-\u0447\u04af"};e.defineLocale("ky",{months:"\u044f\u043d\u0432\u0430\u0440\u044c_\u0444\u0435\u0432\u0440\u0430\u043b\u044c_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0435\u043b\u044c_\u043c\u0430\u0439_\u0438\u044e\u043d\u044c_\u0438\u044e\u043b\u044c_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c_\u043e\u043a\u0442\u044f\u0431\u0440\u044c_\u043d\u043e\u044f\u0431\u0440\u044c_\u0434\u0435\u043a\u0430\u0431\u0440\u044c".split("_"),monthsShort:"\u044f\u043d\u0432_\u0444\u0435\u0432_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440_\u043c\u0430\u0439_\u0438\u044e\u043d\u044c_\u0438\u044e\u043b\u044c_\u0430\u0432\u0433_\u0441\u0435\u043d_\u043e\u043a\u0442_\u043d\u043e\u044f_\u0434\u0435\u043a".split("_"),weekdays:"\u0416\u0435\u043a\u0448\u0435\u043c\u0431\u0438_\u0414\u04af\u0439\u0448\u04e9\u043c\u0431\u04af_\u0428\u0435\u0439\u0448\u0435\u043c\u0431\u0438_\u0428\u0430\u0440\u0448\u0435\u043c\u0431\u0438_\u0411\u0435\u0439\u0448\u0435\u043c\u0431\u0438_\u0416\u0443\u043c\u0430_\u0418\u0448\u0435\u043c\u0431\u0438".split("_"),weekdaysShort:"\u0416\u0435\u043a_\u0414\u04af\u0439_\u0428\u0435\u0439_\u0428\u0430\u0440_\u0411\u0435\u0439_\u0416\u0443\u043c_\u0418\u0448\u0435".split("_"),weekdaysMin:"\u0416\u043a_\u0414\u0439_\u0428\u0439_\u0428\u0440_\u0411\u0439_\u0416\u043c_\u0418\u0448".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0411\u04af\u0433\u04af\u043d \u0441\u0430\u0430\u0442] LT",nextDay:"[\u042d\u0440\u0442\u0435\u04a3 \u0441\u0430\u0430\u0442] LT",nextWeek:"dddd [\u0441\u0430\u0430\u0442] LT",lastDay:"[\u041a\u0435\u0447\u0435 \u0441\u0430\u0430\u0442] LT",lastWeek:"[\u04e8\u0442\u043a\u0435\u043d \u0430\u043f\u0442\u0430\u043d\u044b\u043d] dddd [\u043a\u04af\u043d\u04af] [\u0441\u0430\u0430\u0442] LT",sameElse:"L"},relativeTime:{future:"%s \u0438\u0447\u0438\u043d\u0434\u0435",past:"%s \u043c\u0443\u0440\u0443\u043d",s:"\u0431\u0438\u0440\u043d\u0435\u0447\u0435 \u0441\u0435\u043a\u0443\u043d\u0434",ss:"%d \u0441\u0435\u043a\u0443\u043d\u0434",m:"\u0431\u0438\u0440 \u043c\u04af\u043d\u04e9\u0442",mm:"%d \u043c\u04af\u043d\u04e9\u0442",h:"\u0431\u0438\u0440 \u0441\u0430\u0430\u0442",hh:"%d \u0441\u0430\u0430\u0442",d:"\u0431\u0438\u0440 \u043a\u04af\u043d",dd:"%d \u043a\u04af\u043d",M:"\u0431\u0438\u0440 \u0430\u0439",MM:"%d \u0430\u0439",y:"\u0431\u0438\u0440 \u0436\u044b\u043b",yy:"%d \u0436\u044b\u043b"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0447\u0438|\u0447\u044b|\u0447\u04af|\u0447\u0443)/,ordinal:function(e){return e+(pn[e]||pn[e%10]||pn[e>=100?100:null])},week:{dow:1,doy:7}}),e.defineLocale("lb",{months:"Januar_Februar_M\xe4erz_Abr\xebll_Mee_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Abr._Mee_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonndeg_M\xe9indeg_D\xebnschdeg_M\xebttwoch_Donneschdeg_Freideg_Samschdeg".split("_"),weekdaysShort:"So._M\xe9._D\xeb._M\xeb._Do._Fr._Sa.".split("_"),weekdaysMin:"So_M\xe9_D\xeb_M\xeb_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm [Auer]",LTS:"H:mm:ss [Auer]",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm [Auer]",LLLL:"dddd, D. MMMM YYYY H:mm [Auer]"},calendar:{sameDay:"[Haut um] LT",sameElse:"L",nextDay:"[Muer um] LT",nextWeek:"dddd [um] LT",lastDay:"[G\xebschter um] LT",lastWeek:function(){switch(this.day()){case 2:case 4:return"[Leschten] dddd [um] LT";default:return"[Leschte] dddd [um] LT"}}},relativeTime:{future:function(e){return La(e.substr(0,e.indexOf(" ")))?"a "+e:"an "+e},past:function(e){return La(e.substr(0,e.indexOf(" ")))?"viru "+e:"virun "+e},s:"e puer Sekonnen",ss:"%d Sekonnen",m:ha,mm:"%d Minutten",h:ha,hh:"%d Stonnen",d:ha,dd:"%d Deeg",M:ha,MM:"%d M\xe9int",y:ha,yy:"%d Joer"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("lo",{months:"\u0ea1\u0eb1\u0e87\u0e81\u0ead\u0e99_\u0e81\u0eb8\u0ea1\u0e9e\u0eb2_\u0ea1\u0eb5\u0e99\u0eb2_\u0ec0\u0ea1\u0eaa\u0eb2_\u0e9e\u0eb6\u0e94\u0eaa\u0eb0\u0e9e\u0eb2_\u0ea1\u0eb4\u0e96\u0eb8\u0e99\u0eb2_\u0e81\u0ecd\u0ea5\u0eb0\u0e81\u0ebb\u0e94_\u0eaa\u0eb4\u0e87\u0eab\u0eb2_\u0e81\u0eb1\u0e99\u0e8d\u0eb2_\u0e95\u0eb8\u0ea5\u0eb2_\u0e9e\u0eb0\u0e88\u0eb4\u0e81_\u0e97\u0eb1\u0e99\u0ea7\u0eb2".split("_"),monthsShort:"\u0ea1\u0eb1\u0e87\u0e81\u0ead\u0e99_\u0e81\u0eb8\u0ea1\u0e9e\u0eb2_\u0ea1\u0eb5\u0e99\u0eb2_\u0ec0\u0ea1\u0eaa\u0eb2_\u0e9e\u0eb6\u0e94\u0eaa\u0eb0\u0e9e\u0eb2_\u0ea1\u0eb4\u0e96\u0eb8\u0e99\u0eb2_\u0e81\u0ecd\u0ea5\u0eb0\u0e81\u0ebb\u0e94_\u0eaa\u0eb4\u0e87\u0eab\u0eb2_\u0e81\u0eb1\u0e99\u0e8d\u0eb2_\u0e95\u0eb8\u0ea5\u0eb2_\u0e9e\u0eb0\u0e88\u0eb4\u0e81_\u0e97\u0eb1\u0e99\u0ea7\u0eb2".split("_"),weekdays:"\u0ead\u0eb2\u0e97\u0eb4\u0e94_\u0e88\u0eb1\u0e99_\u0ead\u0eb1\u0e87\u0e84\u0eb2\u0e99_\u0e9e\u0eb8\u0e94_\u0e9e\u0eb0\u0eab\u0eb1\u0e94_\u0eaa\u0eb8\u0e81_\u0ec0\u0eaa\u0ebb\u0eb2".split("_"),weekdaysShort:"\u0e97\u0eb4\u0e94_\u0e88\u0eb1\u0e99_\u0ead\u0eb1\u0e87\u0e84\u0eb2\u0e99_\u0e9e\u0eb8\u0e94_\u0e9e\u0eb0\u0eab\u0eb1\u0e94_\u0eaa\u0eb8\u0e81_\u0ec0\u0eaa\u0ebb\u0eb2".split("_"),weekdaysMin:"\u0e97_\u0e88_\u0ead\u0e84_\u0e9e_\u0e9e\u0eab_\u0eaa\u0e81_\u0eaa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"\u0ea7\u0eb1\u0e99dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0e95\u0ead\u0e99\u0ec0\u0e8a\u0ebb\u0ec9\u0eb2|\u0e95\u0ead\u0e99\u0ec1\u0ea5\u0e87/,isPM:function(e){return"\u0e95\u0ead\u0e99\u0ec1\u0ea5\u0e87"===e},meridiem:function(e,a,t){return e<12?"\u0e95\u0ead\u0e99\u0ec0\u0e8a\u0ebb\u0ec9\u0eb2":"\u0e95\u0ead\u0e99\u0ec1\u0ea5\u0e87"},calendar:{sameDay:"[\u0ea1\u0eb7\u0ec9\u0e99\u0eb5\u0ec9\u0ec0\u0ea7\u0ea5\u0eb2] LT",nextDay:"[\u0ea1\u0eb7\u0ec9\u0ead\u0eb7\u0ec8\u0e99\u0ec0\u0ea7\u0ea5\u0eb2] LT",nextWeek:"[\u0ea7\u0eb1\u0e99]dddd[\u0edc\u0ec9\u0eb2\u0ec0\u0ea7\u0ea5\u0eb2] LT",lastDay:"[\u0ea1\u0eb7\u0ec9\u0ea7\u0eb2\u0e99\u0e99\u0eb5\u0ec9\u0ec0\u0ea7\u0ea5\u0eb2] LT",lastWeek:"[\u0ea7\u0eb1\u0e99]dddd[\u0ec1\u0ea5\u0ec9\u0ea7\u0e99\u0eb5\u0ec9\u0ec0\u0ea7\u0ea5\u0eb2] LT",sameElse:"L"},relativeTime:{future:"\u0ead\u0eb5\u0e81 %s",past:"%s\u0e9c\u0ec8\u0eb2\u0e99\u0ea1\u0eb2",s:"\u0e9a\u0ecd\u0ec8\u0ec0\u0e97\u0ebb\u0ec8\u0eb2\u0ec3\u0e94\u0ea7\u0eb4\u0e99\u0eb2\u0e97\u0eb5",ss:"%d \u0ea7\u0eb4\u0e99\u0eb2\u0e97\u0eb5",m:"1 \u0e99\u0eb2\u0e97\u0eb5",mm:"%d \u0e99\u0eb2\u0e97\u0eb5",h:"1 \u0e8a\u0ebb\u0ec8\u0ea7\u0ec2\u0ea1\u0e87",hh:"%d \u0e8a\u0ebb\u0ec8\u0ea7\u0ec2\u0ea1\u0e87",d:"1 \u0ea1\u0eb7\u0ec9",dd:"%d \u0ea1\u0eb7\u0ec9",M:"1 \u0ec0\u0e94\u0eb7\u0ead\u0e99",MM:"%d \u0ec0\u0e94\u0eb7\u0ead\u0e99",y:"1 \u0e9b\u0eb5",yy:"%d \u0e9b\u0eb5"},dayOfMonthOrdinalParse:/(\u0e97\u0eb5\u0ec8)\d{1,2}/,ordinal:function(e){return"\u0e97\u0eb5\u0ec8"+e}});var Dn={ss:"sekund\u0117_sekund\u017ei\u0173_sekundes",m:"minut\u0117_minut\u0117s_minut\u0119",mm:"minut\u0117s_minu\u010di\u0173_minutes",h:"valanda_valandos_valand\u0105",hh:"valandos_valand\u0173_valandas",d:"diena_dienos_dien\u0105",dd:"dienos_dien\u0173_dienas",M:"m\u0117nuo_m\u0117nesio_m\u0117nes\u012f",MM:"m\u0117nesiai_m\u0117nesi\u0173_m\u0117nesius",y:"metai_met\u0173_metus",yy:"metai_met\u0173_metus"};e.defineLocale("lt",{months:{format:"sausio_vasario_kovo_baland\u017eio_gegu\u017e\u0117s_bir\u017eelio_liepos_rugpj\u016b\u010dio_rugs\u0117jo_spalio_lapkri\u010dio_gruod\u017eio".split("_"),standalone:"sausis_vasaris_kovas_balandis_gegu\u017e\u0117_bir\u017eelis_liepa_rugpj\u016btis_rugs\u0117jis_spalis_lapkritis_gruodis".split("_"),isFormat:/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?|MMMM?(\[[^\[\]]*\]|\s)+D[oD]?/},monthsShort:"sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd".split("_"),weekdays:{format:"sekmadien\u012f_pirmadien\u012f_antradien\u012f_tre\u010diadien\u012f_ketvirtadien\u012f_penktadien\u012f_\u0161e\u0161tadien\u012f".split("_"),standalone:"sekmadienis_pirmadienis_antradienis_tre\u010diadienis_ketvirtadienis_penktadienis_\u0161e\u0161tadienis".split("_"),isFormat:/dddd HH:mm/},weekdaysShort:"Sek_Pir_Ant_Tre_Ket_Pen_\u0160e\u0161".split("_"),weekdaysMin:"S_P_A_T_K_Pn_\u0160".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY [m.] MMMM D [d.]",LLL:"YYYY [m.] MMMM D [d.], HH:mm [val.]",LLLL:"YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]",l:"YYYY-MM-DD",ll:"YYYY [m.] MMMM D [d.]",lll:"YYYY [m.] MMMM D [d.], HH:mm [val.]",llll:"YYYY [m.] MMMM D [d.], ddd, HH:mm [val.]"},calendar:{sameDay:"[\u0160iandien] LT",nextDay:"[Rytoj] LT",nextWeek:"dddd LT",lastDay:"[Vakar] LT",lastWeek:"[Pra\u0117jus\u012f] dddd LT",sameElse:"L"},relativeTime:{future:"po %s",past:"prie\u0161 %s",s:function(e,a,t,s){return a?"kelios sekund\u0117s":s?"keli\u0173 sekund\u017ei\u0173":"kelias sekundes"},ss:fa,m:ca,mm:fa,h:ca,hh:fa,d:ca,dd:fa,M:ca,MM:fa,y:ca,yy:fa},dayOfMonthOrdinalParse:/\d{1,2}-oji/,ordinal:function(e){return e+"-oji"},week:{dow:1,doy:4}});var Tn={ss:"sekundes_sekund\u0113m_sekunde_sekundes".split("_"),m:"min\u016btes_min\u016bt\u0113m_min\u016bte_min\u016btes".split("_"),mm:"min\u016btes_min\u016bt\u0113m_min\u016bte_min\u016btes".split("_"),h:"stundas_stund\u0101m_stunda_stundas".split("_"),hh:"stundas_stund\u0101m_stunda_stundas".split("_"),d:"dienas_dien\u0101m_diena_dienas".split("_"),dd:"dienas_dien\u0101m_diena_dienas".split("_"),M:"m\u0113ne\u0161a_m\u0113ne\u0161iem_m\u0113nesis_m\u0113ne\u0161i".split("_"),MM:"m\u0113ne\u0161a_m\u0113ne\u0161iem_m\u0113nesis_m\u0113ne\u0161i".split("_"),y:"gada_gadiem_gads_gadi".split("_"),yy:"gada_gadiem_gads_gadi".split("_")};e.defineLocale("lv",{months:"janv\u0101ris_febru\u0101ris_marts_apr\u012blis_maijs_j\u016bnijs_j\u016blijs_augusts_septembris_oktobris_novembris_decembris".split("_"),monthsShort:"jan_feb_mar_apr_mai_j\u016bn_j\u016bl_aug_sep_okt_nov_dec".split("_"),weekdays:"sv\u0113tdiena_pirmdiena_otrdiena_tre\u0161diena_ceturtdiena_piektdiena_sestdiena".split("_"),weekdaysShort:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysMin:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY.",LL:"YYYY. [gada] D. MMMM",LLL:"YYYY. [gada] D. MMMM, HH:mm",LLLL:"YYYY. [gada] D. MMMM, dddd, HH:mm"},calendar:{sameDay:"[\u0160odien pulksten] LT",nextDay:"[R\u012bt pulksten] LT",nextWeek:"dddd [pulksten] LT",lastDay:"[Vakar pulksten] LT",lastWeek:"[Pag\u0101ju\u0161\u0101] dddd [pulksten] LT",sameElse:"L"},relativeTime:{future:"p\u0113c %s",past:"pirms %s",s:function(e,a){return a?"da\u017eas sekundes":"da\u017e\u0101m sekund\u0113m"},ss:pa,m:Da,mm:pa,h:Da,hh:pa,d:Da,dd:pa,M:Da,MM:pa,y:Da,yy:pa},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});var gn={words:{ss:["sekund","sekunda","sekundi"],m:["jedan minut","jednog minuta"],mm:["minut","minuta","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mjesec","mjeseca","mjeseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(e,a){return 1===e?a[0]:e>=2&&e<=4?a[1]:a[2]},translate:function(e,a,t){var s=gn.words[t];return 1===t.length?a?s[0]:s[1]:e+" "+gn.correctGrammaticalCase(e,s)}};e.defineLocale("me",{months:"januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedjelja_ponedjeljak_utorak_srijeda_\u010detvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._\u010det._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_\u010de_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sjutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[ju\u010de u] LT",lastWeek:function(){return["[pro\u0161le] [nedjelje] [u] LT","[pro\u0161log] [ponedjeljka] [u] LT","[pro\u0161log] [utorka] [u] LT","[pro\u0161le] [srijede] [u] LT","[pro\u0161log] [\u010detvrtka] [u] LT","[pro\u0161log] [petka] [u] LT","[pro\u0161le] [subote] [u] LT"][this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"nekoliko sekundi",ss:gn.translate,m:gn.translate,mm:gn.translate,h:gn.translate,hh:gn.translate,d:"dan",dd:gn.translate,M:"mjesec",MM:gn.translate,y:"godinu",yy:gn.translate},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),e.defineLocale("mi",{months:"Kohi-t\u0101te_Hui-tanguru_Pout\u016b-te-rangi_Paenga-wh\u0101wh\u0101_Haratua_Pipiri_H\u014dngoingoi_Here-turi-k\u014dk\u0101_Mahuru_Whiringa-\u0101-nuku_Whiringa-\u0101-rangi_Hakihea".split("_"),monthsShort:"Kohi_Hui_Pou_Pae_Hara_Pipi_H\u014dngoi_Here_Mahu_Whi-nu_Whi-ra_Haki".split("_"),monthsRegex:/(?:['a-z\u0101\u014D\u016B]+\-?){1,3}/i,monthsStrictRegex:/(?:['a-z\u0101\u014D\u016B]+\-?){1,3}/i,monthsShortRegex:/(?:['a-z\u0101\u014D\u016B]+\-?){1,3}/i,monthsShortStrictRegex:/(?:['a-z\u0101\u014D\u016B]+\-?){1,2}/i,weekdays:"R\u0101tapu_Mane_T\u016brei_Wenerei_T\u0101ite_Paraire_H\u0101tarei".split("_"),weekdaysShort:"Ta_Ma_T\u016b_We_T\u0101i_Pa_H\u0101".split("_"),weekdaysMin:"Ta_Ma_T\u016b_We_T\u0101i_Pa_H\u0101".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [i] HH:mm",LLLL:"dddd, D MMMM YYYY [i] HH:mm"},calendar:{sameDay:"[i teie mahana, i] LT",nextDay:"[apopo i] LT",nextWeek:"dddd [i] LT",lastDay:"[inanahi i] LT",lastWeek:"dddd [whakamutunga i] LT",sameElse:"L"},relativeTime:{future:"i roto i %s",past:"%s i mua",s:"te h\u0113kona ruarua",ss:"%d h\u0113kona",m:"he meneti",mm:"%d meneti",h:"te haora",hh:"%d haora",d:"he ra",dd:"%d ra",M:"he marama",MM:"%d marama",y:"he tau",yy:"%d tau"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}}),e.defineLocale("mk",{months:"\u0458\u0430\u043d\u0443\u0430\u0440\u0438_\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0438\u043b_\u043c\u0430\u0458_\u0458\u0443\u043d\u0438_\u0458\u0443\u043b\u0438_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438_\u043e\u043a\u0442\u043e\u043c\u0432\u0440\u0438_\u043d\u043e\u0435\u043c\u0432\u0440\u0438_\u0434\u0435\u043a\u0435\u043c\u0432\u0440\u0438".split("_"),monthsShort:"\u0458\u0430\u043d_\u0444\u0435\u0432_\u043c\u0430\u0440_\u0430\u043f\u0440_\u043c\u0430\u0458_\u0458\u0443\u043d_\u0458\u0443\u043b_\u0430\u0432\u0433_\u0441\u0435\u043f_\u043e\u043a\u0442_\u043d\u043e\u0435_\u0434\u0435\u043a".split("_"),weekdays:"\u043d\u0435\u0434\u0435\u043b\u0430_\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a_\u0432\u0442\u043e\u0440\u043d\u0438\u043a_\u0441\u0440\u0435\u0434\u0430_\u0447\u0435\u0442\u0432\u0440\u0442\u043e\u043a_\u043f\u0435\u0442\u043e\u043a_\u0441\u0430\u0431\u043e\u0442\u0430".split("_"),weekdaysShort:"\u043d\u0435\u0434_\u043f\u043e\u043d_\u0432\u0442\u043e_\u0441\u0440\u0435_\u0447\u0435\u0442_\u043f\u0435\u0442_\u0441\u0430\u0431".split("_"),weekdaysMin:"\u043de_\u043fo_\u0432\u0442_\u0441\u0440_\u0447\u0435_\u043f\u0435_\u0441a".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[\u0414\u0435\u043d\u0435\u0441 \u0432\u043e] LT",nextDay:"[\u0423\u0442\u0440\u0435 \u0432\u043e] LT",nextWeek:"[\u0412\u043e] dddd [\u0432\u043e] LT",lastDay:"[\u0412\u0447\u0435\u0440\u0430 \u0432\u043e] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[\u0418\u0437\u043c\u0438\u043d\u0430\u0442\u0430\u0442\u0430] dddd [\u0432\u043e] LT";case 1:case 2:case 4:case 5:return"[\u0418\u0437\u043c\u0438\u043d\u0430\u0442\u0438\u043e\u0442] dddd [\u0432\u043e] LT"}},sameElse:"L"},relativeTime:{future:"\u043f\u043e\u0441\u043b\u0435 %s",past:"\u043f\u0440\u0435\u0434 %s",s:"\u043d\u0435\u043a\u043e\u043b\u043a\u0443 \u0441\u0435\u043a\u0443\u043d\u0434\u0438",ss:"%d \u0441\u0435\u043a\u0443\u043d\u0434\u0438",m:"\u043c\u0438\u043d\u0443\u0442\u0430",mm:"%d \u043c\u0438\u043d\u0443\u0442\u0438",h:"\u0447\u0430\u0441",hh:"%d \u0447\u0430\u0441\u0430",d:"\u0434\u0435\u043d",dd:"%d \u0434\u0435\u043d\u0430",M:"\u043c\u0435\u0441\u0435\u0446",MM:"%d \u043c\u0435\u0441\u0435\u0446\u0438",y:"\u0433\u043e\u0434\u0438\u043d\u0430",yy:"%d \u0433\u043e\u0434\u0438\u043d\u0438"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0435\u0432|\u0435\u043d|\u0442\u0438|\u0432\u0438|\u0440\u0438|\u043c\u0438)/,ordinal:function(e){var a=e%10,t=e%100;return 0===e?e+"-\u0435\u0432":0===t?e+"-\u0435\u043d":t>10&&t<20?e+"-\u0442\u0438":1===a?e+"-\u0432\u0438":2===a?e+"-\u0440\u0438":7===a||8===a?e+"-\u043c\u0438":e+"-\u0442\u0438"},week:{dow:1,doy:7}}),e.defineLocale("ml",{months:"\u0d1c\u0d28\u0d41\u0d35\u0d30\u0d3f_\u0d2b\u0d46\u0d2c\u0d4d\u0d30\u0d41\u0d35\u0d30\u0d3f_\u0d2e\u0d3e\u0d7c\u0d1a\u0d4d\u0d1a\u0d4d_\u0d0f\u0d2a\u0d4d\u0d30\u0d3f\u0d7d_\u0d2e\u0d47\u0d2f\u0d4d_\u0d1c\u0d42\u0d7a_\u0d1c\u0d42\u0d32\u0d48_\u0d13\u0d17\u0d38\u0d4d\u0d31\u0d4d\u0d31\u0d4d_\u0d38\u0d46\u0d2a\u0d4d\u0d31\u0d4d\u0d31\u0d02\u0d2c\u0d7c_\u0d12\u0d15\u0d4d\u0d1f\u0d4b\u0d2c\u0d7c_\u0d28\u0d35\u0d02\u0d2c\u0d7c_\u0d21\u0d3f\u0d38\u0d02\u0d2c\u0d7c".split("_"),monthsShort:"\u0d1c\u0d28\u0d41._\u0d2b\u0d46\u0d2c\u0d4d\u0d30\u0d41._\u0d2e\u0d3e\u0d7c._\u0d0f\u0d2a\u0d4d\u0d30\u0d3f._\u0d2e\u0d47\u0d2f\u0d4d_\u0d1c\u0d42\u0d7a_\u0d1c\u0d42\u0d32\u0d48._\u0d13\u0d17._\u0d38\u0d46\u0d2a\u0d4d\u0d31\u0d4d\u0d31._\u0d12\u0d15\u0d4d\u0d1f\u0d4b._\u0d28\u0d35\u0d02._\u0d21\u0d3f\u0d38\u0d02.".split("_"),monthsParseExact:!0,weekdays:"\u0d1e\u0d3e\u0d2f\u0d31\u0d3e\u0d34\u0d4d\u0d1a_\u0d24\u0d3f\u0d19\u0d4d\u0d15\u0d33\u0d3e\u0d34\u0d4d\u0d1a_\u0d1a\u0d4a\u0d35\u0d4d\u0d35\u0d3e\u0d34\u0d4d\u0d1a_\u0d2c\u0d41\u0d27\u0d28\u0d3e\u0d34\u0d4d\u0d1a_\u0d35\u0d4d\u0d2f\u0d3e\u0d34\u0d3e\u0d34\u0d4d\u0d1a_\u0d35\u0d46\u0d33\u0d4d\u0d33\u0d3f\u0d2f\u0d3e\u0d34\u0d4d\u0d1a_\u0d36\u0d28\u0d3f\u0d2f\u0d3e\u0d34\u0d4d\u0d1a".split("_"),weekdaysShort:"\u0d1e\u0d3e\u0d2f\u0d7c_\u0d24\u0d3f\u0d19\u0d4d\u0d15\u0d7e_\u0d1a\u0d4a\u0d35\u0d4d\u0d35_\u0d2c\u0d41\u0d27\u0d7b_\u0d35\u0d4d\u0d2f\u0d3e\u0d34\u0d02_\u0d35\u0d46\u0d33\u0d4d\u0d33\u0d3f_\u0d36\u0d28\u0d3f".split("_"),weekdaysMin:"\u0d1e\u0d3e_\u0d24\u0d3f_\u0d1a\u0d4a_\u0d2c\u0d41_\u0d35\u0d4d\u0d2f\u0d3e_\u0d35\u0d46_\u0d36".split("_"),longDateFormat:{LT:"A h:mm -\u0d28\u0d41",LTS:"A h:mm:ss -\u0d28\u0d41",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm -\u0d28\u0d41",LLLL:"dddd, D MMMM YYYY, A h:mm -\u0d28\u0d41"},calendar:{sameDay:"[\u0d07\u0d28\u0d4d\u0d28\u0d4d] LT",nextDay:"[\u0d28\u0d3e\u0d33\u0d46] LT",nextWeek:"dddd, LT",lastDay:"[\u0d07\u0d28\u0d4d\u0d28\u0d32\u0d46] LT",lastWeek:"[\u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e\u0d4d",past:"%s \u0d2e\u0d41\u0d7b\u0d2a\u0d4d",s:"\u0d05\u0d7d\u0d2a \u0d28\u0d3f\u0d2e\u0d3f\u0d37\u0d19\u0d4d\u0d19\u0d7e",ss:"%d \u0d38\u0d46\u0d15\u0d4d\u0d15\u0d7b\u0d21\u0d4d",m:"\u0d12\u0d30\u0d41 \u0d2e\u0d3f\u0d28\u0d3f\u0d31\u0d4d\u0d31\u0d4d",mm:"%d \u0d2e\u0d3f\u0d28\u0d3f\u0d31\u0d4d\u0d31\u0d4d",h:"\u0d12\u0d30\u0d41 \u0d2e\u0d23\u0d3f\u0d15\u0d4d\u0d15\u0d42\u0d7c",hh:"%d \u0d2e\u0d23\u0d3f\u0d15\u0d4d\u0d15\u0d42\u0d7c",d:"\u0d12\u0d30\u0d41 \u0d26\u0d3f\u0d35\u0d38\u0d02",dd:"%d \u0d26\u0d3f\u0d35\u0d38\u0d02",M:"\u0d12\u0d30\u0d41 \u0d2e\u0d3e\u0d38\u0d02",MM:"%d \u0d2e\u0d3e\u0d38\u0d02",y:"\u0d12\u0d30\u0d41 \u0d35\u0d7c\u0d37\u0d02",yy:"%d \u0d35\u0d7c\u0d37\u0d02"},meridiemParse:/\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f|\u0d30\u0d3e\u0d35\u0d3f\u0d32\u0d46|\u0d09\u0d1a\u0d4d\u0d1a \u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e\u0d4d|\u0d35\u0d48\u0d15\u0d41\u0d28\u0d4d\u0d28\u0d47\u0d30\u0d02|\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f/i,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f"===a&&e>=4||"\u0d09\u0d1a\u0d4d\u0d1a \u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e\u0d4d"===a||"\u0d35\u0d48\u0d15\u0d41\u0d28\u0d4d\u0d28\u0d47\u0d30\u0d02"===a?e+12:e},meridiem:function(e,a,t){return e<4?"\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f":e<12?"\u0d30\u0d3e\u0d35\u0d3f\u0d32\u0d46":e<17?"\u0d09\u0d1a\u0d4d\u0d1a \u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e\u0d4d":e<20?"\u0d35\u0d48\u0d15\u0d41\u0d28\u0d4d\u0d28\u0d47\u0d30\u0d02":"\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f"}});var wn={1:"\u0967",2:"\u0968",3:"\u0969",4:"\u096a",5:"\u096b",6:"\u096c",7:"\u096d",8:"\u096e",9:"\u096f",0:"\u0966"},vn={"\u0967":"1","\u0968":"2","\u0969":"3","\u096a":"4","\u096b":"5","\u096c":"6","\u096d":"7","\u096e":"8","\u096f":"9","\u0966":"0"};e.defineLocale("mr",{months:"\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940_\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u093e\u0930\u0940_\u092e\u093e\u0930\u094d\u091a_\u090f\u092a\u094d\u0930\u093f\u0932_\u092e\u0947_\u091c\u0942\u0928_\u091c\u0941\u0932\u0948_\u0911\u0917\u0938\u094d\u091f_\u0938\u092a\u094d\u091f\u0947\u0902\u092c\u0930_\u0911\u0915\u094d\u091f\u094b\u092c\u0930_\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930_\u0921\u093f\u0938\u0947\u0902\u092c\u0930".split("_"),monthsShort:"\u091c\u093e\u0928\u0947._\u092b\u0947\u092c\u094d\u0930\u0941._\u092e\u093e\u0930\u094d\u091a._\u090f\u092a\u094d\u0930\u093f._\u092e\u0947._\u091c\u0942\u0928._\u091c\u0941\u0932\u0948._\u0911\u0917._\u0938\u092a\u094d\u091f\u0947\u0902._\u0911\u0915\u094d\u091f\u094b._\u0928\u094b\u0935\u094d\u0939\u0947\u0902._\u0921\u093f\u0938\u0947\u0902.".split("_"),monthsParseExact:!0,weekdays:"\u0930\u0935\u093f\u0935\u093e\u0930_\u0938\u094b\u092e\u0935\u093e\u0930_\u092e\u0902\u0917\u0933\u0935\u093e\u0930_\u092c\u0941\u0927\u0935\u093e\u0930_\u0917\u0941\u0930\u0942\u0935\u093e\u0930_\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930_\u0936\u0928\u093f\u0935\u093e\u0930".split("_"),weekdaysShort:"\u0930\u0935\u093f_\u0938\u094b\u092e_\u092e\u0902\u0917\u0933_\u092c\u0941\u0927_\u0917\u0941\u0930\u0942_\u0936\u0941\u0915\u094d\u0930_\u0936\u0928\u093f".split("_"),weekdaysMin:"\u0930_\u0938\u094b_\u092e\u0902_\u092c\u0941_\u0917\u0941_\u0936\u0941_\u0936".split("_"),longDateFormat:{LT:"A h:mm \u0935\u093e\u091c\u0924\u093e",LTS:"A h:mm:ss \u0935\u093e\u091c\u0924\u093e",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u0935\u093e\u091c\u0924\u093e",LLLL:"dddd, D MMMM YYYY, A h:mm \u0935\u093e\u091c\u0924\u093e"},calendar:{sameDay:"[\u0906\u091c] LT",nextDay:"[\u0909\u0926\u094d\u092f\u093e] LT",nextWeek:"dddd, LT",lastDay:"[\u0915\u093e\u0932] LT",lastWeek:"[\u092e\u093e\u0917\u0940\u0932] dddd, LT",sameElse:"L"},relativeTime:{future:"%s\u092e\u0927\u094d\u092f\u0947",past:"%s\u092a\u0942\u0930\u094d\u0935\u0940",s:Ta,ss:Ta,m:Ta,mm:Ta,h:Ta,hh:Ta,d:Ta,dd:Ta,M:Ta,MM:Ta,y:Ta,yy:Ta},preparse:function(e){return e.replace(/[\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u0966]/g,function(e){return vn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return wn[e]})},meridiemParse:/\u0930\u093e\u0924\u094d\u0930\u0940|\u0938\u0915\u093e\u0933\u0940|\u0926\u0941\u092a\u093e\u0930\u0940|\u0938\u093e\u092f\u0902\u0915\u093e\u0933\u0940/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0930\u093e\u0924\u094d\u0930\u0940"===a?e<4?e:e+12:"\u0938\u0915\u093e\u0933\u0940"===a?e:"\u0926\u0941\u092a\u093e\u0930\u0940"===a?e>=10?e:e+12:"\u0938\u093e\u092f\u0902\u0915\u093e\u0933\u0940"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"\u0930\u093e\u0924\u094d\u0930\u0940":e<10?"\u0938\u0915\u093e\u0933\u0940":e<17?"\u0926\u0941\u092a\u093e\u0930\u0940":e<20?"\u0938\u093e\u092f\u0902\u0915\u093e\u0933\u0940":"\u0930\u093e\u0924\u094d\u0930\u0940"},week:{dow:0,doy:6}}),e.defineLocale("ms-my",{months:"Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember".split("_"),monthsShort:"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis".split("_"),weekdays:"Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu".split("_"),weekdaysShort:"Ahd_Isn_Sel_Rab_Kha_Jum_Sab".split("_"),weekdaysMin:"Ah_Is_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|tengahari|petang|malam/,meridiemHour:function(e,a){return 12===e&&(e=0),"pagi"===a?e:"tengahari"===a?e>=11?e:e+12:"petang"===a||"malam"===a?e+12:void 0},meridiem:function(e,a,t){return e<11?"pagi":e<15?"tengahari":e<19?"petang":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Esok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kelmarin pukul] LT",lastWeek:"dddd [lepas pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lepas",s:"beberapa saat",ss:"%d saat",m:"seminit",mm:"%d minit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),e.defineLocale("ms",{months:"Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember".split("_"),monthsShort:"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis".split("_"),weekdays:"Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu".split("_"),weekdaysShort:"Ahd_Isn_Sel_Rab_Kha_Jum_Sab".split("_"),weekdaysMin:"Ah_Is_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|tengahari|petang|malam/,meridiemHour:function(e,a){return 12===e&&(e=0),"pagi"===a?e:"tengahari"===a?e>=11?e:e+12:"petang"===a||"malam"===a?e+12:void 0},meridiem:function(e,a,t){return e<11?"pagi":e<15?"tengahari":e<19?"petang":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Esok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kelmarin pukul] LT",lastWeek:"dddd [lepas pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lepas",s:"beberapa saat",ss:"%d saat",m:"seminit",mm:"%d minit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),e.defineLocale("mt",{months:"Jannar_Frar_Marzu_April_Mejju_\u0120unju_Lulju_Awwissu_Settembru_Ottubru_Novembru_Di\u010bembru".split("_"),monthsShort:"Jan_Fra_Mar_Apr_Mej_\u0120un_Lul_Aww_Set_Ott_Nov_Di\u010b".split("_"),weekdays:"Il-\u0126add_It-Tnejn_It-Tlieta_L-Erbg\u0127a_Il-\u0126amis_Il-\u0120img\u0127a_Is-Sibt".split("_"),weekdaysShort:"\u0126ad_Tne_Tli_Erb_\u0126am_\u0120im_Sib".split("_"),weekdaysMin:"\u0126a_Tn_Tl_Er_\u0126a_\u0120i_Si".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Illum fil-]LT",nextDay:"[G\u0127ada fil-]LT",nextWeek:"dddd [fil-]LT",lastDay:"[Il-biera\u0127 fil-]LT",lastWeek:"dddd [li g\u0127adda] [fil-]LT",sameElse:"L"},relativeTime:{future:"f\u2019 %s",past:"%s ilu",s:"ftit sekondi",ss:"%d sekondi",m:"minuta",mm:"%d minuti",h:"sieg\u0127a",hh:"%d sieg\u0127at",d:"\u0121urnata",dd:"%d \u0121ranet",M:"xahar",MM:"%d xhur",y:"sena",yy:"%d sni"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}});var Sn={1:"\u1041",2:"\u1042",3:"\u1043",4:"\u1044",5:"\u1045",6:"\u1046",7:"\u1047",8:"\u1048",9:"\u1049",0:"\u1040"},Hn={"\u1041":"1","\u1042":"2","\u1043":"3","\u1044":"4","\u1045":"5","\u1046":"6","\u1047":"7","\u1048":"8","\u1049":"9","\u1040":"0"};e.defineLocale("my",{months:"\u1007\u1014\u103a\u1014\u101d\u102b\u101b\u102e_\u1016\u1031\u1016\u1031\u102c\u103a\u101d\u102b\u101b\u102e_\u1019\u1010\u103a_\u1027\u1015\u103c\u102e_\u1019\u1031_\u1007\u103d\u1014\u103a_\u1007\u1030\u101c\u102d\u102f\u1004\u103a_\u101e\u103c\u1002\u102f\u1010\u103a_\u1005\u1000\u103a\u1010\u1004\u103a\u1018\u102c_\u1021\u1031\u102c\u1000\u103a\u1010\u102d\u102f\u1018\u102c_\u1014\u102d\u102f\u101d\u1004\u103a\u1018\u102c_\u1012\u102e\u1007\u1004\u103a\u1018\u102c".split("_"),monthsShort:"\u1007\u1014\u103a_\u1016\u1031_\u1019\u1010\u103a_\u1015\u103c\u102e_\u1019\u1031_\u1007\u103d\u1014\u103a_\u101c\u102d\u102f\u1004\u103a_\u101e\u103c_\u1005\u1000\u103a_\u1021\u1031\u102c\u1000\u103a_\u1014\u102d\u102f_\u1012\u102e".split("_"),weekdays:"\u1010\u1014\u1004\u103a\u1039\u1002\u1014\u103d\u1031_\u1010\u1014\u1004\u103a\u1039\u101c\u102c_\u1021\u1004\u103a\u1039\u1002\u102b_\u1017\u102f\u1012\u1039\u1013\u101f\u1030\u1038_\u1000\u103c\u102c\u101e\u1015\u1010\u1031\u1038_\u101e\u1031\u102c\u1000\u103c\u102c_\u1005\u1014\u1031".split("_"),weekdaysShort:"\u1014\u103d\u1031_\u101c\u102c_\u1002\u102b_\u101f\u1030\u1038_\u1000\u103c\u102c_\u101e\u1031\u102c_\u1014\u1031".split("_"),weekdaysMin:"\u1014\u103d\u1031_\u101c\u102c_\u1002\u102b_\u101f\u1030\u1038_\u1000\u103c\u102c_\u101e\u1031\u102c_\u1014\u1031".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u101a\u1014\u1031.] LT [\u1019\u103e\u102c]",nextDay:"[\u1019\u1014\u1000\u103a\u1016\u103c\u1014\u103a] LT [\u1019\u103e\u102c]",nextWeek:"dddd LT [\u1019\u103e\u102c]",lastDay:"[\u1019\u1014\u1031.\u1000] LT [\u1019\u103e\u102c]",lastWeek:"[\u1015\u103c\u102e\u1038\u1001\u1032\u1037\u101e\u1031\u102c] dddd LT [\u1019\u103e\u102c]",sameElse:"L"},relativeTime:{future:"\u101c\u102c\u1019\u100a\u103a\u1037 %s \u1019\u103e\u102c",past:"\u101c\u103d\u1014\u103a\u1001\u1032\u1037\u101e\u1031\u102c %s \u1000",s:"\u1005\u1000\u1039\u1000\u1014\u103a.\u1021\u1014\u100a\u103a\u1038\u1004\u101a\u103a",ss:"%d \u1005\u1000\u1039\u1000\u1014\u1037\u103a",m:"\u1010\u1005\u103a\u1019\u102d\u1014\u1005\u103a",mm:"%d \u1019\u102d\u1014\u1005\u103a",h:"\u1010\u1005\u103a\u1014\u102c\u101b\u102e",hh:"%d \u1014\u102c\u101b\u102e",d:"\u1010\u1005\u103a\u101b\u1000\u103a",dd:"%d \u101b\u1000\u103a",M:"\u1010\u1005\u103a\u101c",MM:"%d \u101c",y:"\u1010\u1005\u103a\u1014\u103e\u1005\u103a",yy:"%d \u1014\u103e\u1005\u103a"},preparse:function(e){return e.replace(/[\u1041\u1042\u1043\u1044\u1045\u1046\u1047\u1048\u1049\u1040]/g,function(e){return Hn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Sn[e]})},week:{dow:1,doy:4}}),e.defineLocale("nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"),monthsParseExact:!0,weekdays:"s\xf8ndag_mandag_tirsdag_onsdag_torsdag_fredag_l\xf8rdag".split("_"),weekdaysShort:"s\xf8._ma._ti._on._to._fr._l\xf8.".split("_"),weekdaysMin:"s\xf8_ma_ti_on_to_fr_l\xf8".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] HH:mm",LLLL:"dddd D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i g\xe5r kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"noen sekunder",ss:"%d sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en m\xe5ned",MM:"%d m\xe5neder",y:"ett \xe5r",yy:"%d \xe5r"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});var bn={1:"\u0967",2:"\u0968",3:"\u0969",4:"\u096a",5:"\u096b",6:"\u096c",7:"\u096d",8:"\u096e",9:"\u096f",0:"\u0966"},jn={"\u0967":"1","\u0968":"2","\u0969":"3","\u096a":"4","\u096b":"5","\u096c":"6","\u096d":"7","\u096e":"8","\u096f":"9","\u0966":"0"};e.defineLocale("ne",{months:"\u091c\u0928\u0935\u0930\u0940_\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u0930\u0940_\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u093f\u0932_\u092e\u0908_\u091c\u0941\u0928_\u091c\u0941\u0932\u093e\u0908_\u0905\u0917\u0937\u094d\u091f_\u0938\u0947\u092a\u094d\u091f\u0947\u092e\u094d\u092c\u0930_\u0905\u0915\u094d\u091f\u094b\u092c\u0930_\u0928\u094b\u092d\u0947\u092e\u094d\u092c\u0930_\u0921\u093f\u0938\u0947\u092e\u094d\u092c\u0930".split("_"),monthsShort:"\u091c\u0928._\u092b\u0947\u092c\u094d\u0930\u0941._\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u093f._\u092e\u0908_\u091c\u0941\u0928_\u091c\u0941\u0932\u093e\u0908._\u0905\u0917._\u0938\u0947\u092a\u094d\u091f._\u0905\u0915\u094d\u091f\u094b._\u0928\u094b\u092d\u0947._\u0921\u093f\u0938\u0947.".split("_"),monthsParseExact:!0,weekdays:"\u0906\u0907\u0924\u092c\u093e\u0930_\u0938\u094b\u092e\u092c\u093e\u0930_\u092e\u0919\u094d\u0917\u0932\u092c\u093e\u0930_\u092c\u0941\u0927\u092c\u093e\u0930_\u092c\u093f\u0939\u093f\u092c\u093e\u0930_\u0936\u0941\u0915\u094d\u0930\u092c\u093e\u0930_\u0936\u0928\u093f\u092c\u093e\u0930".split("_"),weekdaysShort:"\u0906\u0907\u0924._\u0938\u094b\u092e._\u092e\u0919\u094d\u0917\u0932._\u092c\u0941\u0927._\u092c\u093f\u0939\u093f._\u0936\u0941\u0915\u094d\u0930._\u0936\u0928\u093f.".split("_"),weekdaysMin:"\u0906._\u0938\u094b._\u092e\u0902._\u092c\u0941._\u092c\u093f._\u0936\u0941._\u0936.".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"A\u0915\u094b h:mm \u092c\u091c\u0947",LTS:"A\u0915\u094b h:mm:ss \u092c\u091c\u0947",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A\u0915\u094b h:mm \u092c\u091c\u0947",LLLL:"dddd, D MMMM YYYY, A\u0915\u094b h:mm \u092c\u091c\u0947"},preparse:function(e){return e.replace(/[\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u0966]/g,function(e){return jn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return bn[e]})},meridiemParse:/\u0930\u093e\u0924\u093f|\u092c\u093f\u0939\u093e\u0928|\u0926\u093f\u0909\u0901\u0938\u094b|\u0938\u093e\u0901\u091d/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0930\u093e\u0924\u093f"===a?e<4?e:e+12:"\u092c\u093f\u0939\u093e\u0928"===a?e:"\u0926\u093f\u0909\u0901\u0938\u094b"===a?e>=10?e:e+12:"\u0938\u093e\u0901\u091d"===a?e+12:void 0},meridiem:function(e,a,t){return e<3?"\u0930\u093e\u0924\u093f":e<12?"\u092c\u093f\u0939\u093e\u0928":e<16?"\u0926\u093f\u0909\u0901\u0938\u094b":e<20?"\u0938\u093e\u0901\u091d":"\u0930\u093e\u0924\u093f"},calendar:{sameDay:"[\u0906\u091c] LT",nextDay:"[\u092d\u094b\u0932\u093f] LT",nextWeek:"[\u0906\u0909\u0901\u0926\u094b] dddd[,] LT",lastDay:"[\u0939\u093f\u091c\u094b] LT",lastWeek:"[\u0917\u090f\u0915\u094b] dddd[,] LT",sameElse:"L"},relativeTime:{future:"%s\u092e\u093e",past:"%s \u0905\u0917\u093e\u0921\u093f",s:"\u0915\u0947\u0939\u0940 \u0915\u094d\u0937\u0923",ss:"%d \u0938\u0947\u0915\u0947\u0923\u094d\u0921",m:"\u090f\u0915 \u092e\u093f\u0928\u0947\u091f",mm:"%d \u092e\u093f\u0928\u0947\u091f",h:"\u090f\u0915 \u0918\u0923\u094d\u091f\u093e",hh:"%d \u0918\u0923\u094d\u091f\u093e",d:"\u090f\u0915 \u0926\u093f\u0928",dd:"%d \u0926\u093f\u0928",M:"\u090f\u0915 \u092e\u0939\u093f\u0928\u093e",MM:"%d \u092e\u0939\u093f\u0928\u093e",y:"\u090f\u0915 \u092c\u0930\u094d\u0937",yy:"%d \u092c\u0930\u094d\u0937"},week:{dow:0,doy:6}});var xn="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),Pn="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"),On=[/^jan/i,/^feb/i,/^maart|mrt.?$/i,/^apr/i,/^mei$/i,/^jun[i.]?$/i,/^jul[i.]?$/i,/^aug/i,/^sep/i,/^okt/i,/^nov/i,/^dec/i],Wn=/^(januari|februari|maart|april|mei|april|ju[nl]i|augustus|september|oktober|november|december|jan\.?|feb\.?|mrt\.?|apr\.?|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i;e.defineLocale("nl-be",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(e,a){return e?/-MMM-/.test(a)?Pn[e.month()]:xn[e.month()]:xn},monthsRegex:Wn,monthsShortRegex:Wn,monthsStrictRegex:/^(januari|februari|maart|mei|ju[nl]i|april|augustus|september|oktober|november|december)/i,monthsShortStrictRegex:/^(jan\.?|feb\.?|mrt\.?|apr\.?|mei|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i,monthsParse:On,longMonthsParse:On,shortMonthsParse:On,weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"zo_ma_di_wo_do_vr_za".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",ss:"%d seconden",m:"\xe9\xe9n minuut",mm:"%d minuten",h:"\xe9\xe9n uur",hh:"%d uur",d:"\xe9\xe9n dag",dd:"%d dagen",M:"\xe9\xe9n maand",MM:"%d maanden",y:"\xe9\xe9n jaar",yy:"%d jaar"},dayOfMonthOrdinalParse:/\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||e>=20?"ste":"de")},week:{dow:1,doy:4}});var En="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),An="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"),Fn=[/^jan/i,/^feb/i,/^maart|mrt.?$/i,/^apr/i,/^mei$/i,/^jun[i.]?$/i,/^jul[i.]?$/i,/^aug/i,/^sep/i,/^okt/i,/^nov/i,/^dec/i],zn=/^(januari|februari|maart|april|mei|april|ju[nl]i|augustus|september|oktober|november|december|jan\.?|feb\.?|mrt\.?|apr\.?|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i;e.defineLocale("nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(e,a){return e?/-MMM-/.test(a)?An[e.month()]:En[e.month()]:En},monthsRegex:zn,monthsShortRegex:zn,monthsStrictRegex:/^(januari|februari|maart|mei|ju[nl]i|april|augustus|september|oktober|november|december)/i,monthsShortStrictRegex:/^(jan\.?|feb\.?|mrt\.?|apr\.?|mei|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i,monthsParse:Fn,longMonthsParse:Fn,shortMonthsParse:Fn,weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"zo_ma_di_wo_do_vr_za".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",ss:"%d seconden",m:"\xe9\xe9n minuut",mm:"%d minuten",h:"\xe9\xe9n uur",hh:"%d uur",d:"\xe9\xe9n dag",dd:"%d dagen",M:"\xe9\xe9n maand",MM:"%d maanden",y:"\xe9\xe9n jaar",yy:"%d jaar"},dayOfMonthOrdinalParse:/\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||e>=20?"ste":"de")},week:{dow:1,doy:4}}),e.defineLocale("nn",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"sundag_m\xe5ndag_tysdag_onsdag_torsdag_fredag_laurdag".split("_"),weekdaysShort:"sun_m\xe5n_tys_ons_tor_fre_lau".split("_"),weekdaysMin:"su_m\xe5_ty_on_to_fr_l\xf8".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] H:mm",LLLL:"dddd D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[I dag klokka] LT",nextDay:"[I morgon klokka] LT",nextWeek:"dddd [klokka] LT",lastDay:"[I g\xe5r klokka] LT",lastWeek:"[F\xf8reg\xe5ande] dddd [klokka] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s sidan",s:"nokre sekund",ss:"%d sekund",m:"eit minutt",mm:"%d minutt",h:"ein time",hh:"%d timar",d:"ein dag",dd:"%d dagar",M:"ein m\xe5nad",MM:"%d m\xe5nader",y:"eit \xe5r",yy:"%d \xe5r"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});var Jn={1:"\u0a67",2:"\u0a68",3:"\u0a69",4:"\u0a6a",5:"\u0a6b",6:"\u0a6c",7:"\u0a6d",8:"\u0a6e",9:"\u0a6f",0:"\u0a66"},Nn={"\u0a67":"1","\u0a68":"2","\u0a69":"3","\u0a6a":"4","\u0a6b":"5","\u0a6c":"6","\u0a6d":"7","\u0a6e":"8","\u0a6f":"9","\u0a66":"0"};e.defineLocale("pa-in",{months:"\u0a1c\u0a28\u0a35\u0a30\u0a40_\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40_\u0a2e\u0a3e\u0a30\u0a1a_\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32_\u0a2e\u0a08_\u0a1c\u0a42\u0a28_\u0a1c\u0a41\u0a32\u0a3e\u0a08_\u0a05\u0a17\u0a38\u0a24_\u0a38\u0a24\u0a70\u0a2c\u0a30_\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30_\u0a28\u0a35\u0a70\u0a2c\u0a30_\u0a26\u0a38\u0a70\u0a2c\u0a30".split("_"),monthsShort:"\u0a1c\u0a28\u0a35\u0a30\u0a40_\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40_\u0a2e\u0a3e\u0a30\u0a1a_\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32_\u0a2e\u0a08_\u0a1c\u0a42\u0a28_\u0a1c\u0a41\u0a32\u0a3e\u0a08_\u0a05\u0a17\u0a38\u0a24_\u0a38\u0a24\u0a70\u0a2c\u0a30_\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30_\u0a28\u0a35\u0a70\u0a2c\u0a30_\u0a26\u0a38\u0a70\u0a2c\u0a30".split("_"),weekdays:"\u0a10\u0a24\u0a35\u0a3e\u0a30_\u0a38\u0a4b\u0a2e\u0a35\u0a3e\u0a30_\u0a2e\u0a70\u0a17\u0a32\u0a35\u0a3e\u0a30_\u0a2c\u0a41\u0a27\u0a35\u0a3e\u0a30_\u0a35\u0a40\u0a30\u0a35\u0a3e\u0a30_\u0a38\u0a3c\u0a41\u0a71\u0a15\u0a30\u0a35\u0a3e\u0a30_\u0a38\u0a3c\u0a28\u0a40\u0a1a\u0a30\u0a35\u0a3e\u0a30".split("_"),weekdaysShort:"\u0a10\u0a24_\u0a38\u0a4b\u0a2e_\u0a2e\u0a70\u0a17\u0a32_\u0a2c\u0a41\u0a27_\u0a35\u0a40\u0a30_\u0a38\u0a3c\u0a41\u0a15\u0a30_\u0a38\u0a3c\u0a28\u0a40".split("_"),weekdaysMin:"\u0a10\u0a24_\u0a38\u0a4b\u0a2e_\u0a2e\u0a70\u0a17\u0a32_\u0a2c\u0a41\u0a27_\u0a35\u0a40\u0a30_\u0a38\u0a3c\u0a41\u0a15\u0a30_\u0a38\u0a3c\u0a28\u0a40".split("_"),longDateFormat:{LT:"A h:mm \u0a35\u0a1c\u0a47",LTS:"A h:mm:ss \u0a35\u0a1c\u0a47",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u0a35\u0a1c\u0a47",LLLL:"dddd, D MMMM YYYY, A h:mm \u0a35\u0a1c\u0a47"},calendar:{sameDay:"[\u0a05\u0a1c] LT",nextDay:"[\u0a15\u0a32] LT",nextWeek:"dddd, LT",lastDay:"[\u0a15\u0a32] LT",lastWeek:"[\u0a2a\u0a3f\u0a1b\u0a32\u0a47] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0a35\u0a3f\u0a71\u0a1a",past:"%s \u0a2a\u0a3f\u0a1b\u0a32\u0a47",s:"\u0a15\u0a41\u0a1d \u0a38\u0a15\u0a3f\u0a70\u0a1f",ss:"%d \u0a38\u0a15\u0a3f\u0a70\u0a1f",m:"\u0a07\u0a15 \u0a2e\u0a3f\u0a70\u0a1f",mm:"%d \u0a2e\u0a3f\u0a70\u0a1f",h:"\u0a07\u0a71\u0a15 \u0a18\u0a70\u0a1f\u0a3e",hh:"%d \u0a18\u0a70\u0a1f\u0a47",d:"\u0a07\u0a71\u0a15 \u0a26\u0a3f\u0a28",dd:"%d \u0a26\u0a3f\u0a28",M:"\u0a07\u0a71\u0a15 \u0a2e\u0a39\u0a40\u0a28\u0a3e",MM:"%d \u0a2e\u0a39\u0a40\u0a28\u0a47",y:"\u0a07\u0a71\u0a15 \u0a38\u0a3e\u0a32",yy:"%d \u0a38\u0a3e\u0a32"},preparse:function(e){return e.replace(/[\u0a67\u0a68\u0a69\u0a6a\u0a6b\u0a6c\u0a6d\u0a6e\u0a6f\u0a66]/g,function(e){return Nn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Jn[e]})},meridiemParse:/\u0a30\u0a3e\u0a24|\u0a38\u0a35\u0a47\u0a30|\u0a26\u0a41\u0a2a\u0a39\u0a3f\u0a30|\u0a38\u0a3c\u0a3e\u0a2e/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0a30\u0a3e\u0a24"===a?e<4?e:e+12:"\u0a38\u0a35\u0a47\u0a30"===a?e:"\u0a26\u0a41\u0a2a\u0a39\u0a3f\u0a30"===a?e>=10?e:e+12:"\u0a38\u0a3c\u0a3e\u0a2e"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"\u0a30\u0a3e\u0a24":e<10?"\u0a38\u0a35\u0a47\u0a30":e<17?"\u0a26\u0a41\u0a2a\u0a39\u0a3f\u0a30":e<20?"\u0a38\u0a3c\u0a3e\u0a2e":"\u0a30\u0a3e\u0a24"},week:{dow:0,doy:6}});var Rn="stycze\u0144_luty_marzec_kwiecie\u0144_maj_czerwiec_lipiec_sierpie\u0144_wrzesie\u0144_pa\u017adziernik_listopad_grudzie\u0144".split("_"),In="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_wrze\u015bnia_pa\u017adziernika_listopada_grudnia".split("_");e.defineLocale("pl",{months:function(e,a){return e?""===a?"("+In[e.month()]+"|"+Rn[e.month()]+")":/D MMMM/.test(a)?In[e.month()]:Rn[e.month()]:Rn},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_pa\u017a_lis_gru".split("_"),weekdays:"niedziela_poniedzia\u0142ek_wtorek_\u015broda_czwartek_pi\u0105tek_sobota".split("_"),weekdaysShort:"ndz_pon_wt_\u015br_czw_pt_sob".split("_"),weekdaysMin:"Nd_Pn_Wt_\u015ar_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Dzi\u015b o] LT",nextDay:"[Jutro o] LT",nextWeek:function(){switch(this.day()){case 0:return"[W niedziel\u0119 o] LT";case 2:return"[We wtorek o] LT";case 3:return"[W \u015brod\u0119 o] LT";case 6:return"[W sobot\u0119 o] LT";default:return"[W] dddd [o] LT"}},lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zesz\u0142\u0105 niedziel\u0119 o] LT";case 3:return"[W zesz\u0142\u0105 \u015brod\u0119 o] LT";case 6:return"[W zesz\u0142\u0105 sobot\u0119 o] LT";default:return"[W zesz\u0142y] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",ss:wa,m:wa,mm:wa,h:wa,hh:wa,d:"1 dzie\u0144",dd:"%d dni",M:"miesi\u0105c",MM:wa,y:"rok",yy:wa},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("pt-br",{months:"janeiro_fevereiro_mar\xe7o_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"Domingo_Segunda-feira_Ter\xe7a-feira_Quarta-feira_Quinta-feira_Sexta-feira_S\xe1bado".split("_"),weekdaysShort:"Dom_Seg_Ter_Qua_Qui_Sex_S\xe1b".split("_"),weekdaysMin:"Do_2\xaa_3\xaa_4\xaa_5\xaa_6\xaa_S\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [\xe0s] HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY [\xe0s] HH:mm"},calendar:{sameDay:"[Hoje \xe0s] LT",nextDay:"[Amanh\xe3 \xe0s] LT",nextWeek:"dddd [\xe0s] LT",lastDay:"[Ontem \xe0s] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[\xdaltimo] dddd [\xe0s] LT":"[\xdaltima] dddd [\xe0s] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atr\xe1s",s:"poucos segundos",ss:"%d segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um m\xeas",MM:"%d meses",y:"um ano",yy:"%d anos"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba"}),e.defineLocale("pt",{months:"janeiro_fevereiro_mar\xe7o_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"Domingo_Segunda-feira_Ter\xe7a-feira_Quarta-feira_Quinta-feira_Sexta-feira_S\xe1bado".split("_"),weekdaysShort:"Dom_Seg_Ter_Qua_Qui_Sex_S\xe1b".split("_"),weekdaysMin:"Do_2\xaa_3\xaa_4\xaa_5\xaa_6\xaa_S\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY HH:mm"},calendar:{sameDay:"[Hoje \xe0s] LT",nextDay:"[Amanh\xe3 \xe0s] LT",nextWeek:"dddd [\xe0s] LT",lastDay:"[Ontem \xe0s] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[\xdaltimo] dddd [\xe0s] LT":"[\xdaltima] dddd [\xe0s] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"h\xe1 %s",s:"segundos",ss:"%d segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um m\xeas",MM:"%d meses",y:"um ano",yy:"%d anos"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}}),e.defineLocale("ro",{months:"ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie".split("_"),monthsShort:"ian._febr._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"duminic\u0103_luni_mar\u021bi_miercuri_joi_vineri_s\xe2mb\u0103t\u0103".split("_"),weekdaysShort:"Dum_Lun_Mar_Mie_Joi_Vin_S\xe2m".split("_"),weekdaysMin:"Du_Lu_Ma_Mi_Jo_Vi_S\xe2".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[azi la] LT",nextDay:"[m\xe2ine la] LT",nextWeek:"dddd [la] LT",lastDay:"[ieri la] LT",lastWeek:"[fosta] dddd [la] LT",sameElse:"L"},relativeTime:{future:"peste %s",past:"%s \xeen urm\u0103",s:"c\xe2teva secunde",ss:va,m:"un minut",mm:va,h:"o or\u0103",hh:va,d:"o zi",dd:va,M:"o lun\u0103",MM:va,y:"un an",yy:va},week:{dow:1,doy:7}});var Cn=[/^\u044f\u043d\u0432/i,/^\u0444\u0435\u0432/i,/^\u043c\u0430\u0440/i,/^\u0430\u043f\u0440/i,/^\u043c\u0430[\u0439\u044f]/i,/^\u0438\u044e\u043d/i,/^\u0438\u044e\u043b/i,/^\u0430\u0432\u0433/i,/^\u0441\u0435\u043d/i,/^\u043e\u043a\u0442/i,/^\u043d\u043e\u044f/i,/^\u0434\u0435\u043a/i];e.defineLocale("ru",{months:{format:"\u044f\u043d\u0432\u0430\u0440\u044f_\u0444\u0435\u0432\u0440\u0430\u043b\u044f_\u043c\u0430\u0440\u0442\u0430_\u0430\u043f\u0440\u0435\u043b\u044f_\u043c\u0430\u044f_\u0438\u044e\u043d\u044f_\u0438\u044e\u043b\u044f_\u0430\u0432\u0433\u0443\u0441\u0442\u0430_\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f_\u043e\u043a\u0442\u044f\u0431\u0440\u044f_\u043d\u043e\u044f\u0431\u0440\u044f_\u0434\u0435\u043a\u0430\u0431\u0440\u044f".split("_"),standalone:"\u044f\u043d\u0432\u0430\u0440\u044c_\u0444\u0435\u0432\u0440\u0430\u043b\u044c_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0435\u043b\u044c_\u043c\u0430\u0439_\u0438\u044e\u043d\u044c_\u0438\u044e\u043b\u044c_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c_\u043e\u043a\u0442\u044f\u0431\u0440\u044c_\u043d\u043e\u044f\u0431\u0440\u044c_\u0434\u0435\u043a\u0430\u0431\u0440\u044c".split("_")},monthsShort:{format:"\u044f\u043d\u0432._\u0444\u0435\u0432\u0440._\u043c\u0430\u0440._\u0430\u043f\u0440._\u043c\u0430\u044f_\u0438\u044e\u043d\u044f_\u0438\u044e\u043b\u044f_\u0430\u0432\u0433._\u0441\u0435\u043d\u0442._\u043e\u043a\u0442._\u043d\u043e\u044f\u0431._\u0434\u0435\u043a.".split("_"),standalone:"\u044f\u043d\u0432._\u0444\u0435\u0432\u0440._\u043c\u0430\u0440\u0442_\u0430\u043f\u0440._\u043c\u0430\u0439_\u0438\u044e\u043d\u044c_\u0438\u044e\u043b\u044c_\u0430\u0432\u0433._\u0441\u0435\u043d\u0442._\u043e\u043a\u0442._\u043d\u043e\u044f\u0431._\u0434\u0435\u043a.".split("_")},weekdays:{standalone:"\u0432\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435_\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a_\u0432\u0442\u043e\u0440\u043d\u0438\u043a_\u0441\u0440\u0435\u0434\u0430_\u0447\u0435\u0442\u0432\u0435\u0440\u0433_\u043f\u044f\u0442\u043d\u0438\u0446\u0430_\u0441\u0443\u0431\u0431\u043e\u0442\u0430".split("_"),format:"\u0432\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435_\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a_\u0432\u0442\u043e\u0440\u043d\u0438\u043a_\u0441\u0440\u0435\u0434\u0443_\u0447\u0435\u0442\u0432\u0435\u0440\u0433_\u043f\u044f\u0442\u043d\u0438\u0446\u0443_\u0441\u0443\u0431\u0431\u043e\u0442\u0443".split("_"),isFormat:/\[ ?[\u0412\u0432] ?(?:\u043f\u0440\u043e\u0448\u043b\u0443\u044e|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e|\u044d\u0442\u0443)? ?\] ?dddd/},weekdaysShort:"\u0432\u0441_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),weekdaysMin:"\u0432\u0441_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),monthsParse:Cn,longMonthsParse:Cn,shortMonthsParse:Cn,monthsRegex:/^(\u044f\u043d\u0432\u0430\u0440[\u044c\u044f]|\u044f\u043d\u0432\.?|\u0444\u0435\u0432\u0440\u0430\u043b[\u044c\u044f]|\u0444\u0435\u0432\u0440?\.?|\u043c\u0430\u0440\u0442\u0430?|\u043c\u0430\u0440\.?|\u0430\u043f\u0440\u0435\u043b[\u044c\u044f]|\u0430\u043f\u0440\.?|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d[\u044c\u044f]|\u0438\u044e\u043d\.?|\u0438\u044e\u043b[\u044c\u044f]|\u0438\u044e\u043b\.?|\u0430\u0432\u0433\u0443\u0441\u0442\u0430?|\u0430\u0432\u0433\.?|\u0441\u0435\u043d\u0442\u044f\u0431\u0440[\u044c\u044f]|\u0441\u0435\u043d\u0442?\.?|\u043e\u043a\u0442\u044f\u0431\u0440[\u044c\u044f]|\u043e\u043a\u0442\.?|\u043d\u043e\u044f\u0431\u0440[\u044c\u044f]|\u043d\u043e\u044f\u0431?\.?|\u0434\u0435\u043a\u0430\u0431\u0440[\u044c\u044f]|\u0434\u0435\u043a\.?)/i,monthsShortRegex:/^(\u044f\u043d\u0432\u0430\u0440[\u044c\u044f]|\u044f\u043d\u0432\.?|\u0444\u0435\u0432\u0440\u0430\u043b[\u044c\u044f]|\u0444\u0435\u0432\u0440?\.?|\u043c\u0430\u0440\u0442\u0430?|\u043c\u0430\u0440\.?|\u0430\u043f\u0440\u0435\u043b[\u044c\u044f]|\u0430\u043f\u0440\.?|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d[\u044c\u044f]|\u0438\u044e\u043d\.?|\u0438\u044e\u043b[\u044c\u044f]|\u0438\u044e\u043b\.?|\u0430\u0432\u0433\u0443\u0441\u0442\u0430?|\u0430\u0432\u0433\.?|\u0441\u0435\u043d\u0442\u044f\u0431\u0440[\u044c\u044f]|\u0441\u0435\u043d\u0442?\.?|\u043e\u043a\u0442\u044f\u0431\u0440[\u044c\u044f]|\u043e\u043a\u0442\.?|\u043d\u043e\u044f\u0431\u0440[\u044c\u044f]|\u043d\u043e\u044f\u0431?\.?|\u0434\u0435\u043a\u0430\u0431\u0440[\u044c\u044f]|\u0434\u0435\u043a\.?)/i,monthsStrictRegex:/^(\u044f\u043d\u0432\u0430\u0440[\u044f\u044c]|\u0444\u0435\u0432\u0440\u0430\u043b[\u044f\u044c]|\u043c\u0430\u0440\u0442\u0430?|\u0430\u043f\u0440\u0435\u043b[\u044f\u044c]|\u043c\u0430[\u044f\u0439]|\u0438\u044e\u043d[\u044f\u044c]|\u0438\u044e\u043b[\u044f\u044c]|\u0430\u0432\u0433\u0443\u0441\u0442\u0430?|\u0441\u0435\u043d\u0442\u044f\u0431\u0440[\u044f\u044c]|\u043e\u043a\u0442\u044f\u0431\u0440[\u044f\u044c]|\u043d\u043e\u044f\u0431\u0440[\u044f\u044c]|\u0434\u0435\u043a\u0430\u0431\u0440[\u044f\u044c])/i,monthsShortStrictRegex:/^(\u044f\u043d\u0432\.|\u0444\u0435\u0432\u0440?\.|\u043c\u0430\u0440[\u0442.]|\u0430\u043f\u0440\.|\u043c\u0430[\u044f\u0439]|\u0438\u044e\u043d[\u044c\u044f.]|\u0438\u044e\u043b[\u044c\u044f.]|\u0430\u0432\u0433\.|\u0441\u0435\u043d\u0442?\.|\u043e\u043a\u0442\.|\u043d\u043e\u044f\u0431?\.|\u0434\u0435\u043a\.)/i,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY \u0433.",LLL:"D MMMM YYYY \u0433., H:mm",LLLL:"dddd, D MMMM YYYY \u0433., H:mm"},calendar:{sameDay:"[\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0432] LT",nextDay:"[\u0417\u0430\u0432\u0442\u0440\u0430 \u0432] LT",lastDay:"[\u0412\u0447\u0435\u0440\u0430 \u0432] LT",nextWeek:function(e){if(e.week()===this.week())return 2===this.day()?"[\u0412\u043e] dddd [\u0432] LT":"[\u0412] dddd [\u0432] LT";switch(this.day()){case 0:return"[\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435] dddd [\u0432] LT";case 1:case 2:case 4:return"[\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439] dddd [\u0432] LT";case 3:case 5:case 6:return"[\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e] dddd [\u0432] LT"}},lastWeek:function(e){if(e.week()===this.week())return 2===this.day()?"[\u0412\u043e] dddd [\u0432] LT":"[\u0412] dddd [\u0432] LT";switch(this.day()){case 0:return"[\u0412 \u043f\u0440\u043e\u0448\u043b\u043e\u0435] dddd [\u0432] LT";case 1:case 2:case 4:return"[\u0412 \u043f\u0440\u043e\u0448\u043b\u044b\u0439] dddd [\u0432] LT";case 3:case 5:case 6:return"[\u0412 \u043f\u0440\u043e\u0448\u043b\u0443\u044e] dddd [\u0432] LT"}},sameElse:"L"},relativeTime:{future:"\u0447\u0435\u0440\u0435\u0437 %s",past:"%s \u043d\u0430\u0437\u0430\u0434",s:"\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434",ss:Sa,m:Sa,mm:Sa,h:"\u0447\u0430\u0441",hh:Sa,d:"\u0434\u0435\u043d\u044c",dd:Sa,M:"\u043c\u0435\u0441\u044f\u0446",MM:Sa,y:"\u0433\u043e\u0434",yy:Sa},meridiemParse:/\u043d\u043e\u0447\u0438|\u0443\u0442\u0440\u0430|\u0434\u043d\u044f|\u0432\u0435\u0447\u0435\u0440\u0430/i,isPM:function(e){return/^(\u0434\u043d\u044f|\u0432\u0435\u0447\u0435\u0440\u0430)$/.test(e)},meridiem:function(e,a,t){return e<4?"\u043d\u043e\u0447\u0438":e<12?"\u0443\u0442\u0440\u0430":e<17?"\u0434\u043d\u044f":"\u0432\u0435\u0447\u0435\u0440\u0430"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0439|\u0433\u043e|\u044f)/,ordinal:function(e,a){switch(a){case"M":case"d":case"DDD":return e+"-\u0439";case"D":return e+"-\u0433\u043e";case"w":case"W":return e+"-\u044f";default:return e}},week:{dow:1,doy:4}});var Gn=["\u062c\u0646\u0648\u0631\u064a","\u0641\u064a\u0628\u0631\u0648\u0631\u064a","\u0645\u0627\u0631\u0686","\u0627\u067e\u0631\u064a\u0644","\u0645\u0626\u064a","\u062c\u0648\u0646","\u062c\u0648\u0644\u0627\u0621\u0650","\u0622\u06af\u0633\u067d","\u0633\u064a\u067e\u067d\u0645\u0628\u0631","\u0622\u06aa\u067d\u0648\u0628\u0631","\u0646\u0648\u0645\u0628\u0631","\u068a\u0633\u0645\u0628\u0631"],Un=["\u0622\u0686\u0631","\u0633\u0648\u0645\u0631","\u0627\u06b1\u0627\u0631\u0648","\u0627\u0631\u0628\u0639","\u062e\u0645\u064a\u0633","\u062c\u0645\u0639","\u0687\u0646\u0687\u0631"];e.defineLocale("sd",{months:Gn,monthsShort:Gn,weekdays:Un,weekdaysShort:Un,weekdaysMin:Un,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd\u060c D MMMM YYYY HH:mm"},meridiemParse:/\u0635\u0628\u062d|\u0634\u0627\u0645/,isPM:function(e){return"\u0634\u0627\u0645"===e},meridiem:function(e,a,t){return e<12?"\u0635\u0628\u062d":"\u0634\u0627\u0645"},calendar:{sameDay:"[\u0627\u0684] LT",nextDay:"[\u0633\u0680\u0627\u06bb\u064a] LT",nextWeek:"dddd [\u0627\u06b3\u064a\u0646 \u0647\u0641\u062a\u064a \u062a\u064a] LT",lastDay:"[\u06aa\u0627\u0644\u0647\u0647] LT",lastWeek:"[\u06af\u0632\u0631\u064a\u0644 \u0647\u0641\u062a\u064a] dddd [\u062a\u064a] LT",sameElse:"L"},relativeTime:{future:"%s \u067e\u0648\u0621",past:"%s \u0627\u06b3",s:"\u0686\u0646\u062f \u0633\u064a\u06aa\u0646\u068a",ss:"%d \u0633\u064a\u06aa\u0646\u068a",m:"\u0647\u06aa \u0645\u0646\u067d",mm:"%d \u0645\u0646\u067d",h:"\u0647\u06aa \u06aa\u0644\u0627\u06aa",hh:"%d \u06aa\u0644\u0627\u06aa",d:"\u0647\u06aa \u068f\u064a\u0646\u0647\u0646",dd:"%d \u068f\u064a\u0646\u0647\u0646",M:"\u0647\u06aa \u0645\u0647\u064a\u0646\u0648",MM:"%d \u0645\u0647\u064a\u0646\u0627",y:"\u0647\u06aa \u0633\u0627\u0644",yy:"%d \u0633\u0627\u0644"},preparse:function(e){return e.replace(/\u060c/g,",")},postformat:function(e){return e.replace(/,/g,"\u060c")},week:{dow:1,doy:4}}),e.defineLocale("se",{months:"o\u0111\u0111ajagem\xe1nnu_guovvam\xe1nnu_njuk\u010dam\xe1nnu_cuo\u014bom\xe1nnu_miessem\xe1nnu_geassem\xe1nnu_suoidnem\xe1nnu_borgem\xe1nnu_\u010dak\u010dam\xe1nnu_golggotm\xe1nnu_sk\xe1bmam\xe1nnu_juovlam\xe1nnu".split("_"),monthsShort:"o\u0111\u0111j_guov_njuk_cuo_mies_geas_suoi_borg_\u010dak\u010d_golg_sk\xe1b_juov".split("_"),weekdays:"sotnabeaivi_vuoss\xe1rga_ma\u014b\u014beb\xe1rga_gaskavahkku_duorastat_bearjadat_l\xe1vvardat".split("_"),weekdaysShort:"sotn_vuos_ma\u014b_gask_duor_bear_l\xe1v".split("_"),weekdaysMin:"s_v_m_g_d_b_L".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"MMMM D. [b.] YYYY",LLL:"MMMM D. [b.] YYYY [ti.] HH:mm",LLLL:"dddd, MMMM D. [b.] YYYY [ti.] HH:mm"},calendar:{sameDay:"[otne ti] LT",nextDay:"[ihttin ti] LT",nextWeek:"dddd [ti] LT",lastDay:"[ikte ti] LT",lastWeek:"[ovddit] dddd [ti] LT",sameElse:"L"},relativeTime:{future:"%s gea\u017ees",past:"ma\u014bit %s",s:"moadde sekunddat",ss:"%d sekunddat",m:"okta minuhta",mm:"%d minuhtat",h:"okta diimmu",hh:"%d diimmut",d:"okta beaivi",dd:"%d beaivvit",M:"okta m\xe1nnu",MM:"%d m\xe1nut",y:"okta jahki",yy:"%d jagit"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("si",{months:"\u0da2\u0db1\u0dc0\u0dcf\u0dbb\u0dd2_\u0db4\u0dd9\u0db6\u0dbb\u0dc0\u0dcf\u0dbb\u0dd2_\u0db8\u0dcf\u0dbb\u0dca\u0dad\u0dd4_\u0d85\u0db4\u0dca\u200d\u0dbb\u0dda\u0dbd\u0dca_\u0db8\u0dd0\u0dba\u0dd2_\u0da2\u0dd6\u0db1\u0dd2_\u0da2\u0dd6\u0dbd\u0dd2_\u0d85\u0d9c\u0ddd\u0dc3\u0dca\u0dad\u0dd4_\u0dc3\u0dd0\u0db4\u0dca\u0dad\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca_\u0d94\u0d9a\u0dca\u0dad\u0ddd\u0db6\u0dbb\u0dca_\u0db1\u0ddc\u0dc0\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca_\u0daf\u0dd9\u0dc3\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca".split("_"),monthsShort:"\u0da2\u0db1_\u0db4\u0dd9\u0db6_\u0db8\u0dcf\u0dbb\u0dca_\u0d85\u0db4\u0dca_\u0db8\u0dd0\u0dba\u0dd2_\u0da2\u0dd6\u0db1\u0dd2_\u0da2\u0dd6\u0dbd\u0dd2_\u0d85\u0d9c\u0ddd_\u0dc3\u0dd0\u0db4\u0dca_\u0d94\u0d9a\u0dca_\u0db1\u0ddc\u0dc0\u0dd0_\u0daf\u0dd9\u0dc3\u0dd0".split("_"),weekdays:"\u0d89\u0dbb\u0dd2\u0daf\u0dcf_\u0dc3\u0db3\u0dd4\u0daf\u0dcf_\u0d85\u0d9f\u0dc4\u0dbb\u0dd4\u0dc0\u0dcf\u0daf\u0dcf_\u0db6\u0daf\u0dcf\u0daf\u0dcf_\u0db6\u0dca\u200d\u0dbb\u0dc4\u0dc3\u0dca\u0db4\u0dad\u0dd2\u0db1\u0dca\u0daf\u0dcf_\u0dc3\u0dd2\u0d9a\u0dd4\u0dbb\u0dcf\u0daf\u0dcf_\u0dc3\u0dd9\u0db1\u0dc3\u0dd4\u0dbb\u0dcf\u0daf\u0dcf".split("_"),weekdaysShort:"\u0d89\u0dbb\u0dd2_\u0dc3\u0db3\u0dd4_\u0d85\u0d9f_\u0db6\u0daf\u0dcf_\u0db6\u0dca\u200d\u0dbb\u0dc4_\u0dc3\u0dd2\u0d9a\u0dd4_\u0dc3\u0dd9\u0db1".split("_"),weekdaysMin:"\u0d89_\u0dc3_\u0d85_\u0db6_\u0db6\u0dca\u200d\u0dbb_\u0dc3\u0dd2_\u0dc3\u0dd9".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"a h:mm",LTS:"a h:mm:ss",L:"YYYY/MM/DD",LL:"YYYY MMMM D",LLL:"YYYY MMMM D, a h:mm",LLLL:"YYYY MMMM D [\u0dc0\u0dd0\u0db1\u0dd2] dddd, a h:mm:ss"},calendar:{sameDay:"[\u0d85\u0daf] LT[\u0da7]",nextDay:"[\u0dc4\u0dd9\u0da7] LT[\u0da7]",nextWeek:"dddd LT[\u0da7]",lastDay:"[\u0d8a\u0dba\u0dda] LT[\u0da7]",lastWeek:"[\u0db4\u0dc3\u0dd4\u0d9c\u0dd2\u0dba] dddd LT[\u0da7]",sameElse:"L"},relativeTime:{future:"%s\u0d9a\u0dd2\u0db1\u0dca",past:"%s\u0d9a\u0da7 \u0db4\u0dd9\u0dbb",s:"\u0dad\u0dad\u0dca\u0db4\u0dbb \u0d9a\u0dd2\u0dc4\u0dd2\u0db4\u0dba",ss:"\u0dad\u0dad\u0dca\u0db4\u0dbb %d",m:"\u0db8\u0dd2\u0db1\u0dd2\u0dad\u0dca\u0dad\u0dd4\u0dc0",mm:"\u0db8\u0dd2\u0db1\u0dd2\u0dad\u0dca\u0dad\u0dd4 %d",h:"\u0db4\u0dd0\u0dba",hh:"\u0db4\u0dd0\u0dba %d",d:"\u0daf\u0dd2\u0db1\u0dba",dd:"\u0daf\u0dd2\u0db1 %d",M:"\u0db8\u0dcf\u0dc3\u0dba",MM:"\u0db8\u0dcf\u0dc3 %d",y:"\u0dc0\u0dc3\u0dbb",yy:"\u0dc0\u0dc3\u0dbb %d"},dayOfMonthOrdinalParse:/\d{1,2} \u0dc0\u0dd0\u0db1\u0dd2/,ordinal:function(e){return e+" \u0dc0\u0dd0\u0db1\u0dd2"},meridiemParse:/\u0db4\u0dd9\u0dbb \u0dc0\u0dbb\u0dd4|\u0db4\u0dc3\u0dca \u0dc0\u0dbb\u0dd4|\u0db4\u0dd9.\u0dc0|\u0db4.\u0dc0./,isPM:function(e){return"\u0db4.\u0dc0."===e||"\u0db4\u0dc3\u0dca \u0dc0\u0dbb\u0dd4"===e},meridiem:function(e,a,t){return e>11?t?"\u0db4.\u0dc0.":"\u0db4\u0dc3\u0dca \u0dc0\u0dbb\u0dd4":t?"\u0db4\u0dd9.\u0dc0.":"\u0db4\u0dd9\u0dbb \u0dc0\u0dbb\u0dd4"}});var Vn="janu\xe1r_febru\xe1r_marec_apr\xedl_m\xe1j_j\xfan_j\xfal_august_september_okt\xf3ber_november_december".split("_"),Kn="jan_feb_mar_apr_m\xe1j_j\xfan_j\xfal_aug_sep_okt_nov_dec".split("_");e.defineLocale("sk",{months:Vn,monthsShort:Kn,weekdays:"nede\u013ea_pondelok_utorok_streda_\u0161tvrtok_piatok_sobota".split("_"),weekdaysShort:"ne_po_ut_st_\u0161t_pi_so".split("_"),weekdaysMin:"ne_po_ut_st_\u0161t_pi_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd D. MMMM YYYY H:mm"},calendar:{sameDay:"[dnes o] LT",nextDay:"[zajtra o] LT",nextWeek:function(){switch(this.day()){case 0:return"[v nede\u013eu o] LT";case 1:case 2:return"[v] dddd [o] LT";case 3:return"[v stredu o] LT";case 4:return"[vo \u0161tvrtok o] LT";case 5:return"[v piatok o] LT";case 6:return"[v sobotu o] LT"}},lastDay:"[v\u010dera o] LT",lastWeek:function(){switch(this.day()){case 0:return"[minul\xfa nede\u013eu o] LT";case 1:case 2:return"[minul\xfd] dddd [o] LT";case 3:return"[minul\xfa stredu o] LT";case 4:case 5:return"[minul\xfd] dddd [o] LT";case 6:return"[minul\xfa sobotu o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"pred %s",s:ba,ss:ba,m:ba,mm:ba,h:ba,hh:ba,d:ba,dd:ba,M:ba,MM:ba,y:ba,yy:ba},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("sl",{months:"januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december".split("_"),monthsShort:"jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedelja_ponedeljek_torek_sreda_\u010detrtek_petek_sobota".split("_"),weekdaysShort:"ned._pon._tor._sre._\u010det._pet._sob.".split("_"),weekdaysMin:"ne_po_to_sr_\u010de_pe_so".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danes ob] LT",nextDay:"[jutri ob] LT",nextWeek:function(){switch(this.day()){case 0:return"[v] [nedeljo] [ob] LT";case 3:return"[v] [sredo] [ob] LT";case 6:return"[v] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[v] dddd [ob] LT"}},lastDay:"[v\u010deraj ob] LT",lastWeek:function(){switch(this.day()){case 0:return"[prej\u0161njo] [nedeljo] [ob] LT";case 3:return"[prej\u0161njo] [sredo] [ob] LT";case 6:return"[prej\u0161njo] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[prej\u0161nji] dddd [ob] LT"}},sameElse:"L"},relativeTime:{future:"\u010dez %s",past:"pred %s",s:ja,ss:ja,m:ja,mm:ja,h:ja,hh:ja,d:ja,dd:ja,M:ja,MM:ja,y:ja,yy:ja},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),e.defineLocale("sq",{months:"Janar_Shkurt_Mars_Prill_Maj_Qershor_Korrik_Gusht_Shtator_Tetor_N\xebntor_Dhjetor".split("_"),monthsShort:"Jan_Shk_Mar_Pri_Maj_Qer_Kor_Gus_Sht_Tet_N\xebn_Dhj".split("_"),weekdays:"E Diel_E H\xebn\xeb_E Mart\xeb_E M\xebrkur\xeb_E Enjte_E Premte_E Shtun\xeb".split("_"),weekdaysShort:"Die_H\xebn_Mar_M\xebr_Enj_Pre_Sht".split("_"),weekdaysMin:"D_H_Ma_M\xeb_E_P_Sh".split("_"),weekdaysParseExact:!0,meridiemParse:/PD|MD/,isPM:function(e){return"M"===e.charAt(0)},meridiem:function(e,a,t){return e<12?"PD":"MD"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Sot n\xeb] LT",nextDay:"[Nes\xebr n\xeb] LT",nextWeek:"dddd [n\xeb] LT",lastDay:"[Dje n\xeb] LT",lastWeek:"dddd [e kaluar n\xeb] LT",sameElse:"L"},relativeTime:{future:"n\xeb %s",past:"%s m\xeb par\xeb",s:"disa sekonda",ss:"%d sekonda",m:"nj\xeb minut\xeb",mm:"%d minuta",h:"nj\xeb or\xeb",hh:"%d or\xeb",d:"nj\xeb dit\xeb",dd:"%d dit\xeb",M:"nj\xeb muaj",MM:"%d muaj",y:"nj\xeb vit",yy:"%d vite"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});var Zn={words:{ss:["\u0441\u0435\u043a\u0443\u043d\u0434\u0430","\u0441\u0435\u043a\u0443\u043d\u0434\u0435","\u0441\u0435\u043a\u0443\u043d\u0434\u0438"],m:["\u0458\u0435\u0434\u0430\u043d \u043c\u0438\u043d\u0443\u0442","\u0458\u0435\u0434\u043d\u0435 \u043c\u0438\u043d\u0443\u0442\u0435"],mm:["\u043c\u0438\u043d\u0443\u0442","\u043c\u0438\u043d\u0443\u0442\u0435","\u043c\u0438\u043d\u0443\u0442\u0430"],h:["\u0458\u0435\u0434\u0430\u043d \u0441\u0430\u0442","\u0458\u0435\u0434\u043d\u043e\u0433 \u0441\u0430\u0442\u0430"],hh:["\u0441\u0430\u0442","\u0441\u0430\u0442\u0430","\u0441\u0430\u0442\u0438"],dd:["\u0434\u0430\u043d","\u0434\u0430\u043d\u0430","\u0434\u0430\u043d\u0430"],MM:["\u043c\u0435\u0441\u0435\u0446","\u043c\u0435\u0441\u0435\u0446\u0430","\u043c\u0435\u0441\u0435\u0446\u0438"],yy:["\u0433\u043e\u0434\u0438\u043d\u0430","\u0433\u043e\u0434\u0438\u043d\u0435","\u0433\u043e\u0434\u0438\u043d\u0430"]},correctGrammaticalCase:function(e,a){return 1===e?a[0]:e>=2&&e<=4?a[1]:a[2]},translate:function(e,a,t){var s=Zn.words[t];return 1===t.length?a?s[0]:s[1]:e+" "+Zn.correctGrammaticalCase(e,s)}};e.defineLocale("sr-cyrl",{months:"\u0458\u0430\u043d\u0443\u0430\u0440_\u0444\u0435\u0431\u0440\u0443\u0430\u0440_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0438\u043b_\u043c\u0430\u0458_\u0458\u0443\u043d_\u0458\u0443\u043b_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440_\u043e\u043a\u0442\u043e\u0431\u0430\u0440_\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440_\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440".split("_"),monthsShort:"\u0458\u0430\u043d._\u0444\u0435\u0431._\u043c\u0430\u0440._\u0430\u043f\u0440._\u043c\u0430\u0458_\u0458\u0443\u043d_\u0458\u0443\u043b_\u0430\u0432\u0433._\u0441\u0435\u043f._\u043e\u043a\u0442._\u043d\u043e\u0432._\u0434\u0435\u0446.".split("_"),monthsParseExact:!0,weekdays:"\u043d\u0435\u0434\u0435\u0459\u0430_\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a_\u0443\u0442\u043e\u0440\u0430\u043a_\u0441\u0440\u0435\u0434\u0430_\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a_\u043f\u0435\u0442\u0430\u043a_\u0441\u0443\u0431\u043e\u0442\u0430".split("_"),weekdaysShort:"\u043d\u0435\u0434._\u043f\u043e\u043d._\u0443\u0442\u043e._\u0441\u0440\u0435._\u0447\u0435\u0442._\u043f\u0435\u0442._\u0441\u0443\u0431.".split("_"),weekdaysMin:"\u043d\u0435_\u043f\u043e_\u0443\u0442_\u0441\u0440_\u0447\u0435_\u043f\u0435_\u0441\u0443".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[\u0434\u0430\u043d\u0430\u0441 \u0443] LT",nextDay:"[\u0441\u0443\u0442\u0440\u0430 \u0443] LT",nextWeek:function(){switch(this.day()){case 0:return"[\u0443] [\u043d\u0435\u0434\u0435\u0459\u0443] [\u0443] LT";case 3:return"[\u0443] [\u0441\u0440\u0435\u0434\u0443] [\u0443] LT";case 6:return"[\u0443] [\u0441\u0443\u0431\u043e\u0442\u0443] [\u0443] LT";case 1:case 2:case 4:case 5:return"[\u0443] dddd [\u0443] LT"}},lastDay:"[\u0458\u0443\u0447\u0435 \u0443] LT",lastWeek:function(){return["[\u043f\u0440\u043e\u0448\u043b\u0435] [\u043d\u0435\u0434\u0435\u0459\u0435] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u043e\u0433] [\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u043a\u0430] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u043e\u0433] [\u0443\u0442\u043e\u0440\u043a\u0430] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u0435] [\u0441\u0440\u0435\u0434\u0435] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u043e\u0433] [\u0447\u0435\u0442\u0432\u0440\u0442\u043a\u0430] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u043e\u0433] [\u043f\u0435\u0442\u043a\u0430] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u0435] [\u0441\u0443\u0431\u043e\u0442\u0435] [\u0443] LT"][this.day()]},sameElse:"L"},relativeTime:{future:"\u0437\u0430 %s",past:"\u043f\u0440\u0435 %s",s:"\u043d\u0435\u043a\u043e\u043b\u0438\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434\u0438",ss:Zn.translate,m:Zn.translate,mm:Zn.translate,h:Zn.translate,hh:Zn.translate,d:"\u0434\u0430\u043d",dd:Zn.translate,M:"\u043c\u0435\u0441\u0435\u0446",MM:Zn.translate,y:"\u0433\u043e\u0434\u0438\u043d\u0443",yy:Zn.translate},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});var $n={words:{ss:["sekunda","sekunde","sekundi"],m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(e,a){return 1===e?a[0]:e>=2&&e<=4?a[1]:a[2]},translate:function(e,a,t){var s=$n.words[t];return 1===t.length?a?s[0]:s[1]:e+" "+$n.correctGrammaticalCase(e,s)}};e.defineLocale("sr",{months:"januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedelja_ponedeljak_utorak_sreda_\u010detvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sre._\u010det._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_\u010de_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[ju\u010de u] LT",lastWeek:function(){return["[pro\u0161le] [nedelje] [u] LT","[pro\u0161log] [ponedeljka] [u] LT","[pro\u0161log] [utorka] [u] LT","[pro\u0161le] [srede] [u] LT","[pro\u0161log] [\u010detvrtka] [u] LT","[pro\u0161log] [petka] [u] LT","[pro\u0161le] [subote] [u] LT"][this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",ss:$n.translate,m:$n.translate,mm:$n.translate,h:$n.translate,hh:$n.translate,d:"dan",dd:$n.translate,M:"mesec",MM:$n.translate,y:"godinu",yy:$n.translate},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),e.defineLocale("ss",{months:"Bhimbidvwane_Indlovana_Indlov'lenkhulu_Mabasa_Inkhwekhweti_Inhlaba_Kholwane_Ingci_Inyoni_Imphala_Lweti_Ingongoni".split("_"),monthsShort:"Bhi_Ina_Inu_Mab_Ink_Inh_Kho_Igc_Iny_Imp_Lwe_Igo".split("_"),weekdays:"Lisontfo_Umsombuluko_Lesibili_Lesitsatfu_Lesine_Lesihlanu_Umgcibelo".split("_"),weekdaysShort:"Lis_Umb_Lsb_Les_Lsi_Lsh_Umg".split("_"),weekdaysMin:"Li_Us_Lb_Lt_Ls_Lh_Ug".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Namuhla nga] LT",nextDay:"[Kusasa nga] LT",nextWeek:"dddd [nga] LT",lastDay:"[Itolo nga] LT",lastWeek:"dddd [leliphelile] [nga] LT",sameElse:"L"},relativeTime:{future:"nga %s",past:"wenteka nga %s",s:"emizuzwana lomcane",ss:"%d mzuzwana",m:"umzuzu",mm:"%d emizuzu",h:"lihora",hh:"%d emahora",d:"lilanga",dd:"%d emalanga",M:"inyanga",MM:"%d tinyanga",y:"umnyaka",yy:"%d iminyaka"},meridiemParse:/ekuseni|emini|entsambama|ebusuku/,meridiem:function(e,a,t){return e<11?"ekuseni":e<15?"emini":e<19?"entsambama":"ebusuku"},meridiemHour:function(e,a){return 12===e&&(e=0),"ekuseni"===a?e:"emini"===a?e>=11?e:e+12:"entsambama"===a||"ebusuku"===a?0===e?0:e+12:void 0},dayOfMonthOrdinalParse:/\d{1,2}/,ordinal:"%d",week:{dow:1,doy:4}}),e.defineLocale("sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"s\xf6ndag_m\xe5ndag_tisdag_onsdag_torsdag_fredag_l\xf6rdag".split("_"),weekdaysShort:"s\xf6n_m\xe5n_tis_ons_tor_fre_l\xf6r".split("_"),weekdaysMin:"s\xf6_m\xe5_ti_on_to_fr_l\xf6".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [kl.] HH:mm",LLLL:"dddd D MMMM YYYY [kl.] HH:mm",lll:"D MMM YYYY HH:mm",llll:"ddd D MMM YYYY HH:mm"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Ig\xe5r] LT",nextWeek:"[P\xe5] dddd LT",lastWeek:"[I] dddd[s] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"f\xf6r %s sedan",s:"n\xe5gra sekunder",ss:"%d sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en m\xe5nad",MM:"%d m\xe5nader",y:"ett \xe5r",yy:"%d \xe5r"},dayOfMonthOrdinalParse:/\d{1,2}(e|a)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"e":1===a?"a":2===a?"a":"e")},week:{dow:1,doy:4}}),e.defineLocale("sw",{months:"Januari_Februari_Machi_Aprili_Mei_Juni_Julai_Agosti_Septemba_Oktoba_Novemba_Desemba".split("_"),monthsShort:"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ago_Sep_Okt_Nov_Des".split("_"),weekdays:"Jumapili_Jumatatu_Jumanne_Jumatano_Alhamisi_Ijumaa_Jumamosi".split("_"),weekdaysShort:"Jpl_Jtat_Jnne_Jtan_Alh_Ijm_Jmos".split("_"),weekdaysMin:"J2_J3_J4_J5_Al_Ij_J1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[leo saa] LT",nextDay:"[kesho saa] LT",nextWeek:"[wiki ijayo] dddd [saat] LT",lastDay:"[jana] LT",lastWeek:"[wiki iliyopita] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s baadaye",past:"tokea %s",s:"hivi punde",ss:"sekunde %d",m:"dakika moja",mm:"dakika %d",h:"saa limoja",hh:"masaa %d",d:"siku moja",dd:"masiku %d",M:"mwezi mmoja",MM:"miezi %d",y:"mwaka mmoja",yy:"miaka %d"},week:{dow:1,doy:7}});var Bn={1:"\u0be7",2:"\u0be8",3:"\u0be9",4:"\u0bea",5:"\u0beb",6:"\u0bec",7:"\u0bed",8:"\u0bee",9:"\u0bef",0:"\u0be6"},qn={"\u0be7":"1","\u0be8":"2","\u0be9":"3","\u0bea":"4","\u0beb":"5","\u0bec":"6","\u0bed":"7","\u0bee":"8","\u0bef":"9","\u0be6":"0"};e.defineLocale("ta",{months:"\u0b9c\u0ba9\u0bb5\u0bb0\u0bbf_\u0baa\u0bbf\u0baa\u0bcd\u0bb0\u0bb5\u0bb0\u0bbf_\u0bae\u0bbe\u0bb0\u0bcd\u0b9a\u0bcd_\u0b8f\u0baa\u0bcd\u0bb0\u0bb2\u0bcd_\u0bae\u0bc7_\u0b9c\u0bc2\u0ba9\u0bcd_\u0b9c\u0bc2\u0bb2\u0bc8_\u0b86\u0b95\u0bb8\u0bcd\u0b9f\u0bcd_\u0b9a\u0bc6\u0baa\u0bcd\u0b9f\u0bc6\u0bae\u0bcd\u0baa\u0bb0\u0bcd_\u0b85\u0b95\u0bcd\u0b9f\u0bc7\u0bbe\u0baa\u0bb0\u0bcd_\u0ba8\u0bb5\u0bae\u0bcd\u0baa\u0bb0\u0bcd_\u0b9f\u0bbf\u0b9a\u0bae\u0bcd\u0baa\u0bb0\u0bcd".split("_"),monthsShort:"\u0b9c\u0ba9\u0bb5\u0bb0\u0bbf_\u0baa\u0bbf\u0baa\u0bcd\u0bb0\u0bb5\u0bb0\u0bbf_\u0bae\u0bbe\u0bb0\u0bcd\u0b9a\u0bcd_\u0b8f\u0baa\u0bcd\u0bb0\u0bb2\u0bcd_\u0bae\u0bc7_\u0b9c\u0bc2\u0ba9\u0bcd_\u0b9c\u0bc2\u0bb2\u0bc8_\u0b86\u0b95\u0bb8\u0bcd\u0b9f\u0bcd_\u0b9a\u0bc6\u0baa\u0bcd\u0b9f\u0bc6\u0bae\u0bcd\u0baa\u0bb0\u0bcd_\u0b85\u0b95\u0bcd\u0b9f\u0bc7\u0bbe\u0baa\u0bb0\u0bcd_\u0ba8\u0bb5\u0bae\u0bcd\u0baa\u0bb0\u0bcd_\u0b9f\u0bbf\u0b9a\u0bae\u0bcd\u0baa\u0bb0\u0bcd".split("_"),weekdays:"\u0b9e\u0bbe\u0baf\u0bbf\u0bb1\u0bcd\u0bb1\u0bc1\u0b95\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0ba4\u0bbf\u0b99\u0bcd\u0b95\u0b9f\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0b9a\u0bc6\u0bb5\u0bcd\u0bb5\u0bbe\u0baf\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0baa\u0bc1\u0ba4\u0ba9\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0bb5\u0bbf\u0baf\u0bbe\u0bb4\u0b95\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0bb5\u0bc6\u0bb3\u0bcd\u0bb3\u0bbf\u0b95\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0b9a\u0ba9\u0bbf\u0b95\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8".split("_"),weekdaysShort:"\u0b9e\u0bbe\u0baf\u0bbf\u0bb1\u0bc1_\u0ba4\u0bbf\u0b99\u0bcd\u0b95\u0bb3\u0bcd_\u0b9a\u0bc6\u0bb5\u0bcd\u0bb5\u0bbe\u0baf\u0bcd_\u0baa\u0bc1\u0ba4\u0ba9\u0bcd_\u0bb5\u0bbf\u0baf\u0bbe\u0bb4\u0ba9\u0bcd_\u0bb5\u0bc6\u0bb3\u0bcd\u0bb3\u0bbf_\u0b9a\u0ba9\u0bbf".split("_"),weekdaysMin:"\u0b9e\u0bbe_\u0ba4\u0bbf_\u0b9a\u0bc6_\u0baa\u0bc1_\u0bb5\u0bbf_\u0bb5\u0bc6_\u0b9a".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, HH:mm",LLLL:"dddd, D MMMM YYYY, HH:mm"},calendar:{sameDay:"[\u0b87\u0ba9\u0bcd\u0bb1\u0bc1] LT",nextDay:"[\u0ba8\u0bbe\u0bb3\u0bc8] LT",nextWeek:"dddd, LT",lastDay:"[\u0ba8\u0bc7\u0bb1\u0bcd\u0bb1\u0bc1] LT",lastWeek:"[\u0b95\u0b9f\u0ba8\u0bcd\u0ba4 \u0bb5\u0bbe\u0bb0\u0bae\u0bcd] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0b87\u0bb2\u0bcd",past:"%s \u0bae\u0bc1\u0ba9\u0bcd",s:"\u0b92\u0bb0\u0bc1 \u0b9a\u0bbf\u0bb2 \u0bb5\u0bbf\u0ba8\u0bbe\u0b9f\u0bbf\u0b95\u0bb3\u0bcd",ss:"%d \u0bb5\u0bbf\u0ba8\u0bbe\u0b9f\u0bbf\u0b95\u0bb3\u0bcd",m:"\u0b92\u0bb0\u0bc1 \u0ba8\u0bbf\u0bae\u0bbf\u0b9f\u0bae\u0bcd",mm:"%d \u0ba8\u0bbf\u0bae\u0bbf\u0b9f\u0b99\u0bcd\u0b95\u0bb3\u0bcd",h:"\u0b92\u0bb0\u0bc1 \u0bae\u0ba3\u0bbf \u0ba8\u0bc7\u0bb0\u0bae\u0bcd",hh:"%d \u0bae\u0ba3\u0bbf \u0ba8\u0bc7\u0bb0\u0bae\u0bcd",d:"\u0b92\u0bb0\u0bc1 \u0ba8\u0bbe\u0bb3\u0bcd",dd:"%d \u0ba8\u0bbe\u0b9f\u0bcd\u0b95\u0bb3\u0bcd",M:"\u0b92\u0bb0\u0bc1 \u0bae\u0bbe\u0ba4\u0bae\u0bcd",MM:"%d \u0bae\u0bbe\u0ba4\u0b99\u0bcd\u0b95\u0bb3\u0bcd",y:"\u0b92\u0bb0\u0bc1 \u0bb5\u0bb0\u0bc1\u0b9f\u0bae\u0bcd",yy:"%d \u0b86\u0ba3\u0bcd\u0b9f\u0bc1\u0b95\u0bb3\u0bcd"},dayOfMonthOrdinalParse:/\d{1,2}\u0bb5\u0ba4\u0bc1/,ordinal:function(e){return e+"\u0bb5\u0ba4\u0bc1"},preparse:function(e){return e.replace(/[\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef\u0be6]/g,function(e){return qn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Bn[e]})},meridiemParse:/\u0baf\u0bbe\u0bae\u0bae\u0bcd|\u0bb5\u0bc8\u0b95\u0bb1\u0bc8|\u0b95\u0bbe\u0bb2\u0bc8|\u0ba8\u0ba3\u0bcd\u0baa\u0b95\u0bb2\u0bcd|\u0b8e\u0bb1\u0bcd\u0baa\u0bbe\u0b9f\u0bc1|\u0bae\u0bbe\u0bb2\u0bc8/,meridiem:function(e,a,t){return e<2?" \u0baf\u0bbe\u0bae\u0bae\u0bcd":e<6?" \u0bb5\u0bc8\u0b95\u0bb1\u0bc8":e<10?" \u0b95\u0bbe\u0bb2\u0bc8":e<14?" \u0ba8\u0ba3\u0bcd\u0baa\u0b95\u0bb2\u0bcd":e<18?" \u0b8e\u0bb1\u0bcd\u0baa\u0bbe\u0b9f\u0bc1":e<22?" \u0bae\u0bbe\u0bb2\u0bc8":" \u0baf\u0bbe\u0bae\u0bae\u0bcd"},meridiemHour:function(e,a){return 12===e&&(e=0),"\u0baf\u0bbe\u0bae\u0bae\u0bcd"===a?e<2?e:e+12:"\u0bb5\u0bc8\u0b95\u0bb1\u0bc8"===a||"\u0b95\u0bbe\u0bb2\u0bc8"===a?e:"\u0ba8\u0ba3\u0bcd\u0baa\u0b95\u0bb2\u0bcd"===a&&e>=10?e:e+12},week:{dow:0,doy:6}}),e.defineLocale("te",{months:"\u0c1c\u0c28\u0c35\u0c30\u0c3f_\u0c2b\u0c3f\u0c2c\u0c4d\u0c30\u0c35\u0c30\u0c3f_\u0c2e\u0c3e\u0c30\u0c4d\u0c1a\u0c3f_\u0c0f\u0c2a\u0c4d\u0c30\u0c3f\u0c32\u0c4d_\u0c2e\u0c47_\u0c1c\u0c42\u0c28\u0c4d_\u0c1c\u0c42\u0c32\u0c46\u0c56_\u0c06\u0c17\u0c38\u0c4d\u0c1f\u0c41_\u0c38\u0c46\u0c2a\u0c4d\u0c1f\u0c46\u0c02\u0c2c\u0c30\u0c4d_\u0c05\u0c15\u0c4d\u0c1f\u0c4b\u0c2c\u0c30\u0c4d_\u0c28\u0c35\u0c02\u0c2c\u0c30\u0c4d_\u0c21\u0c3f\u0c38\u0c46\u0c02\u0c2c\u0c30\u0c4d".split("_"),monthsShort:"\u0c1c\u0c28._\u0c2b\u0c3f\u0c2c\u0c4d\u0c30._\u0c2e\u0c3e\u0c30\u0c4d\u0c1a\u0c3f_\u0c0f\u0c2a\u0c4d\u0c30\u0c3f._\u0c2e\u0c47_\u0c1c\u0c42\u0c28\u0c4d_\u0c1c\u0c42\u0c32\u0c46\u0c56_\u0c06\u0c17._\u0c38\u0c46\u0c2a\u0c4d._\u0c05\u0c15\u0c4d\u0c1f\u0c4b._\u0c28\u0c35._\u0c21\u0c3f\u0c38\u0c46.".split("_"),monthsParseExact:!0,weekdays:"\u0c06\u0c26\u0c3f\u0c35\u0c3e\u0c30\u0c02_\u0c38\u0c4b\u0c2e\u0c35\u0c3e\u0c30\u0c02_\u0c2e\u0c02\u0c17\u0c33\u0c35\u0c3e\u0c30\u0c02_\u0c2c\u0c41\u0c27\u0c35\u0c3e\u0c30\u0c02_\u0c17\u0c41\u0c30\u0c41\u0c35\u0c3e\u0c30\u0c02_\u0c36\u0c41\u0c15\u0c4d\u0c30\u0c35\u0c3e\u0c30\u0c02_\u0c36\u0c28\u0c3f\u0c35\u0c3e\u0c30\u0c02".split("_"),weekdaysShort:"\u0c06\u0c26\u0c3f_\u0c38\u0c4b\u0c2e_\u0c2e\u0c02\u0c17\u0c33_\u0c2c\u0c41\u0c27_\u0c17\u0c41\u0c30\u0c41_\u0c36\u0c41\u0c15\u0c4d\u0c30_\u0c36\u0c28\u0c3f".split("_"),weekdaysMin:"\u0c06_\u0c38\u0c4b_\u0c2e\u0c02_\u0c2c\u0c41_\u0c17\u0c41_\u0c36\u0c41_\u0c36".split("_"),longDateFormat:{LT:"A h:mm",LTS:"A h:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm",LLLL:"dddd, D MMMM YYYY, A h:mm"},calendar:{sameDay:"[\u0c28\u0c47\u0c21\u0c41] LT",nextDay:"[\u0c30\u0c47\u0c2a\u0c41] LT",nextWeek:"dddd, LT",lastDay:"[\u0c28\u0c3f\u0c28\u0c4d\u0c28] LT",lastWeek:"[\u0c17\u0c24] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0c32\u0c4b",past:"%s \u0c15\u0c4d\u0c30\u0c3f\u0c24\u0c02",s:"\u0c15\u0c4a\u0c28\u0c4d\u0c28\u0c3f \u0c15\u0c4d\u0c37\u0c23\u0c3e\u0c32\u0c41",ss:"%d \u0c38\u0c46\u0c15\u0c28\u0c4d\u0c32\u0c41",m:"\u0c12\u0c15 \u0c28\u0c3f\u0c2e\u0c3f\u0c37\u0c02",mm:"%d \u0c28\u0c3f\u0c2e\u0c3f\u0c37\u0c3e\u0c32\u0c41",h:"\u0c12\u0c15 \u0c17\u0c02\u0c1f",hh:"%d \u0c17\u0c02\u0c1f\u0c32\u0c41",d:"\u0c12\u0c15 \u0c30\u0c4b\u0c1c\u0c41",dd:"%d \u0c30\u0c4b\u0c1c\u0c41\u0c32\u0c41",M:"\u0c12\u0c15 \u0c28\u0c46\u0c32",MM:"%d \u0c28\u0c46\u0c32\u0c32\u0c41",y:"\u0c12\u0c15 \u0c38\u0c02\u0c35\u0c24\u0c4d\u0c38\u0c30\u0c02",yy:"%d \u0c38\u0c02\u0c35\u0c24\u0c4d\u0c38\u0c30\u0c3e\u0c32\u0c41"},dayOfMonthOrdinalParse:/\d{1,2}\u0c35/,ordinal:"%d\u0c35",meridiemParse:/\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f|\u0c09\u0c26\u0c2f\u0c02|\u0c2e\u0c27\u0c4d\u0c2f\u0c3e\u0c39\u0c4d\u0c28\u0c02|\u0c38\u0c3e\u0c2f\u0c02\u0c24\u0c4d\u0c30\u0c02/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f"===a?e<4?e:e+12:"\u0c09\u0c26\u0c2f\u0c02"===a?e:"\u0c2e\u0c27\u0c4d\u0c2f\u0c3e\u0c39\u0c4d\u0c28\u0c02"===a?e>=10?e:e+12:"\u0c38\u0c3e\u0c2f\u0c02\u0c24\u0c4d\u0c30\u0c02"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f":e<10?"\u0c09\u0c26\u0c2f\u0c02":e<17?"\u0c2e\u0c27\u0c4d\u0c2f\u0c3e\u0c39\u0c4d\u0c28\u0c02":e<20?"\u0c38\u0c3e\u0c2f\u0c02\u0c24\u0c4d\u0c30\u0c02":"\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f"},week:{dow:0,doy:6}}),e.defineLocale("tet",{months:"Janeiru_Fevereiru_Marsu_Abril_Maiu_Juniu_Juliu_Augustu_Setembru_Outubru_Novembru_Dezembru".split("_"),monthsShort:"Jan_Fev_Mar_Abr_Mai_Jun_Jul_Aug_Set_Out_Nov_Dez".split("_"),weekdays:"Domingu_Segunda_Tersa_Kuarta_Kinta_Sexta_Sabadu".split("_"),weekdaysShort:"Dom_Seg_Ters_Kua_Kint_Sext_Sab".split("_"),weekdaysMin:"Do_Seg_Te_Ku_Ki_Sex_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Ohin iha] LT",nextDay:"[Aban iha] LT",nextWeek:"dddd [iha] LT",lastDay:"[Horiseik iha] LT",lastWeek:"dddd [semana kotuk] [iha] LT",sameElse:"L"},relativeTime:{future:"iha %s",past:"%s liuba",s:"minutu balun",ss:"minutu %d",m:"minutu ida",mm:"minutus %d",h:"horas ida",hh:"horas %d",d:"loron ida",dd:"loron %d",M:"fulan ida",MM:"fulan %d",y:"tinan ida",yy:"tinan %d"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")},week:{dow:1,doy:4}}),e.defineLocale("th",{months:"\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21_\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c_\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21_\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19_\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21_\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19_\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21_\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21_\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19_\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21_\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19_\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21".split("_"),monthsShort:"\u0e21.\u0e04._\u0e01.\u0e1e._\u0e21\u0e35.\u0e04._\u0e40\u0e21.\u0e22._\u0e1e.\u0e04._\u0e21\u0e34.\u0e22._\u0e01.\u0e04._\u0e2a.\u0e04._\u0e01.\u0e22._\u0e15.\u0e04._\u0e1e.\u0e22._\u0e18.\u0e04.".split("_"),monthsParseExact:!0,weekdays:"\u0e2d\u0e32\u0e17\u0e34\u0e15\u0e22\u0e4c_\u0e08\u0e31\u0e19\u0e17\u0e23\u0e4c_\u0e2d\u0e31\u0e07\u0e04\u0e32\u0e23_\u0e1e\u0e38\u0e18_\u0e1e\u0e24\u0e2b\u0e31\u0e2a\u0e1a\u0e14\u0e35_\u0e28\u0e38\u0e01\u0e23\u0e4c_\u0e40\u0e2a\u0e32\u0e23\u0e4c".split("_"),weekdaysShort:"\u0e2d\u0e32\u0e17\u0e34\u0e15\u0e22\u0e4c_\u0e08\u0e31\u0e19\u0e17\u0e23\u0e4c_\u0e2d\u0e31\u0e07\u0e04\u0e32\u0e23_\u0e1e\u0e38\u0e18_\u0e1e\u0e24\u0e2b\u0e31\u0e2a_\u0e28\u0e38\u0e01\u0e23\u0e4c_\u0e40\u0e2a\u0e32\u0e23\u0e4c".split("_"),weekdaysMin:"\u0e2d\u0e32._\u0e08._\u0e2d._\u0e1e._\u0e1e\u0e24._\u0e28._\u0e2a.".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY \u0e40\u0e27\u0e25\u0e32 H:mm",LLLL:"\u0e27\u0e31\u0e19dddd\u0e17\u0e35\u0e48 D MMMM YYYY \u0e40\u0e27\u0e25\u0e32 H:mm"},meridiemParse:/\u0e01\u0e48\u0e2d\u0e19\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07|\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07/,isPM:function(e){return"\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07"===e},meridiem:function(e,a,t){return e<12?"\u0e01\u0e48\u0e2d\u0e19\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07":"\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07"},calendar:{sameDay:"[\u0e27\u0e31\u0e19\u0e19\u0e35\u0e49 \u0e40\u0e27\u0e25\u0e32] LT",nextDay:"[\u0e1e\u0e23\u0e38\u0e48\u0e07\u0e19\u0e35\u0e49 \u0e40\u0e27\u0e25\u0e32] LT",nextWeek:"dddd[\u0e2b\u0e19\u0e49\u0e32 \u0e40\u0e27\u0e25\u0e32] LT",lastDay:"[\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e27\u0e32\u0e19\u0e19\u0e35\u0e49 \u0e40\u0e27\u0e25\u0e32] LT",lastWeek:"[\u0e27\u0e31\u0e19]dddd[\u0e17\u0e35\u0e48\u0e41\u0e25\u0e49\u0e27 \u0e40\u0e27\u0e25\u0e32] LT",sameElse:"L"},relativeTime:{future:"\u0e2d\u0e35\u0e01 %s",past:"%s\u0e17\u0e35\u0e48\u0e41\u0e25\u0e49\u0e27",s:"\u0e44\u0e21\u0e48\u0e01\u0e35\u0e48\u0e27\u0e34\u0e19\u0e32\u0e17\u0e35",ss:"%d \u0e27\u0e34\u0e19\u0e32\u0e17\u0e35",m:"1 \u0e19\u0e32\u0e17\u0e35",mm:"%d \u0e19\u0e32\u0e17\u0e35",h:"1 \u0e0a\u0e31\u0e48\u0e27\u0e42\u0e21\u0e07",hh:"%d \u0e0a\u0e31\u0e48\u0e27\u0e42\u0e21\u0e07",d:"1 \u0e27\u0e31\u0e19",dd:"%d \u0e27\u0e31\u0e19",M:"1 \u0e40\u0e14\u0e37\u0e2d\u0e19",MM:"%d \u0e40\u0e14\u0e37\u0e2d\u0e19",y:"1 \u0e1b\u0e35",yy:"%d \u0e1b\u0e35"}}),e.defineLocale("tl-ph",{months:"Enero_Pebrero_Marso_Abril_Mayo_Hunyo_Hulyo_Agosto_Setyembre_Oktubre_Nobyembre_Disyembre".split("_"),monthsShort:"Ene_Peb_Mar_Abr_May_Hun_Hul_Ago_Set_Okt_Nob_Dis".split("_"),weekdays:"Linggo_Lunes_Martes_Miyerkules_Huwebes_Biyernes_Sabado".split("_"),weekdaysShort:"Lin_Lun_Mar_Miy_Huw_Biy_Sab".split("_"),weekdaysMin:"Li_Lu_Ma_Mi_Hu_Bi_Sab".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"MM/D/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY HH:mm",LLLL:"dddd, MMMM DD, YYYY HH:mm"},calendar:{sameDay:"LT [ngayong araw]",nextDay:"[Bukas ng] LT",nextWeek:"LT [sa susunod na] dddd",lastDay:"LT [kahapon]",lastWeek:"LT [noong nakaraang] dddd",sameElse:"L"},relativeTime:{future:"sa loob ng %s",past:"%s ang nakalipas",s:"ilang segundo",ss:"%d segundo",m:"isang minuto",mm:"%d minuto",h:"isang oras",hh:"%d oras",d:"isang araw",dd:"%d araw",M:"isang buwan",MM:"%d buwan",y:"isang taon",yy:"%d taon"},dayOfMonthOrdinalParse:/\d{1,2}/,ordinal:function(e){return e},week:{dow:1,doy:4}});var Qn="pagh_wa\u2019_cha\u2019_wej_loS_vagh_jav_Soch_chorgh_Hut".split("_");e.defineLocale("tlh",{months:"tera\u2019 jar wa\u2019_tera\u2019 jar cha\u2019_tera\u2019 jar wej_tera\u2019 jar loS_tera\u2019 jar vagh_tera\u2019 jar jav_tera\u2019 jar Soch_tera\u2019 jar chorgh_tera\u2019 jar Hut_tera\u2019 jar wa\u2019maH_tera\u2019 jar wa\u2019maH wa\u2019_tera\u2019 jar wa\u2019maH cha\u2019".split("_"),monthsShort:"jar wa\u2019_jar cha\u2019_jar wej_jar loS_jar vagh_jar jav_jar Soch_jar chorgh_jar Hut_jar wa\u2019maH_jar wa\u2019maH wa\u2019_jar wa\u2019maH cha\u2019".split("_"),monthsParseExact:!0,weekdays:"lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj".split("_"),weekdaysShort:"lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj".split("_"),weekdaysMin:"lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[DaHjaj] LT",nextDay:"[wa\u2019leS] LT",nextWeek:"LLL",lastDay:"[wa\u2019Hu\u2019] LT",lastWeek:"LLL",sameElse:"L"},relativeTime:{future:function(e){var a=e;return a=-1!==e.indexOf("jaj")?a.slice(0,-3)+"leS":-1!==e.indexOf("jar")?a.slice(0,-3)+"waQ":-1!==e.indexOf("DIS")?a.slice(0,-3)+"nem":a+" pIq"},past:function(e){var a=e;return a=-1!==e.indexOf("jaj")?a.slice(0,-3)+"Hu\u2019":-1!==e.indexOf("jar")?a.slice(0,-3)+"wen":-1!==e.indexOf("DIS")?a.slice(0,-3)+"ben":a+" ret"},s:"puS lup",ss:xa,m:"wa\u2019 tup",mm:xa,h:"wa\u2019 rep",hh:xa,d:"wa\u2019 jaj",dd:xa,M:"wa\u2019 jar",MM:xa,y:"wa\u2019 DIS",yy:xa},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});var Xn={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'\xfcnc\xfc",4:"'\xfcnc\xfc",100:"'\xfcnc\xfc",6:"'nc\u0131",9:"'uncu",10:"'uncu",30:"'uncu",60:"'\u0131nc\u0131",90:"'\u0131nc\u0131"};e.defineLocale("tr",{months:"Ocak_\u015eubat_Mart_Nisan_May\u0131s_Haziran_Temmuz_A\u011fustos_Eyl\xfcl_Ekim_Kas\u0131m_Aral\u0131k".split("_"),monthsShort:"Oca_\u015eub_Mar_Nis_May_Haz_Tem_A\u011fu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Sal\u0131_\xc7ar\u015famba_Per\u015fembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_\xc7ar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_\xc7a_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[bug\xfcn saat] LT",nextDay:"[yar\u0131n saat] LT",nextWeek:"[gelecek] dddd [saat] LT",lastDay:"[d\xfcn] LT",lastWeek:"[ge\xe7en] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s \xf6nce",s:"birka\xe7 saniye",ss:"%d saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir g\xfcn",dd:"%d g\xfcn",M:"bir ay",MM:"%d ay",y:"bir y\u0131l",yy:"%d y\u0131l"},dayOfMonthOrdinalParse:/\d{1,2}'(inci|nci|\xfcnc\xfc|nc\u0131|uncu|\u0131nc\u0131)/,ordinal:function(e){if(0===e)return e+"'\u0131nc\u0131";var a=e%10;return e+(Xn[a]||Xn[e%100-a]||Xn[e>=100?100:null])},week:{dow:1,doy:7}}),e.defineLocale("tzl",{months:"Januar_Fevraglh_Mar\xe7_Avr\xefu_Mai_G\xfcn_Julia_Guscht_Setemvar_Listop\xe4ts_Noemvar_Zecemvar".split("_"),monthsShort:"Jan_Fev_Mar_Avr_Mai_G\xfcn_Jul_Gus_Set_Lis_Noe_Zec".split("_"),weekdays:"S\xfaladi_L\xfane\xe7i_Maitzi_M\xe1rcuri_Xh\xfaadi_Vi\xe9ner\xe7i_S\xe1turi".split("_"),weekdaysShort:"S\xfal_L\xfan_Mai_M\xe1r_Xh\xfa_Vi\xe9_S\xe1t".split("_"),weekdaysMin:"S\xfa_L\xfa_Ma_M\xe1_Xh_Vi_S\xe1".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"D. MMMM [dallas] YYYY",LLL:"D. MMMM [dallas] YYYY HH.mm",LLLL:"dddd, [li] D. MMMM [dallas] YYYY HH.mm"},meridiemParse:/d\'o|d\'a/i,isPM:function(e){return"d'o"===e.toLowerCase()},meridiem:function(e,a,t){return e>11?t?"d'o":"D'O":t?"d'a":"D'A"},calendar:{sameDay:"[oxhi \xe0] LT",nextDay:"[dem\xe0 \xe0] LT",nextWeek:"dddd [\xe0] LT",lastDay:"[ieiri \xe0] LT",lastWeek:"[s\xfcr el] dddd [lasteu \xe0] LT",sameElse:"L"},relativeTime:{future:"osprei %s",past:"ja%s",s:Pa,ss:Pa,m:Pa,mm:Pa,h:Pa,hh:Pa,d:Pa,dd:Pa,M:Pa,MM:Pa,y:Pa,yy:Pa},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("tzm-latn",{months:"innayr_br\u02e4ayr\u02e4_mar\u02e4s\u02e4_ibrir_mayyw_ywnyw_ywlywz_\u0263w\u0161t_\u0161wtanbir_kt\u02e4wbr\u02e4_nwwanbir_dwjnbir".split("_"),monthsShort:"innayr_br\u02e4ayr\u02e4_mar\u02e4s\u02e4_ibrir_mayyw_ywnyw_ywlywz_\u0263w\u0161t_\u0161wtanbir_kt\u02e4wbr\u02e4_nwwanbir_dwjnbir".split("_"),weekdays:"asamas_aynas_asinas_akras_akwas_asimwas_asi\u1e0dyas".split("_"),weekdaysShort:"asamas_aynas_asinas_akras_akwas_asimwas_asi\u1e0dyas".split("_"),weekdaysMin:"asamas_aynas_asinas_akras_akwas_asimwas_asi\u1e0dyas".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[asdkh g] LT",nextDay:"[aska g] LT",nextWeek:"dddd [g] LT",lastDay:"[assant g] LT",lastWeek:"dddd [g] LT",sameElse:"L"},relativeTime:{future:"dadkh s yan %s",past:"yan %s",s:"imik",ss:"%d imik",m:"minu\u1e0d",mm:"%d minu\u1e0d",h:"sa\u025ba",hh:"%d tassa\u025bin",d:"ass",dd:"%d ossan",M:"ayowr",MM:"%d iyyirn",y:"asgas",yy:"%d isgasn"},week:{dow:6,doy:12}}),e.defineLocale("tzm",{months:"\u2d49\u2d4f\u2d4f\u2d30\u2d62\u2d54_\u2d31\u2d55\u2d30\u2d62\u2d55_\u2d4e\u2d30\u2d55\u2d5a_\u2d49\u2d31\u2d54\u2d49\u2d54_\u2d4e\u2d30\u2d62\u2d62\u2d53_\u2d62\u2d53\u2d4f\u2d62\u2d53_\u2d62\u2d53\u2d4d\u2d62\u2d53\u2d63_\u2d56\u2d53\u2d5b\u2d5c_\u2d5b\u2d53\u2d5c\u2d30\u2d4f\u2d31\u2d49\u2d54_\u2d3d\u2d5f\u2d53\u2d31\u2d55_\u2d4f\u2d53\u2d61\u2d30\u2d4f\u2d31\u2d49\u2d54_\u2d37\u2d53\u2d4a\u2d4f\u2d31\u2d49\u2d54".split("_"),monthsShort:"\u2d49\u2d4f\u2d4f\u2d30\u2d62\u2d54_\u2d31\u2d55\u2d30\u2d62\u2d55_\u2d4e\u2d30\u2d55\u2d5a_\u2d49\u2d31\u2d54\u2d49\u2d54_\u2d4e\u2d30\u2d62\u2d62\u2d53_\u2d62\u2d53\u2d4f\u2d62\u2d53_\u2d62\u2d53\u2d4d\u2d62\u2d53\u2d63_\u2d56\u2d53\u2d5b\u2d5c_\u2d5b\u2d53\u2d5c\u2d30\u2d4f\u2d31\u2d49\u2d54_\u2d3d\u2d5f\u2d53\u2d31\u2d55_\u2d4f\u2d53\u2d61\u2d30\u2d4f\u2d31\u2d49\u2d54_\u2d37\u2d53\u2d4a\u2d4f\u2d31\u2d49\u2d54".split("_"),weekdays:"\u2d30\u2d59\u2d30\u2d4e\u2d30\u2d59_\u2d30\u2d62\u2d4f\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4f\u2d30\u2d59_\u2d30\u2d3d\u2d54\u2d30\u2d59_\u2d30\u2d3d\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4e\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d39\u2d62\u2d30\u2d59".split("_"),weekdaysShort:"\u2d30\u2d59\u2d30\u2d4e\u2d30\u2d59_\u2d30\u2d62\u2d4f\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4f\u2d30\u2d59_\u2d30\u2d3d\u2d54\u2d30\u2d59_\u2d30\u2d3d\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4e\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d39\u2d62\u2d30\u2d59".split("_"),weekdaysMin:"\u2d30\u2d59\u2d30\u2d4e\u2d30\u2d59_\u2d30\u2d62\u2d4f\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4f\u2d30\u2d59_\u2d30\u2d3d\u2d54\u2d30\u2d59_\u2d30\u2d3d\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4e\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d39\u2d62\u2d30\u2d59".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u2d30\u2d59\u2d37\u2d45 \u2d34] LT",nextDay:"[\u2d30\u2d59\u2d3d\u2d30 \u2d34] LT",nextWeek:"dddd [\u2d34] LT",lastDay:"[\u2d30\u2d5a\u2d30\u2d4f\u2d5c \u2d34] LT",lastWeek:"dddd [\u2d34] LT",sameElse:"L"},relativeTime:{future:"\u2d37\u2d30\u2d37\u2d45 \u2d59 \u2d62\u2d30\u2d4f %s",past:"\u2d62\u2d30\u2d4f %s",s:"\u2d49\u2d4e\u2d49\u2d3d",ss:"%d \u2d49\u2d4e\u2d49\u2d3d",m:"\u2d4e\u2d49\u2d4f\u2d53\u2d3a",mm:"%d \u2d4e\u2d49\u2d4f\u2d53\u2d3a",h:"\u2d59\u2d30\u2d44\u2d30",hh:"%d \u2d5c\u2d30\u2d59\u2d59\u2d30\u2d44\u2d49\u2d4f",d:"\u2d30\u2d59\u2d59",dd:"%d o\u2d59\u2d59\u2d30\u2d4f",M:"\u2d30\u2d62o\u2d53\u2d54",MM:"%d \u2d49\u2d62\u2d62\u2d49\u2d54\u2d4f",y:"\u2d30\u2d59\u2d33\u2d30\u2d59",yy:"%d \u2d49\u2d59\u2d33\u2d30\u2d59\u2d4f"},week:{dow:6,doy:12}}),e.defineLocale("uk",{months:{format:"\u0441\u0456\u0447\u043d\u044f_\u043b\u044e\u0442\u043e\u0433\u043e_\u0431\u0435\u0440\u0435\u0437\u043d\u044f_\u043a\u0432\u0456\u0442\u043d\u044f_\u0442\u0440\u0430\u0432\u043d\u044f_\u0447\u0435\u0440\u0432\u043d\u044f_\u043b\u0438\u043f\u043d\u044f_\u0441\u0435\u0440\u043f\u043d\u044f_\u0432\u0435\u0440\u0435\u0441\u043d\u044f_\u0436\u043e\u0432\u0442\u043d\u044f_\u043b\u0438\u0441\u0442\u043e\u043f\u0430\u0434\u0430_\u0433\u0440\u0443\u0434\u043d\u044f".split("_"),standalone:"\u0441\u0456\u0447\u0435\u043d\u044c_\u043b\u044e\u0442\u0438\u0439_\u0431\u0435\u0440\u0435\u0437\u0435\u043d\u044c_\u043a\u0432\u0456\u0442\u0435\u043d\u044c_\u0442\u0440\u0430\u0432\u0435\u043d\u044c_\u0447\u0435\u0440\u0432\u0435\u043d\u044c_\u043b\u0438\u043f\u0435\u043d\u044c_\u0441\u0435\u0440\u043f\u0435\u043d\u044c_\u0432\u0435\u0440\u0435\u0441\u0435\u043d\u044c_\u0436\u043e\u0432\u0442\u0435\u043d\u044c_\u043b\u0438\u0441\u0442\u043e\u043f\u0430\u0434_\u0433\u0440\u0443\u0434\u0435\u043d\u044c".split("_")},monthsShort:"\u0441\u0456\u0447_\u043b\u044e\u0442_\u0431\u0435\u0440_\u043a\u0432\u0456\u0442_\u0442\u0440\u0430\u0432_\u0447\u0435\u0440\u0432_\u043b\u0438\u043f_\u0441\u0435\u0440\u043f_\u0432\u0435\u0440_\u0436\u043e\u0432\u0442_\u043b\u0438\u0441\u0442_\u0433\u0440\u0443\u0434".split("_"),weekdays:function(e,a){var t={nominative:"\u043d\u0435\u0434\u0456\u043b\u044f_\u043f\u043e\u043d\u0435\u0434\u0456\u043b\u043e\u043a_\u0432\u0456\u0432\u0442\u043e\u0440\u043e\u043a_\u0441\u0435\u0440\u0435\u0434\u0430_\u0447\u0435\u0442\u0432\u0435\u0440_\u043f\u2019\u044f\u0442\u043d\u0438\u0446\u044f_\u0441\u0443\u0431\u043e\u0442\u0430".split("_"),accusative:"\u043d\u0435\u0434\u0456\u043b\u044e_\u043f\u043e\u043d\u0435\u0434\u0456\u043b\u043e\u043a_\u0432\u0456\u0432\u0442\u043e\u0440\u043e\u043a_\u0441\u0435\u0440\u0435\u0434\u0443_\u0447\u0435\u0442\u0432\u0435\u0440_\u043f\u2019\u044f\u0442\u043d\u0438\u0446\u044e_\u0441\u0443\u0431\u043e\u0442\u0443".split("_"),genitive:"\u043d\u0435\u0434\u0456\u043b\u0456_\u043f\u043e\u043d\u0435\u0434\u0456\u043b\u043a\u0430_\u0432\u0456\u0432\u0442\u043e\u0440\u043a\u0430_\u0441\u0435\u0440\u0435\u0434\u0438_\u0447\u0435\u0442\u0432\u0435\u0440\u0433\u0430_\u043f\u2019\u044f\u0442\u043d\u0438\u0446\u0456_\u0441\u0443\u0431\u043e\u0442\u0438".split("_")};return e?t[/(\[[\u0412\u0432\u0423\u0443]\]) ?dddd/.test(a)?"accusative":/\[?(?:\u043c\u0438\u043d\u0443\u043b\u043e\u0457|\u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u0457)? ?\] ?dddd/.test(a)?"genitive":"nominative"][e.day()]:t.nominative},weekdaysShort:"\u043d\u0434_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),weekdaysMin:"\u043d\u0434_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY \u0440.",LLL:"D MMMM YYYY \u0440., HH:mm",LLLL:"dddd, D MMMM YYYY \u0440., HH:mm"},calendar:{sameDay:Wa("[\u0421\u044c\u043e\u0433\u043e\u0434\u043d\u0456 "),nextDay:Wa("[\u0417\u0430\u0432\u0442\u0440\u0430 "),lastDay:Wa("[\u0412\u0447\u043e\u0440\u0430 "),nextWeek:Wa("[\u0423] dddd ["),lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return Wa("[\u041c\u0438\u043d\u0443\u043b\u043e\u0457] dddd [").call(this);case 1:case 2:case 4:return Wa("[\u041c\u0438\u043d\u0443\u043b\u043e\u0433\u043e] dddd [").call(this)}},sameElse:"L"},relativeTime:{future:"\u0437\u0430 %s",past:"%s \u0442\u043e\u043c\u0443",s:"\u0434\u0435\u043a\u0456\u043b\u044c\u043a\u0430 \u0441\u0435\u043a\u0443\u043d\u0434",ss:Oa,m:Oa,mm:Oa,h:"\u0433\u043e\u0434\u0438\u043d\u0443",hh:Oa,d:"\u0434\u0435\u043d\u044c",dd:Oa,M:"\u043c\u0456\u0441\u044f\u0446\u044c",MM:Oa,y:"\u0440\u0456\u043a",yy:Oa},meridiemParse:/\u043d\u043e\u0447\u0456|\u0440\u0430\u043d\u043a\u0443|\u0434\u043d\u044f|\u0432\u0435\u0447\u043e\u0440\u0430/,isPM:function(e){return/^(\u0434\u043d\u044f|\u0432\u0435\u0447\u043e\u0440\u0430)$/.test(e)},meridiem:function(e,a,t){return e<4?"\u043d\u043e\u0447\u0456":e<12?"\u0440\u0430\u043d\u043a\u0443":e<17?"\u0434\u043d\u044f":"\u0432\u0435\u0447\u043e\u0440\u0430"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0439|\u0433\u043e)/,ordinal:function(e,a){switch(a){case"M":case"d":case"DDD":case"w":case"W":return e+"-\u0439";case"D":return e+"-\u0433\u043e";default:return e}},week:{dow:1,doy:7}});var ed=["\u062c\u0646\u0648\u0631\u06cc","\u0641\u0631\u0648\u0631\u06cc","\u0645\u0627\u0631\u0686","\u0627\u067e\u0631\u06cc\u0644","\u0645\u0626\u06cc","\u062c\u0648\u0646","\u062c\u0648\u0644\u0627\u0626\u06cc","\u0627\u06af\u0633\u062a","\u0633\u062a\u0645\u0628\u0631","\u0627\u06a9\u062a\u0648\u0628\u0631","\u0646\u0648\u0645\u0628\u0631","\u062f\u0633\u0645\u0628\u0631"],ad=["\u0627\u062a\u0648\u0627\u0631","\u067e\u06cc\u0631","\u0645\u0646\u06af\u0644","\u0628\u062f\u06be","\u062c\u0645\u0639\u0631\u0627\u062a","\u062c\u0645\u0639\u06c1","\u06c1\u0641\u062a\u06c1"];return e.defineLocale("ur",{months:ed,monthsShort:ed,weekdays:ad,weekdaysShort:ad,weekdaysMin:ad,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd\u060c D MMMM YYYY HH:mm"},meridiemParse:/\u0635\u0628\u062d|\u0634\u0627\u0645/,isPM:function(e){return"\u0634\u0627\u0645"===e},meridiem:function(e,a,t){return e<12?"\u0635\u0628\u062d":"\u0634\u0627\u0645"},calendar:{sameDay:"[\u0622\u062c \u0628\u0648\u0642\u062a] LT",nextDay:"[\u06a9\u0644 \u0628\u0648\u0642\u062a] LT",nextWeek:"dddd [\u0628\u0648\u0642\u062a] LT",lastDay:"[\u06af\u0630\u0634\u062a\u06c1 \u0631\u0648\u0632 \u0628\u0648\u0642\u062a] LT",lastWeek:"[\u06af\u0630\u0634\u062a\u06c1] dddd [\u0628\u0648\u0642\u062a] LT",sameElse:"L"},relativeTime:{future:"%s \u0628\u0639\u062f",past:"%s \u0642\u0628\u0644",s:"\u0686\u0646\u062f \u0633\u06cc\u06a9\u0646\u0688",ss:"%d \u0633\u06cc\u06a9\u0646\u0688",m:"\u0627\u06cc\u06a9 \u0645\u0646\u0679",mm:"%d \u0645\u0646\u0679",h:"\u0627\u06cc\u06a9 \u06af\u06be\u0646\u0679\u06c1",hh:"%d \u06af\u06be\u0646\u0679\u06d2",d:"\u0627\u06cc\u06a9 \u062f\u0646",dd:"%d \u062f\u0646",M:"\u0627\u06cc\u06a9 \u0645\u0627\u06c1",MM:"%d \u0645\u0627\u06c1",y:"\u0627\u06cc\u06a9 \u0633\u0627\u0644",yy:"%d \u0633\u0627\u0644"},preparse:function(e){return e.replace(/\u060c/g,",")},postformat:function(e){return e.replace(/,/g,"\u060c")},week:{dow:1,doy:4}}),e.defineLocale("uz-latn",{months:"Yanvar_Fevral_Mart_Aprel_May_Iyun_Iyul_Avgust_Sentabr_Oktabr_Noyabr_Dekabr".split("_"),monthsShort:"Yan_Fev_Mar_Apr_May_Iyun_Iyul_Avg_Sen_Okt_Noy_Dek".split("_"),weekdays:"Yakshanba_Dushanba_Seshanba_Chorshanba_Payshanba_Juma_Shanba".split("_"),weekdaysShort:"Yak_Dush_Sesh_Chor_Pay_Jum_Shan".split("_"),weekdaysMin:"Ya_Du_Se_Cho_Pa_Ju_Sha".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"D MMMM YYYY, dddd HH:mm"},calendar:{sameDay:"[Bugun soat] LT [da]",nextDay:"[Ertaga] LT [da]",nextWeek:"dddd [kuni soat] LT [da]",lastDay:"[Kecha soat] LT [da]",lastWeek:"[O'tgan] dddd [kuni soat] LT [da]",sameElse:"L"},relativeTime:{future:"Yaqin %s ichida",past:"Bir necha %s oldin",s:"soniya",ss:"%d soniya",m:"bir daqiqa",mm:"%d daqiqa",h:"bir soat",hh:"%d soat",d:"bir kun",dd:"%d kun",M:"bir oy",MM:"%d oy",y:"bir yil",yy:"%d yil"},week:{dow:1,doy:7}}),e.defineLocale("uz",{months:"\u044f\u043d\u0432\u0430\u0440_\u0444\u0435\u0432\u0440\u0430\u043b_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0435\u043b_\u043c\u0430\u0439_\u0438\u044e\u043d_\u0438\u044e\u043b_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043d\u0442\u044f\u0431\u0440_\u043e\u043a\u0442\u044f\u0431\u0440_\u043d\u043e\u044f\u0431\u0440_\u0434\u0435\u043a\u0430\u0431\u0440".split("_"),monthsShort:"\u044f\u043d\u0432_\u0444\u0435\u0432_\u043c\u0430\u0440_\u0430\u043f\u0440_\u043c\u0430\u0439_\u0438\u044e\u043d_\u0438\u044e\u043b_\u0430\u0432\u0433_\u0441\u0435\u043d_\u043e\u043a\u0442_\u043d\u043e\u044f_\u0434\u0435\u043a".split("_"),weekdays:"\u042f\u043a\u0448\u0430\u043d\u0431\u0430_\u0414\u0443\u0448\u0430\u043d\u0431\u0430_\u0421\u0435\u0448\u0430\u043d\u0431\u0430_\u0427\u043e\u0440\u0448\u0430\u043d\u0431\u0430_\u041f\u0430\u0439\u0448\u0430\u043d\u0431\u0430_\u0416\u0443\u043c\u0430_\u0428\u0430\u043d\u0431\u0430".split("_"),weekdaysShort:"\u042f\u043a\u0448_\u0414\u0443\u0448_\u0421\u0435\u0448_\u0427\u043e\u0440_\u041f\u0430\u0439_\u0416\u0443\u043c_\u0428\u0430\u043d".split("_"),weekdaysMin:"\u042f\u043a_\u0414\u0443_\u0421\u0435_\u0427\u043e_\u041f\u0430_\u0416\u0443_\u0428\u0430".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"D MMMM YYYY, dddd HH:mm"},calendar:{sameDay:"[\u0411\u0443\u0433\u0443\u043d \u0441\u043e\u0430\u0442] LT [\u0434\u0430]",nextDay:"[\u042d\u0440\u0442\u0430\u0433\u0430] LT [\u0434\u0430]",nextWeek:"dddd [\u043a\u0443\u043d\u0438 \u0441\u043e\u0430\u0442] LT [\u0434\u0430]",lastDay:"[\u041a\u0435\u0447\u0430 \u0441\u043e\u0430\u0442] LT [\u0434\u0430]",lastWeek:"[\u0423\u0442\u0433\u0430\u043d] dddd [\u043a\u0443\u043d\u0438 \u0441\u043e\u0430\u0442] LT [\u0434\u0430]",sameElse:"L"},relativeTime:{future:"\u042f\u043a\u0438\u043d %s \u0438\u0447\u0438\u0434\u0430",past:"\u0411\u0438\u0440 \u043d\u0435\u0447\u0430 %s \u043e\u043b\u0434\u0438\u043d",s:"\u0444\u0443\u0440\u0441\u0430\u0442",ss:"%d \u0444\u0443\u0440\u0441\u0430\u0442",m:"\u0431\u0438\u0440 \u0434\u0430\u043a\u0438\u043a\u0430",mm:"%d \u0434\u0430\u043a\u0438\u043a\u0430",h:"\u0431\u0438\u0440 \u0441\u043e\u0430\u0442",hh:"%d \u0441\u043e\u0430\u0442",d:"\u0431\u0438\u0440 \u043a\u0443\u043d",dd:"%d \u043a\u0443\u043d",M:"\u0431\u0438\u0440 \u043e\u0439",MM:"%d \u043e\u0439",y:"\u0431\u0438\u0440 \u0439\u0438\u043b",yy:"%d \u0439\u0438\u043b"},week:{dow:1,doy:7}}),e.defineLocale("vi",{months:"th\xe1ng 1_th\xe1ng 2_th\xe1ng 3_th\xe1ng 4_th\xe1ng 5_th\xe1ng 6_th\xe1ng 7_th\xe1ng 8_th\xe1ng 9_th\xe1ng 10_th\xe1ng 11_th\xe1ng 12".split("_"),monthsShort:"Th01_Th02_Th03_Th04_Th05_Th06_Th07_Th08_Th09_Th10_Th11_Th12".split("_"),monthsParseExact:!0,weekdays:"ch\u1ee7 nh\u1eadt_th\u1ee9 hai_th\u1ee9 ba_th\u1ee9 t\u01b0_th\u1ee9 n\u0103m_th\u1ee9 s\xe1u_th\u1ee9 b\u1ea3y".split("_"),weekdaysShort:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysMin:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysParseExact:!0,meridiemParse:/sa|ch/i,isPM:function(e){return/^ch$/i.test(e)},meridiem:function(e,a,t){return e<12?t?"sa":"SA":t?"ch":"CH"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM [n\u0103m] YYYY",LLL:"D MMMM [n\u0103m] YYYY HH:mm",LLLL:"dddd, D MMMM [n\u0103m] YYYY HH:mm",l:"DD/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY HH:mm",llll:"ddd, D MMM YYYY HH:mm"},calendar:{sameDay:"[H\xf4m nay l\xfac] LT",nextDay:"[Ng\xe0y mai l\xfac] LT",nextWeek:"dddd [tu\u1ea7n t\u1edbi l\xfac] LT",lastDay:"[H\xf4m qua l\xfac] LT",lastWeek:"dddd [tu\u1ea7n r\u1ed3i l\xfac] LT",sameElse:"L"},relativeTime:{future:"%s t\u1edbi",past:"%s tr\u01b0\u1edbc",s:"v\xe0i gi\xe2y",ss:"%d gi\xe2y",m:"m\u1ed9t ph\xfat",mm:"%d ph\xfat",h:"m\u1ed9t gi\u1edd",hh:"%d gi\u1edd",d:"m\u1ed9t ng\xe0y",dd:"%d ng\xe0y",M:"m\u1ed9t th\xe1ng",MM:"%d th\xe1ng",y:"m\u1ed9t n\u0103m",yy:"%d n\u0103m"},dayOfMonthOrdinalParse:/\d{1,2}/,ordinal:function(e){return e},week:{dow:1,doy:4}}),e.defineLocale("x-pseudo",{months:"J~\xe1\xf1\xfa\xe1~r\xfd_F~\xe9br\xfa~\xe1r\xfd_~M\xe1rc~h_\xc1p~r\xedl_~M\xe1\xfd_~J\xfa\xf1\xe9~_J\xfal~\xfd_\xc1\xfa~g\xfast~_S\xe9p~t\xe9mb~\xe9r_\xd3~ct\xf3b~\xe9r_\xd1~\xf3v\xe9m~b\xe9r_~D\xe9c\xe9~mb\xe9r".split("_"),monthsShort:"J~\xe1\xf1_~F\xe9b_~M\xe1r_~\xc1pr_~M\xe1\xfd_~J\xfa\xf1_~J\xfal_~\xc1\xfag_~S\xe9p_~\xd3ct_~\xd1\xf3v_~D\xe9c".split("_"),monthsParseExact:!0,weekdays:"S~\xfa\xf1d\xe1~\xfd_M\xf3~\xf1d\xe1\xfd~_T\xfa\xe9~sd\xe1\xfd~_W\xe9d~\xf1\xe9sd~\xe1\xfd_T~h\xfars~d\xe1\xfd_~Fr\xedd~\xe1\xfd_S~\xe1t\xfar~d\xe1\xfd".split("_"),weekdaysShort:"S~\xfa\xf1_~M\xf3\xf1_~T\xfa\xe9_~W\xe9d_~Th\xfa_~Fr\xed_~S\xe1t".split("_"),weekdaysMin:"S~\xfa_M\xf3~_T\xfa_~W\xe9_T~h_Fr~_S\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[T~\xf3d\xe1~\xfd \xe1t] LT",nextDay:"[T~\xf3m\xf3~rr\xf3~w \xe1t] LT",nextWeek:"dddd [\xe1t] LT",lastDay:"[\xdd~\xe9st~\xe9rd\xe1~\xfd \xe1t] LT",lastWeek:"[L~\xe1st] dddd [\xe1t] LT",sameElse:"L"},relativeTime:{future:"\xed~\xf1 %s",past:"%s \xe1~g\xf3",s:"\xe1 ~f\xe9w ~s\xe9c\xf3~\xf1ds",ss:"%d s~\xe9c\xf3\xf1~ds",m:"\xe1 ~m\xed\xf1~\xfat\xe9",mm:"%d m~\xed\xf1\xfa~t\xe9s",h:"\xe1~\xf1 h\xf3~\xfar",hh:"%d h~\xf3\xfars",d:"\xe1 ~d\xe1\xfd",dd:"%d d~\xe1\xfds",M:"\xe1 ~m\xf3\xf1~th",MM:"%d m~\xf3\xf1t~hs",y:"\xe1 ~\xfd\xe9\xe1r",yy:"%d \xfd~\xe9\xe1rs"},dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")},week:{dow:1,doy:4}}),e.defineLocale("yo",{months:"S\u1eb9\u0301r\u1eb9\u0301_E\u0300re\u0300le\u0300_\u1eb8r\u1eb9\u0300na\u0300_I\u0300gbe\u0301_E\u0300bibi_O\u0300ku\u0300du_Ag\u1eb9mo_O\u0300gu\u0301n_Owewe_\u1ecc\u0300wa\u0300ra\u0300_Be\u0301lu\u0301_\u1ecc\u0300p\u1eb9\u0300\u0300".split("_"),monthsShort:"S\u1eb9\u0301r_E\u0300rl_\u1eb8rn_I\u0300gb_E\u0300bi_O\u0300ku\u0300_Ag\u1eb9_O\u0300gu\u0301_Owe_\u1ecc\u0300wa\u0300_Be\u0301l_\u1ecc\u0300p\u1eb9\u0300\u0300".split("_"),weekdays:"A\u0300i\u0300ku\u0301_Aje\u0301_I\u0300s\u1eb9\u0301gun_\u1eccj\u1ecd\u0301ru\u0301_\u1eccj\u1ecd\u0301b\u1ecd_\u1eb8ti\u0300_A\u0300ba\u0301m\u1eb9\u0301ta".split("_"),weekdaysShort:"A\u0300i\u0300k_Aje\u0301_I\u0300s\u1eb9\u0301_\u1eccjr_\u1eccjb_\u1eb8ti\u0300_A\u0300ba\u0301".split("_"),weekdaysMin:"A\u0300i\u0300_Aj_I\u0300s_\u1eccr_\u1eccb_\u1eb8t_A\u0300b".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[O\u0300ni\u0300 ni] LT",nextDay:"[\u1ecc\u0300la ni] LT",nextWeek:"dddd [\u1eccs\u1eb9\u0300 to\u0301n'b\u1ecd] [ni] LT",lastDay:"[A\u0300na ni] LT",lastWeek:"dddd [\u1eccs\u1eb9\u0300 to\u0301l\u1ecd\u0301] [ni] LT",sameElse:"L"},relativeTime:{future:"ni\u0301 %s",past:"%s k\u1ecdja\u0301",s:"i\u0300s\u1eb9ju\u0301 aaya\u0301 die",ss:"aaya\u0301 %d",m:"i\u0300s\u1eb9ju\u0301 kan",mm:"i\u0300s\u1eb9ju\u0301 %d",h:"wa\u0301kati kan",hh:"wa\u0301kati %d",d:"\u1ecdj\u1ecd\u0301 kan",dd:"\u1ecdj\u1ecd\u0301 %d",M:"osu\u0300 kan",MM:"osu\u0300 %d",y:"\u1ecddu\u0301n kan",yy:"\u1ecddu\u0301n %d"},dayOfMonthOrdinalParse:/\u1ecdj\u1ecd\u0301\s\d{1,2}/,ordinal:"\u1ecdj\u1ecd\u0301 %d",week:{dow:1,doy:4}}),e.defineLocale("zh-cn",{months:"\u4e00\u6708_\u4e8c\u6708_\u4e09\u6708_\u56db\u6708_\u4e94\u6708_\u516d\u6708_\u4e03\u6708_\u516b\u6708_\u4e5d\u6708_\u5341\u6708_\u5341\u4e00\u6708_\u5341\u4e8c\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u661f\u671f\u65e5_\u661f\u671f\u4e00_\u661f\u671f\u4e8c_\u661f\u671f\u4e09_\u661f\u671f\u56db_\u661f\u671f\u4e94_\u661f\u671f\u516d".split("_"),weekdaysShort:"\u5468\u65e5_\u5468\u4e00_\u5468\u4e8c_\u5468\u4e09_\u5468\u56db_\u5468\u4e94_\u5468\u516d".split("_"),weekdaysMin:"\u65e5_\u4e00_\u4e8c_\u4e09_\u56db_\u4e94_\u516d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5Ah\u70b9mm\u5206",LLLL:"YYYY\u5e74M\u6708D\u65e5ddddAh\u70b9mm\u5206",l:"YYYY/M/D",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm"},meridiemParse:/\u51cc\u6668|\u65e9\u4e0a|\u4e0a\u5348|\u4e2d\u5348|\u4e0b\u5348|\u665a\u4e0a/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u51cc\u6668"===a||"\u65e9\u4e0a"===a||"\u4e0a\u5348"===a?e:"\u4e0b\u5348"===a||"\u665a\u4e0a"===a?e+12:e>=11?e:e+12},meridiem:function(e,a,t){var s=100*e+a;return s<600?"\u51cc\u6668":s<900?"\u65e9\u4e0a":s<1130?"\u4e0a\u5348":s<1230?"\u4e2d\u5348":s<1800?"\u4e0b\u5348":"\u665a\u4e0a"},calendar:{sameDay:"[\u4eca\u5929]LT",nextDay:"[\u660e\u5929]LT",nextWeek:"[\u4e0b]ddddLT",lastDay:"[\u6628\u5929]LT",lastWeek:"[\u4e0a]ddddLT",sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}(\u65e5|\u6708|\u5468)/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"\u65e5";case"M":return e+"\u6708";case"w":case"W":return e+"\u5468";default:return e}},relativeTime:{future:"%s\u5185",past:"%s\u524d",s:"\u51e0\u79d2",ss:"%d \u79d2",m:"1 \u5206\u949f",mm:"%d \u5206\u949f",h:"1 \u5c0f\u65f6",hh:"%d \u5c0f\u65f6",d:"1 \u5929",dd:"%d \u5929",M:"1 \u4e2a\u6708",MM:"%d \u4e2a\u6708",y:"1 \u5e74",yy:"%d \u5e74"},week:{dow:1,doy:4}}),e.defineLocale("zh-hk",{months:"\u4e00\u6708_\u4e8c\u6708_\u4e09\u6708_\u56db\u6708_\u4e94\u6708_\u516d\u6708_\u4e03\u6708_\u516b\u6708_\u4e5d\u6708_\u5341\u6708_\u5341\u4e00\u6708_\u5341\u4e8c\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u661f\u671f\u65e5_\u661f\u671f\u4e00_\u661f\u671f\u4e8c_\u661f\u671f\u4e09_\u661f\u671f\u56db_\u661f\u671f\u4e94_\u661f\u671f\u516d".split("_"),weekdaysShort:"\u9031\u65e5_\u9031\u4e00_\u9031\u4e8c_\u9031\u4e09_\u9031\u56db_\u9031\u4e94_\u9031\u516d".split("_"),weekdaysMin:"\u65e5_\u4e00_\u4e8c_\u4e09_\u56db_\u4e94_\u516d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5 HH:mm",LLLL:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm",l:"YYYY/M/D",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm"},meridiemParse:/\u51cc\u6668|\u65e9\u4e0a|\u4e0a\u5348|\u4e2d\u5348|\u4e0b\u5348|\u665a\u4e0a/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u51cc\u6668"===a||"\u65e9\u4e0a"===a||"\u4e0a\u5348"===a?e:"\u4e2d\u5348"===a?e>=11?e:e+12:"\u4e0b\u5348"===a||"\u665a\u4e0a"===a?e+12:void 0},meridiem:function(e,a,t){var s=100*e+a;return s<600?"\u51cc\u6668":s<900?"\u65e9\u4e0a":s<1130?"\u4e0a\u5348":s<1230?"\u4e2d\u5348":s<1800?"\u4e0b\u5348":"\u665a\u4e0a"},calendar:{sameDay:"[\u4eca\u5929]LT",nextDay:"[\u660e\u5929]LT",nextWeek:"[\u4e0b]ddddLT",lastDay:"[\u6628\u5929]LT",lastWeek:"[\u4e0a]ddddLT",sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}(\u65e5|\u6708|\u9031)/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"\u65e5";case"M":return e+"\u6708";case"w":case"W":return e+"\u9031";default:return e}},relativeTime:{future:"%s\u5167",past:"%s\u524d",s:"\u5e7e\u79d2",ss:"%d \u79d2",m:"1 \u5206\u9418",mm:"%d \u5206\u9418",h:"1 \u5c0f\u6642",hh:"%d \u5c0f\u6642",d:"1 \u5929",dd:"%d \u5929",M:"1 \u500b\u6708",MM:"%d \u500b\u6708",y:"1 \u5e74",yy:"%d \u5e74"}}),e.defineLocale("zh-tw",{months:"\u4e00\u6708_\u4e8c\u6708_\u4e09\u6708_\u56db\u6708_\u4e94\u6708_\u516d\u6708_\u4e03\u6708_\u516b\u6708_\u4e5d\u6708_\u5341\u6708_\u5341\u4e00\u6708_\u5341\u4e8c\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u661f\u671f\u65e5_\u661f\u671f\u4e00_\u661f\u671f\u4e8c_\u661f\u671f\u4e09_\u661f\u671f\u56db_\u661f\u671f\u4e94_\u661f\u671f\u516d".split("_"),weekdaysShort:"\u9031\u65e5_\u9031\u4e00_\u9031\u4e8c_\u9031\u4e09_\u9031\u56db_\u9031\u4e94_\u9031\u516d".split("_"),weekdaysMin:"\u65e5_\u4e00_\u4e8c_\u4e09_\u56db_\u4e94_\u516d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5 HH:mm",LLLL:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm",l:"YYYY/M/D",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm"},meridiemParse:/\u51cc\u6668|\u65e9\u4e0a|\u4e0a\u5348|\u4e2d\u5348|\u4e0b\u5348|\u665a\u4e0a/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u51cc\u6668"===a||"\u65e9\u4e0a"===a||"\u4e0a\u5348"===a?e:"\u4e2d\u5348"===a?e>=11?e:e+12:"\u4e0b\u5348"===a||"\u665a\u4e0a"===a?e+12:void 0},meridiem:function(e,a,t){var s=100*e+a;return s<600?"\u51cc\u6668":s<900?"\u65e9\u4e0a":s<1130?"\u4e0a\u5348":s<1230?"\u4e2d\u5348":s<1800?"\u4e0b\u5348":"\u665a\u4e0a"},calendar:{sameDay:"[\u4eca\u5929]LT",nextDay:"[\u660e\u5929]LT",nextWeek:"[\u4e0b]ddddLT",lastDay:"[\u6628\u5929]LT",lastWeek:"[\u4e0a]ddddLT",sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}(\u65e5|\u6708|\u9031)/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"\u65e5";case"M":return e+"\u6708";case"w":case"W":return e+"\u9031";default:return e}},relativeTime:{future:"%s\u5167",past:"%s\u524d",s:"\u5e7e\u79d2",ss:"%d \u79d2",m:"1 \u5206\u9418",mm:"%d \u5206\u9418",h:"1 \u5c0f\u6642",hh:"%d \u5c0f\u6642",d:"1 \u5929",dd:"%d \u5929",M:"1 \u500b\u6708",MM:"%d \u500b\u6708",y:"1 \u5e74",yy:"%d \u5e74"}}),e.locale("en"),e}); \ No newline at end of file diff --git a/domain-server/resources/web/css/bootstrap-sortable.css b/domain-server/resources/web/css/bootstrap-sortable.css new file mode 100755 index 0000000000..aed89cd62e --- /dev/null +++ b/domain-server/resources/web/css/bootstrap-sortable.css @@ -0,0 +1,110 @@ +/** + * adding sorting ability to HTML tables with Bootstrap styling + * @summary HTML tables sorting ability + * @version 2.0.0 + * @requires tinysort, moment.js, jQuery + * @license MIT + * @author Matus Brlit (drvic10k) + * @copyright Matus Brlit (drvic10k), bootstrap-sortable contributors + */ + +table.sortable span.sign { + display: block; + position: absolute; + top: 50%; + right: 5px; + font-size: 12px; + margin-top: -10px; + color: #bfbfc1; +} + +table.sortable th:after { + display: block; + position: absolute; + top: 50%; + right: 5px; + font-size: 12px; + margin-top: -10px; + color: #bfbfc1; +} + +table.sortable th.arrow:after { + content: ''; +} + +table.sortable span.arrow, span.reversed, th.arrow.down:after, th.reversedarrow.down:after, th.arrow.up:after, th.reversedarrow.up:after { + border-style: solid; + border-width: 5px; + font-size: 0; + border-color: #ccc transparent transparent transparent; + line-height: 0; + height: 0; + width: 0; + margin-top: -2px; +} + + table.sortable span.arrow.up, th.arrow.up:after { + border-color: transparent transparent #ccc transparent; + margin-top: -7px; + } + +table.sortable span.reversed, th.reversedarrow.down:after { + border-color: transparent transparent #ccc transparent; + margin-top: -7px; +} + + table.sortable span.reversed.up, th.reversedarrow.up:after { + border-color: #ccc transparent transparent transparent; + margin-top: -2px; + } + +table.sortable span.az:before, th.az.down:after { + content: "a .. z"; +} + +table.sortable span.az.up:before, th.az.up:after { + content: "z .. a"; +} + +table.sortable th.az.nosort:after, th.AZ.nosort:after, th._19.nosort:after, th.month.nosort:after { + content: ".."; +} + +table.sortable span.AZ:before, th.AZ.down:after { + content: "A .. Z"; +} + +table.sortable span.AZ.up:before, th.AZ.up:after { + content: "Z .. A"; +} + +table.sortable span._19:before, th._19.down:after { + content: "1 .. 9"; +} + +table.sortable span._19.up:before, th._19.up:after { + content: "9 .. 1"; +} + +table.sortable span.month:before, th.month.down:after { + content: "jan .. dec"; +} + +table.sortable span.month.up:before, th.month.up:after { + content: "dec .. jan"; +} + +table.sortable>thead th:not([data-defaultsort=disabled]) { + cursor: pointer; + position: relative; + top: 0; + left: 0; +} + +table.sortable>thead th:hover:not([data-defaultsort=disabled]) { + background: #efefef; +} + +table.sortable>thead th div.mozilla { + position: relative; +} diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 5121b85a42..62f442584e 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -355,21 +355,31 @@ table .headers + .headers td { } } -ul.nav li.dropdown ul.dropdown-menu { +ul.dropdown-menu { padding: 0px 0px; } -ul.nav li.dropdown li a { +ul.dropdown-menu li a { padding-top: 7px; padding-bottom: 7px; } -ul.nav li.dropdown li a:hover { +ul.dropdown-menu li a:hover { color: white; background-color: #337ab7; } -ul.nav li.dropdown ul.dropdown-menu .divider { +table ul.dropdown-menu li:first-child a:hover { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +ul.dropdown-menu li:last-child a:hover { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +ul.dropdown-menu .divider { margin: 0px 0; } @@ -434,3 +444,42 @@ ul.nav li.dropdown ul.dropdown-menu .divider { .save-button-text { pointer-events: none; } + +#content_archives .panel-body { + padding: 0; +} + +#content_archives .panel-body .form-group { + padding: 15px; +} + +#content_archives .panel-body th, #content_archives .panel-body td { + padding: 8px 15px; +} + +#content_archives table { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; +} + +tr.gray-tr { + background-color: #f5f5f5; +} + +table .action-menu { + text-align: right; + width: 90px; +} + +.dropdown-toggle span.glyphicon-option-vertical { + font-size: 110%; + cursor: pointer; + border-radius: 50%; + background-color: #F5F5F5; + padding: 4px 4px 4px 6px; +} + +.dropdown.open span.glyphicon-option-vertical { + background-color: #337AB7; + color: white; +} diff --git a/domain-server/resources/web/footer.html b/domain-server/resources/web/footer.html index e8ea392b49..49e883509e 100644 --- a/domain-server/resources/web/footer.html +++ b/domain-server/resources/web/footer.html @@ -1,4 +1,5 @@ + diff --git a/domain-server/resources/web/header.html b/domain-server/resources/web/header.html index 1b7b306fff..bf1d1d1df1 100644 --- a/domain-server/resources/web/header.html +++ b/domain-server/resources/web/header.html @@ -9,6 +9,7 @@ + diff --git a/domain-server/resources/web/js/base-settings.js b/domain-server/resources/web/js/base-settings.js index 17f06f3ad1..7a860098d2 100644 --- a/domain-server/resources/web/js/base-settings.js +++ b/domain-server/resources/web/js/base-settings.js @@ -106,8 +106,12 @@ function reloadSettings(callback) { $.getJSON(Settings.endpoint, function(data){ _.extend(data, viewHelpers); - for (var spliceIndex in Settings.extraGroups) { - data.descriptions.splice(spliceIndex, 0, Settings.extraGroups[spliceIndex]); + for (var spliceIndex in Settings.extraGroupsAtIndex) { + data.descriptions.splice(spliceIndex, 0, Settings.extraGroupsAtIndex[spliceIndex]); + } + + for (var endGroupIndex in Settings.extraGroupsAtEnd) { + data.descriptions.push(Settings.extraGroupsAtEnd[endGroupIndex]); } $('#panels').html(Settings.panelsTemplate(data)); @@ -122,6 +126,8 @@ function reloadSettings(callback) { $('[data-toggle="tooltip"]').tooltip(); + Settings.pendingChanges = 0; + // call the callback now that settings are loaded callback(true); }).fail(function() { @@ -257,7 +263,7 @@ $(document).ready(function(){ } }); - $('#' + Settings.FORM_ID).on('change keyup paste', '.' + Settings.TRIGGER_CHANGE_CLASS , function(e){ + $('#' + Settings.FORM_ID).on('change input propertychange', '.' + Settings.TRIGGER_CHANGE_CLASS , function(e){ // this input was changed, add the changed data attribute to it $(this).attr('data-changed', true); @@ -676,11 +682,11 @@ function makeTableHiddenInputs(setting, initialValues, categoryValue) { } else { html += "" + - "" + - ""; + "name='" + col.name + "'>" + + "" + ""; } }) @@ -801,6 +807,8 @@ function badgeForDifferences(changedElement) { } }); + Settings.pendingChanges = totalChanges; + if (totalChanges == 0) { totalChanges = "" } @@ -830,7 +838,7 @@ function addTableRow(row) { var keyInput = row.children(".key").children("input"); // whenever the keyInput changes, re-badge for differences - keyInput.on('change keyup paste', function(e){ + keyInput.on('change input propertychange', function(e){ // update siblings in the row to have the correct name var currentKey = $(this).val(); diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index 2f75794786..2c12e2683a 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -28,7 +28,7 @@ function settingsGroupAnchor(base, html_id) { } $(document).ready(function(){ - var url = window.location; + var url = location.protocol + '//' + location.host+location.pathname; // Will only work if string in href matches with location $('ul.nav a[href="'+ url +'"]').parent().addClass('active'); @@ -39,22 +39,49 @@ $(document).ready(function(){ }).parent().addClass('active'); $('body').on('click', '#restart-server', function(e) { - swal( { - title: "Are you sure?", - text: "This will restart your domain server, causing your domain to be briefly offline.", - type: "warning", - html: true, - showCancelButton: true - }, function() { - $.get("/restart"); - showRestartModal(); - }); + swalAreYouSure( + "This will restart your domain server, causing your domain to be briefly offline.", + "Restart", + function() { + swal.close(); + $.get("/restart"); + showRestartModal(); + } + ) return false; }); var $contentDropdown = $('#content-settings-nav-dropdown'); var $settingsDropdown = $('#domain-settings-nav-dropdown'); + // define extra groups to add to setting panels, with their splice index + Settings.extraContentGroupsAtIndex = { + 0: { + html_id: Settings.CONTENT_ARCHIVES_PANEL_ID, + label: 'Content Archives' + }, + 1: { + html_id: Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID, + label: 'Upload Content' + } + }; + + Settings.extraContentGroupsAtEnd = []; + + Settings.extraDomainGroupsAtIndex = { + 1: { + html_id: 'places', + label: 'Places' + } + } + + Settings.extraDomainGroupsAtEnd = [ + { + html_id: 'settings_backup', + label: 'Settings Backup / Restore' + } + ] + // for pages that have the settings dropdowns if ($contentDropdown.length && $settingsDropdown.length) { // make a JSON request to get the dropdown menus for content and settings @@ -65,6 +92,15 @@ $(document).ready(function(){ return "
  • " + group.label + "
  • "; } + // add the dummy settings groups that get populated via JS + for (var spliceIndex in Settings.extraContentGroupsAtIndex) { + data.content_settings.splice(spliceIndex, 0, Settings.extraContentGroupsAtIndex[spliceIndex]); + } + + for (var endIndex in Settings.extraContentGroupsAtEnd) { + data.content_settings.push(Settings.extraContentGroupsAtEnd[endIndex]); + } + $.each(data.content_settings, function(index, group){ if (index > 0) { $contentDropdown.append(""); @@ -73,25 +109,22 @@ $(document).ready(function(){ $contentDropdown.append(makeGroupDropdownElement(group, "/content/")); }); + // add the dummy settings groups that get populated via JS + for (var spliceIndex in Settings.extraDomainGroupsAtIndex) { + data.domain_settings.splice(spliceIndex, 0, Settings.extraDomainGroupsAtIndex[spliceIndex]); + } + + for (var endIndex in Settings.extraDomainGroupsAtEnd) { + data.domain_settings.push(Settings.extraDomainGroupsAtEnd[endIndex]); + } + $.each(data.domain_settings, function(index, group){ if (index > 0) { $settingsDropdown.append(""); } $settingsDropdown.append(makeGroupDropdownElement(group, "/settings/")); - - // for domain settings, we add a dummy "Places" group that we fill - // via the API - add it to the dropdown menu in the right spot - // which is after "Metaverse / Networking" - if (group.name == "metaverse") { - $settingsDropdown.append(""); - $settingsDropdown.append(makeGroupDropdownElement({ html_id: 'places', label: 'Places' }, "/settings/")); - } }); - - // append a link for the "Settings Backup" panel - $settingsDropdown.append(""); - $settingsDropdown.append(makeGroupDropdownElement({ html_id: 'settings_backup', label: 'Settings Backup'}, "/settings")); }); } }); diff --git a/domain-server/resources/web/js/shared.js b/domain-server/resources/web/js/shared.js index 69721ee924..84bba4de56 100644 --- a/domain-server/resources/web/js/shared.js +++ b/domain-server/resources/web/js/shared.js @@ -42,7 +42,9 @@ Object.assign(Settings, { ADD_PLACE_BTN_ID: 'add-place-btn', FORM_ID: 'settings-form', INVALID_ROW_CLASS: 'invalid-input', - DATA_ROW_INDEX: 'data-row-index' + DATA_ROW_INDEX: 'data-row-index', + CONTENT_ARCHIVES_PANEL_ID: 'content_archives', + UPLOAD_CONTENT_BACKUP_PANEL_ID: 'upload_content' }); var URLs = { @@ -96,6 +98,17 @@ var DOMAIN_ID_TYPE_TEMP = 1; var DOMAIN_ID_TYPE_FULL = 2; var DOMAIN_ID_TYPE_UNKNOWN = 3; +function swalAreYouSure(text, confirmButtonText, callback) { + swal({ + title: "Are you sure?", + text: text, + type: "warning", + showCancelButton: true, + confirmButtonText: confirmButtonText, + closeOnConfirm: false + }, callback); +} + function domainIDIsSet() { if (typeof Settings.data.values.metaverse !== 'undefined' && typeof Settings.data.values.metaverse.id !== 'undefined') { @@ -164,7 +177,7 @@ function getDomainFromAPI(callback) { if (callback === undefined) { callback = function() {}; } - + if (!domainIDIsSet()) { callback({ status: 'fail' }); return null; diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 68684c9106..e67ea43158 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -14,17 +14,8 @@ $(document).ready(function(){ return b; })(window.location.search.substr(1).split('&')); - // define extra groups to add to description, with their splice index - Settings.extraGroups = { - 1: { - html_id: 'places', - label: 'Places' - }, - "-1": { - html_id: 'settings_backup', - label: 'Settings Backup' - } - } + Settings.extraGroupsAtEnd = Settings.extraDomainGroupsAtEnd; + Settings.extraGroupsAtIndex = Settings.extraDomainGroupsAtIndex; Settings.afterReloadActions = function() { // append the domain selection modal @@ -103,20 +94,17 @@ $(document).ready(function(){ var password = formJSON["security"]["http_password"]; if ((password == sha256_digest("")) && (username == undefined || (username && username.length != 0))) { - swal({ - title: "Are you sure?", - text: "You have entered a blank password with a non-blank username. Are you sure you want to require a blank password?", - type: "warning", - showCancelButton: true, - confirmButtonColor: "#5cb85c", - confirmButtonText: "Yes!", - closeOnConfirm: true - }, - function () { + swalAreYouSure( + "You have entered a blank password with a non-blank username. Are you sure you want to require a blank password?", + "Use blank password", + function() { + swal.close(); + formJSON["security"]["http_password"] = ""; postSettings(formJSON); - }); + } + ); return; } @@ -643,7 +631,6 @@ $(document).ready(function(){ autoNetworkingEl.after(form); } - function setupPlacesTable() { // create a dummy table using our view helper var placesTableSetting = { @@ -1043,32 +1030,38 @@ $(document).ready(function(){ $('body').on('click', '#' + RESTORE_SETTINGS_UPLOAD_ID, function(e){ e.preventDefault(); - var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); + swalAreYouSure( + "Your domain settings will be replaced by the uploaded settings", + "Restore settings", + function() { + var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); - var fileFormData = new FormData(); - fileFormData.append('restore-file', files[0]); + var fileFormData = new FormData(); + fileFormData.append('restore-file', files[0]); - showSpinnerAlert("Restoring Settings"); + showSpinnerAlert("Restoring Settings"); - $.ajax({ - url: '/settings/restore', - type: 'POST', - processData: false, - contentType: false, - dataType: 'json', - data: fileFormData - }).done(function(data, textStatus, jqXHR) { - swal.close(); - showRestartModal(); - }).fail(function(jqXHR, textStatus, errorThrown) { - showErrorMessage( - "Error", - "There was a problem restoring domain settings.\n" - + "Please ensure that your current domain settings are valid and try again." - ); + $.ajax({ + url: '/settings/restore', + type: 'POST', + processData: false, + contentType: false, + dataType: 'json', + data: fileFormData + }).done(function(data, textStatus, jqXHR) { + swal.close(); + showRestartModal(); + }).fail(function(jqXHR, textStatus, errorThrown) { + showErrorMessage( + "Error", + "There was a problem restoring domain settings.\n" + + "Please ensure that your current domain settings are valid and try again." + ); - reloadSettings(); - }); + reloadSettings(); + }); + } + ); }); $('body').on('change', '#' + RESTORE_SETTINGS_FILE_ID, function() { @@ -1089,7 +1082,7 @@ $(document).ready(function(){ html += "
    "; html += ""; html += "Upload a settings configuration to quickly configure this domain"; - html += "
    Note: Your domain's settings will be replaced by the settings you upload
    "; + html += "
    Note: Your domain settings will be replaced by the settings you upload"; html += ""; html += ""; @@ -1097,8 +1090,5 @@ $(document).ready(function(){ html += "
    "; $('#settings_backup .panel-body').html(html); - - // add an upload button to the footer to kick off the upload form - } }); diff --git a/domain-server/resources/web/wizard/index.shtml b/domain-server/resources/web/wizard/index.shtml index b526a5719b..5a3286296d 100644 --- a/domain-server/resources/web/wizard/index.shtml +++ b/domain-server/resources/web/wizard/index.shtml @@ -261,6 +261,5 @@ - diff --git a/domain-server/resources/web/wizard/js/wizard.js b/domain-server/resources/web/wizard/js/wizard.js index c0d17ca02c..efd3b28116 100644 --- a/domain-server/resources/web/wizard/js/wizard.js +++ b/domain-server/resources/web/wizard/js/wizard.js @@ -396,10 +396,12 @@ function savePermissions() { var admins = $('#admin-usernames').val().split(','); - var existingAdmins = Settings.data.values.security.permissions.map(function(value) { - return value.permissions_id; - }); - admins = admins.concat(existingAdmins); + if (Settings.data.values.security.permissions) { + var existingAdmins = Settings.data.values.security.permissions.map(function(value) { + return value.permissions_id; + }); + admins = admins.concat(existingAdmins); + } // Filter out unique values admins = _.uniq(admins.map(function(username) { diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp new file mode 100644 index 0000000000..2369b01690 --- /dev/null +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -0,0 +1,605 @@ +// +// AssetsBackupHandler.cpp +// domain-server/src +// +// Created by Clement Brisset on 1/12/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AssetsBackupHandler.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +using namespace std; + +static const QString ASSETS_DIR { "/assets/" }; +static const QString MAPPINGS_FILE { "mappings.json" }; +static const QString ZIP_ASSETS_FOLDER { "files" }; +static const chrono::minutes MAX_REFRESH_TIME { 5 }; + +Q_DECLARE_LOGGING_CATEGORY(asset_backup) +Q_LOGGING_CATEGORY(asset_backup, "hifi.asset-backup"); + +AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory) : + _assetsDirectory(backupDirectory + ASSETS_DIR) +{ + // Make sure the asset directory exists. + QDir(_assetsDirectory).mkpath("."); + + refreshAssetsOnDisk(); + + setupRefreshTimer(); +} + +void AssetsBackupHandler::setupRefreshTimer() { + _mappingsRefreshTimer.setTimerType(Qt::CoarseTimer); + _mappingsRefreshTimer.setSingleShot(true); + QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &AssetsBackupHandler::refreshMappings); + + auto nodeList = DependencyManager::get(); + QObject::connect(nodeList.data(), &LimitedNodeList::nodeActivated, this, [this](SharedNodePointer node) { + if (node->getType() == NodeType::AssetServer) { + // run immediately for the first time. + _mappingsRefreshTimer.start(0); + } + }); + QObject::connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, [this](SharedNodePointer node) { + if (node->getType() == NodeType::AssetServer) { + _mappingsRefreshTimer.stop(); + } + }); +} + +void AssetsBackupHandler::refreshAssetsOnDisk() { + QDir assetsDir { _assetsDirectory }; + auto assetNames = assetsDir.entryList(QDir::Files); + + // store all valid hashes + copy_if(begin(assetNames), end(assetNames), + inserter(_assetsOnDisk, begin(_assetsOnDisk)), + AssetUtils::isValidHash); + +} + +void AssetsBackupHandler::refreshAssetsInBackups() { + _assetsInBackups.clear(); + for (const auto& backup : _backups) { + for (const auto& mapping : backup.mappings) { + _assetsInBackups.insert(mapping.second); + } + } +} + +void AssetsBackupHandler::checkForMissingAssets() { + vector missingAssets; + set_difference(begin(_assetsInBackups), end(_assetsInBackups), + begin(_assetsOnDisk), end(_assetsOnDisk), + back_inserter(missingAssets)); + if (missingAssets.size() > 0) { + qCWarning(asset_backup) << "Found" << missingAssets.size() << "backup assets missing from disk."; + } +} + +void AssetsBackupHandler::checkForAssetsToDelete() { + vector deprecatedAssets; + set_difference(begin(_assetsOnDisk), end(_assetsOnDisk), + begin(_assetsInBackups), end(_assetsInBackups), + back_inserter(deprecatedAssets)); + + if (deprecatedAssets.size() > 0) { + qCDebug(asset_backup) << "Found" << deprecatedAssets.size() << "backup assets to delete from disk."; + const auto noCorruptedBackups = none_of(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { + return backup.corruptedBackup; + }); + if (noCorruptedBackups) { + for (const auto& hash : deprecatedAssets) { + auto success = QFile::remove(_assetsDirectory + hash); + if (success) { + _assetsOnDisk.erase(hash); + } else { + qCWarning(asset_backup) << "Could not delete asset:" << hash; + } + } + } else { + qCWarning(asset_backup) << "Some backups did not load properly, aborting delete operation for safety."; + } + } +} + +bool AssetsBackupHandler::isCorruptedBackup(const QString& backupName) { + auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& value) { + return value.name == backupName; + }); + + if (it == end(_backups)) { + return false; + } + + return it->corruptedBackup; +} + +std::pair AssetsBackupHandler::isAvailable(const QString& backupName) { + const auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { + return backup.name == backupName; + }); + if (it == end(_backups)) { + return { true, 1.0f }; + } + + int mappingsMissing = 0; + for (const auto& mapping : it->mappings) { + if (_assetsLeftToRequest.find(mapping.second) != end(_assetsLeftToRequest)) { + ++mappingsMissing; + } + } + + if (mappingsMissing == 0) { + return { true, 1.0f }; + } + + float progress = (float)it->mappings.size(); + progress -= (float)mappingsMissing; + progress /= it->mappings.size(); + + return { false, progress }; +} + +std::pair AssetsBackupHandler::getRecoveryStatus() { + if (_assetsLeftToUpload.empty() && + _mappingsLeftToSet.empty() && + _mappingsLeftToDelete.empty() && + _mappingRequestsInFlight == 0) { + return { false, 1.0f }; + } + + float progress = (float)_numRestoreOperations; + progress -= (float)_assetsLeftToUpload.size(); + progress -= (float)_mappingRequestsInFlight; + progress /= (float)_numRestoreOperations; + + return { true, progress }; +} + +void AssetsBackupHandler::loadBackup(const QString& backupName, QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + + _backups.emplace_back(backupName, AssetUtils::Mappings(), false); + auto& backup = _backups.back(); + + if (!zip.setCurrentFile(MAPPINGS_FILE)) { + qCCritical(asset_backup) << "Failed to find" << MAPPINGS_FILE << "while loading backup"; + qCCritical(asset_backup) << " Error:" << zip.getZipError(); + backup.corruptedBackup = true; + return; + } + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QFile::ReadOnly)) { + qCCritical(asset_backup) << "Could not unzip backup file for load:" << MAPPINGS_FILE; + qCCritical(asset_backup) << " Error:" << zip.getZipError(); + backup.corruptedBackup = true; + return; + } + + QJsonParseError error; + auto document = QJsonDocument::fromJson(zipFile.readAll(), &error); + if (document.isNull() || !document.isObject()) { + qCCritical(asset_backup) << "Could not parse backup file to JSON object for load:" << MAPPINGS_FILE; + qCCritical(asset_backup) << " Error:" << error.errorString(); + backup.corruptedBackup = true; + return; + } + + auto jsonObject = document.object(); + for (auto it = begin(jsonObject); it != end(jsonObject); ++it) { + const auto& assetPath = it.key(); + const auto& assetHash = it.value().toString(); + + if (!AssetUtils::isValidHash(assetHash)) { + qCCritical(asset_backup) << "Corrupted mapping in loading backup file" << backupName << ":" << it.key(); + backup.corruptedBackup = true; + continue; + } + + backup.mappings[assetPath] = assetHash; + _assetsInBackups.insert(assetHash); + } +} + +void AssetsBackupHandler::loadingComplete() { + checkForMissingAssets(); + checkForAssetsToDelete(); +} + +void AssetsBackupHandler::createBackup(const QString& backupName, QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + + if (operationInProgress()) { + qCWarning(asset_backup) << "There is already an operation in progress."; + return; + } + + if (_lastMappingsRefresh.time_since_epoch().count() == 0) { + qCWarning(asset_backup) << "Current mappings not yet loaded."; + return; + } + + if ((p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) { + qCWarning(asset_backup) << "Backing up asset mappings that might be stale."; + } + + AssetUtils::Mappings mappings; + + QJsonObject jsonObject; + for (const auto& mapping : _currentMappings) { + mappings[mapping.first] = mapping.second; + _assetsInBackups.insert(mapping.second); + jsonObject.insert(mapping.first, mapping.second); + } + QJsonDocument document(jsonObject); + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(MAPPINGS_FILE))) { + qCDebug(asset_backup) << "Could not open zip file:" << zipFile.getZipError(); + return; + } + zipFile.write(document.toJson()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qCDebug(asset_backup) << "Could not close zip file: " << zipFile.getZipError(); + return; + } + _backups.emplace_back(backupName, mappings, false); + qDebug() << "Created asset backup:" << backupName; +} + +void AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + + if (operationInProgress()) { + qCWarning(asset_backup) << "There is already a backup/restore in progress."; + return; + } + + if (_lastMappingsRefresh.time_since_epoch().count() == 0) { + qCWarning(asset_backup) << "Current mappings not yet loaded."; + return; + } + + if ((p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) { + qCWarning(asset_backup) << "Recovering while current asset mappings might be stale."; + } + + auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { + return backup.name == backupName; + }); + if (it == end(_backups)) { + loadBackup(backupName, zip); + + QuaZipDir zipDir { &zip, ZIP_ASSETS_FOLDER }; + + auto assetNames = zipDir.entryList(QDir::Files); + for (const auto& asset : assetNames) { + if (AssetUtils::isValidHash(asset)) { + if (!zip.setCurrentFile(zipDir.filePath(asset))) { + qCCritical(asset_backup) << "Failed to find" << asset << "while recovering backup"; + qCCritical(asset_backup) << " Error:" << zip.getZipError(); + continue; + } + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QFile::ReadOnly)) { + qCCritical(asset_backup) << "Could not unzip asset file:" << asset; + qCCritical(asset_backup) << " Error:" << zip.getZipError(); + continue; + } + + writeAssetFile(asset, zipFile.readAll()); + } + } + + // iterator is end() and has been invalidated in the `loadBackup` call + // grab the new iterator + it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { + return backup.name == backupName; + }); + + if (it == end(_backups)) { + qCCritical(asset_backup) << "Failed to recover backup:" << backupName; + return; + } + } + + const auto& newMappings = it->mappings; + computeServerStateDifference(_currentMappings, newMappings); + + restoreAllAssets(); +} + +void AssetsBackupHandler::deleteBackup(const QString& backupName) { + Q_ASSERT(QThread::currentThread() == thread()); + + if (operationInProgress()) { + qCWarning(asset_backup) << "There is a backup/restore in progress."; + return; + } + + const auto it = remove_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { + return backup.name == backupName; + }); + if (it == end(_backups)) { + qCDebug(asset_backup) << "Could not find backup" << backupName << "to delete."; + return; + } + + _backups.erase(it, end(_backups)); + + refreshAssetsInBackups(); + checkForAssetsToDelete(); + qDebug() << "Deleted asset backup:" << backupName; +} + +void AssetsBackupHandler::consolidateBackup(const QString& backupName, QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + + if (operationInProgress()) { + qCWarning(asset_backup) << "There is a backup/restore in progress."; + return; + } + + const auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { + return backup.name == backupName; + }); + if (it == end(_backups)) { + qCDebug(asset_backup) << "Could not find backup" << backupName << "to consolidate."; + return; + } + + for (const auto& mapping : it->mappings) { + const auto& hash = mapping.second; + + QDir assetsDir { _assetsDirectory }; + QFile file { assetsDir.filePath(hash) }; + if (!file.open(QFile::ReadOnly)) { + qCCritical(asset_backup) << "Could not open asset file" << file.fileName(); + continue; + } + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ZIP_ASSETS_FOLDER + "/" + hash))) { + qCDebug(asset_backup) << "Could not open zip file:" << zipFile.getZipError(); + continue; + } + zipFile.write(file.readAll()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qCDebug(asset_backup) << "Could not close zip file: " << zipFile.getZipError(); + continue; + } + } + +} + +void AssetsBackupHandler::refreshMappings() { + auto assetClient = DependencyManager::get(); + auto request = assetClient->createGetAllMappingsRequest(); + + QObject::connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) { + if (request->getError() == MappingRequest::NoError) { + const auto& mappings = request->getMappings(); + + // Clear existing mappings + _currentMappings.clear(); + + // Set new mapping, but ignore baked assets + for (const auto& mapping : mappings) { + if (!mapping.first.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { + _currentMappings.insert({ mapping.first, mapping.second.hash }); + } + } + _lastMappingsRefresh = p_high_resolution_clock::now(); + + downloadMissingFiles(_currentMappings); + } else { + qCCritical(asset_backup) << "Could not refresh asset server mappings."; + qCCritical(asset_backup) << " Error:" << request->getErrorString(); + } + + request->deleteLater(); + + // Launch next mappings request + static constexpr int MAPPINGS_REFRESH_INTERVAL = 30 * 1000; + _mappingsRefreshTimer.start(MAPPINGS_REFRESH_INTERVAL); + }); + + request->start(); +} + +void AssetsBackupHandler::downloadMissingFiles(const AssetUtils::Mappings& mappings) { + auto wasEmpty = _assetsLeftToRequest.empty(); + + for (const auto& mapping : mappings) { + const auto& hash = mapping.second; + if (_assetsOnDisk.find(hash) == end(_assetsOnDisk)) { + _assetsLeftToRequest.insert(hash); + } + } + + // If we were empty, that means no download chain was already going, start one. + if (wasEmpty) { + downloadNextMissingFile(); + } +} + +void AssetsBackupHandler::downloadNextMissingFile() { + if (_assetsLeftToRequest.empty()) { + return; + } + auto hash = *begin(_assetsLeftToRequest); + + auto assetClient = DependencyManager::get(); + auto assetRequest = assetClient->createRequest(hash); + + QObject::connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) { + if (request->getError() == AssetRequest::NoError) { + qCDebug(asset_backup) << "Backing up asset" << request->getHash(); + + bool success = writeAssetFile(request->getHash(), request->getData()); + if (!success) { + qCCritical(asset_backup) << "Failed to write asset file" << request->getHash(); + } + } else { + qCCritical(asset_backup) << "Failed to backup asset" << request->getHash(); + } + + _assetsLeftToRequest.erase(request->getHash()); + downloadNextMissingFile(); + + request->deleteLater(); + }); + + assetRequest->start(); +} + +bool AssetsBackupHandler::writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data) { + QDir assetsDir { _assetsDirectory }; + QFile file { assetsDir.filePath(hash) }; + if (!file.open(QFile::WriteOnly)) { + qCCritical(asset_backup) << "Could not open asset file for write:" << file.fileName(); + return false; + } + + auto bytesWritten = file.write(data); + if (bytesWritten != data.size()) { + qCCritical(asset_backup) << "Could not write data to file" << file.fileName(); + file.remove(); + return false; + } + + _assetsOnDisk.insert(hash); + + return true; +} + +void AssetsBackupHandler::computeServerStateDifference(const AssetUtils::Mappings& currentMappings, + const AssetUtils::Mappings& newMappings) { + _mappingsLeftToSet.reserve((int)newMappings.size()); + _assetsLeftToUpload.reserve((int)newMappings.size()); + _mappingsLeftToDelete.reserve((int)currentMappings.size()); + + set currentAssets; + for (const auto& currentMapping : currentMappings) { + const auto& currentPath = currentMapping.first; + const auto& currentHash = currentMapping.second; + + if (newMappings.find(currentPath) == end(newMappings)) { + _mappingsLeftToDelete.push_back(currentPath); + } + currentAssets.insert(currentHash); + } + + for (const auto& newMapping : newMappings) { + const auto& newPath = newMapping.first; + const auto& newHash = newMapping.second; + + auto it = currentMappings.find(newPath); + if (it == end(currentMappings) || it->second != newHash) { + _mappingsLeftToSet.push_back({ newPath, newHash }); + } + if (currentAssets.find(newHash) == end(currentAssets)) { + _assetsLeftToUpload.push_back(newHash); + } + } + + _numRestoreOperations = (int)_assetsLeftToUpload.size() + (int)_mappingsLeftToSet.size(); + if (!_mappingsLeftToDelete.empty()) { + ++_numRestoreOperations; + } + + qCDebug(asset_backup) << "Mappings to set:" << _mappingsLeftToSet.size(); + qCDebug(asset_backup) << "Mappings to del:" << _mappingsLeftToDelete.size(); + qCDebug(asset_backup) << "Assets to upload:" << _assetsLeftToUpload.size(); +} + +void AssetsBackupHandler::restoreAllAssets() { + restoreNextAsset(); +} + +void AssetsBackupHandler::restoreNextAsset() { + if (_assetsLeftToUpload.empty()) { + updateMappings(); + return; + } + + auto hash = _assetsLeftToUpload.back(); + _assetsLeftToUpload.pop_back(); + + auto assetFilename = _assetsDirectory + hash; + + auto assetClient = DependencyManager::get(); + auto request = assetClient->createUpload(assetFilename); + + QObject::connect(request, &AssetUpload::finished, this, [this](AssetUpload* request) { + if (request->getError() != AssetUpload::NoError) { + qCCritical(asset_backup) << "Failed to restore asset:" << request->getFilename(); + qCCritical(asset_backup) << " Error:" << request->getErrorString(); + } + + restoreNextAsset(); + + request->deleteLater(); + }); + + request->start(); +} + +void AssetsBackupHandler::updateMappings() { + auto assetClient = DependencyManager::get(); + for (const auto& mapping : _mappingsLeftToSet) { + auto request = assetClient->createSetMappingRequest(mapping.first, mapping.second); + QObject::connect(request, &SetMappingRequest::finished, this, [this](SetMappingRequest* request) { + if (request->getError() != MappingRequest::NoError) { + qCCritical(asset_backup) << "Failed to set mapping:" << request->getPath(); + qCCritical(asset_backup) << " Error:" << request->getErrorString(); + } + + --_mappingRequestsInFlight; + + request->deleteLater(); + }); + + request->start(); + ++_mappingRequestsInFlight; + } + _mappingsLeftToSet.clear(); + + auto request = assetClient->createDeleteMappingsRequest(_mappingsLeftToDelete); + QObject::connect(request, &DeleteMappingsRequest::finished, this, [this](DeleteMappingsRequest* request) { + if (request->getError() != MappingRequest::NoError) { + qCCritical(asset_backup) << "Failed to delete mappings"; + qCCritical(asset_backup) << " Error:" << request->getErrorString(); + } + + --_mappingRequestsInFlight; + + request->deleteLater(); + }); + _mappingsLeftToDelete.clear(); + + request->start(); + ++_mappingRequestsInFlight; +} diff --git a/domain-server/src/AssetsBackupHandler.h b/domain-server/src/AssetsBackupHandler.h new file mode 100644 index 0000000000..82d684c2c3 --- /dev/null +++ b/domain-server/src/AssetsBackupHandler.h @@ -0,0 +1,98 @@ +// +// AssetsBackupHandler.h +// domain-server/src +// +// Created by Clement Brisset on 1/12/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AssetsBackupHandler_h +#define hifi_AssetsBackupHandler_h + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "BackupHandler.h" + +class AssetsBackupHandler : public QObject, public BackupHandlerInterface { + Q_OBJECT + +public: + AssetsBackupHandler(const QString& backupDirectory); + + std::pair isAvailable(const QString& backupName) override; + std::pair getRecoveryStatus() override; + + void loadBackup(const QString& backupName, QuaZip& zip) override; + void loadingComplete() override; + void createBackup(const QString& backupName, QuaZip& zip) override; + void recoverBackup(const QString& backupName, QuaZip& zip) override; + void deleteBackup(const QString& backupName) override; + void consolidateBackup(const QString& backupName, QuaZip& zip) override; + bool isCorruptedBackup(const QString& backupName) override; + + bool operationInProgress() { return getRecoveryStatus().first; } + +private: + void setupRefreshTimer(); + void refreshMappings(); + + void refreshAssetsInBackups(); + void refreshAssetsOnDisk(); + void checkForMissingAssets(); + void checkForAssetsToDelete(); + + void downloadMissingFiles(const AssetUtils::Mappings& mappings); + void downloadNextMissingFile(); + bool writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data); + + void computeServerStateDifference(const AssetUtils::Mappings& currentMappings, + const AssetUtils::Mappings& newMappings); + void restoreAllAssets(); + void restoreNextAsset(); + void updateMappings(); + + QString _assetsDirectory; + + QTimer _mappingsRefreshTimer; + p_high_resolution_clock::time_point _lastMappingsRefresh; + AssetUtils::Mappings _currentMappings; + + struct AssetServerBackup { + AssetServerBackup(const QString& pName, AssetUtils::Mappings pMappings, bool pCorruptedBackup) : + name(pName), mappings(pMappings), corruptedBackup(pCorruptedBackup) {} + + QString name; + AssetUtils::Mappings mappings; + bool corruptedBackup; + }; + + // Internal storage for backups on disk + std::vector _backups; + std::set _assetsInBackups; + std::set _assetsOnDisk; + + // Internal storage for backup in progress + std::set _assetsLeftToRequest; + + // Internal storage for restore in progress + std::vector _assetsLeftToUpload; + std::vector> _mappingsLeftToSet; + AssetUtils::AssetPathList _mappingsLeftToDelete; + int _mappingRequestsInFlight { 0 }; + int _numRestoreOperations { 0 }; // Used to compute a restore progress. +}; + +#endif /* hifi_AssetsBackupHandler_h */ diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h new file mode 100644 index 0000000000..547339e01b --- /dev/null +++ b/domain-server/src/BackupHandler.h @@ -0,0 +1,40 @@ +// +// BackupHandler.h +// domain-server/src +// +// Created by Clement Brisset on 2/5/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_BackupHandler_h +#define hifi_BackupHandler_h + +#include + +#include + +class QuaZip; + +class BackupHandlerInterface { +public: + virtual ~BackupHandlerInterface() = default; + + virtual std::pair isAvailable(const QString& backupName) = 0; + + // Returns whether a recovery is ongoing and a progress between 0 and 1 if one is. + virtual std::pair getRecoveryStatus() = 0; + + virtual void loadBackup(const QString& backupName, QuaZip& zip) = 0; + virtual void loadingComplete() = 0; + virtual void createBackup(const QString& backupName, QuaZip& zip) = 0; + virtual void recoverBackup(const QString& backupName, QuaZip& zip) = 0; + virtual void deleteBackup(const QString& backupName) = 0; + virtual void consolidateBackup(const QString& backupName, QuaZip& zip) = 0; + virtual bool isCorruptedBackup(const QString& backupName) = 0; +}; +using BackupHandlerPointer = std::unique_ptr; + +#endif /* hifi_BackupHandler_h */ diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp deleted file mode 100644 index 03ad5de558..0000000000 --- a/domain-server/src/BackupSupervisor.cpp +++ /dev/null @@ -1,400 +0,0 @@ -// -// BackupSupervisor.cpp -// domain-server/src -// -// Created by Clement Brisset on 1/12/18. -// Copyright 2018 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "BackupSupervisor.h" - -#include -#include - -#include -#include -#include -#include -#include - -const QString BACKUPS_DIR = "backups/"; -const QString ASSETS_DIR = "files/"; -const QString MAPPINGS_PREFIX = "mappings-"; - -using namespace std; - -BackupSupervisor::BackupSupervisor() { - _backupsDirectory = PathUtils::getAppDataPath() + BACKUPS_DIR; - QDir backupDir { _backupsDirectory }; - if (!backupDir.exists()) { - backupDir.mkpath("."); - } - - _assetsDirectory = PathUtils::getAppDataPath() + BACKUPS_DIR + ASSETS_DIR; - QDir assetsDir { _assetsDirectory }; - if (!assetsDir.exists()) { - assetsDir.mkpath("."); - } - - loadAllBackups(); -} - -void BackupSupervisor::loadAllBackups() { - _backups.clear(); - _assetsInBackups.clear(); - _assetsOnDisk.clear(); - _allBackupsLoadedSuccessfully = true; - - QDir assetsDir { _assetsDirectory }; - auto assetNames = assetsDir.entryList(QDir::Files); - qDebug() << "Loading" << assetNames.size() << "assets."; - - // store all valid hashes - copy_if(begin(assetNames), end(assetNames), - inserter(_assetsOnDisk, begin(_assetsOnDisk)), AssetUtils::isValidHash); - - QDir backupsDir { _backupsDirectory }; - auto files = backupsDir.entryList({ MAPPINGS_PREFIX + "*.json" }, QDir::Files); - qDebug() << "Loading" << files.size() << "backups."; - - for (const auto& fileName : files) { - auto filePath = backupsDir.filePath(fileName); - auto success = loadBackup(filePath); - if (!success) { - qCritical() << "Failed to load backup file" << filePath; - _allBackupsLoadedSuccessfully = false; - } - } - - vector missingAssets; - set_difference(begin(_assetsInBackups), end(_assetsInBackups), - begin(_assetsOnDisk), end(_assetsOnDisk), - back_inserter(missingAssets)); - if (missingAssets.size() > 0) { - qWarning() << "Found" << missingAssets.size() << "assets missing."; - } - - vector deprecatedAssets; - set_difference(begin(_assetsOnDisk), end(_assetsOnDisk), - begin(_assetsInBackups), end(_assetsInBackups), - back_inserter(deprecatedAssets)); - - if (deprecatedAssets.size() > 0) { - qDebug() << "Found" << deprecatedAssets.size() << "assets to delete."; - if (_allBackupsLoadedSuccessfully) { - for (const auto& hash : deprecatedAssets) { - QFile::remove(_assetsDirectory + hash); - } - } else { - qWarning() << "Some backups did not load properly, aborting deleting for safety."; - } - } -} - -bool BackupSupervisor::loadBackup(const QString& backupFile) { - _backups.push_back({ backupFile.toStdString(), {}, false }); - auto& backup = _backups.back(); - - QFile file { backupFile }; - if (!file.open(QFile::ReadOnly)) { - qCritical() << "Could not open backup file:" << backupFile; - backup.corruptedBackup = true; - return false; - } - QJsonParseError error; - auto document = QJsonDocument::fromJson(file.readAll(), &error); - if (document.isNull() || !document.isObject()) { - qCritical() << "Could not parse backup file to JSON object:" << backupFile; - qCritical() << " Error:" << error.errorString(); - backup.corruptedBackup = true; - return false; - } - - auto jsonObject = document.object(); - for (auto it = begin(jsonObject); it != end(jsonObject); ++it) { - const auto& assetPath = it.key(); - const auto& assetHash = it.value().toString(); - - if (!AssetUtils::isValidHash(assetHash)) { - qCritical() << "Corrupted mapping in backup file" << backupFile << ":" << it.key(); - backup.corruptedBackup = true; - return false; - } - - backup.mappings[assetPath] = assetHash; - _assetsInBackups.insert(assetHash); - } - - _backups.push_back(backup); - return true; -} - -void BackupSupervisor::backupAssetServer() { - if (backupInProgress() || restoreInProgress()) { - qWarning() << "There is already a backup/restore in progress."; - return; - } - - auto assetClient = DependencyManager::get(); - auto request = assetClient->createGetAllMappingsRequest(); - - connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) { - qDebug() << "Got" << request->getMappings().size() << "mappings!"; - - if (request->getError() != MappingRequest::NoError) { - qCritical() << "Could not complete backup."; - qCritical() << " Error:" << request->getErrorString(); - finishBackup(); - request->deleteLater(); - return; - } - - if (!writeBackupFile(request->getMappings())) { - finishBackup(); - request->deleteLater(); - return; - } - - assert(!_backups.empty()); - const auto& mappings = _backups.back().mappings; - backupMissingFiles(mappings); - - request->deleteLater(); - }); - - startBackup(); - request->start(); -} - -void BackupSupervisor::backupMissingFiles(const AssetUtils::Mappings& mappings) { - _assetsLeftToRequest.reserve(mappings.size()); - for (auto& mapping : mappings) { - const auto& hash = mapping.second; - if (_assetsOnDisk.find(hash) == end(_assetsOnDisk)) { - _assetsLeftToRequest.push_back(hash); - } - } - - backupNextMissingFile(); -} - -void BackupSupervisor::backupNextMissingFile() { - if (_assetsLeftToRequest.empty()) { - finishBackup(); - return; - } - - auto hash = _assetsLeftToRequest.back(); - _assetsLeftToRequest.pop_back(); - - auto assetClient = DependencyManager::get(); - auto assetRequest = assetClient->createRequest(hash); - - connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) { - if (request->getError() == AssetRequest::NoError) { - qDebug() << "Got" << request->getHash(); - - bool success = writeAssetFile(request->getHash(), request->getData()); - if (!success) { - qCritical() << "Failed to write asset file" << request->getHash(); - } - } else { - qCritical() << "Failed to backup asset" << request->getHash(); - } - - backupNextMissingFile(); - - request->deleteLater(); - }); - - assetRequest->start(); -} - -bool BackupSupervisor::writeBackupFile(const AssetUtils::AssetMappings& mappings) { - auto filename = MAPPINGS_PREFIX + QDateTime::currentDateTimeUtc().toString(Qt::ISODate) + ".json"; - QFile file { PathUtils::getAppDataPath() + BACKUPS_DIR + filename }; - if (!file.open(QFile::WriteOnly)) { - qCritical() << "Could not open backup file" << file.fileName(); - return false; - } - - AssetServerBackup backup; - QJsonObject jsonObject; - for (auto& mapping : mappings) { - backup.mappings[mapping.first] = mapping.second.hash; - _assetsInBackups.insert(mapping.second.hash); - jsonObject.insert(mapping.first, mapping.second.hash); - } - - QJsonDocument document(jsonObject); - file.write(document.toJson()); - - backup.filePath = file.fileName().toStdString(); - _backups.push_back(backup); - - return true; -} - -bool BackupSupervisor::writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data) { - QDir assetsDir { _assetsDirectory }; - QFile file { assetsDir.filePath(hash) }; - if (!file.open(QFile::WriteOnly)) { - qCritical() << "Could not open backup file" << file.fileName(); - return false; - } - - file.write(data); - - _assetsOnDisk.insert(hash); - - return true; -} - -void BackupSupervisor::restoreAssetServer(int backupIndex) { - if (backupInProgress() || restoreInProgress()) { - qWarning() << "There is already a backup/restore in progress."; - return; - } - - auto assetClient = DependencyManager::get(); - auto request = assetClient->createGetAllMappingsRequest(); - - connect(request, &GetAllMappingsRequest::finished, this, [this, backupIndex](GetAllMappingsRequest* request) { - if (request->getError() == MappingRequest::NoError) { - const auto& newMappings = _backups.at(backupIndex).mappings; - computeServerStateDifference(request->getMappings(), newMappings); - - restoreAllAssets(); - } else { - finishRestore(); - } - - request->deleteLater(); - }); - - startRestore(); - request->start(); -} - -void BackupSupervisor::computeServerStateDifference(const AssetUtils::AssetMappings& currentMappings, - const AssetUtils::Mappings& newMappings) { - _mappingsLeftToSet.reserve((int)newMappings.size()); - _assetsLeftToUpload.reserve((int)newMappings.size()); - _mappingsLeftToDelete.reserve((int)currentMappings.size()); - - set currentAssets; - for (const auto& currentMapping : currentMappings) { - const auto& currentPath = currentMapping.first; - const auto& currentHash = currentMapping.second.hash; - - if (newMappings.find(currentPath) == end(newMappings)) { - _mappingsLeftToDelete.push_back(currentPath); - } - currentAssets.insert(currentHash); - } - - for (const auto& newMapping : newMappings) { - const auto& newPath = newMapping.first; - const auto& newHash = newMapping.second; - - auto it = currentMappings.find(newPath); - if (it == end(currentMappings) || it->second.hash != newHash) { - _mappingsLeftToSet.push_back({ newPath, newHash }); - } - if (currentAssets.find(newHash) == end(currentAssets)) { - _assetsLeftToUpload.push_back(newHash); - } - } - - qDebug() << "Mappings to set:" << _mappingsLeftToSet.size(); - qDebug() << "Mappings to del:" << _mappingsLeftToDelete.size(); - qDebug() << "Assets to upload:" << _assetsLeftToUpload.size(); -} - -void BackupSupervisor::restoreAllAssets() { - restoreNextAsset(); -} - -void BackupSupervisor::restoreNextAsset() { - if (_assetsLeftToUpload.empty()) { - updateMappings(); - return; - } - - auto hash = _assetsLeftToUpload.back(); - _assetsLeftToUpload.pop_back(); - - auto assetFilename = _assetsDirectory + hash; - - auto assetClient = DependencyManager::get(); - auto request = assetClient->createUpload(assetFilename); - - connect(request, &AssetUpload::finished, this, [this](AssetUpload* request) { - if (request->getError() != AssetUpload::NoError) { - qCritical() << "Failed to restore asset:" << request->getFilename(); - qCritical() << " Error:" << request->getErrorString(); - } - - restoreNextAsset(); - - request->deleteLater(); - }); - - request->start(); -} - -void BackupSupervisor::updateMappings() { - auto assetClient = DependencyManager::get(); - for (const auto& mapping : _mappingsLeftToSet) { - auto request = assetClient->createSetMappingRequest(mapping.first, mapping.second); - connect(request, &SetMappingRequest::finished, this, [this](SetMappingRequest* request) { - if (request->getError() != MappingRequest::NoError) { - qCritical() << "Failed to set mapping:" << request->getPath(); - qCritical() << " Error:" << request->getErrorString(); - } - - if (--_mappingRequestsInFlight == 0) { - finishRestore(); - } - - request->deleteLater(); - }); - - request->start(); - ++_mappingRequestsInFlight; - } - _mappingsLeftToSet.clear(); - - auto request = assetClient->createDeleteMappingsRequest(_mappingsLeftToDelete); - connect(request, &DeleteMappingsRequest::finished, this, [this](DeleteMappingsRequest* request) { - if (request->getError() != MappingRequest::NoError) { - qCritical() << "Failed to delete mappings"; - qCritical() << " Error:" << request->getErrorString(); - } - - if (--_mappingRequestsInFlight == 0) { - finishRestore(); - } - - request->deleteLater(); - }); - _mappingsLeftToDelete.clear(); - - request->start(); - ++_mappingRequestsInFlight; -} -bool BackupSupervisor::deleteBackup(int backupIndex) { - if (backupInProgress() || restoreInProgress()) { - qWarning() << "There is a backup/restore in progress."; - return false; - } - const auto& filePath = _backups.at(backupIndex).filePath; - auto success = QFile::remove(filePath.c_str()); - - loadAllBackups(); - - return success; -} diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h deleted file mode 100644 index 067abdc25c..0000000000 --- a/domain-server/src/BackupSupervisor.h +++ /dev/null @@ -1,85 +0,0 @@ -// -// BackupSupervisor.h -// domain-server/src -// -// Created by Clement Brisset on 1/12/18. -// Copyright 2018 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_BackupSupervisor_h -#define hifi_BackupSupervisor_h - -#include -#include - -#include - -#include - -#include - -struct AssetServerBackup { - std::string filePath; - AssetUtils::Mappings mappings; - bool corruptedBackup; -}; - -class BackupSupervisor : public QObject { - Q_OBJECT - -public: - BackupSupervisor(); - - void backupAssetServer(); - void restoreAssetServer(int backupIndex); - bool deleteBackup(int backupIndex); - - const std::vector& getBackups() const { return _backups; }; - - bool backupInProgress() const { return _backupInProgress; } - bool restoreInProgress() const { return _restoreInProgress; } - -private: - void loadAllBackups(); - bool loadBackup(const QString& backupFile); - - void startBackup() { _backupInProgress = true; } - void finishBackup() { _backupInProgress = false; } - void backupMissingFiles(const AssetUtils::Mappings& mappings); - void backupNextMissingFile(); - bool writeBackupFile(const AssetUtils::AssetMappings& mappings); - bool writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data); - - void startRestore() { _restoreInProgress = true; } - void finishRestore() { _restoreInProgress = false; } - void computeServerStateDifference(const AssetUtils::AssetMappings& currentMappings, - const AssetUtils::Mappings& newMappings); - void restoreAllAssets(); - void restoreNextAsset(); - void updateMappings(); - - QString _backupsDirectory; - QString _assetsDirectory; - - // Internal storage for backups on disk - bool _allBackupsLoadedSuccessfully { false }; - std::vector _backups; - std::set _assetsInBackups; - std::set _assetsOnDisk; - - // Internal storage for backup in progress - bool _backupInProgress { false }; - std::vector _assetsLeftToRequest; - - // Internal storage for restore in progress - bool _restoreInProgress { false }; - std::vector _assetsLeftToUpload; - std::vector> _mappingsLeftToSet; - AssetUtils::AssetPathList _mappingsLeftToDelete; - int _mappingRequestsInFlight { 0 }; -}; - -#endif /* hifi_BackupSupervisor_h */ diff --git a/domain-server/src/ContentSettingsBackupHandler.cpp b/domain-server/src/ContentSettingsBackupHandler.cpp new file mode 100644 index 0000000000..8ffd25b7a4 --- /dev/null +++ b/domain-server/src/ContentSettingsBackupHandler.cpp @@ -0,0 +1,74 @@ +// +// ContentSettingsBackupHandler.cpp +// domain-server/src +// +// Created by Stephen Birarda on 2/15/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ContentSettingsBackupHandler.h" + +#include +#include + +ContentSettingsBackupHandler::ContentSettingsBackupHandler(DomainServerSettingsManager& domainServerSettingsManager) : + _settingsManager(domainServerSettingsManager) +{ +} + +static const QString CONTENT_SETTINGS_BACKUP_FILENAME = "content-settings.json"; + +void ContentSettingsBackupHandler::createBackup(const QString& backupName, QuaZip& zip) { + + // grab the content settings as JSON, excluding default values and values hidden from backup + QJsonObject contentSettingsJSON = _settingsManager.settingsResponseObjectForType( + "", // include all settings types + DomainServerSettingsManager::Authenticated, DomainServerSettingsManager::NoDomainSettings, + DomainServerSettingsManager::IncludeContentSettings, DomainServerSettingsManager::NoDefaultSettings, + DomainServerSettingsManager::ForBackup + ); + + // make a QJsonDocument using the object + QJsonDocument contentSettingsDocument { contentSettingsJSON }; + + QuaZipFile zipFile { &zip }; + + if (zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(CONTENT_SETTINGS_BACKUP_FILENAME))) { + if (zipFile.write(contentSettingsDocument.toJson()) == -1) { + qCritical().nospace() << "Failed to write to " << CONTENT_SETTINGS_BACKUP_FILENAME << ": " << zipFile.getZipError(); + } + + zipFile.close(); + + if (zipFile.getZipError() != UNZ_OK) { + qCritical().nospace() << "Failed to zip " << CONTENT_SETTINGS_BACKUP_FILENAME << ": " << zipFile.getZipError(); + } + } else { + qCritical().nospace() << "Failed to open " << CONTENT_SETTINGS_BACKUP_FILENAME << ": " << zipFile.getZipError(); + } +} + +void ContentSettingsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip) { + if (!zip.setCurrentFile(CONTENT_SETTINGS_BACKUP_FILENAME)) { + qWarning() << "Failed to find" << CONTENT_SETTINGS_BACKUP_FILENAME << "while recovering backup"; + return; + } + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open" << CONTENT_SETTINGS_BACKUP_FILENAME << "in backup"; + return; + } + + auto rawData = zipFile.readAll(); + zipFile.close(); + + QJsonDocument jsonDocument = QJsonDocument::fromJson(rawData); + + if (!_settingsManager.restoreSettingsFromObject(jsonDocument.object(), ContentSettings)) { + qCritical() << "Failed to restore settings from" << CONTENT_SETTINGS_BACKUP_FILENAME << "in content archive"; + } +} diff --git a/domain-server/src/ContentSettingsBackupHandler.h b/domain-server/src/ContentSettingsBackupHandler.h new file mode 100644 index 0000000000..8454de0786 --- /dev/null +++ b/domain-server/src/ContentSettingsBackupHandler.h @@ -0,0 +1,43 @@ +// +// ContentSettingsBackupHandler.h +// domain-server/src +// +// Created by Stephen Birarda on 2/15/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ContentSettingsBackupHandler_h +#define hifi_ContentSettingsBackupHandler_h + +#include "BackupHandler.h" +#include "DomainServerSettingsManager.h" + +class ContentSettingsBackupHandler : public BackupHandlerInterface { +public: + ContentSettingsBackupHandler(DomainServerSettingsManager& domainServerSettingsManager); + + std::pair isAvailable(const QString& backupName) override { return { true, 1.0f }; } + std::pair getRecoveryStatus() override { return { false, 1.0f }; } + + void loadBackup(const QString& backupName, QuaZip& zip) override {} + + void loadingComplete() override {} + + void createBackup(const QString& backupName, QuaZip& zip) override; + + void recoverBackup(const QString& backupName, QuaZip& zip) override; + + void deleteBackup(const QString& backupName) override {} + + void consolidateBackup(const QString& backupName, QuaZip& zip) override {} + + bool isCorruptedBackup(const QString& backupName) override { return false; } + +private: + DomainServerSettingsManager& _settingsManager; +}; + +#endif // hifi_ContentSettingsBackupHandler_h diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp new file mode 100644 index 0000000000..85040d8c35 --- /dev/null +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -0,0 +1,599 @@ +// +// DomainContentBackupManager.cpp +// libraries/domain-server/src +// +// Created by Ryan Huffman on 1/01/18. +// Adapted from OctreePersistThread +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "DomainContentBackupManager.h" + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "DomainServer.h" + +const std::chrono::seconds DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL { 30 }; + +// Backup format looks like: daily_backup-TIMESTAMP.zip +static const QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" }; +static const QString DATETIME_FORMAT_RE { "\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}" }; +static const QString AUTOMATIC_BACKUP_PREFIX { "autobackup-" }; +static const QString MANUAL_BACKUP_PREFIX { "backup-" }; +static const QString MANUAL_BACKUP_NAME_RE { "[a-zA-Z0-9\\-_ ]+" }; + +void DomainContentBackupManager::addBackupHandler(BackupHandlerPointer handler) { + _backupHandlers.push_back(std::move(handler)); +} + +DomainContentBackupManager::DomainContentBackupManager(const QString& backupDirectory, + const QVariantList& backupRules, + std::chrono::milliseconds persistInterval, + bool debugTimestampNow) : + _backupDirectory(backupDirectory), _persistInterval(persistInterval), _lastCheck(p_high_resolution_clock::now()) +{ + + setObjectName("DomainContentBackupManager"); + + // Make sure the backup directory exists. + QDir(_backupDirectory).mkpath("."); + + parseBackupRules(backupRules); +} + +void DomainContentBackupManager::parseBackupRules(const QVariantList& backupRules) { + qCDebug(domain_server) << "BACKUP RULES:"; + + for (const QVariant& value : backupRules) { + QVariantMap map = value.toMap(); + + int interval = map["backupInterval"].toInt(); + int count = map["maxBackupVersions"].toInt(); + auto name = map["Name"].toString(); + auto format = name.toLower(); + QRegExp matchDisallowedCharacters { "[^a-zA-Z0-9\\-_]+" }; + format.replace(matchDisallowedCharacters, "_"); + + qCDebug(domain_server) << " Name:" << name; + qCDebug(domain_server) << " format:" << format; + qCDebug(domain_server) << " interval:" << interval; + qCDebug(domain_server) << " count:" << count; + + BackupRule newRule = { name, interval, format, count, 0 }; + + newRule.lastBackupSeconds = getMostRecentBackupTimeInSecs(format); + + if (newRule.lastBackupSeconds > 0) { + auto now = QDateTime::currentSecsSinceEpoch(); + auto sinceLastBackup = now - newRule.lastBackupSeconds; + qCDebug(domain_server).noquote() << " lastBackup:" << formatSecTime(sinceLastBackup) << "ago"; + } else { + qCDebug(domain_server) << " lastBackup: NEVER"; + } + + _backupRules.push_back(newRule); + } +} + +void DomainContentBackupManager::refreshBackupRules() { + for (auto& backup : _backupRules) { + backup.lastBackupSeconds = getMostRecentBackupTimeInSecs(backup.extensionFormat); + } +} + +int64_t DomainContentBackupManager::getMostRecentBackupTimeInSecs(const QString& format) { + int64_t mostRecentBackupInSecs = 0; + + QString mostRecentBackupFileName; + QDateTime mostRecentBackupTime; + + bool recentBackup = getMostRecentBackup(format, mostRecentBackupFileName, mostRecentBackupTime); + + if (recentBackup) { + mostRecentBackupInSecs = mostRecentBackupTime.toSecsSinceEpoch(); + } + + return mostRecentBackupInSecs; +} + +void DomainContentBackupManager::setup() { + auto backups = getAllBackups(); + for (auto& backup : backups) { + QFile backupFile { backup.absolutePath }; + if (!backupFile.open(QIODevice::ReadOnly)) { + qCritical() << "Could not open file:" << backup.absolutePath; + qCritical() << " ERROR:" << backupFile.errorString(); + continue; + } + + QuaZip zip { &backupFile }; + if (!zip.open(QuaZip::mdUnzip)) { + qCritical() << "Could not open backup archive:" << backup.absolutePath; + qCritical() << " ERROR:" << zip.getZipError(); + continue; + } + + for (auto& handler : _backupHandlers) { + handler->loadBackup(backup.id, zip); + } + + zip.close(); + } + + for (auto& handler : _backupHandlers) { + handler->loadingComplete(); + } +} + +bool DomainContentBackupManager::process() { + if (isStillRunning()) { + constexpr int64_t MSECS_TO_USECS = 1000; + constexpr int64_t USECS_TO_SLEEP = 10 * MSECS_TO_USECS; // every 10ms + std::this_thread::sleep_for(std::chrono::microseconds(USECS_TO_SLEEP)); + + if (_isRecovering) { + bool isStillRecovering = any_of(begin(_backupHandlers), end(_backupHandlers), [](const BackupHandlerPointer& handler) { + return handler->getRecoveryStatus().first; + }); + + if (!isStillRecovering) { + _isRecovering = false; + _recoveryFilename = ""; + emit recoveryCompleted(); + } + } + + auto now = p_high_resolution_clock::now(); + auto sinceLastSave = now - _lastCheck; + if (sinceLastSave > _persistInterval) { + _lastCheck = now; + + if (!_isRecovering) { + backup(); + } + } + } + + return isStillRunning(); +} + +void DomainContentBackupManager::shutdown() { + // Destroy handlers on the correct thread so that they can cleanup timers + _backupHandlers.clear(); +} + +void DomainContentBackupManager::aboutToFinish() { + _stopThread = true; +} + +bool DomainContentBackupManager::getMostRecentBackup(const QString& format, + QString& mostRecentBackupFileName, + QDateTime& mostRecentBackupTime) { + QRegExp formatRE { AUTOMATIC_BACKUP_PREFIX + QRegExp::escape(format) + "\\-(" + DATETIME_FORMAT_RE + ")" + "\\.zip" }; + + QStringList filters; + filters << AUTOMATIC_BACKUP_PREFIX + format + "*.zip"; + + bool bestBackupFound = false; + QString bestBackupFile; + QDateTime bestBackupFileTime; + + // Iterate over all of the backup files in the persist location + QDirIterator dirIterator(_backupDirectory, filters, QDir::Files | QDir::NoSymLinks, QDirIterator::NoIteratorFlags); + while (dirIterator.hasNext()) { + dirIterator.next(); + auto fileName = dirIterator.fileInfo().fileName(); + + if (formatRE.exactMatch(fileName)) { + auto datetime = formatRE.cap(1); + auto createdAt = QDateTime::fromString(datetime, DATETIME_FORMAT); + + if (!createdAt.isValid()) { + qDebug() << "Skipping backup with invalid timestamp: " << datetime; + continue; + } + + qDebug() << "Checking " << dirIterator.fileInfo().filePath(); + + // Based on last modified date, track the most recently modified file as the best backup + if (createdAt > bestBackupFileTime) { + bestBackupFound = true; + bestBackupFile = dirIterator.filePath(); + bestBackupFileTime = createdAt; + } + } else { + qDebug() << "NO match: " << fileName << formatRE; + } + } + + // If we found a backup then return the results + if (bestBackupFound) { + mostRecentBackupFileName = bestBackupFile; + mostRecentBackupTime = bestBackupFileTime; + } + return bestBackupFound; +} + +void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, const QString& backupName) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "deleteBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(const QString&, backupName)); + return; + } + + if (_isRecovering && backupName == _recoveryFilename) { + promise->resolve({ + { "success", false } + }); + return; + } + + QDir backupDir { _backupDirectory }; + QFile backupFile { backupDir.filePath(backupName) }; + auto success = backupFile.remove(); + + refreshBackupRules(); + + for (auto& handler : _backupHandlers) { + handler->deleteBackup(backupName); + } + + promise->resolve({ + { "success", success } + }); +} + +bool DomainContentBackupManager::recoverFromBackupZip(const QString& backupName, QuaZip& zip) { + if (!zip.open(QuaZip::Mode::mdUnzip)) { + qWarning() << "Failed to unzip file: " << backupName; + return false; + } else { + _isRecovering = true; + _recoveryFilename = backupName; + + for (auto& handler : _backupHandlers) { + handler->recoverBackup(backupName, zip); + } + + qDebug() << "Successfully started recovering from " << backupName; + return true; + } +} + +void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, const QString& backupName) { + if (_isRecovering) { + promise->resolve({ + { "success", false } + }); + return; + }; + + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "recoverFromBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(const QString&, backupName)); + return; + } + + qDebug() << "Recovering from" << backupName; + + bool success { false }; + QDir backupDir { _backupDirectory }; + auto backupFilePath { backupDir.filePath(backupName) }; + QFile backupFile { backupFilePath }; + if (backupFile.open(QIODevice::ReadOnly)) { + QuaZip zip { &backupFile }; + + success = recoverFromBackupZip(backupName, zip); + + backupFile.close(); + } else { + success = false; + qWarning() << "Failed to open backup file for reading: " << backupFilePath; + } + + promise->resolve({ + { "success", success } + }); +} + +void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup) { + + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "recoverFromUploadedBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(QByteArray, uploadedBackup)); + return; + } + + qDebug() << "Recovering from uploaded content archive"; + + // create a buffer and then a QuaZip from that buffer + QBuffer uploadedBackupBuffer { &uploadedBackup }; + QuaZip uploadedZip { &uploadedBackupBuffer }; + + QString backupName = MANUAL_BACKUP_PREFIX + "uploaded.zip"; + bool success = recoverFromBackupZip(backupName, uploadedZip); + + promise->resolve({ + { "success", success } + }); +} + +std::vector DomainContentBackupManager::getAllBackups() { + + QDir backupDir { _backupDirectory }; + auto matchingFiles = + backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + "*.zip", MANUAL_BACKUP_PREFIX + "*.zip" }, + QDir::Files | QDir::NoSymLinks, QDir::Name); + QString prefixFormat = "(" + QRegExp::escape(AUTOMATIC_BACKUP_PREFIX) + "|" + QRegExp::escape(MANUAL_BACKUP_PREFIX) + ")"; + QString nameFormat = "(.+)"; + QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")"; + QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" }; + + std::vector backups; + + for (const auto& fileInfo : matchingFiles) { + auto fileName = fileInfo.fileName(); + if (backupNameFormat.exactMatch(fileName)) { + auto type = backupNameFormat.cap(1); + auto name = backupNameFormat.cap(2); + auto dateTime = backupNameFormat.cap(3); + auto createdAt = QDateTime::fromString(dateTime, DATETIME_FORMAT); + if (!createdAt.isValid()) { + qDebug().nospace() << "Skipping backup (" << fileName << ") with invalid timestamp: " << dateTime; + continue; + } + + backups.emplace_back(fileInfo.fileName(), name, fileInfo.absoluteFilePath(), createdAt, + type == MANUAL_BACKUP_PREFIX); + } + } + + return backups; +} + +void DomainContentBackupManager::getAllBackupsAndStatus(MiniPromise::Promise promise) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "getAllBackupsAndStatus", Q_ARG(MiniPromise::Promise, promise)); + return; + } + + auto backups = getAllBackups(); + + QVariantList variantBackups; + + for (auto& backup : backups) { + bool isAvailable { true }; + bool isCorrupted { false }; + float availabilityProgress { 0.0f }; + for (auto& handler : _backupHandlers) { + bool handlerIsAvailable { true }; + float progress { 0.0f }; + std::tie(handlerIsAvailable, progress) = handler->isAvailable(backup.id); + isAvailable &= handlerIsAvailable; + availabilityProgress += progress / _backupHandlers.size(); + + isCorrupted = isCorrupted || handler->isCorruptedBackup(backup.id); + } + variantBackups.push_back(QVariantMap({ + { "id", backup.id }, + { "name", backup.name }, + { "createdAtMillis", backup.createdAt.toMSecsSinceEpoch() }, + { "isAvailable", isAvailable }, + { "availabilityProgress", availabilityProgress }, + { "isManualBackup", backup.isManualBackup }, + { "isCorrupted", isCorrupted } + })); + } + + float recoveryProgress = 0.0f; + bool isRecovering = _isRecovering.load(); + if (_isRecovering) { + for (auto& handler : _backupHandlers) { + float progress = handler->getRecoveryStatus().second; + recoveryProgress += progress / _backupHandlers.size(); + } + } + + QVariantMap status { + { "isRecovering", isRecovering }, + { "recoveringBackupId", _recoveryFilename }, + { "recoveryProgress", recoveryProgress } + }; + + QVariantMap info { + { "backups", variantBackups }, + { "status", status } + }; + + promise->resolve(info); +} + +void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) { + QDir backupDir { _backupDirectory }; + if (backupDir.exists() && rule.maxBackupVersions > 0) { + qCDebug(domain_server) << "Rolling old backup versions for rule" << rule.name; + + auto matchingFiles = + backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); + + int backupsToDelete = matchingFiles.length() - rule.maxBackupVersions; + if (backupsToDelete <= 0) { + qCDebug(domain_server) << "Found" << matchingFiles.length() << "backups, no backups need to be deleted"; + } else { + qCDebug(domain_server) << "Found" << matchingFiles.length() << "backups, deleting " << backupsToDelete << "backup(s)"; + for (int i = 0; i < backupsToDelete; ++i) { + auto fileInfo = matchingFiles[i].absoluteFilePath(); + QFile backupFile(fileInfo); + if (backupFile.remove()) { + qCDebug(domain_server) << "Removed old backup: " << backupFile.fileName(); + } else { + qCDebug(domain_server) << "Failed to remove old backup: " << backupFile.fileName(); + } + } + qCDebug(domain_server) << "Done removing old backup versions"; + } + } else { + qCDebug(domain_server) << "Rolling backups for rule" << rule.name << "." + << " Max Rolled Backup Versions less than 1 [" << rule.maxBackupVersions << "]." + << " No need to roll backups"; + } +} + +void DomainContentBackupManager::backup() { + auto nowDateTime = QDateTime::currentDateTime(); + auto nowSeconds = nowDateTime.toSecsSinceEpoch(); + + for (BackupRule& rule : _backupRules) { + auto secondsSinceLastBackup = nowSeconds - rule.lastBackupSeconds; + + qCDebug(domain_server) << "Checking [" << rule.name << "] - Time since last backup [" << secondsSinceLastBackup + << "] " + << "compared to backup interval [" << rule.intervalSeconds << "]..."; + + if (secondsSinceLastBackup > rule.intervalSeconds) { + qCDebug(domain_server) << "Time since last backup [" << secondsSinceLastBackup << "] for rule [" << rule.name + << "] exceeds backup interval [" << rule.intervalSeconds << "] doing backup now..."; + + bool success; + QString path; + std::tie(success, path) = createBackup(AUTOMATIC_BACKUP_PREFIX, rule.extensionFormat); + if (!success) { + qCWarning(domain_server) << "Failed to create backup for" << rule.name << "at" << path; + continue; + } + + qDebug() << "Created backup: " << path; + + rule.lastBackupSeconds = nowSeconds; + + removeOldBackupVersions(rule); + } else { + qCDebug(domain_server) << "Backup not needed for this rule [" << rule.name << "]..."; + } + } +} + +void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise, QString fileName) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "consolidateBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(QString, fileName)); + return; + } + + QDir backupDir { _backupDirectory }; + if (!backupDir.exists()) { + qCritical() << "Backup directory does not exist, bailing consolidation of backup"; + promise->resolve({ { "success", false } }); + return; + } + + auto filePath = backupDir.absoluteFilePath(fileName); + + auto copyFilePath = QDir::tempPath() + "/" + fileName; + + { + QFile copyFile(copyFilePath); + copyFile.remove(); + copyFile.close(); + } + auto copySuccess = QFile::copy(filePath, copyFilePath); + if (!copySuccess) { + qCritical() << "Failed to create copy of backup."; + promise->resolve({ { "success", false } }); + return; + } + + QuaZip zip(copyFilePath); + if (!zip.open(QuaZip::mdAdd)) { + qCritical() << "Could not open backup archive:" << filePath; + qCritical() << " ERROR:" << zip.getZipError(); + promise->resolve({ { "success", false } }); + return; + } + + for (auto& handler : _backupHandlers) { + handler->consolidateBackup(fileName, zip); + } + + zip.close(); + + if (zip.getZipError() != UNZ_OK) { + qCritical() << "Failed to consolidate backup: " << zip.getZipError(); + promise->resolve({ { "success", false } }); + return; + } + + promise->resolve({ + { "success", true }, + { "backupFilePath", copyFilePath } + }); +} + +void DomainContentBackupManager::createManualBackup(MiniPromise::Promise promise, const QString& name) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "createManualBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(const QString&, name)); + return; + } + + + QRegExp nameRE { MANUAL_BACKUP_NAME_RE }; + bool success; + + if (!nameRE.exactMatch(name)) { + qDebug() << "Cannot create manual backup with invalid name: " << name; + success = false; + } else { + QString path; + std::tie(success, path) = createBackup(MANUAL_BACKUP_PREFIX, name); + } + + promise->resolve({ + { "success", success } + }); +} + +std::pair DomainContentBackupManager::createBackup(const QString& prefix, const QString& name) { + auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); + auto fileName = prefix + name + "-" + timestamp + ".zip"; + auto path = _backupDirectory + "/" + fileName; + QuaZip zip(path); + if (!zip.open(QuaZip::mdAdd)) { + qCWarning(domain_server) << "Failed to open zip file at " << path; + qCWarning(domain_server) << " ERROR:" << zip.getZipError(); + return { false, path }; + } + + for (auto& handler : _backupHandlers) { + handler->createBackup(fileName, zip); + } + + zip.close(); + + return { true, path }; +} diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h new file mode 100644 index 0000000000..fbc8084ac6 --- /dev/null +++ b/domain-server/src/DomainContentBackupManager.h @@ -0,0 +1,106 @@ +// +// DomainContentBackupManager.h +// libraries/domain-server/src +// +// Created by Ryan Huffman on 1/01/18. +// Adapted from OctreePersistThread +// Copyright 2018 High Fidelity, Inc. +// +// +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DomainContentBackupManager_h +#define hifi_DomainContentBackupManager_h + +#include +#include +#include + +#include + +#include "BackupHandler.h" + +#include + +#include + +struct BackupItemInfo { + BackupItemInfo(QString pId, QString pName, QString pAbsolutePath, QDateTime pCreatedAt, bool pIsManualBackup) : + id(pId), name(pName), absolutePath(pAbsolutePath), createdAt(pCreatedAt), isManualBackup(pIsManualBackup) { }; + + QString id; + QString name; + QString absolutePath; + QDateTime createdAt; + bool isManualBackup; +}; + +class DomainContentBackupManager : public GenericThread { + Q_OBJECT +public: + class BackupRule { + public: + QString name; + int intervalSeconds; + QString extensionFormat; + int maxBackupVersions; + qint64 lastBackupSeconds; + }; + + static const std::chrono::seconds DEFAULT_PERSIST_INTERVAL; + + DomainContentBackupManager(const QString& rootBackupDirectory, + const QVariantList& settings, + std::chrono::milliseconds persistInterval = DEFAULT_PERSIST_INTERVAL, + bool debugTimestampNow = false); + + std::vector getAllBackups(); + void addBackupHandler(BackupHandlerPointer handler); + void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist + void replaceData(QByteArray data); + +public slots: + void getAllBackupsAndStatus(MiniPromise::Promise promise); + void createManualBackup(MiniPromise::Promise promise, const QString& name); + void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); + void recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup); + void deleteBackup(MiniPromise::Promise promise, const QString& backupName); + void consolidateBackup(MiniPromise::Promise promise, QString fileName); + +signals: + void loadCompleted(); + void recoveryCompleted(); + +protected: + /// Implements generic processing behavior for this thread. + virtual void setup() override; + virtual bool process() override; + virtual void shutdown() override; + + void backup(); + void removeOldBackupVersions(const BackupRule& rule); + void refreshBackupRules(); + bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); + int64_t getMostRecentBackupTimeInSecs(const QString& format); + void parseBackupRules(const QVariantList& backupRules); + + std::pair createBackup(const QString& prefix, const QString& name); + + bool recoverFromBackupZip(const QString& backupName, QuaZip& backupZip); + +private: + const QString _backupDirectory; + std::vector _backupHandlers; + std::chrono::milliseconds _persistInterval { 0 }; + + std::atomic _isRecovering { false }; + QString _recoveryFilename { }; + + p_high_resolution_clock::time_point _lastCheck; + std::vector _backupRules; +}; + +#endif // hifi_DomainContentBackupManager_h diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 3aab7b4563..7d0b538f6e 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -435,10 +435,11 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (!userPerms.can(NodePermissions::Permission::canConnectPastMaxCapacity) && !isWithinMaxCapacity()) { // we can't allow this user to connect because we are at max capacity QString redirectOnMaxCapacity; - const QVariant* redirectOnMaxCapacityVariant = - valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION); - if (redirectOnMaxCapacityVariant && redirectOnMaxCapacityVariant->canConvert()) { - redirectOnMaxCapacity = redirectOnMaxCapacityVariant->toString(); + + QVariant redirectOnMaxCapacityVariant = + _server->_settingsManager.valueForKeyPath(MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION); + if (redirectOnMaxCapacityVariant.canConvert()) { + redirectOnMaxCapacity = redirectOnMaxCapacityVariant.toString(); qDebug() << "Redirection domain:" << redirectOnMaxCapacity; } @@ -610,9 +611,9 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, bool DomainGatekeeper::isWithinMaxCapacity() { // find out what our maximum capacity is - const QVariant* maximumUserCapacityVariant = - valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY); - unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0; + QVariant maximumUserCapacityVariant = + _server->_settingsManager.valueForKeyPath(MAXIMUM_USER_CAPACITY); + unsigned int maximumUserCapacity = maximumUserCapacityVariant.isValid() ? maximumUserCapacityVariant.toUInt() : 0; if (maximumUserCapacity > 0) { unsigned int connectedUsers = _server->countConnectedUsers(); diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index eee5673af3..24d55d74b6 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -84,21 +84,22 @@ void DomainMetadata::descriptorsChanged() { // get descriptors assert(_metadata[DESCRIPTORS].canConvert()); auto& state = *static_cast(_metadata[DESCRIPTORS].data()); - auto& settings = static_cast(parent())->_settingsManager.getSettingsMap(); - auto& descriptors = static_cast(parent())->_settingsManager.getDescriptorsMap(); + + static const QString DESCRIPTORS_GROUP_KEYPATH = "descriptors"; + auto descriptorsMap = static_cast(parent())->_settingsManager.valueForKeyPath(DESCRIPTORS).toMap(); // copy simple descriptors (description/maturity) - state[Descriptors::DESCRIPTION] = descriptors[Descriptors::DESCRIPTION]; - state[Descriptors::MATURITY] = descriptors[Descriptors::MATURITY]; + state[Descriptors::DESCRIPTION] = descriptorsMap[Descriptors::DESCRIPTION]; + state[Descriptors::MATURITY] = descriptorsMap[Descriptors::MATURITY]; // copy array descriptors (hosts/tags) - state[Descriptors::HOSTS] = descriptors[Descriptors::HOSTS].toList(); - state[Descriptors::TAGS] = descriptors[Descriptors::TAGS].toList(); + state[Descriptors::HOSTS] = descriptorsMap[Descriptors::HOSTS].toList(); + state[Descriptors::TAGS] = descriptorsMap[Descriptors::TAGS].toList(); // parse capacity static const QString CAPACITY = "security.maximum_user_capacity"; - const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY); - unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0; + QVariant capacityVariant = static_cast(parent())->_settingsManager.valueForKeyPath(CAPACITY); + unsigned int capacity = capacityVariant.isValid() ? capacityVariant.toUInt() : 0; state[Descriptors::CAPACITY] = capacity; #if DEV_BUILD || PR_BUILD diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 68a36195d9..d2ef1a4156 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -44,10 +45,20 @@ #include #include +#include "AssetsBackupHandler.h" +#include "ContentSettingsBackupHandler.h" #include "DomainServerNodeData.h" +#include "EntitiesBackupHandler.h" #include "NodeConnectionData.h" +#include + +#include + +Q_LOGGING_CATEGORY(domain_server, "hifi.domain_server") + const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token"; +const QString DomainServer::REPLACEMENT_FILE_EXTENSION = ".replace"; int const DomainServer::EXIT_CODE_REBOOT = 234923; @@ -64,8 +75,8 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection, std::initializer_list optionalData, bool requireAccessToken) { - auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); - if (accessTokenVariant == nullptr && requireAccessToken) { + auto accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); + if (!accessTokenVariant.isValid() && requireAccessToken) { connection->respond(HTTPConnection::StatusCode400, "User access token has not been set"); return true; } @@ -101,8 +112,8 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection, req.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - if (accessTokenVariant != nullptr) { - auto accessTokenHeader = QString("Bearer ") + accessTokenVariant->toString(); + if (accessTokenVariant.isValid()) { + auto accessTokenHeader = QString("Bearer ") + accessTokenVariant.toString(); req.setRawHeader("Authorization", accessTokenHeader.toLatin1()); } @@ -280,6 +291,27 @@ DomainServer::DomainServer(int argc, char* argv[]) : qDebug() << "Ignoring subnet in whitelist, invalid ip portion: " << subnet; } } + + if (QDir(getEntitiesDirPath()).mkpath(".")) { + qCDebug(domain_server) << "Created entities data directory"; + } + maybeHandleReplacementEntityFile(); + + + static const QString BACKUP_RULES_KEYPATH = AUTOMATIC_CONTENT_ARCHIVES_GROUP + ".backup_rules"; + auto backupRulesVariant = _settingsManager.valueOrDefaultValueForKeyPath(BACKUP_RULES_KEYPATH); + + _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), backupRulesVariant.toList())); + + connect(_contentManager.get(), &DomainContentBackupManager::started, _contentManager.get(), [this](){ + _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); + _contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir()))); + _contentManager->addBackupHandler(BackupHandlerPointer(new ContentSettingsBackupHandler(_settingsManager))); + }); + + _contentManager->initialize(true); + + connect(_contentManager.get(), &DomainContentBackupManager::recoveryCompleted, this, &DomainServer::restart); } void DomainServer::parseCommandLine() { @@ -345,6 +377,11 @@ void DomainServer::parseCommandLine() { DomainServer::~DomainServer() { qInfo() << "Domain Server is shutting down."; + if (_contentManager) { + _contentManager->aboutToFinish(); + _contentManager->terminate(); + } + // cleanup the AssetClient thread DependencyManager::destroy(); _assetClientThread.quit(); @@ -377,8 +414,8 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { const QString X509_PRIVATE_KEY_OPTION = "key"; const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE"; - QString certPath = _settingsManager.getSettingsMap().value(X509_CERTIFICATE_OPTION).toString(); - QString keyPath = _settingsManager.getSettingsMap().value(X509_PRIVATE_KEY_OPTION).toString(); + QString certPath = _settingsManager.valueForKeyPath(X509_CERTIFICATE_OPTION).toString(); + QString keyPath = _settingsManager.valueForKeyPath(X509_PRIVATE_KEY_OPTION).toString(); if (!certPath.isEmpty() && !keyPath.isEmpty()) { // the user wants to use the following cert and key for HTTPS @@ -421,8 +458,7 @@ bool DomainServer::optionallySetupOAuth() { const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET"; const QString REDIRECT_HOSTNAME_OPTION = "hostname"; - const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - _oauthProviderURL = QUrl(settingsMap.value(OAUTH_PROVIDER_URL_OPTION).toString()); + _oauthProviderURL = QUrl(_settingsManager.valueForKeyPath(OAUTH_PROVIDER_URL_OPTION).toString()); // if we don't have an oauth provider URL then we default to the default node auth url if (_oauthProviderURL.isEmpty()) { @@ -432,9 +468,9 @@ bool DomainServer::optionallySetupOAuth() { auto accountManager = DependencyManager::get(); accountManager->setAuthURL(_oauthProviderURL); - _oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString(); + _oauthClientID = _settingsManager.valueForKeyPath(OAUTH_CLIENT_ID_OPTION).toString(); _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); - _hostname = settingsMap.value(REDIRECT_HOSTNAME_OPTION).toString(); + _hostname = _settingsManager.valueForKeyPath(REDIRECT_HOSTNAME_OPTION).toString(); if (!_oauthClientID.isEmpty()) { if (_oauthProviderURL.isEmpty() @@ -459,11 +495,11 @@ static const QString METAVERSE_DOMAIN_ID_KEY_PATH = "metaverse.id"; void DomainServer::getTemporaryName(bool force) { // check if we already have a domain ID - const QVariant* idValueVariant = valueForKeyPath(_settingsManager.getSettingsMap(), METAVERSE_DOMAIN_ID_KEY_PATH); + QVariant idValueVariant = _settingsManager.valueForKeyPath(METAVERSE_DOMAIN_ID_KEY_PATH); qInfo() << "Requesting temporary domain name"; - if (idValueVariant) { - qDebug() << "A domain ID is already present in domain-server settings:" << idValueVariant->toString(); + if (idValueVariant.isValid()) { + qDebug() << "A domain ID is already present in domain-server settings:" << idValueVariant.toString(); if (force) { qDebug() << "Requesting temporary domain name to replace current ID:" << getID(); } else { @@ -503,9 +539,6 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { auto settingsDocument = QJsonDocument::fromJson(newSettingsJSON.toUtf8()); _settingsManager.recurseJSONObjectAndOverwriteSettings(settingsDocument.object(), DomainSettings); - // store the new ID and auto networking setting on disk - _settingsManager.persistToFile(); - // store the new token to the account info auto accountManager = DependencyManager::get(); accountManager->setTemporaryDomain(id, key); @@ -607,8 +640,6 @@ void DomainServer::setupNodeListAndAssignments() { QVariant localPortValue = _settingsManager.valueOrDefaultValueForKeyPath(CUSTOM_LOCAL_PORT_OPTION); int domainServerPort = localPortValue.toInt(); - QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - int domainServerDTLSPort = INVALID_PORT; if (_isUsingDTLS) { @@ -616,8 +647,9 @@ void DomainServer::setupNodeListAndAssignments() { const QString CUSTOM_DTLS_PORT_OPTION = "dtls-port"; - if (settingsMap.contains(CUSTOM_DTLS_PORT_OPTION)) { - domainServerDTLSPort = (unsigned short) settingsMap.value(CUSTOM_DTLS_PORT_OPTION).toUInt(); + auto dtlsPortVariant = _settingsManager.valueForKeyPath(CUSTOM_DTLS_PORT_OPTION); + if (dtlsPortVariant.isValid()) { + domainServerDTLSPort = (unsigned short) dtlsPortVariant.toUInt(); } } @@ -647,9 +679,9 @@ void DomainServer::setupNodeListAndAssignments() { nodeList->setSessionUUID(_overridingDomainID); isMetaverseDomain = true; // assume metaverse domain } else { - const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH); - if (idValueVariant) { - nodeList->setSessionUUID(idValueVariant->toString()); + QVariant idValueVariant = _settingsManager.valueForKeyPath(METAVERSE_DOMAIN_ID_KEY_PATH); + if (idValueVariant.isValid()) { + nodeList->setSessionUUID(idValueVariant.toString()); isMetaverseDomain = true; // if we have an ID, we'll assume we're a metaverse domain } else { nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID @@ -691,13 +723,18 @@ void DomainServer::setupNodeListAndAssignments() { packetReceiver.registerListener(PacketType::ICEServerHeartbeatDenied, this, "processICEServerHeartbeatDenialPacket"); packetReceiver.registerListener(PacketType::ICEServerHeartbeatACK, this, "processICEServerHeartbeatACK"); + packetReceiver.registerListener(PacketType::OctreeDataFileRequest, this, "processOctreeDataRequestMessage"); + packetReceiver.registerListener(PacketType::OctreeDataPersist, this, "processOctreeDataPersistMessage"); + + packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacementRequest"); + packetReceiver.registerListener(PacketType::OctreeFileReplacementFromUrl, this, "handleOctreeFileReplacementFromURLRequest"); + // set a custom packetVersionMatch as the verify packet operator for the udt::Socket nodeList->setPacketFilterOperator(&DomainServer::isPacketVerified); _assetClientThread.setObjectName("AssetClient Thread"); auto assetClient = DependencyManager::set(); assetClient->moveToThread(&_assetClientThread); - QObject::connect(&_assetClientThread, &QThread::started, assetClient.data(), &AssetClient::init); _assetClientThread.start(); // add whatever static assignments that have been parsed to the queue @@ -712,10 +749,10 @@ bool DomainServer::resetAccountManagerAccessToken() { QString accessToken = QProcessEnvironment::systemEnvironment().value(ENV_ACCESS_TOKEN_KEY); if (accessToken.isEmpty()) { - const QVariant* accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); + QVariant accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); - if (accessTokenVariant && accessTokenVariant->canConvert(QMetaType::QString)) { - accessToken = accessTokenVariant->toString(); + if (accessTokenVariant.canConvert(QMetaType::QString)) { + accessToken = accessTokenVariant.toString(); } else { qWarning() << "No access token is present. Some operations that use the metaverse API will fail."; qDebug() << "Set an access token via the web interface, in your user config" @@ -846,31 +883,26 @@ void DomainServer::updateICEServerAddresses() { } void DomainServer::parseAssignmentConfigs(QSet& excludedTypes) { - const QString ASSIGNMENT_CONFIG_REGEX_STRING = "config-([\\d]+)"; - QRegExp assignmentConfigRegex(ASSIGNMENT_CONFIG_REGEX_STRING); - - const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); + const QString ASSIGNMENT_CONFIG_PREFIX = "config-"; // scan for assignment config keys - QStringList variantMapKeys = settingsMap.keys(); - int configIndex = variantMapKeys.indexOf(assignmentConfigRegex); + for (int i = 0; i < Assignment::AllTypes; ++i) { + QVariant assignmentConfigVariant = _settingsManager.valueOrDefaultValueForKeyPath(ASSIGNMENT_CONFIG_PREFIX + QString::number(i)); - while (configIndex != -1) { - // figure out which assignment type this matches - Assignment::Type assignmentType = (Assignment::Type) assignmentConfigRegex.cap(1).toInt(); + if (assignmentConfigVariant.isValid()) { + // figure out which assignment type this matches + Assignment::Type assignmentType = static_cast(i); - if (assignmentType < Assignment::AllTypes && !excludedTypes.contains(assignmentType)) { - QVariant mapValue = settingsMap[variantMapKeys[configIndex]]; - QVariantList assignmentList = mapValue.toList(); + if (!excludedTypes.contains(assignmentType)) { + QVariantList assignmentList = assignmentConfigVariant.toList(); - if (assignmentType != Assignment::AgentType) { - createStaticAssignmentsForType(assignmentType, assignmentList); + if (assignmentType != Assignment::AgentType) { + createStaticAssignmentsForType(assignmentType, assignmentList); + } + + excludedTypes.insert(assignmentType); } - - excludedTypes.insert(assignmentType); } - - configIndex = variantMapKeys.indexOf(assignmentConfigRegex, configIndex + 1); } } @@ -882,10 +914,10 @@ void DomainServer::addStaticAssignmentToAssignmentHash(Assignment* newAssignment void DomainServer::populateStaticScriptedAssignmentsFromSettings() { const QString PERSISTENT_SCRIPTS_KEY_PATH = "scripts.persistent_scripts"; - const QVariant* persistentScriptsVariant = valueForKeyPath(_settingsManager.getSettingsMap(), PERSISTENT_SCRIPTS_KEY_PATH); + QVariant persistentScriptsVariant = _settingsManager.valueOrDefaultValueForKeyPath(PERSISTENT_SCRIPTS_KEY_PATH); - if (persistentScriptsVariant) { - QVariantList persistentScriptsList = persistentScriptsVariant->toList(); + if (persistentScriptsVariant.isValid()) { + QVariantList persistentScriptsList = persistentScriptsVariant.toList(); foreach(const QVariant& persistentScriptVariant, persistentScriptsList) { QVariantMap persistentScript = persistentScriptVariant.toMap(); @@ -1695,10 +1727,96 @@ void DomainServer::sendHeartbeatToIceServer() { } else { qDebug() << "Not sending ice-server heartbeat since there is no selected ice-server."; qDebug() << "Waiting for" << _iceServerAddr << "host lookup response"; - } } +void DomainServer::processOctreeDataPersistMessage(QSharedPointer message) { + qDebug() << "Received octree data persist message"; + auto data = message->readAll(); + auto filePath = getEntitiesFilePath(); + + QDir dir(getEntitiesDirPath()); + if (!dir.exists()) { + qCDebug(domain_server) << "Creating entities content directory:" << dir.absolutePath(); + dir.mkpath("."); + } + + QFile f(filePath); + if (f.open(QIODevice::WriteOnly)) { + f.write(data); + OctreeUtils::RawEntityData entityData; + if (entityData.readOctreeDataInfoFromData(data)) { + qCDebug(domain_server) << "Wrote new entities file" << entityData.id << entityData.version; + } else { + qCDebug(domain_server) << "Failed to read new octree data info"; + } + } else { + qCDebug(domain_server) << "Failed to write new entities file:" << filePath; + } +} + +QString DomainServer::getContentBackupDir() { + return PathUtils::getAppDataFilePath("backups"); +} + +QString DomainServer::getEntitiesDirPath() { + return PathUtils::getAppDataFilePath("entities"); +} + +QString DomainServer::getEntitiesFilePath() { + return PathUtils::getAppDataFilePath("entities/models.json.gz"); +} + +QString DomainServer::getEntitiesReplacementFilePath() { + return getEntitiesFilePath().append(REPLACEMENT_FILE_EXTENSION); +} + +void DomainServer::processOctreeDataRequestMessage(QSharedPointer message) { + qDebug() << "Got request for octree data from " << message->getSenderSockAddr(); + + maybeHandleReplacementEntityFile(); + + bool remoteHasExistingData { false }; + QUuid id; + int version; + message->readPrimitive(&remoteHasExistingData); + if (remoteHasExistingData) { + constexpr size_t UUID_SIZE_BYTES = 16; + auto idData = message->read(UUID_SIZE_BYTES); + id = QUuid::fromRfc4122(idData); + message->readPrimitive(&version); + qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << version << ")"; + } else { + qCDebug(domain_server) << "Entity server does not have existing data"; + } + auto entityFilePath = getEntitiesFilePath(); + + auto reply = NLPacketList::create(PacketType::OctreeDataFileReply, QByteArray(), true, true); + OctreeUtils::RawEntityData data; + if (data.readOctreeDataInfoFromFile(entityFilePath)) { + if (data.id == id && data.version <= version) { + qCDebug(domain_server) << "ES has sufficient octree data, not sending data"; + reply->writePrimitive(false); + } else { + qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.version << ")"; + QFile file(entityFilePath); + if (file.open(QIODevice::ReadOnly)) { + reply->writePrimitive(true); + reply->write(file.readAll()); + } else { + qCDebug(domain_server) << "Unable to load entity file"; + reply->writePrimitive(false); + } + } + } else { + qCDebug(domain_server) << "Domain server does not have valid octree data"; + reply->writePrimitive(false); + } + + auto nodeList = DependencyManager::get(); + nodeList->sendPacketList(std::move(reply), message->getSenderSockAddr()); +} + void DomainServer::processNodeJSONStatsPacket(QSharedPointer packetList, SharedNodePointer sendingNode) { auto nodeData = static_cast(sendingNode->getLinkedData()); if (nodeData) { @@ -1808,23 +1926,25 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url const QString URI_ASSIGNMENT = "/assignment"; const QString URI_NODES = "/nodes"; const QString URI_SETTINGS = "/settings"; - const QString URI_ENTITY_FILE_UPLOAD = "/content/upload"; + const QString URI_CONTENT_UPLOAD = "/content/upload"; const QString URI_RESTART = "/restart"; const QString URI_API_PLACES = "/api/places"; const QString URI_API_DOMAINS = "/api/domains"; const QString URI_API_DOMAINS_ID = "/api/domains/"; + const QString URI_API_BACKUPS = "/api/backups"; + const QString URI_API_BACKUPS_ID = "/api/backups/"; + const QString URI_API_BACKUPS_RECOVER = "/api/backups/recover/"; const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; auto nodeList = DependencyManager::get(); - auto getSetting = [this](QString keyPath, QVariant& value) -> bool { - QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - QVariant* var = valueForKeyPath(settingsMap, keyPath); - if (var == nullptr) { + auto getSetting = [this](QString keyPath, QVariant value) -> bool { + + value = _settingsManager.valueForKeyPath(keyPath); + if (!value.isValid()) { return false; } - value = *var; return true; }; @@ -1892,8 +2012,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url if (connection->requestOperation() == QNetworkAccessManager::GetOperation) { const QString URI_WIZARD = "/wizard/"; const QString WIZARD_COMPLETED_ONCE_KEY_PATH = "wizard.completed_once"; - const QVariant* wizardCompletedOnce = valueForKeyPath(_settingsManager.getSettingsMap(), WIZARD_COMPLETED_ONCE_KEY_PATH); - const bool completedOnce = wizardCompletedOnce && wizardCompletedOnce->toBool(); + QVariant wizardCompletedOnce = _settingsManager.valueForKeyPath(WIZARD_COMPLETED_ONCE_KEY_PATH); + const bool completedOnce = wizardCompletedOnce.isValid() && wizardCompletedOnce.toBool(); if (url.path() != URI_WIZARD && url.path().endsWith('/') && !completedOnce) { // First visit, redirect to the wizard @@ -1997,6 +2117,37 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url // send the response connection->respond(HTTPConnection::StatusCode200, nodesDocument.toJson(), qPrintable(JSON_MIME_TYPE)); + return true; + } else if (url.path() == URI_API_BACKUPS) { + auto deferred = makePromise("getAllBackupsAndStatus"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonDocument docJSON(QJsonObject::fromVariantMap(result)); + + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + }); + _contentManager->getAllBackupsAndStatus(deferred); + return true; + } else if (url.path().startsWith(URI_API_BACKUPS_ID)) { + auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length()); + auto deferred = makePromise("consolidateBackup"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + auto success = result["success"].toBool(); + if (success) { + auto path = result["backupFilePath"].toString(); + auto file { std::unique_ptr(new QFile(path)) }; + if (file->open(QIODevice::ReadOnly)) { + connection->respond(HTTPConnection::StatusCode200, std::move(file)); + } else { + qCritical(domain_server) << "Unable to load consolidated backup at:" << path << result; + connection->respond(HTTPConnection::StatusCode500, "Error opening backup"); + } + } else { + connection->respond(HTTPConnection::StatusCode400); + } + }); + _contentManager->consolidateBackup(deferred, id); + return true; } else if (url.path() == URI_RESTART) { connection->respond(HTTPConnection::StatusCode200); @@ -2084,17 +2235,52 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url connection->respond(HTTPConnection::StatusCode200); return true; - } else if (url.path() == URI_ENTITY_FILE_UPLOAD) { + } else if (url.path() == URI_CONTENT_UPLOAD) { // this is an entity file upload, ask the HTTPConnection to parse the data QList formData = connection->parseFormData(); if (formData.size() > 0 && formData[0].second.size() > 0) { - // invoke our method to hand the new octree file off to the octree server - QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", - Qt::QueuedConnection, Q_ARG(QByteArray, formData[0].second)); + auto& firstFormData = formData[0]; + + // check the file extension to see what kind of file this is + // to make sure we handle this filetype for a content restore + auto dispositionValue = QString(firstFormData.first.value("Content-Disposition")); + auto formDataFilenameRegex = QRegExp("filename=\"(.+)\""); + auto matchIndex = formDataFilenameRegex.indexIn(dispositionValue); + + QString uploadedFilename = ""; + if (matchIndex != -1) { + uploadedFilename = formDataFilenameRegex.cap(1); + } + + if (uploadedFilename.endsWith(".json", Qt::CaseInsensitive) + || uploadedFilename.endsWith(".json.gz", Qt::CaseInsensitive)) { + // invoke our method to hand the new octree file off to the octree server + QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", + Qt::QueuedConnection, Q_ARG(QByteArray, firstFormData.second)); + + // respond with a 200 for success + connection->respond(HTTPConnection::StatusCode200); + } else if (uploadedFilename.endsWith(".zip", Qt::CaseInsensitive)) { + auto deferred = makePromise("recoverFromUploadedBackup"); + + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + auto success = result["success"].toBool(); + rootJSON["success"] = success; + QJsonDocument docJSON(rootJSON); + connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); + }); + + _contentManager->recoverFromUploadedBackup(deferred, firstFormData.second); + + return true; + } else { + // we don't have handling for this filetype, send back a 400 for failure + connection->respond(HTTPConnection::StatusCode400); + } - // respond with a 200 for success - connection->respond(HTTPConnection::StatusCode200); } else { // respond with a 400 for failure connection->respond(HTTPConnection::StatusCode400); @@ -2102,16 +2288,50 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; + } else if (url.path() == URI_API_BACKUPS) { + auto params = connection->parseUrlEncodedForm(); + auto it = params.find("name"); + if (it == params.end()) { + connection->respond(HTTPConnection::StatusCode400, "Bad request, missing `name`"); + return true; + } + + auto deferred = makePromise("createManualBackup"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + auto success = result["success"].toBool(); + rootJSON["success"] = success; + QJsonDocument docJSON(rootJSON); + connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); + }); + _contentManager->createManualBackup(deferred, it.value()); + + return true; + } else if (url.path() == "/domain_settings") { - auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); - if (!accessTokenVariant) { + auto accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); + if (!accessTokenVariant.isValid()) { connection->respond(HTTPConnection::StatusCode400); return true; } } else if (url.path() == URI_API_DOMAINS) { - return forwardMetaverseAPIRequest(connection, "/api/v1/domains", "domain", { "label" }); + + } else if (url.path().startsWith(URI_API_BACKUPS_RECOVER)) { + auto id = url.path().mid(QString(URI_API_BACKUPS_RECOVER).length()); + auto deferred = makePromise("recoverFromBackup"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + auto success = result["success"].toBool(); + rootJSON["success"] = success; + QJsonDocument docJSON(rootJSON); + connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); + }); + _contentManager->recoverFromBackup(deferred, id); + return true; } } else if (connection->requestOperation() == QNetworkAccessManager::PutOperation) { if (url.path() == URI_API_DOMAINS) { @@ -2124,8 +2344,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return forwardMetaverseAPIRequest(connection, "/api/v1/domains/" + domainID, "domain", { }, { "network_address", "network_port", "label" }); } else if (url.path() == URI_API_PLACES) { - auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); - if (!accessTokenVariant->isValid()) { + auto accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); + if (!accessTokenVariant.isValid()) { connection->respond(HTTPConnection::StatusCode400, "User access token has not been set"); return true; } @@ -2173,7 +2393,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url QUrl url { NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/api/v1/places/" + place_id }; - url.setQuery("access_token=" + accessTokenVariant->toString()); + url.setQuery("access_token=" + accessTokenVariant.toString()); QNetworkRequest req(url); req.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); @@ -2200,7 +2420,22 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url QRegExp allNodesDeleteRegex(ALL_NODE_DELETE_REGEX_STRING); QRegExp nodeDeleteRegex(NODE_DELETE_REGEX_STRING); - if (nodeDeleteRegex.indexIn(url.path()) != -1) { + if (url.path().startsWith(URI_API_BACKUPS_ID)) { + auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length()); + auto deferred = makePromise("deleteBackup"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + auto success = result["success"].toBool(); + rootJSON["success"] = success; + QJsonDocument docJSON(rootJSON); + connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); + }); + _contentManager->deleteBackup(deferred, id); + + return true; + + } else if (nodeDeleteRegex.indexIn(url.path()) != -1) { // this is a request to DELETE one node by UUID // pull the captured string, if it exists @@ -2353,10 +2588,11 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server."; - QVariantMap& settingsMap = _settingsManager.getSettingsMap(); + QVariant adminUsersVariant = _settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY); + QVariant adminRolesVariant = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY); if (!_oauthProviderURL.isEmpty() - && (settingsMap.contains(ADMIN_USERS_CONFIG_KEY) || settingsMap.contains(ADMIN_ROLES_CONFIG_KEY))) { + && (adminUsersVariant.isValid() || adminRolesVariant.isValid())) { QString cookieString = connection->requestHeaders().value(HTTP_COOKIE_HEADER_KEY); const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)"; @@ -2367,7 +2603,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl cookieUUID = cookieUUIDRegex.cap(1); } - if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) { + if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) { qDebug() << "Config file contains web admin settings for OAuth and basic HTTP authentication." << "These cannot be combined - using OAuth for authentication."; } @@ -2377,13 +2613,13 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl DomainServerWebSessionData sessionData = _cookieSessionHash.value(cookieUUID); QString profileUsername = sessionData.getUsername(); - if (settingsMap.value(ADMIN_USERS_CONFIG_KEY).toStringList().contains(profileUsername)) { + if (_settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY).toStringList().contains(profileUsername)) { // this is an authenticated user return true; } // loop the roles of this user and see if they are in the admin-roles array - QStringList adminRolesArray = settingsMap.value(ADMIN_ROLES_CONFIG_KEY).toStringList(); + QStringList adminRolesArray = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY).toStringList(); if (!adminRolesArray.isEmpty()) { foreach(const QString& userRole, sessionData.getRoles()) { @@ -2428,7 +2664,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl // we don't know about this user yet, so they are not yet authenticated return false; } - } else if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) { + } else if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) { // config file contains username and password combinations for basic auth const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization"; @@ -2447,10 +2683,10 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl QString headerPassword = credentialList[1]; // we've pulled a username and password - now check if there is a match in our basic auth hash - QString settingsUsername = valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)->toString(); - const QVariant* settingsPasswordVariant = valueForKeyPath(settingsMap, BASIC_AUTH_PASSWORD_KEY_PATH); + QString settingsUsername = _settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).toString(); + QVariant settingsPasswordVariant = _settingsManager.valueForKeyPath(BASIC_AUTH_PASSWORD_KEY_PATH); - QString settingsPassword = settingsPasswordVariant ? settingsPasswordVariant->toString() : ""; + QString settingsPassword = settingsPasswordVariant.isValid() ? settingsPasswordVariant.toString() : ""; QString hexHeaderPassword = headerPassword.isEmpty() ? "" : QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex(); @@ -2587,13 +2823,14 @@ ReplicationServerInfo serverInformationFromSettings(QVariantMap serverMap, Repli } void DomainServer::updateReplicationNodes(ReplicationServerDirection direction) { - auto settings = _settingsManager.getSettingsMap(); - if (settings.contains(BROADCASTING_SETTINGS_KEY)) { + auto broadcastSettingsVariant = _settingsManager.valueForKeyPath(BROADCASTING_SETTINGS_KEY); + + if (broadcastSettingsVariant.isValid()) { auto nodeList = DependencyManager::get(); std::vector replicationNodesInSettings; - auto replicationSettings = settings.value(BROADCASTING_SETTINGS_KEY).toMap(); + auto replicationSettings = broadcastSettingsVariant.toMap(); QString serversKey = direction == Upstream ? "upstream_servers" : "downstream_servers"; QString replicationDirection = direction == Upstream ? "upstream" : "downstream"; @@ -2669,13 +2906,12 @@ void DomainServer::updateUpstreamNodes() { void DomainServer::updateReplicatedNodes() { // Make sure we have downstream nodes in our list - auto settings = _settingsManager.getSettingsMap(); - static const QString REPLICATED_USERS_KEY = "users"; _replicatedUsernames.clear(); - - if (settings.contains(BROADCASTING_SETTINGS_KEY)) { - auto replicationSettings = settings.value(BROADCASTING_SETTINGS_KEY).toMap(); + + auto replicationVariant = _settingsManager.valueForKeyPath(BROADCASTING_SETTINGS_KEY); + if (replicationVariant.isValid()) { + auto replicationSettings = replicationVariant.toMap(); if (replicationSettings.contains(REPLICATED_USERS_KEY)) { auto usersSettings = replicationSettings.value(REPLICATED_USERS_KEY).toList(); for (auto& username : usersSettings) { @@ -2863,17 +3099,17 @@ void DomainServer::processPathQueryPacket(QSharedPointer messag // check out paths in the _configMap to see if we have a match auto keypath = QString(PATHS_SETTINGS_KEYPATH_FORMAT).arg(SETTINGS_PATHS_KEY).arg(pathQuery); - const QVariant* pathMatch = valueForKeyPath(_settingsManager.getSettingsMap(), keypath); + QVariant pathMatch = _settingsManager.valueForKeyPath(keypath); - if (pathMatch || pathQuery == INDEX_PATH) { + if (pathMatch.isValid() || pathQuery == INDEX_PATH) { // we got a match, respond with the resulting viewpoint auto nodeList = DependencyManager::get(); QString responseViewpoint; // if we didn't match the path BUT this is for the index path then send back our default - if (pathMatch) { - responseViewpoint = pathMatch->toMap()[PATH_VIEWPOINT_KEY].toString(); + if (pathMatch.isValid()) { + responseViewpoint = pathMatch.toMap()[PATH_VIEWPOINT_KEY].toString(); } else { const QString DEFAULT_INDEX_PATH = "/0,0,0/0,0,0,1"; responseViewpoint = DEFAULT_INDEX_PATH; @@ -3105,19 +3341,101 @@ void DomainServer::setupGroupCacheRefresh() { } } -void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) { - // enumerate the nodes and find any octree type servers with active sockets +void DomainServer::maybeHandleReplacementEntityFile() { + const auto replacementFilePath = getEntitiesReplacementFilePath(); + OctreeUtils::RawEntityData data; + if (!data.readOctreeDataInfoFromFile(replacementFilePath)) { + qCWarning(domain_server) << "Replacement file could not be read, it either doesn't exist or is invalid."; + } else { + qCDebug(domain_server) << "Replacing existing entity date with replacement file"; - auto limitedNodeList = DependencyManager::get(); - limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) { - return node->getType() == NodeType::EntityServer && node->getActiveSocket(); - }, [&octreeFile, limitedNodeList](const SharedNodePointer& octreeNode) { - // setup a packet to send to this octree server with the new octree file data - auto octreeFilePacketList = NLPacketList::create(PacketType::OctreeFileReplacement, QByteArray(), true, true); - octreeFilePacketList->write(octreeFile); + QFile replacementFile(replacementFilePath); + if (!replacementFile.remove()) { + // If we can't remove the replacement file, we are at risk of getting into a state where + // we continually replace the primary entity file with the replacement entity file. + qCWarning(domain_server) << "Unable to remove replacement file, bailing"; + } else { + data.resetIdAndVersion(); + auto gzippedData = data.toGzippedByteArray(); - qDebug() << "Sending an octree file replacement of" << octreeFile.size() << "bytes to" << octreeNode; - - limitedNodeList->sendPacketList(std::move(octreeFilePacketList), *octreeNode); - }); + QFile currentFile(getEntitiesFilePath()); + if (!currentFile.open(QIODevice::WriteOnly)) { + qCWarning(domain_server) + << "Failed to update entities data file with replacement file, unable to open entities file for writing"; + } else { + currentFile.write(gzippedData); + } + } + } +} + +void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) { + //Assume we have compressed data + auto compressedOctree = octreeFile; + QByteArray jsonOctree; + + bool wasCompressed = gunzip(compressedOctree, jsonOctree); + if (!wasCompressed) { + // the source was not compressed, assume we were sent regular JSON data + jsonOctree = compressedOctree; + } + + OctreeUtils::RawEntityData data; + if (data.readOctreeDataInfoFromData(jsonOctree)) { + data.resetIdAndVersion(); + + gzip(data.toByteArray(), compressedOctree); + + // write the compressed octree data to a special file + auto replacementFilePath = getEntitiesReplacementFilePath(); + QFile replacementFile(replacementFilePath); + if (replacementFile.open(QIODevice::WriteOnly) && replacementFile.write(compressedOctree) != -1) { + // we've now written our replacement file, time to take the server down so it can + // process it when it comes back up + qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server"; + + QMetaObject::invokeMethod(this, "restart", Qt::QueuedConnection); + } else { + qWarning() << "Could not write replacement octree data to file - refusing to process"; + } + } else { + qDebug() << "Received replacement octree file that is invalid - refusing to process"; + } +} + +void DomainServer::handleOctreeFileReplacementFromURLRequest(QSharedPointer message) { + qInfo() << "Received request to replace content from a url"; + auto node = DependencyManager::get()->findNodeWithAddr(message->getSenderSockAddr()); + if (node) { + qDebug() << "Found node: " << node->getCanReplaceContent(); + } + if (node->getCanReplaceContent()) { + // Convert message data into our URL + QString url(message->getMessage()); + QUrl modelsURL = QUrl(url, QUrl::StrictMode); + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest request(modelsURL); + QNetworkReply* reply = networkAccessManager.get(request); + + qDebug() << "Downloading JSON from: " << modelsURL; + + connect(reply, &QNetworkReply::finished, [this, reply, modelsURL]() { + QNetworkReply::NetworkError networkError = reply->error(); + if (networkError == QNetworkReply::NoError) { + handleOctreeFileReplacement(reply->readAll()); + } else { + qDebug() << "Error downloading JSON from specified file: " << modelsURL; + } + }); + } +} + + + + +void DomainServer::handleOctreeFileReplacementRequest(QSharedPointer message) { + auto node = DependencyManager::get()->nodeWithUUID(message->getSourceID()); + if (node->getCanReplaceContent()) { + handleOctreeFileReplacement(message->readAll()); + } } diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index c7d779b394..afe2a1cc7c 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -26,15 +26,20 @@ #include #include -#include "BackupSupervisor.h" +#include "AssetsBackupHandler.h" #include "DomainGatekeeper.h" #include "DomainMetadata.h" #include "DomainServerSettingsManager.h" #include "DomainServerWebSessionData.h" #include "WalletTransaction.h" +#include "DomainContentBackupManager.h" #include "PendingAssignedNodeData.h" +#include + +Q_DECLARE_LOGGING_CATEGORY(domain_server) + typedef QSharedPointer SharedAssignmentPointer; typedef QMultiHash TransactionHash; @@ -65,6 +70,8 @@ public: bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override; bool handleHTTPSRequest(HTTPSConnection* connection, const QUrl& url, bool skipSubHandler = false) override; + static const QString REPLACEMENT_FILE_EXTENSION; + public slots: /// Called by NodeList to inform us a node has been added void nodeAdded(SharedNodePointer node); @@ -84,6 +91,13 @@ private slots: void processICEServerHeartbeatDenialPacket(QSharedPointer message); void processICEServerHeartbeatACK(QSharedPointer message); + void handleOctreeFileReplacementFromURLRequest(QSharedPointer message); + void handleOctreeFileReplacementRequest(QSharedPointer message); + void handleOctreeFileReplacement(QByteArray octreeFile); + + void processOctreeDataRequestMessage(QSharedPointer message); + void processOctreeDataPersistMessage(QSharedPointer message); + void setupPendingAssignmentCredits(); void sendPendingTransactionsToServer(); @@ -91,8 +105,7 @@ private slots: void sendHeartbeatToMetaverse() { sendHeartbeatToMetaverse(QString()); } void sendHeartbeatToIceServer(); - void handleConnectedNode(SharedNodePointer newNode); - + void handleConnectedNode(SharedNodePointer newNode); void handleTempDomainSuccess(QNetworkReply& requestReply); void handleTempDomainError(QNetworkReply& requestReply); @@ -109,8 +122,6 @@ private slots: void handleSuccessfulICEServerAddressUpdate(QNetworkReply& requestReply); void handleFailedICEServerAddressUpdate(QNetworkReply& requestReply); - void handleOctreeFileReplacement(QByteArray octreeFile); - void updateReplicatedNodes(); void updateDownstreamNodes(); void updateUpstreamNodes(); @@ -127,6 +138,13 @@ private: const QUuid& getID(); void parseCommandLine(); + QString getContentBackupDir(); + QString getEntitiesDirPath(); + QString getEntitiesFilePath(); + QString getEntitiesReplacementFilePath(); + + void maybeHandleReplacementEntityFile(); + void setupNodeListAndAssignments(); bool optionallySetupOAuth(); bool optionallyReadX509KeyAndCertificate(); @@ -252,6 +270,8 @@ private: bool _sendICEServerAddressToMetaverseAPIInProgress { false }; bool _sendICEServerAddressToMetaverseAPIRedo { false }; + std::unique_ptr _contentManager { nullptr }; + QHash> _pendingOAuthConnections; QThread _assetClientThread; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 3fa36ceb90..c46b2eda49 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -32,9 +33,13 @@ #include #include //for KillAvatarReason #include + #include "DomainServerNodeData.h" const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; +const QString SETTINGS_PATH = "/settings"; +const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; +const QString CONTENT_SETTINGS_PATH_JSON = "/content-settings.json"; const QString DESCRIPTION_SETTINGS_KEY = "settings"; const QString SETTING_DEFAULT_KEY = "default"; @@ -187,6 +192,9 @@ void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointerset(NodePermissions::Permission::canRezTemporaryCertifiedEntities); packPermissions(); } + if (oldVersion < 2.0) { const QString WIZARD_COMPLETED_ONCE = "wizard.completed_once"; @@ -397,6 +406,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList *wizardCompletedOnce = QVariant(true); } + if (oldVersion < 2.1) { // convert old avatar scale settings into avatar height. @@ -418,6 +428,21 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList } } + if (oldVersion < 2.2) { + // migrate entity server rolling backup intervals to new location for automatic content archive intervals + + const QString ENTITY_SERVER_BACKUPS_KEYPATH = "entity_server_settings.backups"; + const QString AUTO_CONTENT_ARCHIVES_RULES_KEYPATH = AUTOMATIC_CONTENT_ARCHIVES_GROUP + ".backup_rules"; + + QVariant* previousBackupsVariant = _configMap.valueForKeyPath(ENTITY_SERVER_BACKUPS_KEYPATH); + + if (previousBackupsVariant) { + auto migratedBackupsVariant = _configMap.valueForKeyPath(AUTO_CONTENT_ARCHIVES_RULES_KEYPATH, true); + *migratedBackupsVariant = *previousBackupsVariant; + } + } + + // write the current description version to our settings *versionVariant = _descriptionVersion; @@ -428,17 +453,6 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList unpackPermissions(); } -QVariantMap& DomainServerSettingsManager::getDescriptorsMap() { - static const QString DESCRIPTORS{ "descriptors" }; - - auto& settingsMap = getSettingsMap(); - if (!getSettingsMap().contains(DESCRIPTORS)) { - settingsMap.insert(DESCRIPTORS, QVariantMap()); - } - - return *static_cast(getSettingsMap()[DESCRIPTORS].data()); -} - void DomainServerSettingsManager::initializeGroupPermissions(NodePermissionsMap& permissionsRows, QString groupName, NodePermissionsPointer perms) { // this is called when someone has used the domain-settings webpage to add a group. They type the group's name @@ -467,6 +481,9 @@ void DomainServerSettingsManager::initializeGroupPermissions(NodePermissionsMap& void DomainServerSettingsManager::packPermissionsForMap(QString mapName, NodePermissionsMap& permissionsRows, QString keyPath) { + // grab a write lock on the settings mutex since we're about to change the config map + QWriteLocker locker(&_settingsLock); + // find (or create) the "security" section of the settings map QVariant* security = _configMap.valueForKeyPath("security", true); if (!security->canConvert(QMetaType::QVariantMap)) { @@ -556,15 +573,20 @@ bool DomainServerSettingsManager::unpackPermissionsForKeypath(const QString& key mapPointer->clear(); - QVariant* permissions = _configMap.valueForKeyPath(keyPath, true); - if (!permissions->canConvert(QMetaType::QVariantList)) { + QVariant permissions = valueOrDefaultValueForKeyPath(keyPath); + + if (!permissions.isValid()) { + // we don't have a permissions object to unpack for this keypath, bail + return false; + } + + if (!permissions.canConvert(QMetaType::QVariantList)) { qDebug() << "Failed to extract permissions for key path" << keyPath << "from settings."; - (*permissions) = QVariantList(); } bool needPack = false; - QList permissionsList = permissions->toList(); + QList permissionsList = permissions.toList(); foreach (QVariant permsHash, permissionsList) { NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) }; QString id = perms->getID(); @@ -591,6 +613,11 @@ bool DomainServerSettingsManager::unpackPermissionsForKeypath(const QString& key void DomainServerSettingsManager::unpackPermissions() { // transfer details from _configMap to _agentPermissions + // NOTE: Defaults for standard permissions (anonymous, friends, localhost, logged-in) used + // to be set here and then immediately persisted to the config JSON file. + // They have since been moved to describe-settings.json as the default value for AGENT_STANDARD_PERMISSIONS_KEYPATH. + // In order to change the default standard permissions you must change the default value in describe-settings.json. + bool needPack = false; needPack |= unpackPermissionsForKeypath(AGENT_STANDARD_PERMISSIONS_KEYPATH, &_standardAgentPermissions); @@ -650,57 +677,39 @@ void DomainServerSettingsManager::unpackPermissions() { } }); - // if any of the standard names are missing, add them - foreach(const QString& standardName, NodePermissions::standardNames) { - NodePermissionsKey standardKey { standardName, 0 }; - if (!_standardAgentPermissions.contains(standardKey)) { - // we don't have permissions for one of the standard groups, so we'll add them now - NodePermissionsPointer perms { new NodePermissions(standardKey) }; - - if (standardKey == NodePermissions::standardNameLocalhost) { - // the localhost user is granted all permissions by default - perms->setAll(true); - } else { - // anonymous, logged in, and friend users get connect permissions by default - perms->set(NodePermissions::Permission::canConnectToDomain); - perms->set(NodePermissions::Permission::canRezTemporaryCertifiedEntities); - } - - // add the permissions to the standard map - _standardAgentPermissions[standardKey] = perms; - - // this will require a packing of permissions - needPack = true; - } - } - needPack |= ensurePermissionsForGroupRanks(); if (needPack) { packPermissions(); } - #ifdef WANT_DEBUG +#ifdef WANT_DEBUG qDebug() << "--------------- permissions ---------------------"; - QList> permissionsSets; - permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get() - << _groupPermissions.get() << _groupForbiddens.get() - << _ipPermissions.get() << _macPermissions.get() - << _machineFingerprintPermissions.get(); + std::array permissionsSets {{ + &_standardAgentPermissions, &_agentPermissions, + &_groupPermissions, &_groupForbiddens, + &_ipPermissions, &_macPermissions, + &_machineFingerprintPermissions + }}; foreach (auto permissionSet, permissionsSets) { - QHashIterator i(permissionSet); - while (i.hasNext()) { - i.next(); - NodePermissionsPointer perms = i.value(); + auto& permissionKeyMap = permissionSet->get(); + auto it = permissionKeyMap.begin(); + + while (it != permissionKeyMap.end()) { + + NodePermissionsPointer perms = it->second; if (perms->isGroup()) { - qDebug() << i.key() << perms->getGroupID() << perms; + qDebug() << it->first << perms->getGroupID() << perms; } else { - qDebug() << i.key() << perms; + qDebug() << it->first << perms; } + + ++it; } } - #endif +#endif + } bool DomainServerSettingsManager::ensurePermissionsForGroupRanks() { @@ -1048,12 +1057,22 @@ NodePermissions DomainServerSettingsManager::getForbiddensForGroup(const QUuid& return getForbiddensForGroup(groupKey.first, groupKey.second); } +QVariant DomainServerSettingsManager::valueForKeyPath(const QString& keyPath) { + QReadLocker locker(&_settingsLock); + auto foundValue = _configMap.valueForKeyPath(keyPath); + return foundValue ? *foundValue : QVariant(); +} + QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString& keyPath) { + QReadLocker locker(&_settingsLock); const QVariant* foundValue = _configMap.valueForKeyPath(keyPath); if (foundValue) { return *foundValue; } else { + // we don't need the settings lock anymore since we're done reading from the config map + locker.unlock(); + int dotIndex = keyPath.indexOf('.'); QString groupKey = keyPath.mid(0, dotIndex); @@ -1092,9 +1111,6 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection // we recurse one level deep below each group for the appropriate setting bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject, endpointType); - // store whatever the current _settingsMap is to file - persistToFile(); - // return success to the caller QString jsonSuccess = "{\"status\": \"success\"}"; connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); @@ -1152,17 +1168,20 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection QJsonObject rootObject; - bool forDomainSettings = (url.path() == SETTINGS_PATH_JSON); - bool forContentSettings = (url.path() == CONTENT_SETTINGS_PATH_JSON);; + DomainSettingsInclusion domainSettingsInclusion = (url.path() == SETTINGS_PATH_JSON) + ? IncludeDomainSettings : NoDomainSettings; + ContentSettingsInclusion contentSettingsInclusion = (url.path() == CONTENT_SETTINGS_PATH_JSON) + ? IncludeContentSettings : NoContentSettings; - rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = forDomainSettings + rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = (url.path() == SETTINGS_PATH_JSON) ? _domainSettingsDescription : _contentSettingsDescription; // grab a domain settings object for all types, filtered for the right class of settings // and exclude default values - rootObject[SETTINGS_RESPONSE_VALUE_KEY] = settingsResponseObjectForType("", true, - forDomainSettings, forContentSettings, - true); + rootObject[SETTINGS_RESPONSE_VALUE_KEY] = settingsResponseObjectForType("", Authenticated, + domainSettingsInclusion, + contentSettingsInclusion, + IncludeDefaultSettings); connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json"); @@ -1174,7 +1193,8 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection } else if (url.path() == SETTINGS_BACKUP_PATH) { // grab the settings backup as an authenticated user // for the domain settings type only, excluding hidden and default values - auto currentDomainSettingsJSON = settingsResponseObjectForType("", true, true, false, false, true); + auto currentDomainSettingsJSON = settingsResponseObjectForType("", Authenticated, IncludeDomainSettings, + NoContentSettings, NoDefaultSettings, ForBackup); // setup headers that tell the client to download the file wth a special name Headers downloadHeaders; @@ -1196,6 +1216,10 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection } bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType) { + + // grab a write lock since we're about to change the settings map + QWriteLocker locker(&_settingsLock); + QJsonArray* filteredDescriptionArray = settingsType == DomainSettings ? &_domainSettingsDescription : &_contentSettingsDescription; @@ -1277,6 +1301,9 @@ bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settings } } else { // we have a value to restore, use update setting to set it + // but clear the existing value first so that no merging between the restored settings + // and existing settings occurs + variantValue->clear(); // we might need to re-grab config group map in case it didn't exist when we looked for it before // but was created by the call to valueForKeyPath before @@ -1310,18 +1337,24 @@ bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settings } else { // restore completed, persist the new settings qDebug() << "Restore completed, persisting restored settings to file"; + + // let go of the write lock since we're done making changes to the config map + locker.unlock(); + persistToFile(); return true; } } -QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QString& typeValue, bool isAuthenticated, - bool includeDomainSettings, - bool includeContentSettings, - bool includeDefaults, bool isForBackup) { +QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QString& typeValue, + SettingsRequestAuthentication authentication, + DomainSettingsInclusion domainSettingsInclusion, + ContentSettingsInclusion contentSettingsInclusion, + DefaultSettingsInclusion defaultSettingsInclusion, + SettingsBackupFlag settingsBackupFlag) { QJsonObject responseObject; - if (!typeValue.isEmpty() || isAuthenticated) { + if (!typeValue.isEmpty() || authentication == Authenticated) { // convert the string type value to a QJsonValue QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt()); @@ -1329,9 +1362,10 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt // only enumerate the requested settings type (domain setting or content setting) QJsonArray* filteredDescriptionArray = &_descriptionArray; - if (includeDomainSettings && !includeContentSettings) { + + if (domainSettingsInclusion == IncludeDomainSettings && contentSettingsInclusion != IncludeContentSettings) { filteredDescriptionArray = &_domainSettingsDescription; - } else if (includeContentSettings && !includeDomainSettings) { + } else if (contentSettingsInclusion == IncludeContentSettings && domainSettingsInclusion != IncludeDomainSettings) { filteredDescriptionArray = &_contentSettingsDescription; } @@ -1354,35 +1388,35 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt bool includedInBackups = !settingObject.contains(DESCRIPTION_BACKUP_FLAG_KEY) || settingObject[DESCRIPTION_BACKUP_FLAG_KEY].toBool(); - if (!settingObject[VALUE_HIDDEN_FLAG_KEY].toBool() && (!isForBackup || includedInBackups)) { + if (!settingObject[VALUE_HIDDEN_FLAG_KEY].toBool() && (settingsBackupFlag != ForBackup || includedInBackups)) { QJsonArray affectedTypesArray = settingObject[AFFECTED_TYPES_JSON_KEY].toArray(); if (affectedTypesArray.isEmpty()) { affectedTypesArray = groupObject[AFFECTED_TYPES_JSON_KEY].toArray(); } if (affectedTypesArray.contains(queryType) || - (queryType.isNull() && isAuthenticated)) { + (queryType.isNull() && authentication == Authenticated)) { QString settingName = settingObject[DESCRIPTION_NAME_KEY].toString(); // we need to check if the settings map has a value for this setting QVariant variantValue; if (!groupKey.isEmpty()) { - QVariant settingsMapGroupValue = _configMap.value(groupKey); + QVariant settingsMapGroupValue = valueForKeyPath(groupKey); if (!settingsMapGroupValue.isNull()) { variantValue = settingsMapGroupValue.toMap().value(settingName); } } else { - variantValue = _configMap.value(settingName); + variantValue = valueForKeyPath(settingName); } // final check for inclusion // either we include default values or we don't but this isn't a default value - if (includeDefaults || !variantValue.isNull()) { + if ((defaultSettingsInclusion == IncludeDefaultSettings) || variantValue.isValid()) { QJsonValue result; - if (variantValue.isNull()) { + if (!variantValue.isValid()) { // no value for this setting, pass the default if (settingObject.contains(SETTING_DEFAULT_KEY)) { result = settingObject[SETTING_DEFAULT_KEY]; @@ -1521,6 +1555,10 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType) { + + // take a write lock since we're about to overwrite settings in the config map + QWriteLocker locker(&_settingsLock); + static const QString SECURITY_ROOT_KEY = "security"; static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist"; static const QString BROADCASTING_KEY = "broadcasting"; @@ -1618,6 +1656,12 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ } } + // we're done making changes to the config map, let go of our read lock + locker.unlock(); + + // store whatever the current config map is to file + persistToFile(); + return needRestart; } @@ -1644,6 +1688,9 @@ bool permissionVariantLessThan(const QVariant &v1, const QVariant &v2) { } void DomainServerSettingsManager::sortPermissions() { + // take a write lock since we're about to change the config map data + QWriteLocker locker(&_settingsLock); + // sort the permission-names QVariant* standardPermissions = _configMap.valueForKeyPath(AGENT_STANDARD_PERMISSIONS_KEYPATH); if (standardPermissions && standardPermissions->canConvert(QMetaType::QVariantList)) { @@ -1680,11 +1727,15 @@ void DomainServerSettingsManager::persistToFile() { QFile settingsFile(_configMap.getUserConfigFilename()); if (settingsFile.open(QIODevice::WriteOnly)) { + // take a read lock so we can grab the config and write it to file + QReadLocker locker(&_settingsLock); settingsFile.write(QJsonDocument::fromVariant(_configMap.getConfig()).toJson()); } else { qCritical("Could not write to JSON settings file. Unable to persist settings."); // failed to write, reload whatever the current config state is + // with a write lock since we're about to overwrite the config map + QWriteLocker locker(&_settingsLock); _configMap.loadConfig(_argumentList); } } diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 9b2427b344..252ff79ae2 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -27,9 +27,6 @@ const QString SETTINGS_PATHS_KEY = "paths"; -const QString SETTINGS_PATH = "/settings"; -const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; -const QString CONTENT_SETTINGS_PATH_JSON = "/content-settings.json"; const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions"; const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions"; const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions"; @@ -37,6 +34,7 @@ const QString MAC_PERMISSIONS_KEYPATH = "security.mac_permissions"; const QString MACHINE_FINGERPRINT_PERMISSIONS_KEYPATH = "security.machine_fingerprint_permissions"; const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions"; const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens"; +const QString AUTOMATIC_CONTENT_ARCHIVES_GROUP = "automatic_content_archives"; using GroupByUUIDKey = QPair; // groupID, rankID @@ -52,11 +50,12 @@ public: bool handleAuthenticatedHTTPRequest(HTTPConnection* connection, const QUrl& url); void setupConfigMap(const QStringList& argumentList); + + // each of the three methods in this group takes a read lock of _settingsLock + // and cannot be called when the a write lock is held by the same thread QVariant valueOrDefaultValueForKeyPath(const QString& keyPath); - - QVariantMap& getSettingsMap() { return _configMap.getConfig(); } - - QVariantMap& getDescriptorsMap(); + QVariant valueForKeyPath(const QString& keyPath); + bool containsKeyPath(const QString& keyPath) { return valueForKeyPath(keyPath).isValid(); } // these give access to anonymous/localhost/logged-in settings from the domain-server settings page bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name, 0); } @@ -111,6 +110,24 @@ public: void debugDumpGroupsState(); + enum SettingsRequestAuthentication { NotAuthenticated, Authenticated }; + enum DomainSettingsInclusion { NoDomainSettings, IncludeDomainSettings }; + enum ContentSettingsInclusion { NoContentSettings, IncludeContentSettings }; + enum DefaultSettingsInclusion { NoDefaultSettings, IncludeDefaultSettings }; + enum SettingsBackupFlag { NotForBackup, ForBackup }; + + /// thread safe method to retrieve a JSON representation of settings + QJsonObject settingsResponseObjectForType(const QString& typeValue, + SettingsRequestAuthentication authentication = NotAuthenticated, + DomainSettingsInclusion domainSettingsInclusion = IncludeDomainSettings, + ContentSettingsInclusion contentSettingsInclusion = IncludeContentSettings, + DefaultSettingsInclusion defaultSettingsInclusion = IncludeDefaultSettings, + SettingsBackupFlag settingsBackupFlag = NotForBackup); + /// thread safe method to restore settings from a JSON object + Q_INVOKABLE bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType); + + bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType); + signals: void updateNodePermissions(); void settingsUpdated(); @@ -130,21 +147,17 @@ private: QStringList _argumentList; QJsonArray filteredDescriptionArray(bool isContentSettings); - QJsonObject settingsResponseObjectForType(const QString& typeValue, bool isAuthenticated = false, - bool includeDomainSettings = true, bool includeContentSettings = true, - bool includeDefaults = true, bool isForBackup = false); - bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType); - void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap, const QJsonObject& settingDescription); QJsonObject settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName); void sortPermissions(); + + // you cannot be holding the _settingsLock when persisting to file from the same thread + // since it may take either a read lock or write lock and recursive locking doesn't allow a change in type void persistToFile(); void splitSettingsDescription(); - bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType); - double _descriptionVersion; QJsonArray _descriptionArray; @@ -152,10 +165,10 @@ private: QJsonArray _contentSettingsDescription; QJsonObject _settingsMenuGroups; + // any method that calls valueForKeyPath on this _configMap must get a write lock it keeps until it + // is done with the returned QVariant* HifiConfigVariantMap _configMap; - friend class DomainServer; - // these cause calls to metaverse's group api void apiGetGroupID(const QString& groupName); void apiGetGroupRanks(const QUuid& groupID); @@ -189,6 +202,9 @@ private: // keep track of answers to api queries about which users are in which groups QHash> _groupMembership; // QHash> + + /// guard read/write access from multiple threads to settings + QReadWriteLock _settingsLock { QReadWriteLock::Recursive }; }; #endif // hifi_DomainServerSettingsManager_h diff --git a/domain-server/src/EntitiesBackupHandler.cpp b/domain-server/src/EntitiesBackupHandler.cpp new file mode 100644 index 0000000000..599a730107 --- /dev/null +++ b/domain-server/src/EntitiesBackupHandler.cpp @@ -0,0 +1,83 @@ +// +// EntitiesBackupHandler.cpp +// domain-server/src +// +// Created by Clement Brisset on 2/14/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "EntitiesBackupHandler.h" + +#include + +#include +#include + +#include + +EntitiesBackupHandler::EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : + _entitiesFilePath(entitiesFilePath), + _entitiesReplacementFilePath(entitiesReplacementFilePath) +{ +} + +static const QString ENTITIES_BACKUP_FILENAME = "models.json.gz"; + +void EntitiesBackupHandler::createBackup(const QString& backupName, QuaZip& zip) { + QFile entitiesFile { _entitiesFilePath }; + + if (entitiesFile.open(QIODevice::ReadOnly)) { + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ENTITIES_BACKUP_FILENAME, _entitiesFilePath))) { + qCritical().nospace() << "Failed to open " << ENTITIES_BACKUP_FILENAME << " for writing in zip"; + return; + } + auto entityData = entitiesFile.readAll(); + if (zipFile.write(entityData) != entityData.size()) { + qCritical() << "Failed to write entities file to backup"; + zipFile.close(); + return; + } + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qCritical().nospace() << "Failed to zip " << ENTITIES_BACKUP_FILENAME << ": " << zipFile.getZipError(); + } + } +} + +void EntitiesBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip) { + if (!zip.setCurrentFile(ENTITIES_BACKUP_FILENAME)) { + qWarning() << "Failed to find" << ENTITIES_BACKUP_FILENAME << "while recovering backup"; + return; + } + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open" << ENTITIES_BACKUP_FILENAME << "in backup"; + return; + } + auto rawData = zipFile.readAll(); + + zipFile.close(); + + OctreeUtils::RawEntityData data; + if (!data.readOctreeDataInfoFromData(rawData)) { + qCritical() << "Unable to parse octree data during backup recovery"; + return; + } + + data.resetIdAndVersion(); + + if (zipFile.getZipError() != UNZ_OK) { + qCritical().nospace() << "Failed to unzip " << ENTITIES_BACKUP_FILENAME << ": " << zipFile.getZipError(); + return; + } + + QFile entitiesFile { _entitiesReplacementFilePath }; + + if (entitiesFile.open(QIODevice::WriteOnly)) { + entitiesFile.write(data.toGzippedByteArray()); + } +} diff --git a/domain-server/src/EntitiesBackupHandler.h b/domain-server/src/EntitiesBackupHandler.h new file mode 100644 index 0000000000..d95ad695a8 --- /dev/null +++ b/domain-server/src/EntitiesBackupHandler.h @@ -0,0 +1,47 @@ +// +// EntitiesBackupHandler.h +// domain-server/src +// +// Created by Clement Brisset on 2/14/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_EntitiesBackupHandler_h +#define hifi_EntitiesBackupHandler_h + +#include "BackupHandler.h" + +class EntitiesBackupHandler : public BackupHandlerInterface { +public: + EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath); + + std::pair isAvailable(const QString& backupName) override { return { true, 1.0f }; } + std::pair getRecoveryStatus() override { return { false, 1.0f }; } + + void loadBackup(const QString& backupName, QuaZip& zip) override {} + + void loadingComplete() override {} + + // Create a skeleton backup + void createBackup(const QString& backupName, QuaZip& zip) override; + + // Recover from a full backup + void recoverBackup(const QString& backupName, QuaZip& zip) override; + + // Delete a skeleton backup + void deleteBackup(const QString& backupName) override {} + + // Create a full backup + void consolidateBackup(const QString& backupName, QuaZip& zip) override {} + + bool isCorruptedBackup(const QString& backupName) override { return false; } + +private: + QString _entitiesFilePath; + QString _entitiesReplacementFilePath; +}; + +#endif /* hifi_EntitiesBackupHandler_h */ diff --git a/domain-server/src/main.cpp b/domain-server/src/main.cpp index dc3ee54fe7..d7856bf867 100644 --- a/domain-server/src/main.cpp +++ b/domain-server/src/main.cpp @@ -22,22 +22,10 @@ #include "DomainServer.h" int main(int argc, char* argv[]) { - disableQtBearerPoll(); // Fixes wifi ping spikes - - QCoreApplication::setApplicationName(BuildInfo::DOMAIN_SERVER_NAME); - QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); - QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); - QCoreApplication::setApplicationVersion(BuildInfo::VERSION); + setupHifiApplication(BuildInfo::DOMAIN_SERVER_NAME); Setting::init(); -#ifndef WIN32 - setvbuf(stdout, NULL, _IOLBF, 0); -#endif - - qInstallMessageHandler(LogHandler::verboseMessageHandler); - qInfo() << "Starting."; - int currentExitCode = 0; // use a do-while to handle domain-server restart diff --git a/ice-server/src/main.cpp b/ice-server/src/main.cpp index ec8b9957cf..aac6cc0422 100644 --- a/ice-server/src/main.cpp +++ b/ice-server/src/main.cpp @@ -11,18 +11,13 @@ #include -#include +#include #include "IceServer.h" int main(int argc, char* argv[]) { -#ifndef WIN32 - setvbuf(stdout, NULL, _IOLBF, 0); -#endif - - qInstallMessageHandler(LogHandler::verboseMessageHandler); - qInfo() << "Starting."; + setupHifiApplication("Ice Server"); IceServer iceServer(argc, argv); return iceServer.exec(); -} \ No newline at end of file +} diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 151e95900a..a1682b1328 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -204,7 +204,7 @@ endif() # link required hifi libraries link_hifi_libraries( - shared workload task octree ktx gpu gl procedural graphics render + shared workload task octree ktx gpu gl procedural graphics graphics-scripting render pointers recording fbx networking model-networking entities avatars trackers audio audio-client animation script-engine physics diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index 8907cf7858..7f7393da18 100644 Binary files a/interface/resources/fonts/hifi-glyphs.ttf and b/interface/resources/fonts/hifi-glyphs.ttf differ diff --git a/interface/resources/qml/controls-uit/Separator.qml b/interface/resources/qml/controls-uit/Separator.qml index 5e2d278454..3350764ae9 100644 --- a/interface/resources/qml/controls-uit/Separator.qml +++ b/interface/resources/qml/controls-uit/Separator.qml @@ -14,8 +14,8 @@ import "../styles-uit" Item { property int colorScheme: 0; - readonly property var topColor: [ hifi.colors.baseGrayShadow, hifi.colors.faintGray ]; - readonly property var bottomColor: [ hifi.colors.baseGrayHighlight, hifi.colors.faintGray ]; + readonly property var topColor: [ hifi.colors.baseGrayShadow, hifi.colors.faintGray, "#89858C" ]; + readonly property var bottomColor: [ hifi.colors.baseGrayHighlight, hifi.colors.faintGray, "#89858C" ]; // Size height: colorScheme === 0 ? 2 : 1; diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index a3e4113d08..3c1d0fcd3c 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -22,6 +22,10 @@ TableView { readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light property bool expandSelectedRow: false property bool centerHeaderText: false + readonly property real headerSpacing: 3 //spacing between sort indicator and table header title + property var titlePaintedPos: [] // storing extra data position behind painted + // title text and sort indicatorin table's header + signal titlePaintedPosSignal(int column) //signal that extradata position gets changed model: ListModel { } @@ -69,36 +73,39 @@ TableView { height: hifi.dimensions.tableHeaderHeight color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark + RalewayRegular { id: titleText + x: centerHeaderText ? (parent.width - paintedWidth - + ((sortIndicatorVisible && + sortIndicatorColumn === styleData.column) ? + (titleSort.paintedWidth / 5 + tableView.headerSpacing) : 0)) / 2 : + hifi.dimensions.tablePadding text: styleData.value size: hifi.fontSizes.tableHeading font.capitalization: Font.AllUppercase color: hifi.colors.baseGrayHighlight horizontalAlignment: (centerHeaderText ? Text.AlignHCenter : Text.AlignLeft) - anchors { - left: parent.left - leftMargin: hifi.dimensions.tablePadding - right: parent.right - rightMargin: hifi.dimensions.tablePadding - verticalCenter: parent.verticalCenter - } + anchors.verticalCenter: parent.verticalCenter } + //actual image of sort indicator in glyph font only 20% of real font size + //i.e. if the charachter size set to 60 pixels, actual image is 12 pixels HiFiGlyphs { id: titleSort text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn color: hifi.colors.darkGray opacity: 0.6; size: hifi.fontSizes.tableHeadingIcon - anchors { - left: titleText.right - leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 15 : 10) - right: parent.right - rightMargin: hifi.dimensions.tablePadding - verticalCenter: titleText.verticalCenter - } + anchors.verticalCenter: titleText.verticalCenter + anchors.left: titleText.right + anchors.leftMargin: -(hifi.fontSizes.tableHeadingIcon / 2.5) + tableView.headerSpacing visible: sortIndicatorVisible && sortIndicatorColumn === styleData.column + onXChanged: { + titlePaintedPos[styleData.column] = titleText.x + titleText.paintedWidth + + paintedWidth / 5 + tableView.headerSpacing*2 + titlePaintedPosSignal(styleData.column) + } } Rectangle { @@ -152,7 +159,7 @@ TableView { color: styleData.selected ? hifi.colors.primaryHighlight : tableView.isLightColorScheme - ? (styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd) - : (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd) + ? (styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd) + : (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd) } } diff --git a/interface/resources/qml/controls-uit/TextField.qml b/interface/resources/qml/controls-uit/TextField.qml index a21d1f8efd..3fc5d83129 100644 --- a/interface/resources/qml/controls-uit/TextField.qml +++ b/interface/resources/qml/controls-uit/TextField.qml @@ -34,11 +34,10 @@ TextField { placeholderText: textField.placeholderText - FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; } FontLoader { id: hifiGlyphs; source: "../../fonts/hifi-glyphs.ttf"; } - font.family: firaSansSemiBold.name + font.family: firaSansRegular.name font.pixelSize: hifi.fontSizes.textFieldInput - font.italic: textField.text == "" height: implicitHeight + 3 // Make surrounding box higher so that highlight is vertically centered. property alias textFieldLabel: textFieldLabel diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index dd0aba9ec5..fea275999e 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -206,7 +206,7 @@ Windows.ScrollingWindow { SHAPE_TYPES[SHAPE_TYPE_BOX] = "Box"; SHAPE_TYPES[SHAPE_TYPE_SPHERE] = "Sphere"; - var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH; + var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_SIMPLE_COMPOUND; var DYNAMIC_DEFAULT = false; var prompt = desktop.customInputDialog({ textInput: { diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index bb54ba9e57..8fb27714ee 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -30,7 +30,7 @@ Rectangle { property int myCardWidth: width - upperRightInfoContainer.width; property int myCardHeight: 100; property int rowHeight: 60; - property int actionButtonWidth: 55; + property int actionButtonWidth: 65; property int locationColumnWidth: 170; property int nearbyNameCardWidth: nearbyTable.width - (iAmAdmin ? (actionButtonWidth * 4) : (actionButtonWidth * 2)) - 4 - hifi.dimensions.scrollbarBackgroundWidth; property int connectionsNameCardWidth: connectionsTable.width - locationColumnWidth - actionButtonWidth - 4 - hifi.dimensions.scrollbarBackgroundWidth; @@ -415,6 +415,7 @@ Rectangle { movable: false; resizable: false; } + TableViewColumn { role: "ignore"; title: "IGNORE"; @@ -599,13 +600,23 @@ Rectangle { } // This Rectangle refers to the [?] popup button next to "NAMES" Rectangle { + id: questionRect color: hifi.colors.tableBackgroundLight; width: 20; height: hifi.dimensions.tableHeaderHeight - 2; anchors.left: nearbyTable.left; anchors.top: nearbyTable.top; anchors.topMargin: 1; - anchors.leftMargin: actionButtonWidth + nearbyNameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6; + + Connections { + target: nearbyTable + onTitlePaintedPosSignal: { + if (column === 1) { // name column + questionRect.anchors.leftMargin = actionButtonWidth + nearbyTable.titlePaintedPos[column] + } + } + } + RalewayRegular { id: helpText; text: "[?]"; diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 809f48361d..372fb3c774 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -28,7 +28,7 @@ Rectangle { id: root; objectName: "checkout" property string activeView: "initialize"; - property bool purchasesReceived: false; + property bool ownershipStatusReceived: false; property bool balanceReceived: false; property string itemName; property string itemId; @@ -37,12 +37,18 @@ Rectangle { property double balanceAfterPurchase; property bool alreadyOwned: false; property int itemPrice: -1; - property bool itemIsJson: true; + property bool isCertified; + property string itemType; + property var itemTypesArray: ["entity", "wearable", "contentSet", "app", "avatar"]; + property var itemTypesText: ["entity", "wearable", "content set", "app", "avatar"]; + property var buttonTextNormal: ["REZ", "WEAR", "REPLACE CONTENT SET", "INSTALL", "WEAR"]; + property var buttonTextClicked: ["REZZED!", "WORN!", "CONTENT SET REPLACED!", "INSTALLED!", "AVATAR CHANGED!"] + property var buttonGlyph: [hifi.glyphs.wand, hifi.glyphs.hat, hifi.glyphs.globe, hifi.glyphs.install, hifi.glyphs.avatar]; property bool shouldBuyWithControlledFailure: false; property bool debugCheckoutSuccess: false; property bool canRezCertifiedItems: Entities.canRezCertified() || Entities.canRezTmpCertified(); - property bool isWearable; property string referrer; + property bool isInstalled; // Style color: hifi.colors.white; Connections { @@ -53,17 +59,17 @@ Rectangle { if (root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; } - } else if (walletStatus === 1) { + } else if ((walletStatus === 1) || (walletStatus === 2) || (walletStatus === 3)) { if (root.activeView !== "notSetUp") { root.activeView = "notSetUp"; notSetUpTimer.start(); } - } else if (walletStatus === 2) { + } else if (walletStatus === 4) { if (root.activeView !== "passphraseModal") { root.activeView = "passphraseModal"; UserActivityLogger.commercePassphraseEntry("marketplace checkout"); } - } else if (walletStatus === 3) { + } else if (walletStatus === 5) { authSuccessStep(); } else { console.log("ERROR in Checkout.qml: Unknown wallet status: " + walletStatus); @@ -85,7 +91,9 @@ Rectangle { UserActivityLogger.commercePurchaseFailure(root.itemId, root.itemAuthor, root.itemPrice, !root.alreadyOwned, result.message); } else { root.itemHref = result.data.download_url; - root.isWearable = result.data.categories.indexOf("Wearables") > -1; + if (result.data.categories.indexOf("Wearables") > -1) { + root.itemType = "wearable"; + } root.activeView = "checkoutSuccess"; UserActivityLogger.commercePurchaseSuccess(root.itemId, root.itemAuthor, root.itemPrice, !root.alreadyOwned); } @@ -97,32 +105,60 @@ Rectangle { } else { root.balanceReceived = true; root.balanceAfterPurchase = result.data.balance - root.itemPrice; - root.setBuyText(); + root.refreshBuyUI(); } } - onInventoryResult: { + onAlreadyOwnedResult: { if (result.status !== 'success') { - console.log("Failed to get purchases", result.data.message); + console.log("Failed to get Already Owned status", result.data.message); } else { - root.purchasesReceived = true; - if (purchasesContains(result.data.assets, itemId)) { - root.alreadyOwned = true; + root.ownershipStatusReceived = true; + if (result.data.marketplace_item_id === root.itemId) { + root.alreadyOwned = result.data.already_owned; } else { + console.log("WARNING - Received 'Already Owned' status about different Marketplace ID!"); root.alreadyOwned = false; } - root.setBuyText(); + root.refreshBuyUI(); + } + } + + onAppInstalled: { + if (appHref === root.itemHref) { + root.isInstalled = true; } } } onItemIdChanged: { - Commerce.inventory(); + root.ownershipStatusReceived = false; + Commerce.alreadyOwned(root.itemId); itemPreviewImage.source = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg"; } onItemHrefChanged: { - itemIsJson = root.itemHref.endsWith('.json'); + if (root.itemHref.indexOf(".fst") > -1) { + root.itemType = "avatar"; + } else if (root.itemHref.indexOf('.json.gz') > -1) { + root.itemType = "contentSet"; + } else if (root.itemHref.indexOf('.app.json') > -1) { + root.itemType = "app"; + } else if (root.itemHref.indexOf('.json') > -1) { + root.itemType = "entity"; // "wearable" type handled later + } else { + console.log("WARNING - Item type is UNKNOWN!"); + root.itemType = "entity"; + } + } + + onItemTypeChanged: { + if (root.itemType === "entity" || root.itemType === "wearable" || + root.itemType === "contentSet" || root.itemType === "avatar" || root.itemType === "app") { + root.isCertified = true; + } else { + root.isCertified = false; + } } onItemPriceChanged: { @@ -203,7 +239,7 @@ Rectangle { color: hifi.colors.white; Component.onCompleted: { - purchasesReceived = false; + ownershipStatusReceived = false; balanceReceived = false; Commerce.getWalletStatus(); } @@ -278,6 +314,32 @@ Rectangle { anchors.left: parent.left; anchors.right: parent.right; + Rectangle { + id: loading; + z: 997; + visible: !root.ownershipStatusReceived || !root.balanceReceived; + anchors.fill: parent; + color: hifi.colors.white; + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup/section. + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + propagateComposedEvents: false; + } + + AnimatedImage { + id: loadingImage; + source: "../common/images/loader-blue.gif" + width: 74; + height: width; + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + } + } + RalewayRegular { id: confirmPurchaseText; anchors.top: parent.top; @@ -286,8 +348,8 @@ Rectangle { anchors.leftMargin: 16; width: paintedWidth; height: paintedHeight; - text: "Confirm Purchase:"; - color: hifi.colors.baseGray; + text: "Review Purchase:"; + color: hifi.colors.black; size: 28; } @@ -400,7 +462,7 @@ Rectangle { width: root.width; // Anchors anchors.top: separator2.bottom; - anchors.topMargin: 16; + anchors.topMargin: 0; anchors.left: parent.left; anchors.leftMargin: 16; anchors.right: parent.right; @@ -411,8 +473,8 @@ Rectangle { Rectangle { id: buyTextContainer; visible: buyText.text !== ""; - anchors.top: cancelPurchaseButton.bottom; - anchors.topMargin: 16; + anchors.top: parent.top; + anchors.topMargin: 10; anchors.left: parent.left; anchors.right: parent.right; height: buyText.height + 30; @@ -454,32 +516,63 @@ Rectangle { // Alignment horizontalAlignment: Text.AlignLeft; verticalAlignment: Text.AlignVCenter; + } + } - onLinkActivated: { - sendToScript({method: 'checkout_goToPurchases', filterText: root.itemName}); - } + // "View in My Purchases" button + HifiControlsUit.Button { + id: viewInMyPurchasesButton; + visible: false; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.light; + anchors.top: buyTextContainer.visible ? buyTextContainer.bottom : checkoutActionButtonsContainer.top; + anchors.topMargin: 10; + height: 50; + anchors.left: parent.left; + anchors.right: parent.right; + text: "VIEW THIS ITEM IN MY PURCHASES"; + onClicked: { + sendToScript({method: 'checkout_goToPurchases', filterText: root.itemName}); } } // "Buy" button HifiControlsUit.Button { id: buyButton; - enabled: (root.balanceAfterPurchase >= 0 && purchasesReceived && balanceReceived) || !itemIsJson; - color: hifi.buttons.blue; + visible: !((root.itemType === "avatar" || root.itemType === "app") && viewInMyPurchasesButton.visible) + enabled: (root.balanceAfterPurchase >= 0 && ownershipStatusReceived && balanceReceived) || (!root.isCertified); + color: viewInMyPurchasesButton.visible ? hifi.buttons.white : hifi.buttons.blue; colorScheme: hifi.colorSchemes.light; - anchors.top: checkoutActionButtonsContainer.top; - anchors.topMargin: 16; - height: 40; + anchors.top: viewInMyPurchasesButton.visible ? viewInMyPurchasesButton.bottom : + (buyTextContainer.visible ? buyTextContainer.bottom : checkoutActionButtonsContainer.top); + anchors.topMargin: 10; + height: 50; anchors.left: parent.left; anchors.right: parent.right; - text: (itemIsJson ? ((purchasesReceived && balanceReceived) ? "Confirm Purchase" : "--") : "Get Item"); + text: ((root.isCertified) ? ((ownershipStatusReceived && balanceReceived) ? + (viewInMyPurchasesButton.visible ? "Buy It Again" : "Confirm Purchase") : "--") : "Get Item"); onClicked: { - if (itemIsJson) { - buyButton.enabled = false; + if (root.isCertified) { if (!root.shouldBuyWithControlledFailure) { - Commerce.buy(itemId, itemPrice); + if (root.itemType === "contentSet" && !Entities.canReplaceContent()) { + lightboxPopup.titleText = "Purchase Content Set"; + lightboxPopup.bodyText = "You will not be able to replace this domain's content with " + root.itemName + + " until the server owner gives you 'Replace Content' permissions.

    Are you sure you want to purchase this content set?"; + lightboxPopup.button1text = "CANCEL"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.button2text = "CONFIRM"; + lightboxPopup.button2method = "Commerce.buy('" + root.itemId + "', " + root.itemPrice + ");" + + "root.visible = false; buyButton.enabled = false; loading.visible = true;"; + lightboxPopup.visible = true; + } else { + buyButton.enabled = false; + loading.visible = true; + Commerce.buy(root.itemId, root.itemPrice); + } } else { - Commerce.buy(itemId, itemPrice, true); + buyButton.enabled = false; + loading.visible = true; + Commerce.buy(root.itemId, root.itemPrice, true); } } else { if (urlHandler.canHandleUrl(itemHref)) { @@ -494,9 +587,9 @@ Rectangle { id: cancelPurchaseButton; color: hifi.buttons.noneBorderlessGray; colorScheme: hifi.colorSchemes.light; - anchors.top: buyButton.bottom; - anchors.topMargin: 16; - height: 40; + anchors.top: buyButton.visible ? buyButton.bottom : viewInMyPurchasesButton.bottom; + anchors.topMargin: 10; + height: 50; anchors.left: parent.left; anchors.right: parent.right; text: "Cancel" @@ -522,31 +615,32 @@ Rectangle { anchors.top: titleBarContainer.bottom; anchors.bottom: root.bottom; anchors.left: parent.left; - anchors.leftMargin: 16; + anchors.leftMargin: 20; anchors.right: parent.right; - anchors.rightMargin: 16; + anchors.rightMargin: 20; RalewayRegular { id: completeText; anchors.top: parent.top; - anchors.topMargin: 30; + anchors.topMargin: 18; anchors.left: parent.left; width: paintedWidth; height: paintedHeight; text: "Thank you for your order!"; color: hifi.colors.baseGray; - size: 28; + size: 36; } RalewaySemiBold { id: completeText2; - text: "The item " + '' + root.itemName + '' + - " has been added to your Purchases and a receipt will appear in your Wallet's transaction history."; + text: "The " + (root.itemTypesText)[itemTypesArray.indexOf(root.itemType)] + + ' ' + root.itemName + '' + + " has been added to your Purchases and a receipt will appear in your Wallet's transaction history."; // Text size - size: 20; + size: 18; // Anchors anchors.top: completeText.bottom; - anchors.topMargin: 10; + anchors.topMargin: 15; height: paintedHeight; anchors.left: parent.left; anchors.right: parent.right; @@ -576,7 +670,7 @@ Rectangle { RalewayBold { anchors.fill: parent; - text: "REZZED"; + text: (root.buttonTextClicked)[itemTypesArray.indexOf(root.itemType)]; size: 18; color: hifi.colors.white; verticalAlignment: Text.AlignVCenter; @@ -592,26 +686,58 @@ Rectangle { // "Rez" button HifiControlsUit.Button { id: rezNowButton; - enabled: root.canRezCertifiedItems || root.isWearable; - buttonGlyph: hifi.glyphs.lightning; + enabled: (root.itemType === "entity" && root.canRezCertifiedItems) || + (root.itemType === "contentSet" && Entities.canReplaceContent()) || + root.itemType === "wearable" || root.itemType === "avatar" || root.itemType === "app"; + buttonGlyph: (root.buttonGlyph)[itemTypesArray.indexOf(root.itemType)]; color: hifi.buttons.red; colorScheme: hifi.colorSchemes.light; anchors.top: completeText2.bottom; - anchors.topMargin: 30; + anchors.topMargin: 27; height: 50; anchors.left: parent.left; anchors.right: parent.right; - text: root.isWearable ? "Wear It" : "Rez It" + text: root.itemType === "app" && root.isInstalled ? "OPEN APP" : (root.buttonTextNormal)[itemTypesArray.indexOf(root.itemType)]; onClicked: { - sendToScript({method: 'checkout_rezClicked', itemHref: root.itemHref, isWearable: root.isWearable}); - rezzedNotifContainer.visible = true; - rezzedNotifContainerTimer.start(); - UserActivityLogger.commerceEntityRezzed(root.itemId, "checkout", root.isWearable ? "rez" : "wear"); + if (root.itemType === "contentSet") { + lightboxPopup.titleText = "Replace Content"; + lightboxPopup.bodyText = "Rezzing this content set will replace the existing environment and all of the items in this domain. " + + "If you want to save the state of the content in this domain, create a backup before proceeding.

    " + + "For more information about backing up and restoring content, " + + "" + + "click here to open info on your desktop browser."; + lightboxPopup.button1text = "CANCEL"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.button2text = "CONFIRM"; + lightboxPopup.button2method = "Commerce.replaceContentSet('" + root.itemHref + "');" + + "root.visible = false;rezzedNotifContainer.visible = true; rezzedNotifContainerTimer.start();" + + "UserActivityLogger.commerceEntityRezzed('" + root.itemId + "', 'checkout', '" + root.itemType + "');"; + lightboxPopup.visible = true; + } else if (root.itemType === "avatar") { + lightboxPopup.titleText = "Change Avatar"; + lightboxPopup.bodyText = "This will change your current avatar to " + root.itemName + " while retaining your wearables."; + lightboxPopup.button1text = "CANCEL"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.button2text = "CONFIRM"; + lightboxPopup.button2method = "MyAvatar.useFullAvatarURL('" + root.itemHref + "'); root.visible = false;"; + lightboxPopup.visible = true; + } else if (root.itemType === "app") { + if (root.isInstalled) { + Commerce.openApp(root.itemHref); + } else { + Commerce.installApp(root.itemHref); + } + } else { + sendToScript({method: 'checkout_rezClicked', itemHref: root.itemHref, itemType: root.itemType}); + rezzedNotifContainer.visible = true; + rezzedNotifContainerTimer.start(); + UserActivityLogger.commerceEntityRezzed(root.itemId, "checkout", root.itemType); + } } } RalewaySemiBold { id: noPermissionText; - visible: !root.canRezCertifiedItems && !root.isWearable; + visible: !root.canRezCertifiedItems && root.itemType === "entity"; text: 'You do not have Certified Rez permissions in this domain.' // Text size size: 16; @@ -640,7 +766,7 @@ Rectangle { } RalewaySemiBold { id: explainRezText; - visible: !root.isWearable; + visible: root.itemType === "entity"; text: 'What does "Rez" mean?' // Text size size: 16; @@ -663,9 +789,9 @@ Rectangle { RalewaySemiBold { id: myPurchasesLink; - text: 'View this item in My Purchases'; + text: 'View this item in My Purchases'; // Text size - size: 20; + size: 18; // Anchors anchors.top: explainRezText.visible ? explainRezText.bottom : (noPermissionText.visible ? noPermissionText.bottom : rezNowButton.bottom); anchors.topMargin: 40; @@ -685,12 +811,12 @@ Rectangle { RalewaySemiBold { id: walletLink; - text: 'View receipt in Wallet'; + text: 'View receipt in Wallet'; // Text size - size: 20; + size: 18; // Anchors anchors.top: myPurchasesLink.bottom; - anchors.topMargin: 20; + anchors.topMargin: 16; height: paintedHeight; anchors.left: parent.left; anchors.right: parent.right; @@ -708,12 +834,12 @@ Rectangle { RalewayRegular { id: pendingText; text: 'Your item is marked "pending" while your purchase is being confirmed. ' + - 'Learn More'; + 'Learn More'; // Text size - size: 20; + size: 18; // Anchors anchors.top: walletLink.bottom; - anchors.topMargin: 60; + anchors.topMargin: 32; height: paintedHeight; anchors.left: parent.left; anchors.right: parent.right; @@ -739,11 +865,10 @@ Rectangle { color: hifi.buttons.noneBorderlessGray; colorScheme: hifi.colorSchemes.light; anchors.bottom: parent.bottom; - anchors.bottomMargin: 20; + anchors.bottomMargin: 54; anchors.right: parent.right; - anchors.rightMargin: 14; - width: parent.width/2 - anchors.rightMargin; - height: 60; + width: 193; + height: 44; text: "Continue Shopping"; onClicked: { sendToScript({method: 'checkout_continueShopping', itemId: itemId}); @@ -851,7 +976,7 @@ Rectangle { buyButton.color = hifi.buttons.red; root.shouldBuyWithControlledFailure = true; } else { - buyButton.text = (itemIsJson ? ((purchasesReceived && balanceReceived) ? (root.alreadyOwned ? "Buy Another" : "Buy"): "--") : "Get Item"); + buyButton.text = (root.isCertified ? ((ownershipStatusReceived && balanceReceived) ? (root.alreadyOwned ? "Buy Another" : "Buy"): "--") : "Get Item"); buyButton.color = hifi.buttons.blue; root.shouldBuyWithControlledFailure = false; } @@ -883,7 +1008,7 @@ Rectangle { itemHref = message.params.itemHref; referrer = message.params.referrer; itemAuthor = message.params.itemAuthor; - setBuyText(); + refreshBuyUI(); break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); @@ -891,22 +1016,13 @@ Rectangle { } signal sendToScript(var message); - function purchasesContains(purchasesJson, id) { - for (var idx = 0; idx < purchasesJson.length; idx++) { - if(purchasesJson[idx].id === id) { - return true; - } - } - return false; - } - - function setBuyText() { - if (root.itemIsJson) { - if (root.purchasesReceived && root.balanceReceived) { + function refreshBuyUI() { + if (root.isCertified) { + if (root.ownershipStatusReceived && root.balanceReceived) { if (root.balanceAfterPurchase < 0) { if (root.alreadyOwned) { - buyText.text = "Your Wallet does not have sufficient funds to purchase this item again.
    " + - 'View the copy you own in My Purchases
    '; + buyText.text = "Your Wallet does not have sufficient funds to purchase this item again."; + viewInMyPurchasesButton.visible = true; } else { buyText.text = "Your Wallet does not have sufficient funds to purchase this item."; } @@ -916,15 +1032,19 @@ Rectangle { buyGlyph.size = 54; } else { if (root.alreadyOwned) { - buyText.text = 'You already own this item.
    Purchasing it will buy another copy.
    View this item in My Purchases
    '; - buyTextContainer.color = "#FFD6AD"; - buyTextContainer.border.color = "#FAC07D"; - buyGlyph.text = hifi.glyphs.alert; - buyGlyph.size = 46; + viewInMyPurchasesButton.visible = true; } else { buyText.text = ""; } + + if (root.itemType === "contentSet" && !Entities.canReplaceContent()) { + buyText.text = "The domain owner must enable 'Replace Content' permissions for you in this " + + "domain's server settings before you can replace this domain's content with " + root.itemName + ""; + buyTextContainer.color = "#FFC3CD"; + buyTextContainer.border.color = "#F3808F"; + buyGlyph.text = hifi.glyphs.alert; + buyGlyph.size = 54; + } } } else { buyText.text = ""; @@ -945,8 +1065,8 @@ Rectangle { root.activeView = "checkoutSuccess"; } root.balanceReceived = false; - root.purchasesReceived = false; - Commerce.inventory(); + root.ownershipStatusReceived = false; + Commerce.alreadyOwned(root.itemId); Commerce.balance(); } diff --git a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml index 8b0ec17232..eeda3afc71 100644 --- a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml +++ b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml @@ -25,10 +25,13 @@ Rectangle { property string titleText; property string bodyImageSource; property string bodyText; + property string button1color: hifi.buttons.noneBorderlessGray; property string button1text; property string button1method; + property string button2color: hifi.buttons.noneBorderless; property string button2text; property string button2method; + property string buttonLayout: "leftright"; readonly property string securityPicBodyText: "When you see your Security Pic, your actions and data are securely making use of your " + "Wallet's private keys.

    You can change your Security Pic in your Wallet."; @@ -39,6 +42,12 @@ Rectangle { color: Qt.rgba(0, 0, 0, 0.5); z: 999; + onVisibleChanged: { + if (!visible) { + resetLightbox(); + } + } + // This object is always used in a popup. // This MouseArea is used to prevent a user from being // able to click on a button/mouseArea underneath the popup. @@ -100,6 +109,10 @@ Rectangle { size: 20; verticalAlignment: Text.AlignTop; wrapMode: Text.WordWrap; + + onLinkActivated: { + sendToParent({ method: 'commerceLightboxLinkClicked', linkUrl: link }); + } } Item { @@ -108,18 +121,21 @@ Rectangle { anchors.topMargin: 30; anchors.left: parent.left; anchors.right: parent.right; - height: 70; + height: root.buttonLayout === "leftright" ? 70 : 150; // Button 1 HifiControlsUit.Button { - color: hifi.buttons.noneBorderlessGray; + id: button1; + color: root.button1color; colorScheme: hifi.colorSchemes.light; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 20; + anchors.top: root.buttonLayout === "leftright" ? parent.top : parent.top; anchors.left: parent.left; anchors.leftMargin: 10; - width: root.button2text ? parent.width/2 - anchors.leftMargin*2 : parent.width - anchors.leftMargin * 2; + anchors.right: root.buttonLayout === "leftright" ? undefined : parent.right; + anchors.rightMargin: root.buttonLayout === "leftright" ? undefined : 10; + width: root.buttonLayout === "leftright" ? (root.button2text ? parent.width/2 - anchors.leftMargin*2 : parent.width - anchors.leftMargin * 2) : + (undefined); + height: 50; text: root.button1text; onClicked: { eval(button1method); @@ -128,15 +144,18 @@ Rectangle { // Button 2 HifiControlsUit.Button { + id: button2; visible: root.button2text; - color: hifi.buttons.noneBorderless; + color: root.button2color; colorScheme: hifi.colorSchemes.light; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 20; + anchors.top: root.buttonLayout === "leftright" ? parent.top : button1.bottom; + anchors.topMargin: root.buttonLayout === "leftright" ? undefined : 20; + anchors.left: root.buttonLayout === "leftright" ? undefined : parent.left; + anchors.leftMargin: root.buttonLayout === "leftright" ? undefined : 10; anchors.right: parent.right; anchors.rightMargin: 10; - width: parent.width/2 - anchors.rightMargin*2; + width: root.buttonLayout === "leftright" ? parent.width/2 - anchors.rightMargin*2 : undefined; + height: 50; text: root.button2text; onClicked: { eval(button2method); @@ -149,6 +168,19 @@ Rectangle { // FUNCTION DEFINITIONS START // signal sendToParent(var msg); + + function resetLightbox() { + root.titleText = ""; + root.bodyImageSource = ""; + root.bodyText = ""; + root.button1color = hifi.buttons.noneBorderlessGray; + root.button1text = ""; + root.button1method = ""; + root.button2color = hifi.buttons.noneBorderless; + root.button2text = ""; + root.button2method = ""; + root.buttonLayout = "leftright"; + } // // FUNCTION DEFINITIONS END // diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index a4b0f57e8f..eb8159a4e7 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -37,9 +37,9 @@ Item { onWalletStatusResult: { if (walletStatus === 0) { sendToParent({method: "needsLogIn"}); - } else if (walletStatus === 3) { + } else if (walletStatus === 5) { Commerce.getSecurityImage(); - } else if (walletStatus > 3) { + } else if (walletStatus > 5) { console.log("ERROR in EmulatedMarketplaceHeader.qml: Unknown wallet status: " + walletStatus); } } diff --git a/interface/resources/qml/hifi/commerce/common/FirstUseTutorial.qml b/interface/resources/qml/hifi/commerce/common/FirstUseTutorial.qml index bcc641acd5..9944838e03 100644 --- a/interface/resources/qml/hifi/commerce/common/FirstUseTutorial.qml +++ b/interface/resources/qml/hifi/commerce/common/FirstUseTutorial.qml @@ -90,11 +90,11 @@ Rectangle { id: introText2; text: "My Purchases"; // Text size - size: 28; + size: 22; // Anchors anchors.top: introText1.bottom; anchors.left: parent.left; - anchors.leftMargin: 12; + anchors.leftMargin: 24; anchors.right: parent.right; height: paintedHeight; // Style diff --git a/interface/resources/qml/hifi/commerce/common/images/loader-blue.gif b/interface/resources/qml/hifi/commerce/common/images/loader-blue.gif new file mode 100644 index 0000000000..8b9e17053b Binary files /dev/null and b/interface/resources/qml/hifi/commerce/common/images/loader-blue.gif differ diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml index f493747c5e..a622349d00 100644 --- a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -208,6 +208,7 @@ Rectangle { // able to click on a button/mouseArea underneath the popup/section. MouseArea { anchors.fill: parent; + hoverEnabled: true; propagateComposedEvents: false; } diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 05e280b839..fb8e509cde 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -20,6 +20,7 @@ import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls import "../wallet" as HifiWallet +import TabletScriptingInterface 1.0 // references XXX from root context @@ -29,7 +30,6 @@ Item { id: root; property string purchaseStatus; property bool purchaseStatusChanged; - property bool canRezCertifiedItems: false; property string itemName; property string itemId; property string itemPreviewImageUrl; @@ -39,7 +39,15 @@ Item { property int itemEdition; property int numberSold; property int limitedRun; - property bool isWearable; + property string itemType; + property var itemTypesArray: ["entity", "wearable", "contentSet", "app", "avatar"]; + property var buttonTextNormal: ["REZ", "WEAR", "REPLACE", "INSTALL", "WEAR"]; + property var buttonTextClicked: ["REZZED", "WORN", "REPLACED", "INSTALLED", "WORN"] + property var buttonGlyph: [hifi.glyphs.wand, hifi.glyphs.hat, hifi.glyphs.globe, hifi.glyphs.install, hifi.glyphs.avatar]; + property bool showConfirmation: false; + property bool hasPermissionToRezThis; + property bool permissionExplanationCardVisible; + property bool isInstalled; property string originalStatusText; property string originalStatusColor; @@ -47,6 +55,47 @@ Item { height: 110; width: parent.width; + Connections { + target: Commerce; + + onContentSetChanged: { + if (contentSetHref === root.itemHref) { + showConfirmation = true; + } + } + + onAppInstalled: { + if (appHref === root.itemHref) { + root.isInstalled = true; + } + } + + onAppUninstalled: { + if (appHref === root.itemHref) { + root.isInstalled = false; + } + } + } + + Connections { + target: MyAvatar; + + onSkeletonModelURLChanged: { + if (skeletonModelURL === root.itemHref) { + showConfirmation = true; + } + } + } + + onItemTypeChanged: { + if ((itemType === "entity" && (!Entities.canRezCertified() && !Entities.canRezTmpCertified())) || + (itemType === "contentSet" && !Entities.canReplaceContent())) { + root.hasPermissionToRezThis = false; + } else { + root.hasPermissionToRezThis = true; + } + } + onPurchaseStatusChangedChanged: { if (root.purchaseStatusChanged === true && root.purchaseStatus === "confirmed") { root.originalStatusText = statusText.text; @@ -57,6 +106,15 @@ Item { } } + onShowConfirmationChanged: { + if (root.showConfirmation) { + rezzedNotifContainer.visible = true; + rezzedNotifContainerTimer.start(); + UserActivityLogger.commerceEntityRezzed(root.itemId, "purchases", root.itemType); + root.showConfirmation = false; + } + } + Timer { id: confirmedTimer; interval: 3000; @@ -73,10 +131,10 @@ Item { color: hifi.colors.white; // Size anchors.left: parent.left; - anchors.leftMargin: 8; + anchors.leftMargin: 16; anchors.right: parent.right; - anchors.rightMargin: 8; - anchors.top: parent.top; + anchors.rightMargin: 16; + anchors.verticalCenter: parent.verticalCenter; height: root.height - 10; Image { @@ -96,15 +154,20 @@ Item { } } - + TextMetrics { + id: itemNameTextMetrics; + font: itemName.font; + text: itemName.text; + } RalewaySemiBold { id: itemName; anchors.top: itemPreviewImage.top; anchors.topMargin: 4; anchors.left: itemPreviewImage.right; anchors.leftMargin: 8; - anchors.right: buttonContainer.left; - anchors.rightMargin: 8; + width: !noPermissionGlyph.visible ? (buttonContainer.x - itemPreviewImage.x - itemPreviewImage.width - anchors.leftMargin) : + Math.min(itemNameTextMetrics.tightBoundingRect.width + 2, + buttonContainer.x - itemPreviewImage.x - itemPreviewImage.width - anchors.leftMargin - noPermissionGlyph.width + 2); height: paintedHeight; // Text size size: 24; @@ -130,6 +193,93 @@ Item { } } } + HiFiGlyphs { + id: noPermissionGlyph; + visible: !root.hasPermissionToRezThis; + anchors.verticalCenter: itemName.verticalCenter; + anchors.left: itemName.right; + anchors.leftMargin: itemName.truncated ? -10 : -2; + text: hifi.glyphs.info; + // Size + size: 40; + width: 32; + // Style + color: hifi.colors.redAccent; + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + + onEntered: { + noPermissionGlyph.color = hifi.colors.redHighlight; + } + onExited: { + noPermissionGlyph.color = hifi.colors.redAccent; + } + onClicked: { + root.sendToPurchases({ method: 'openPermissionExplanationCard' }); + } + } + } + Rectangle { + id: permissionExplanationCard; + z: 995; + visible: root.permissionExplanationCardVisible; + anchors.fill: parent; + color: hifi.colors.white; + + RalewayRegular { + id: permissionExplanationText; + text: { + if (root.itemType === "contentSet") { + "You do not have 'Replace Content' permissions in this domain. Learn more"; + } else if (root.itemType === "entity") { + "You do not have 'Rez Certified' permissions in this domain. Learn more"; + } else { + "" + } + } + size: 16; + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: permissionExplanationGlyph.left; + color: hifi.colors.baseGray; + wrapMode: Text.WordWrap; + verticalAlignment: Text.AlignVCenter; + + onLinkActivated: { + sendToPurchases({method: 'showPermissionsExplanation', itemType: root.itemType}); + } + } + // "Close" button + HiFiGlyphs { + id: permissionExplanationGlyph; + text: hifi.glyphs.close; + color: hifi.colors.baseGray; + size: 26; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + width: 77; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + parent.text = hifi.glyphs.closeInverted; + } + onExited: { + parent.text = hifi.glyphs.close; + } + onClicked: { + root.sendToPurchases({ method: 'openPermissionExplanationCard', closeAll: true }); + } + } + } + } Item { id: certificateContainer; @@ -151,19 +301,19 @@ Item { anchors.bottom: parent.bottom; width: 32; // Style - color: hifi.colors.lightGray; + color: hifi.colors.black; } RalewayRegular { id: viewCertificateText; text: "VIEW CERTIFICATE"; - size: 14; + size: 13; anchors.left: certificateIcon.right; anchors.leftMargin: 4; anchors.top: parent.top; anchors.bottom: parent.bottom; anchors.right: parent.right; - color: hifi.colors.lightGray; + color: hifi.colors.black; } MouseArea { @@ -173,13 +323,13 @@ Item { sendToPurchases({method: 'purchases_itemCertificateClicked', itemCertificateId: root.certificateId}); } onEntered: { - certificateIcon.color = hifi.colors.black; - viewCertificateText.color = hifi.colors.black; - } - onExited: { certificateIcon.color = hifi.colors.lightGray; viewCertificateText.color = hifi.colors.lightGray; } + onExited: { + certificateIcon.color = hifi.colors.black; + viewCertificateText.color = hifi.colors.black; + } } } @@ -193,14 +343,14 @@ Item { anchors.right: buttonContainer.left; anchors.rightMargin: 2; - FiraSansRegular { + RalewayRegular { anchors.left: parent.left; anchors.top: parent.top; anchors.bottom: parent.bottom; width: paintedWidth; text: "#" + root.itemEdition; - size: 15; - color: "#cc6a6a6a"; + size: 13; + color: hifi.colors.black; verticalAlignment: Text.AlignTop; } } @@ -311,7 +461,7 @@ Item { id: rezzedNotifContainer; z: 998; visible: false; - color: hifi.colors.blueHighlight; + color: "#1FC6A6"; anchors.fill: buttonContainer; MouseArea { anchors.fill: parent; @@ -321,8 +471,8 @@ Item { RalewayBold { anchors.fill: parent; - text: "REZZED"; - size: 18; + text: (root.buttonTextClicked)[itemTypesArray.indexOf(root.itemType)]; + size: 15; color: hifi.colors.white; verticalAlignment: Text.AlignVCenter; horizontalAlignment: Text.AlignHCenter; @@ -335,27 +485,89 @@ Item { } } + Rectangle { + id: appButtonContainer; + color: hifi.colors.white; + z: 994; + visible: root.isInstalled; + anchors.fill: buttonContainer; + + HifiControlsUit.Button { + id: openAppButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.light; + anchors.top: parent.top; + anchors.right: parent.right; + anchors.left: parent.left; + width: 92; + height: 44; + text: "OPEN" + onClicked: { + Commerce.openApp(root.itemHref); + } + } + + HifiControlsUit.Button { + id: uninstallAppButton; + color: hifi.buttons.noneBorderless; + colorScheme: hifi.colorSchemes.light; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + anchors.left: parent.left; + height: 44; + text: "UNINSTALL" + onClicked: { + Commerce.uninstallApp(root.itemHref); + } + } + } + Button { id: buttonContainer; - property int color: hifi.buttons.red; + property int color: hifi.buttons.blue; property int colorScheme: hifi.colorSchemes.light; anchors.top: parent.top; + anchors.topMargin: 4; anchors.bottom: parent.bottom; + anchors.bottomMargin: 4; anchors.right: parent.right; + anchors.rightMargin: 4; width: height; - enabled: (root.canRezCertifiedItems || root.isWearable) && root.purchaseStatus !== "invalidated"; + enabled: root.hasPermissionToRezThis && + root.purchaseStatus !== "invalidated" && + MyAvatar.skeletonModelURL !== root.itemHref; + + onHoveredChanged: { + if (hovered) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + + onFocusChanged: { + if (focus) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } onClicked: { - sendToPurchases({method: 'purchases_rezClicked', itemHref: root.itemHref, isWearable: root.isWearable}); - rezzedNotifContainer.visible = true; - rezzedNotifContainerTimer.start(); - UserActivityLogger.commerceEntityRezzed(root.itemId, "purchases", root.isWearable ? "rez" : "wear"); + Tablet.playSound(TabletEnums.ButtonClick); + if (root.itemType === "contentSet") { + sendToPurchases({method: 'showReplaceContentLightbox', itemHref: root.itemHref}); + } else if (root.itemType === "avatar") { + sendToPurchases({method: 'showChangeAvatarLightbox', itemName: root.itemName, itemHref: root.itemHref}); + } else if (root.itemType === "app") { + // "Run" and "Uninstall" buttons are separate. + Commerce.installApp(root.itemHref); + } else { + sendToPurchases({method: 'purchases_rezClicked', itemHref: root.itemHref, itemType: root.itemType}); + root.showConfirmation = true; + } } style: ButtonStyle { - background: Rectangle { + radius: 4; gradient: Gradient { GradientStop { position: 0.2 @@ -390,13 +602,13 @@ Item { label: Item { HiFiGlyphs { - id: lightningIcon; - text: hifi.glyphs.lightning; + id: rezIcon; + text: (root.buttonGlyph)[itemTypesArray.indexOf(root.itemType)]; // Size - size: 32; + size: 60; // Anchors anchors.top: parent.top; - anchors.topMargin: 12; + anchors.topMargin: 0; anchors.left: parent.left; anchors.right: parent.right; horizontalAlignment: Text.AlignHCenter; @@ -405,18 +617,19 @@ Item { : hifi.buttons.disabledTextColor[control.colorScheme] } RalewayBold { - anchors.top: lightningIcon.bottom; - anchors.topMargin: -20; + id: rezIconLabel; + anchors.top: rezIcon.bottom; + anchors.topMargin: -4; anchors.right: parent.right; anchors.left: parent.left; anchors.bottom: parent.bottom; font.capitalization: Font.AllUppercase color: enabled ? hifi.buttons.textColor[control.color] : hifi.buttons.disabledTextColor[control.colorScheme] - size: 16; + size: 15; verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter - text: root.isWearable ? "Wear It" : "Rez It" + text: MyAvatar.skeletonModelURL === root.itemHref ? "CURRENT" : (root.buttonTextNormal)[itemTypesArray.indexOf(root.itemType)]; } } } @@ -425,11 +638,11 @@ Item { DropShadow { anchors.fill: mainContainer; - horizontalOffset: 3; - verticalOffset: 3; - radius: 8.0; - samples: 17; - color: "#80000000"; + horizontalOffset: 0; + verticalOffset: 4; + radius: 4.0; + samples: 9 + color: Qt.rgba(0, 0, 0, 0.25); source: mainContainer; } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 2743677683..c505baebf4 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -32,11 +32,11 @@ Rectangle { property bool securityImageResultReceived: false; property bool purchasesReceived: false; property bool punctuationMode: false; - property bool canRezCertifiedItems: Entities.canRezCertified() || Entities.canRezTmpCertified(); property bool pendingInventoryReply: true; property bool isShowingMyItems: false; property bool isDebuggingFirstUseTutorial: false; property int pendingItemCount: 0; + property string installedApps; // Style color: hifi.colors.white; Connections { @@ -47,21 +47,22 @@ Rectangle { if (root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; } - } else if (walletStatus === 1) { + } else if ((walletStatus === 1) || (walletStatus === 2) || (walletStatus === 3)) { if (root.activeView !== "notSetUp") { root.activeView = "notSetUp"; notSetUpTimer.start(); } - } else if (walletStatus === 2) { + } else if (walletStatus === 4) { if (root.activeView !== "passphraseModal") { root.activeView = "passphraseModal"; UserActivityLogger.commercePassphraseEntry("marketplace purchases"); } - } else if (walletStatus === 3) { + } else if (walletStatus === 5) { if ((Settings.getValue("isFirstUseOfPurchases", true) || root.isDebuggingFirstUseTutorial) && root.activeView !== "firstUseTutorial") { root.activeView = "firstUseTutorial"; } else if (!Settings.getValue("isFirstUseOfPurchases", true) && root.activeView === "initialize") { root.activeView = "purchasesMain"; + root.installedApps = Commerce.getInstalledApps(); Commerce.inventory(); } } else { @@ -148,7 +149,11 @@ Rectangle { Connections { onSendToParent: { - sendToScript(msg); + if (msg.method === 'commerceLightboxLinkClicked') { + Qt.openUrlExternally(msg.linkUrl); + } else { + sendToScript(msg); + } } } } @@ -266,6 +271,7 @@ Rectangle { case 'tutorial_finished': Settings.setValue("isFirstUseOfPurchases", false); root.activeView = "purchasesMain"; + root.installedApps = Commerce.getInstalledApps(); Commerce.inventory(); break; } @@ -297,7 +303,7 @@ Rectangle { anchors.left: parent.left; anchors.leftMargin: 8; anchors.right: parent.right; - anchors.rightMargin: 12; + anchors.rightMargin: 16; anchors.top: parent.top; anchors.topMargin: 4; @@ -308,11 +314,11 @@ Rectangle { anchors.bottom: parent.bottom; anchors.bottomMargin: 10; anchors.left: parent.left; - anchors.leftMargin: 4; + anchors.leftMargin: 16; width: paintedWidth; text: isShowingMyItems ? "My Items" : "My Purchases"; - color: hifi.colors.baseGray; - size: 28; + color: hifi.colors.black; + size: 22; } HifiControlsUit.TextField { @@ -323,8 +329,8 @@ Rectangle { hasRoundedBorder: true; anchors.left: myText.right; anchors.leftMargin: 16; - anchors.top: parent.top; - anchors.bottom: parent.bottom; + height: 39; + anchors.verticalCenter: parent.verticalCenter; anchors.right: parent.right; placeholderText: "filter items"; @@ -345,7 +351,7 @@ Rectangle { HifiControlsUit.Separator { id: separator; - colorScheme: 1; + colorScheme: 2; anchors.left: parent.left; anchors.right: parent.right; anchors.top: filterBarContainer.bottom; @@ -365,69 +371,6 @@ Rectangle { id: filteredPurchasesModel; } - Rectangle { - id: cantRezCertified; - visible: !root.canRezCertifiedItems; - color: "#FFC3CD"; - radius: 4; - border.color: hifi.colors.redAccent; - border.width: 1; - anchors.top: separator.bottom; - anchors.topMargin: 12; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: 80; - - HiFiGlyphs { - id: lightningIcon; - text: hifi.glyphs.lightning; - // Size - size: 36; - // Anchors - anchors.top: parent.top; - anchors.topMargin: 18; - anchors.left: parent.left; - anchors.leftMargin: 12; - horizontalAlignment: Text.AlignHCenter; - // Style - color: hifi.colors.lightGray; - } - - RalewayRegular { - text: "You don't have permission to rez certified items in this domain. " + - 'Learn More'; - // Text size - size: 18; - // Anchors - anchors.top: parent.top; - anchors.topMargin: 4; - anchors.left: lightningIcon.right; - anchors.leftMargin: 8; - anchors.right: parent.right; - anchors.rightMargin: 8; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 4; - // Style - color: hifi.colors.baseGray; - wrapMode: Text.WordWrap; - // Alignment - verticalAlignment: Text.AlignVCenter; - - onLinkActivated: { - lightboxPopup.titleText = "Rez Permission Required"; - lightboxPopup.bodyText = "You don't have permission to rez certified items in this domain.

    " + - "Use the GOTO app to visit another domain or go to your own sandbox."; - lightboxPopup.button1text = "CLOSE"; - lightboxPopup.button1method = "root.visible = false;" - lightboxPopup.button2text = "OPEN GOTO"; - lightboxPopup.button2method = "sendToParent({method: 'purchases_openGoTo'});"; - lightboxPopup.visible = true; - } - } - } - ListView { id: purchasesContentsList; visible: (root.isShowingMyItems && filteredPurchasesModel.count !== 0) || (!root.isShowingMyItems && filteredPurchasesModel.count !== 0); @@ -436,13 +379,12 @@ Rectangle { snapMode: ListView.SnapToItem; highlightRangeMode: ListView.StrictlyEnforceRange; // Anchors - anchors.top: root.canRezCertifiedItems ? separator.bottom : cantRezCertified.bottom; + anchors.top: separator.bottom; anchors.topMargin: 12; anchors.left: parent.left; anchors.bottom: parent.bottom; width: parent.width; delegate: PurchasedItem { - canRezCertifiedItems: root.canRezCertifiedItems; itemName: title; itemId: id; itemPreviewImageUrl: preview; @@ -454,16 +396,32 @@ Rectangle { numberSold: model.number_sold; limitedRun: model.limited_run; displayedItemCount: model.displayedItemCount; - isWearable: model.categories.indexOf("Wearables") > -1; - anchors.topMargin: 12; - anchors.bottomMargin: 12; + permissionExplanationCardVisible: model.permissionExplanationCardVisible; + isInstalled: model.isInstalled; + itemType: { + if (model.root_file_url.indexOf(".fst") > -1) { + "avatar"; + } else if (model.categories.indexOf("Wearables") > -1) { + "wearable"; + } else if (model.root_file_url.endsWith('.json.gz')) { + "contentSet"; + } else if (model.root_file_url.endsWith('.app.json')) { + "app"; + } else if (model.root_file_url.endsWith('.json')) { + "entity"; + } else { + "unknown"; + } + } + anchors.topMargin: 10; + anchors.bottomMargin: 10; Connections { onSendToPurchases: { if (msg.method === 'purchases_itemInfoClicked') { sendToScript({method: 'purchases_itemInfoClicked', itemId: itemId}); } else if (msg.method === "purchases_rezClicked") { - sendToScript({method: 'purchases_rezClicked', itemHref: itemHref, isWearable: isWearable}); + sendToScript({method: 'purchases_rezClicked', itemHref: itemHref, itemType: itemType}); } else if (msg.method === 'purchases_itemCertificateClicked') { inspectionCertificate.visible = true; inspectionCertificate.isLightbox = true; @@ -482,8 +440,51 @@ Rectangle { lightboxPopup.button1text = "CLOSE"; lightboxPopup.button1method = "root.visible = false;" lightboxPopup.visible = true; + } else if (msg.method === "showReplaceContentLightbox") { + lightboxPopup.titleText = "Replace Content"; + lightboxPopup.bodyText = "Rezzing this content set will replace the existing environment and all of the items in this domain. " + + "If you want to save the state of the content in this domain, create a backup before proceeding.

    " + + "For more information about backing up and restoring content, " + + "" + + "click here to open info on your desktop browser."; + lightboxPopup.button1text = "CANCEL"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.button2text = "CONFIRM"; + lightboxPopup.button2method = "Commerce.replaceContentSet('" + msg.itemHref + "'); root.visible = false;"; + lightboxPopup.visible = true; + } else if (msg.method === "showChangeAvatarLightbox") { + lightboxPopup.titleText = "Change Avatar"; + lightboxPopup.bodyText = "This will change your current avatar to " + msg.itemName + " while retaining your wearables."; + lightboxPopup.button1text = "CANCEL"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.button2text = "CONFIRM"; + lightboxPopup.button2method = "MyAvatar.useFullAvatarURL('" + msg.itemHref + "'); root.visible = false;"; + lightboxPopup.visible = true; + } else if (msg.method === "showPermissionsExplanation") { + if (msg.itemType === "entity") { + lightboxPopup.titleText = "Rez Certified Permission"; + lightboxPopup.bodyText = "You don't have permission to rez certified items in this domain.

    " + + "Use the GOTO app to visit another domain or go to your own sandbox."; + lightboxPopup.button2text = "OPEN GOTO"; + lightboxPopup.button2method = "sendToParent({method: 'purchases_openGoTo'});"; + } else if (msg.itemType === "contentSet") { + lightboxPopup.titleText = "Replace Content Permission"; + lightboxPopup.bodyText = "You do not have the permission 'Replace Content' in this domain's server settings. The domain owner " + + "must enable it for you before you can replace content sets in this domain."; + } + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.visible = true; } else if (msg.method === "setFilterText") { filterBar.text = msg.filterText; + } else if (msg.method === "openPermissionExplanationCard") { + for (var i = 0; i < filteredPurchasesModel.count; i++) { + if (i !== index || msg.closeAll) { + filteredPurchasesModel.setProperty(i, "permissionExplanationCardVisible", false); + } else { + filteredPurchasesModel.setProperty(i, "permissionExplanationCardVisible", true); + } + } } } } @@ -683,8 +684,13 @@ Rectangle { if (sameItemCount !== tempPurchasesModel.count || filterBar.text !== filterBar.previousText) { filteredPurchasesModel.clear(); + var currentId; for (var i = 0; i < tempPurchasesModel.count; i++) { + currentId = tempPurchasesModel.get(i).id; + filteredPurchasesModel.append(tempPurchasesModel.get(i)); + filteredPurchasesModel.setProperty(i, 'permissionExplanationCardVisible', false); + filteredPurchasesModel.setProperty(i, 'isInstalled', ((root.installedApps).indexOf(currentId) > -1)); } populateDisplayedItemCounts(); diff --git a/interface/resources/qml/hifi/commerce/wallet/Help.qml b/interface/resources/qml/hifi/commerce/wallet/Help.qml index f0f123f6c0..d76fe0d8a2 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Help.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Help.qml @@ -60,48 +60,88 @@ Item { ListElement { isExpanded: false; - question: "How can I get HFC?" - answer: qsTr("High Fidelity commerce is in closed beta right now.

    To request entry and get free HFC, please contact info@highfidelity.com with your High Fidelity account username and the email address registered to that account."); + question: "How can I get HFC?"; + answer: "High Fidelity commerce is in open beta right now. Want more HFC? \ +Get it by going to

    BankOfHighFidelity. and meeting with the banker!"; } ListElement { isExpanded: false; - question: "What are private keys?" - answer: qsTr("A private key is a secret piece of text that is used to prove ownership, unlock confidential information, and sign transactions.

    In High Fidelity, your private keys are used to securely access the contents of your Wallet and Purchases."); + question: "What are private keys and where are they stored?"; + answer: + "A private key is a secret piece of text that is used to prove ownership, unlock confidential information, and sign transactions. \ +In High Fidelity, your private key is used to securely access the contents of your Wallet and Purchases. \ +After wallet setup, a hifikey file is stored on your computer in High Fidelity Interface's AppData directory. \ +Your hifikey file contains your private key and is protected by your wallet passphrase. \ +

    It is very important to back up your hifikey file! \ +Tap here to open the folder where your HifiKeys are stored on your main display." } ListElement { isExpanded: false; - question: "Where are my private keys stored?" - answer: qsTr('By default, your private keys are only stored on your hard drive in High Fidelity Interface\'s AppData directory.

    Tap here to open the folder where your HifiKeys are stored on your main display.'); + question: "How do I back up my private keys?"; + answer: "You can back up your hifikey file (which contains your private key and is encrypted using your wallet passphrase) by copying it to a USB flash drive, or to a service like Dropbox or Google Drive. \ +Restore your hifikey file by replacing the file in Interface's AppData directory with your backup copy. \ +Others with access to your back up should not be able to spend your HFC without your passphrase. \ +Tap here to open the folder where your HifiKeys are stored on your main display."; } ListElement { isExpanded: false; - question: "How can I backup my private keys?" - answer: qsTr('You may backup the file containing your private keys by copying it to a USB flash drive, or to a service like Dropbox or Google Drive.

    Restore your backup by replacing the file in Interface\'s AppData directory with your backed-up copy.

    Tap here to open the folder where your HifiKeys are stored on your main display.'); + question: "What happens if I lose my private keys?"; + answer: "We cannot stress enough that you should keep a backup! For security reasons, High Fidelity does not keep a copy, and cannot restore it for you. \ +If you lose your private key, you will no longer have access to the contents of your Wallet or My Purchases. \ +Here are some things to try:
      \ +
    • If you have backed up your hifikey file before, search your backup location
    • \ +
    • Search your AppData directory in the last machine you used to set up the Wallet
    • \ +
    • If you are a developer and have installed multiple builds of High Fidelity, your hifikey file might be in another folder
    • \ +


    As a last resort, you can set up your Wallet again and generate a new hifikey file. \ +Unfortunately, this means you will start with 0 HFC and your purchased items will not be transferred over."; } ListElement { isExpanded: false; - question: "What happens if I lose my passphrase?" - answer: qsTr("Your passphrase is used to encrypt your private keys. If you lose your passphrase, you will no longer be able to decrypt your private key file. You will also no longer have access to the contents of your Wallet or My Purchases.

    Nobody can help you recover your passphrase, including High Fidelity. Please write it down and store it securely."); + question: "What if I forget my wallet passphrase?"; + answer: "Your wallet passphrase is used to encrypt your private keys. Please write it down and store it securely! \ +

    If you forget your passphrase, you will no longer be able to decrypt the hifikey file that the passphrase protects. \ +You will also no longer have access to the contents of your Wallet or My Purchases. \ +For security reasons, High Fidelity does not keep a copy of your passphrase, and can't restore it for you. \ +

    If you still cannot remember your wallet passphrase, you can set up your Wallet again and generate a new hifikey file. \ +Unfortunately, this means you will start with 0 HFC and your purchased items will not be transferred over."; } ListElement { isExpanded: false; - question: "What is a 'Security Pic'?" - answer: qsTr("Your Security Pic is an encrypted image that you selected during Wallet Setup. It acts as an extra layer of Wallet security.

    When you see your Security Pic, you know that your actions and data are securely making use of your private keys.

    If you don't see your Security Pic on a page that is asking you for your Wallet passphrase, someone untrustworthy may be trying to gain access to your Wallet.

    The encrypted Pic is stored on your hard drive inside the same file as your private keys."); + question: "How do I send HFC to other people?"; + answer: "You can send HFC to a High Fidelity connection (someone you've shaken hands with in-world) or somebody Nearby (currently in the same domain as you). \ +In your Wallet's Send Money tab, choose from your list of connections, or choose Nearby and select the glowing sphere of the person's avatar."; } ListElement { isExpanded: false; - question: "My HFC balance isn't what I expect it to be. Why?" - answer: qsTr('High Fidelity Coin (HFC) transactions are backed by a blockchain, which takes time to update. The status of a transaction usually updates within a few seconds.

    Tap here to learn more about the blockchain.'); + question: "What is a Security Pic?" + answer: "Your Security Pic is an encrypted image that you select during Wallet Setup. \ +It acts as an extra layer of Wallet security. \ +When you see your Security Pic, you know that your actions and data are securely making use of your private keys.\ +

    Don't enter your passphrase anywhere that doesn't display your Security Pic! \ +If you don't see your Security Pic on a page that requests your Wallet passphrase, someone untrustworthy may be trying to access your Wallet."; } ListElement { isExpanded: false; - question: "Do I get charged money if a transaction fails?" - answer: qsTr("No. Your HFC balance only changes after a transaction is confirmed."); + question: "Why does my HFC balance not update instantly?"; + answer: "HFC transations sometimes takes a few seconds to update as they are backed by a blockchain. \ +

    Tap here to learn more about the blockchain."; + } + ListElement { + isExpanded: false; + question: "Do I get charged money if a transaction fails?"; + answer: "No. Your HFC balance only changes after a transaction is confirmed."; } ListElement { isExpanded: false; question: "How do I convert HFC to other currencies?" - answer: qsTr("We are still building the tools needed to support a vibrant economy in High Fidelity. There is currently no way to convert HFC to other currencies."); + answer: "We are hard at work building the tools needed to support a vibrant economy in High Fidelity. \ +At the moment, there is currently no way to convert HFC to other currencies. Stay tuned..."; + } + ListElement { + isExpanded: false; + question: "Who can I reach out to with questions?"; + answer: "Please email us if you have any issues or questions: \ +support@highfidelity.com"; } } @@ -212,6 +252,10 @@ Item { Qt.openUrlExternally("file:///" + root.keyFilePath.substring(0, root.keyFilePath.lastIndexOf('/'))); } else if (link === "#blockchain") { Qt.openUrlExternally("https://docs.highfidelity.com/high-fidelity-commerce"); + } else if (link === "#bank") { + Qt.openUrlExternally("hifi://BankOfHighFidelity"); + } else if (link === "#support") { + Qt.openUrlExternally("mailto:support@highfidelity.com"); } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml index c586af9e80..f1692acb3d 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml @@ -75,8 +75,6 @@ Item { // TODO: Fix this unlikely bug onVisibleChanged: { if (visible) { - passphraseField.error = false; - passphraseField.focus = true; sendSignalToParent({method: 'disableHmdPreview'}); } else { sendSignalToParent({method: 'maybeEnableHmdPreview'}); @@ -206,6 +204,14 @@ Item { placeholderText: "passphrase"; activeFocusOnPress: true; activeFocusOnTab: true; + + onVisibleChanged: { + if (visible) { + error = false; + focus = true; + forceActiveFocus(); + } + } onFocusChanged: { root.keyboardRaised = focus; diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index ae42b8e3e1..952390a7a4 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -47,20 +47,22 @@ Rectangle { } } else if (walletStatus === 1) { if (root.activeView !== "walletSetup") { - root.activeView = "walletSetup"; - Commerce.resetLocalWalletOnly(); - var timestamp = new Date(); - walletSetup.startingTimestamp = timestamp; - walletSetup.setupAttemptID = generateUUID(); - UserActivityLogger.commerceWalletSetupStarted(timestamp, setupAttemptID, walletSetup.setupFlowVersion, walletSetup.referrer ? walletSetup.referrer : "wallet app", - (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : '')); + walletResetSetup(); } } else if (walletStatus === 2) { + if (root.activeView != "preexisting") { + root.activeView = "preexisting"; + } + } else if (walletStatus === 3) { + if (root.activeView != "conflicting") { + root.activeView = "conflicting"; + } + } else if (walletStatus === 4) { if (root.activeView !== "passphraseModal") { root.activeView = "passphraseModal"; UserActivityLogger.commercePassphraseEntry("wallet app"); } - } else if (walletStatus === 3) { + } else if (walletStatus === 5) { if (root.activeView !== "walletSetup") { root.activeView = "walletHome"; Commerce.getSecurityImage(); @@ -169,6 +171,25 @@ Rectangle { // TITLE BAR END // + WalletChoice { + id: walletChoice; + proceedFunction: function (isReset) { + console.log(isReset ? "Reset wallet." : "Trying again with new wallet."); + Commerce.setSoftReset(); + if (isReset) { + walletResetSetup(); + } else { + var msg = { referrer: walletChoice.referrer } + followReferrer(msg); + } + } + copyFunction: Commerce.copyKeyFileFrom; + z: 997; + visible: (root.activeView === "preexisting") || (root.activeView === "conflicting"); + activeView: root.activeView; + anchors.fill: parent; + } + WalletSetup { id: walletSetup; visible: root.activeView === "walletSetup"; @@ -178,14 +199,7 @@ Rectangle { Connections { onSendSignalToWallet: { if (msg.method === 'walletSetup_finished') { - if (msg.referrer === '' || msg.referrer === 'marketplace cta') { - root.activeView = "initialize"; - Commerce.getWalletStatus(); - } else if (msg.referrer === 'purchases') { - sendToScript({method: 'goToPurchases'}); - } else { - sendToScript({method: 'goToMarketplaceItemPage', itemId: msg.referrer}); - } + followReferrer(msg); } else if (msg.method === 'walletSetup_raiseKeyboard') { root.keyboardRaised = true; root.isPassword = msg.isPasswordField; @@ -738,6 +752,7 @@ Rectangle { switch (message.method) { case 'updateWalletReferrer': walletSetup.referrer = message.referrer; + walletChoice.referrer = message.referrer; break; case 'inspectionCertificate_resetCert': // NOP @@ -768,6 +783,28 @@ Rectangle { return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); } + + function walletResetSetup() { + root.activeView = "walletSetup"; + var timestamp = new Date(); + walletSetup.startingTimestamp = timestamp; + walletSetup.setupAttemptID = generateUUID(); + UserActivityLogger.commerceWalletSetupStarted(timestamp, walletSetup.setupAttemptID, walletSetup.setupFlowVersion, walletSetup.referrer ? walletSetup.referrer : "wallet app", + (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : '')); + } + + function followReferrer(msg) { + if (msg.referrer === '' || msg.referrer === 'marketplace cta') { + root.activeView = "initialize"; + Commerce.getWalletStatus(); + } else if (msg.referrer === 'purchases') { + sendToScript({method: 'goToPurchases'}); + } else if (msg.referrer === 'marketplace cta' || msg.referrer === 'mainPage') { + sendToScript({method: 'goToMarketplaceMainPage', itemId: msg.referrer}); + } else { + sendToScript({method: 'goToMarketplaceItemPage', itemId: msg.referrer}); + } + } // // FUNCTION DEFINITIONS END // diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletChoice.qml b/interface/resources/qml/hifi/commerce/wallet/WalletChoice.qml new file mode 100644 index 0000000000..457db55fd1 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/WalletChoice.qml @@ -0,0 +1,297 @@ +// +// WalletChoice.qml +// qml/hifi/commerce/wallet +// +// WalletChoice +// +// Created by Howard Stearns +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../common" as HifiCommerceCommon +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit + + +Item { + HifiConstants { id: hifi; } + + id: root; + property string activeView: "conflict"; + property var proceedFunction: nil; + property var copyFunction: nil; + property string referrer: ""; + + Image { + anchors.fill: parent; + source: "images/wallet-bg.jpg"; + } + + HifiCommerceCommon.CommerceLightbox { + id: lightboxPopup; + visible: false; + anchors.fill: parent; + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + // + // TITLE BAR START + // + Item { + id: titleBarContainer; + // Size + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + anchors.right: parent.right; + + // Wallet icon + HiFiGlyphs { + id: walletIcon; + text: hifi.glyphs.wallet; + // Size + size: parent.height * 0.8; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 8; + anchors.verticalCenter: parent.verticalCenter; + // Style + color: hifi.colors.blueHighlight; + } + + // Title Bar text + RalewayRegular { + id: titleBarText; + text: "Wallet Setup"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: walletIcon.right; + anchors.leftMargin: 8; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + } + // + // TITLE BAR END + // + + // + // MAIN PAGE START + // + Item { + id: preexistingContainer; + // Anchors + anchors.top: titleBarContainer.bottom; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + + HiFiGlyphs { + id: bigKeyIcon; + text: hifi.glyphs.walletKey; + // Size + size: 180; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 40; + anchors.horizontalCenter: parent.horizontalCenter; + // Style + color: hifi.colors.white; + } + + RalewayRegular { + id: text01; + text: root.activeView === "preexisting" ? + "Where are your private keys?" : + "Hmm, your keys are different" + // Text size + size: 26; + // Anchors + anchors.top: bigKeyIcon.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: paintedHeight; + width: paintedWidth; + // Style + color: hifi.colors.white; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + RalewayRegular { + id: text02; + text: root.activeView === "preexisting" ? + "Our records indicate that you created a wallet, but the private keys are not in the folder where we checked." : + "Our records indicate that you created a wallet with different keys than the keys you're providing." + // Text size + size: 18; + // Anchors + anchors.top: text01.bottom; + anchors.topMargin: 40; + anchors.left: parent.left; + anchors.leftMargin: 65; + anchors.right: parent.right; + anchors.rightMargin: 65; + height: paintedHeight; + width: paintedWidth; + // Style + color: hifi.colors.white; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // "Locate" button + HifiControlsUit.Button { + id: locateButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: text02.bottom; + anchors.topMargin: 40; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width/2; + height: 50; + text: root.activeView === "preexisting" ? + "LOCATE MY KEYS" : + "LOCATE OTHER KEYS" + onClicked: { + walletChooser(); + } + } + + // "Create New" OR "Continue" button + HifiControlsUit.Button { + id: button02; + color: hifi.buttons.none; + colorScheme: hifi.colorSchemes.dark; + anchors.top: locateButton.bottom; + anchors.topMargin: 20; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width/2; + height: 50; + text: root.activeView === "preexisting" ? + "CREATE NEW WALLET" : + "CONTINUE WITH THESE KEYS" + onClicked: { + lightboxPopup.titleText = "Are you sure?"; + lightboxPopup.bodyText = "Taking this step will abandon your old wallet and you will no " + + "longer be able to access your money and your past purchases.

    " + + "This step should only be used if you cannot find your keys.

    " + + "This step cannot be undone."; + lightboxPopup.button1color = hifi.buttons.red; + lightboxPopup.button1text = "YES, CREATE NEW WALLET"; + lightboxPopup.button1method = "root.visible = false;proceed(true);"; + lightboxPopup.button2text = "CANCEL"; + lightboxPopup.button2method = "root.visible = false;" + lightboxPopup.buttonLayout = "topbottom"; + lightboxPopup.visible = true; + } + } + + // "What's This?" link + RalewayRegular { + id: whatsThisLink; + text: 'What\'s this?'; + // Anchors + anchors.bottom: parent.bottom; + anchors.bottomMargin: 48; + anchors.horizontalCenter: parent.horizontalCenter; + width: paintedWidth; + height: paintedHeight; + // Text size + size: 18; + // Style + color: hifi.colors.white; + + MouseArea { + anchors.fill: parent; + + onClicked: { + if (root.activeView === "preexisting") { + lightboxPopup.titleText = "Your wallet's private keys are not in the folder we expected"; + lightboxPopup.bodyText = "We see that you have created a wallet but the private keys " + + "for it seem to have been moved to a different folder.

    " + + "To tell us where the keys are, click 'Locate My Keys'.

    " + + "If you'd prefer to create a new wallet (not recommended - you will lose your money and past " + + "purchases), click 'Create New Wallet'."; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.visible = true; + } else { + lightboxPopup.titleText = "You may have set up more than one wallet"; + lightboxPopup.bodyText = "We see that the private keys stored on your computer are different " + + "from the ones you used last time. This may mean that you set up more than one wallet. " + + "If you would like to use these keys, click 'Continue With These Keys'.

    " + + "If you would prefer to use another wallet, click 'Locate Other Keys' to show us where " + + "you've stored the private keys for that wallet."; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.visible = true; + } + } + } + } + } + // + // MAIN PAGE END + // + + // + // FUNCTION DEFINITIONS START + // + function onFileOpenChanged(filename) { + // disconnect the event, otherwise the requests will stack up + try { // Not all calls to onFileOpenChanged() connect an event. + Window.browseChanged.disconnect(onFileOpenChanged); + } catch (e) { + console.log('WalletChoice.qml ignoring', e); + } + if (filename) { + if (copyFunction && copyFunction(filename)) { + proceed(false); + } else { + console.log("WalletChoice.qml copyFunction", copyFunction, "failed."); + } + } // Else we're still at WalletChoice + } + function walletChooser() { + Window.browseChanged.connect(onFileOpenChanged); + Window.browseAsync("Locate your .hifikey file", "", "*.hifikey"); + } + function proceed(isReset) { + if (!proceedFunction) { + console.log("Provide a function of no arguments to WalletChoice.qml."); + } else { + proceedFunction(isReset); + } + } + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 83c1a2035d..27660b5e9e 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -310,7 +310,7 @@ Item { height: parent.height; HifiControlsUit.Separator { - colorScheme: 1; + colorScheme: 1; anchors.left: parent.left; anchors.right: parent.right; anchors.top: parent.top; @@ -318,20 +318,42 @@ Item { RalewayRegular { id: noActivityText; - text: "The Wallet app is in closed Beta.

    To request entry and receive free HFC, please contact " + - "info@highfidelity.com with your High Fidelity account username and the email address registered to that account."; - // Text size - size: 24; - // Style - color: hifi.colors.blueAccent; - anchors.left: parent.left; - anchors.leftMargin: 12; - anchors.right: parent.right; - anchors.rightMargin: 12; - anchors.verticalCenter: parent.verticalCenter; - height: paintedHeight; - wrapMode: Text.WordWrap; - horizontalAlignment: Text.AlignHCenter; + text: "Congrats! Your wallet is all set!

    " + + "Where's my HFC?
    " + + "High Fidelity commerce is in open beta right now. Want more HFC? Get it by meeting with a banker at " + + "BankOfHighFidelity!" + // Text size + size: 22; + // Style + color: hifi.colors.blueAccent; + anchors.top: parent.top; + anchors.topMargin: 36; + anchors.left: parent.left; + anchors.leftMargin: 12; + anchors.right: parent.right; + anchors.rightMargin: 12; + height: paintedHeight; + wrapMode: Text.WordWrap; + horizontalAlignment: Text.AlignHCenter; + + onLinkActivated: { + sendSignalToWallet({ method: "transactionHistory_goToBank" }); + } + } + + HifiControlsUit.Button { + id: bankButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: noActivityText.bottom; + anchors.topMargin: 30; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width/2; + height: 50; + text: "VISIT BANK OF HIGH FIDELITY"; + onClicked: { + sendSignalToWallet({ method: "transactionHistory_goToBank" }); + } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml index fab27a29bb..bad592067c 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -441,7 +441,7 @@ Item { } Item { id: choosePassphraseContainer; - visible: root.activeView === "step_3"; + visible: root.hasShownSecurityImageTip && root.activeView === "step_3"; // Anchors anchors.top: titleBarContainer.bottom; anchors.topMargin: 30; @@ -451,7 +451,10 @@ Item { onVisibleChanged: { if (visible) { + sendSignalToWallet({method: 'disableHmdPreview'}); Commerce.getWalletAuthenticatedStatus(); + } else { + sendSignalToWallet({method: 'maybeEnableHmdPreview'}); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/sendMoney/SendMoney.qml b/interface/resources/qml/hifi/commerce/wallet/sendMoney/SendMoney.qml index 7dd72b904e..f5b7f42a3f 100644 --- a/interface/resources/qml/hifi/commerce/wallet/sendMoney/SendMoney.qml +++ b/interface/resources/qml/hifi/commerce/wallet/sendMoney/SendMoney.qml @@ -923,7 +923,7 @@ Item { anchors.leftMargin: 20; anchors.right: parent.right; anchors.rightMargin: 20; - height: 140; + height: 95; FontLoader { id: firaSansSemiBold; source: "../../../../../fonts/FiraSans-SemiBold.ttf"; } TextArea { @@ -947,8 +947,14 @@ Item { wrapMode: TextEdit.Wrap; activeFocusOnPress: true; activeFocusOnTab: true; - // Workaround for no max length on TextAreas onTextChanged: { + // Don't allow tabs or newlines + if ((/[\n\r\t]/g).test(text)) { + var cursor = cursorPosition; + text = text.replace(/[\n\r\t]/g, ''); + cursorPosition = cursor-1; + } + // Workaround for no max length on TextAreas if (text.length > maximumLength) { var cursor = cursorPosition; text = previousText; diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 049a7faad8..19297a3251 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -199,7 +199,7 @@ Rectangle { var SHAPE_TYPE_BOX = 4; var SHAPE_TYPE_SPHERE = 5; - var SHAPE_TYPES = [];ww + var SHAPE_TYPES = []; SHAPE_TYPES[SHAPE_TYPE_NONE] = "No Collision"; SHAPE_TYPES[SHAPE_TYPE_SIMPLE_HULL] = "Basic - Whole model"; SHAPE_TYPES[SHAPE_TYPE_SIMPLE_COMPOUND] = "Good - Sub-meshes"; @@ -207,7 +207,7 @@ Rectangle { SHAPE_TYPES[SHAPE_TYPE_BOX] = "Box"; SHAPE_TYPES[SHAPE_TYPE_SPHERE] = "Sphere"; - var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH; + var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_SIMPLE_COMPOUND; var DYNAMIC_DEFAULT = false; var prompt = tabletRoot.customInputDialog({ textInput: { diff --git a/interface/resources/qml/hifi/tablet/EditTabView.qml b/interface/resources/qml/hifi/tablet/EditTabView.qml index c79c05a601..e419b04848 100644 --- a/interface/resources/qml/hifi/tablet/EditTabView.qml +++ b/interface/resources/qml/hifi/tablet/EditTabView.qml @@ -1,5 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtQuick.Controls 2.2 // Need both for short-term fix import QtWebEngine 1.1 import QtWebChannel 1.0 import QtQuick.Controls.Styles 1.4 @@ -10,6 +11,7 @@ import "../../controls-uit" as HifiControls import "../../styles-uit" + TabView { id: editTabView // anchors.fill: parent @@ -23,8 +25,27 @@ TabView { Rectangle { color: "#404040" + id: container + + Flickable { + height: parent.height + width: parent.width + + contentHeight: createEntitiesFlow.height + importButton.height + assetServerButton.height + + header.anchors.topMargin + createEntitiesFlow.anchors.topMargin + + assetServerButton.anchors.topMargin + importButton.anchors.topMargin + contentWidth: width + + ScrollBar.vertical : ScrollBar { + visible: parent.contentHeight > parent.height + width: 20 + background: Rectangle { + color: hifi.colors.tableScrollBackgroundDark + } + } Text { + id: header color: "#ffffff" text: "Choose an Entity Type to Create:" font.pixelSize: 14 @@ -176,6 +197,7 @@ TabView { } HifiControls.Button { + id: importButton text: "Import Entities (.json)" color: hifi.buttons.black colorScheme: hifi.colorSchemes.dark @@ -192,6 +214,7 @@ TabView { } } } + } // Flickable } Tab { diff --git a/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml b/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml index 226c17e8e2..c7fbc9d45a 100644 --- a/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml @@ -131,7 +131,7 @@ Rectangle { spacing: 5 anchors.horizontalCenter: column3.horizontalCenter - anchors.horizontalCenterOffset: -20 + anchors.horizontalCenterOffset: 0 Button { id: button1 diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml index cfff3a3273..1922b02f93 100644 --- a/interface/resources/qml/hifi/tablet/TabletHome.qml +++ b/interface/resources/qml/hifi/tablet/TabletHome.qml @@ -130,6 +130,7 @@ Item { flickableDirection: Flickable.AutoFlickIfNeeded keyNavigationEnabled: false highlightFollowsCurrentItem: false + interactive: false property int previousGridIndex: -1 diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 5da587ea57..43de8333af 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -218,7 +218,7 @@ Item { readonly property var colorStart: [ colors.white, colors.primaryHighlight, "#d42043", "#343434", Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0) ] readonly property var colorFinish: [ colors.lightGrayText, colors.blueAccent, "#94132e", colors.black, Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0) ] readonly property var hoveredColor: [ colorStart[white], colorStart[blue], colorStart[red], colorFinish[black], colorStart[none], colorStart[noneBorderless], colorStart[noneBorderlessWhite], colorStart[noneBorderlessGray] ] - readonly property var pressedColor: [ colorFinish[white], colorFinish[blue], colorFinish[red], colorStart[black], colorStart[none], colorStart[noneBorderless], colorStart[noneBorderlessWhite], colorStart[noneBorderlessGray] ] + readonly property var pressedColor: [ colorFinish[white], colorFinish[blue], colorFinish[red], colorStart[black], colorStart[none], colorStart[noneBorderless], colorStart[noneBorderlessWhite], colors.lightGrayText ] readonly property var focusedColor: [ colors.lightGray50, colors.blueAccent, colors.redAccent, colors.darkGray, colorStart[none], colorStart[noneBorderless], colorStart[noneBorderlessWhite], colorStart[noneBorderlessGray] ] readonly property var disabledColorStart: [ colorStart[white], colors.baseGrayHighlight] readonly property var disabledColorFinish: [ colorFinish[white], colors.baseGrayShadow] @@ -354,5 +354,9 @@ Item { readonly property string wallet: "\ue027" readonly property string paperPlane: "\ue028" readonly property string passphrase: "\ue029" + readonly property string globe: "\ue02c" + readonly property string wand: "\ue02d" + readonly property string hat: "\ue02e" + readonly property string install: "\ue02f" } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 00c4b06378..eedf541086 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -68,7 +68,6 @@ #include #include #include -#include #include #include #include @@ -166,6 +165,7 @@ #include "scripting/AccountServicesScriptingInterface.h" #include "scripting/HMDScriptingInterface.h" #include "scripting/MenuScriptingInterface.h" +#include "graphics-scripting/GraphicsScriptingInterface.h" #include "scripting/SettingsScriptingInterface.h" #include "scripting/WindowScriptingInterface.h" #include "scripting/ControllerScriptingInterface.h" @@ -392,7 +392,7 @@ const QHash Application::_acceptedExtensi class DeadlockWatchdogThread : public QThread { public: static const unsigned long HEARTBEAT_UPDATE_INTERVAL_SECS = 1; - static const unsigned long MAX_HEARTBEAT_AGE_USECS = 30 * USECS_PER_SECOND; + static const unsigned long MAX_HEARTBEAT_AGE_USECS = 120 * USECS_PER_SECOND; // 2 mins with no checkin probably a deadlock static const int WARNING_ELAPSED_HEARTBEAT = 500 * USECS_PER_MSEC; // warn if elapsed heartbeat average is large static const int HEARTBEAT_SAMPLES = 100000; // ~5 seconds worth of samples @@ -575,10 +575,7 @@ void messageHandler(QtMsgType type, const QMessageLogContext& context, const QSt QString logMessage = LogHandler::getInstance().printMessage((LogMsgType) type, context, message); if (!logMessage.isEmpty()) { -#ifdef Q_OS_WIN - OutputDebugStringA(logMessage.toLocal8Bit().constData()); - OutputDebugStringA("\n"); -#elif defined Q_OS_ANDROID +#ifdef Q_OS_ANDROID const char * local=logMessage.toStdString().c_str(); switch (type) { case QtDebugMsg: @@ -599,10 +596,75 @@ void messageHandler(QtMsgType type, const QMessageLogContext& context, const QSt abort(); } #endif - qApp->getLogger()->addMessage(qPrintable(logMessage + "\n")); + qApp->getLogger()->addMessage(qPrintable(logMessage)); } } + +class ApplicationMeshProvider : public scriptable::ModelProviderFactory { +public: + virtual scriptable::ModelProviderPointer lookupModelProvider(const QUuid& uuid) override { + bool success; + if (auto nestable = DependencyManager::get()->find(uuid, success).lock()) { + auto type = nestable->getNestableType(); +#ifdef SCRIPTABLE_MESH_DEBUG + qCDebug(interfaceapp) << "ApplicationMeshProvider::lookupModelProvider" << uuid << SpatiallyNestable::nestableTypeToString(type); +#endif + switch (type) { + case NestableType::Entity: + return getEntityModelProvider(static_cast(uuid)); + case NestableType::Overlay: + return getOverlayModelProvider(static_cast(uuid)); + case NestableType::Avatar: + return getAvatarModelProvider(uuid); + } + } + return nullptr; + } + +private: + scriptable::ModelProviderPointer getEntityModelProvider(EntityItemID entityID) { + scriptable::ModelProviderPointer provider; + auto entityTreeRenderer = qApp->getEntities(); + auto entityTree = entityTreeRenderer->getTree(); + if (auto entity = entityTree->findEntityByID(entityID)) { + if (auto renderer = entityTreeRenderer->renderableForEntityId(entityID)) { + provider = std::dynamic_pointer_cast(renderer); + provider->modelProviderType = NestableType::Entity; + } else { + qCWarning(interfaceapp) << "no renderer for entity ID" << entityID.toString(); + } + } + return provider; + } + + scriptable::ModelProviderPointer getOverlayModelProvider(OverlayID overlayID) { + scriptable::ModelProviderPointer provider; + auto &overlays = qApp->getOverlays(); + if (auto overlay = overlays.getOverlay(overlayID)) { + if (auto base3d = std::dynamic_pointer_cast(overlay)) { + provider = std::dynamic_pointer_cast(base3d); + provider->modelProviderType = NestableType::Overlay; + } else { + qCWarning(interfaceapp) << "no renderer for overlay ID" << overlayID.toString(); + } + } else { + qCWarning(interfaceapp) << "overlay not found" << overlayID.toString(); + } + return provider; + } + + scriptable::ModelProviderPointer getAvatarModelProvider(QUuid sessionUUID) { + scriptable::ModelProviderPointer provider; + auto avatarManager = DependencyManager::get(); + if (auto avatar = avatarManager->getAvatarBySessionID(sessionUUID)) { + provider = std::dynamic_pointer_cast(avatar); + provider->modelProviderType = NestableType::Avatar; + } + return provider; + } +}; + static const QString STATE_IN_HMD = "InHMD"; static const QString STATE_CAMERA_FULL_SCREEN_MIRROR = "CameraFSM"; static const QString STATE_CAMERA_FIRST_PERSON = "CameraFirstPerson"; @@ -719,6 +781,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(std::bind(&Application::getUserAgent, qApp)); DependencyManager::set(); DependencyManager::set(ScriptEngine::CLIENT_SCRIPT); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -747,6 +810,9 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(true); + DependencyManager::set(); + DependencyManager::registerInheritance(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -785,7 +851,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); DependencyManager::set(); - DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -1515,6 +1580,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo settingsTimer->setSingleShot(false); settingsTimer->setInterval(SAVE_SETTINGS_INTERVAL); // 10s, Qt::CoarseTimer acceptable QObject::connect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); + settingsTimer->start(); }, QThread::LowestPriority); if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { @@ -1719,6 +1785,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["has_async_reprojection"] = displayPlugin->hasAsyncReprojection(); properties["hardware_stats"] = displayPlugin->getHardwareStats(); + // deadlock watchdog related stats + properties["deadlock_watchdog_maxElapsed"] = (int)DeadlockWatchdogThread::_maxElapsed; + properties["deadlock_watchdog_maxElapsedAverage"] = (int)DeadlockWatchdogThread::_maxElapsedAverage; + auto bandwidthRecorder = DependencyManager::get(); properties["packet_rate_in"] = bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond(); properties["packet_rate_out"] = bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond(); @@ -2610,7 +2680,6 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarEntitiesBookmarks", DependencyManager::get().data()); surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); // Caches @@ -4844,7 +4913,6 @@ void Application::setKeyboardFocusHighlight(const glm::vec3& position, const glm if (_keyboardFocusHighlightID == UNKNOWN_OVERLAY_ID || !getOverlays().isAddedOverlay(_keyboardFocusHighlightID)) { _keyboardFocusHighlight = std::make_shared(); _keyboardFocusHighlight->setAlpha(1.0f); - _keyboardFocusHighlight->setBorderSize(1.0f); _keyboardFocusHighlight->setColor({ 0xFF, 0xEF, 0x00 }); _keyboardFocusHighlight->setIsSolid(false); _keyboardFocusHighlight->setPulseMin(0.5); @@ -5986,7 +6054,6 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe DependencyManager::get().data()->setToolbarScriptingInterface(toolbarScriptingInterface); scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); - qScriptRegisterMetaType(scriptEngine.data(), CustomPromptResultToScriptValue, CustomPromptResultFromScriptValue); scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter, "Window"); // register `location` on the global object. @@ -6006,7 +6073,6 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get().data()); scriptEngine->registerGlobalObject("AvatarBookmarks", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("AvatarEntitiesBookmarks", DependencyManager::get().data()); scriptEngine->registerGlobalObject("LocationBookmarks", DependencyManager::get().data()); scriptEngine->registerGlobalObject("RayPick", DependencyManager::get().data()); @@ -6044,6 +6110,9 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("Scene", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Render", _renderEngine->getConfiguration().get()); + GraphicsScriptingInterface::registerMetaTypes(scriptEngine.data()); + scriptEngine->registerGlobalObject("Graphics", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); @@ -6303,6 +6372,25 @@ bool Application::askToWearAvatarAttachmentUrl(const QString& url) { return true; } +void Application::replaceDomainContent(const QString& url) { + qCDebug(interfaceapp) << "Attempting to replace domain content: " << url; + QByteArray urlData(url.toUtf8()); + auto limitedNodeList = DependencyManager::get(); + const auto& domainHandler = limitedNodeList->getDomainHandler(); + limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) { + return node->getType() == NodeType::EntityServer && node->getActiveSocket(); + }, [&urlData, limitedNodeList, &domainHandler](const SharedNodePointer& octreeNode) { + auto octreeFilePacket = NLPacket::create(PacketType::OctreeFileReplacementFromUrl, urlData.size(), true); + octreeFilePacket->write(urlData); + limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr()); + }); + auto addressManager = DependencyManager::get(); + addressManager->handleLookupString(DOMAIN_SPAWNING_POINT); + QString newHomeAddress = addressManager->getHost() + DOMAIN_SPAWNING_POINT; + qCDebug(interfaceapp) << "Setting new home bookmark to: " << newHomeAddress; + DependencyManager::get()->setHomeLocationToAddress(newHomeAddress); +} + bool Application::askToReplaceDomainContent(const QString& url) { QString methodDetails; const int MAX_CHARACTERS_PER_LINE = 90; @@ -6322,21 +6410,7 @@ bool Application::askToReplaceDomainContent(const QString& url) { QString details; if (static_cast(answer.toInt()) == QMessageBox::Yes) { // Given confirmation, send request to domain server to replace content - qCDebug(interfaceapp) << "Attempting to replace domain content: " << url; - QByteArray urlData(url.toUtf8()); - auto limitedNodeList = DependencyManager::get(); - limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) { - return node->getType() == NodeType::EntityServer && node->getActiveSocket(); - }, [&urlData, limitedNodeList](const SharedNodePointer& octreeNode) { - auto octreeFilePacket = NLPacket::create(PacketType::OctreeFileReplacementFromUrl, urlData.size(), true); - octreeFilePacket->write(urlData); - limitedNodeList->sendPacket(std::move(octreeFilePacket), *octreeNode); - }); - auto addressManager = DependencyManager::get(); - addressManager->handleLookupString(DOMAIN_SPAWNING_POINT); - QString newHomeAddress = addressManager->getHost() + DOMAIN_SPAWNING_POINT; - qCDebug(interfaceapp) << "Setting new home bookmark to: " << newHomeAddress; - DependencyManager::get()->setHomeLocationToAddress(newHomeAddress); + replaceDomainContent(url); details = "SuccessfulRequestToReplaceContent"; } else { details = "UserDeclinedToReplaceContent"; @@ -7557,6 +7631,18 @@ void Application::deadlockApplication() { } } +// cause main thread to be unresponsive for 35 seconds +void Application::unresponsiveApplication() { + // to avoid compiler warnings about a loop that will never exit + uint64_t start = usecTimestampNow(); + uint64_t UNRESPONSIVE_FOR_SECONDS = 35; + uint64_t UNRESPONSIVE_FOR_USECS = UNRESPONSIVE_FOR_SECONDS * USECS_PER_SECOND; + qCDebug(interfaceapp) << "Intentionally cause Interface to be unresponsive for " << UNRESPONSIVE_FOR_SECONDS << " seconds"; + while (usecTimestampNow() - start < UNRESPONSIVE_FOR_USECS) { + QThread::sleep(1); + } +} + void Application::setActiveDisplayPlugin(const QString& pluginName) { auto menu = Menu::getInstance(); foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 8028dc8c68..8a0d3c2351 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -268,9 +268,8 @@ public: float getGameLoopRate() const { return _gameLoopCounter.rate(); } - // Note that takeSnapshot has a default value, as this method is used internally. void takeSnapshot(bool notify, bool includeAnimated = false, float aspectRatio = 0.0f, const QString& filename = QString()); - void takeSecondaryCameraSnapshot(const QString& filename); + void takeSecondaryCameraSnapshot(const QString& filename = QString()); void shareSnapshot(const QString& filename, const QUrl& href = QUrl("")); @@ -288,6 +287,8 @@ public: bool getSaveAvatarOverrideUrl() { return _saveAvatarOverrideUrl; } void saveNextPhysicsStats(QString filename); + void replaceDomainContent(const QString& url); + signals: void svoImportRequested(const QString& url); @@ -369,6 +370,7 @@ public slots: void updateHeartbeat() const; static void deadlockApplication(); + static void unresponsiveApplication(); // cause main thread to be unresponsive for 35 seconds void rotationModeChanged() const; diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index 0f177e3d15..7845158a80 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -19,6 +19,11 @@ #include #include #include +#include +#include +#include +#include +#include #include "MainWindow.h" #include "Menu.h" @@ -29,6 +34,62 @@ #include + +void addAvatarEntities(const QVariantList& avatarEntities) { + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); + EntityTreePointer entityTree = DependencyManager::get()->getTree(); + if (!entityTree) { + return; + } + EntitySimulationPointer entitySimulation = entityTree->getSimulation(); + PhysicalEntitySimulationPointer physicalEntitySimulation = std::static_pointer_cast(entitySimulation); + EntityEditPacketSender* entityPacketSender = physicalEntitySimulation->getPacketSender(); + QScriptEngine scriptEngine; + for (int index = 0; index < avatarEntities.count(); index++) { + const QVariantMap& avatarEntityProperties = avatarEntities.at(index).toMap(); + QVariant variantProperties = avatarEntityProperties["properties"]; + QVariantMap asMap = variantProperties.toMap(); + QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine); + EntityItemProperties entityProperties; + EntityItemPropertiesFromScriptValueHonorReadOnly(scriptProperties, entityProperties); + + entityProperties.setParentID(myNodeID); + entityProperties.setClientOnly(true); + entityProperties.setOwningAvatarID(myNodeID); + entityProperties.setSimulationOwner(myNodeID, AVATAR_ENTITY_SIMULATION_PRIORITY); + entityProperties.markAllChanged(); + + EntityItemID id = EntityItemID(QUuid::createUuid()); + bool success = true; + entityTree->withWriteLock([&] { + EntityItemPointer entity = entityTree->addEntity(id, entityProperties); + if (entity) { + if (entityProperties.queryAACubeRelatedPropertyChanged()) { + // due to parenting, the server may not know where something is in world-space, so include the bounding cube. + bool success; + AACube queryAACube = entity->getQueryAACube(success); + if (success) { + entityProperties.setQueryAACube(queryAACube); + } + } + + entity->setLastBroadcast(usecTimestampNow()); + // since we're creating this object we will immediately volunteer to own its simulation + entity->flagForOwnershipBid(VOLUNTEER_SIMULATION_PRIORITY); + entityProperties.setLastEdited(entity->getLastEdited()); + } else { + qCDebug(entities) << "AvatarEntitiesBookmark failed to add new Entity to local Octree"; + success = false; + } + }); + + if (success) { + entityPacketSender->queueEditEntityMessage(PacketType::EntityAdd, entityTree, id, entityProperties); + } + } +} + AvatarBookmarks::AvatarBookmarks() { _bookmarksFilename = PathUtils::getAppDataPath() + "/" + AVATARBOOKMARKS_FILENAME; readFromFile(); @@ -38,7 +99,7 @@ void AvatarBookmarks::readFromFile() { // migrate old avatarbookmarks.json, used to be in 'local' folder on windows QString oldConfigPath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + AVATARBOOKMARKS_FILENAME; QFile oldConfig(oldConfigPath); - + // I imagine that in a year from now, this code for migrating (as well as the two lines above) // may be removed since all bookmarks should have been migrated by then // - Robbie Uvanni (6.8.2017) @@ -48,9 +109,9 @@ void AvatarBookmarks::readFromFile() { } else { qCDebug(interfaceapp) << "Failed to migrate" << AVATARBOOKMARKS_FILENAME; } - } - - Bookmarks::readFromFile(); + } + + Bookmarks::readFromFile(); } void AvatarBookmarks::setupMenus(Menu* menubar, MenuWrapper* menu) { @@ -81,23 +142,27 @@ void AvatarBookmarks::changeToBookmarkedAvatar() { myAvatar->useFullAvatarURL(action->data().toString()); qCDebug(interfaceapp) << " Using Legacy V1 Avatar Bookmark "; } else { - + const QMap bookmark = action->data().toMap(); - // Not magic value. This is the current made version, and if it changes this interpreter should be updated to + // Not magic value. This is the current made version, and if it changes this interpreter should be updated to // handle the new one separately. // This is where the avatar bookmark entry is parsed. If adding new Value, make sure to have backward compatability with previous if (bookmark.value(ENTRY_VERSION) == 3) { - const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString(); - myAvatar->useFullAvatarURL(avatarUrl); - qCDebug(interfaceapp) << "Avatar On " << avatarUrl; - const QList& attachments = bookmark.value(ENTRY_AVATAR_ATTACHMENTS, QList()).toList(); + myAvatar->removeAvatarEntities(); + const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString(); + myAvatar->useFullAvatarURL(avatarUrl); + qCDebug(interfaceapp) << "Avatar On " << avatarUrl; + const QList& attachments = bookmark.value(ENTRY_AVATAR_ATTACHMENTS, QList()).toList(); - qCDebug(interfaceapp) << "Attach " << attachments; - myAvatar->setAttachmentsVariant(attachments); + qCDebug(interfaceapp) << "Attach " << attachments; + myAvatar->setAttachmentsVariant(attachments); + + const float& qScale = bookmark.value(ENTRY_AVATAR_SCALE, 1.0f).toFloat(); + myAvatar->setAvatarScale(qScale); + + const QVariantList& avatarEntities = bookmark.value(ENTRY_AVATAR_ENTITIES, QVariantList()).toList(); + addAvatarEntities(avatarEntities); - const float& qScale = bookmark.value(ENTRY_AVATAR_SCALE, 1.0f).toFloat(); - myAvatar->setAvatarScale(qScale); - } else { qCDebug(interfaceapp) << " Bookmark entry does not match client version, make sure client has a handler for the new AvatarBookmark"; } @@ -126,6 +191,7 @@ void AvatarBookmarks::addBookmark() { bookmark.insert(ENTRY_AVATAR_URL, avatarUrl); bookmark.insert(ENTRY_AVATAR_SCALE, avatarScale); bookmark.insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant()); + bookmark.insert(ENTRY_AVATAR_ENTITIES, myAvatar->getAvatarEntitiesVariant()); Bookmarks::addBookmarkToFile(bookmarkName, bookmark); }); diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index 0529eeb516..7e2f64379e 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -34,6 +34,7 @@ private: const QString AVATARBOOKMARKS_FILENAME = "avatarbookmarks.json"; const QString ENTRY_AVATAR_URL = "avatarUrl"; const QString ENTRY_AVATAR_ATTACHMENTS = "attachments"; + const QString ENTRY_AVATAR_ENTITIES = "avatarEntites"; const QString ENTRY_AVATAR_SCALE = "avatarScale"; const QString ENTRY_VERSION = "version"; diff --git a/interface/src/AvatarEntitiesBookmarks.cpp b/interface/src/AvatarEntitiesBookmarks.cpp deleted file mode 100644 index 21d3657346..0000000000 --- a/interface/src/AvatarEntitiesBookmarks.cpp +++ /dev/null @@ -1,168 +0,0 @@ -// -// AvatarEntitiesBookmarks.cpp -// interface/src -// -// Created by Dante Ruiz on 15/01/18. -// Copyright 2018 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "MainWindow.h" -#include "Menu.h" -#include "AvatarEntitiesBookmarks.h" -#include "InterfaceLogging.h" - -#include "QVariantGLM.h" - -#include - -void addAvatarEntities(const QVariantList& avatarEntities) { - auto nodeList = DependencyManager::get(); - const QUuid myNodeID = nodeList->getSessionUUID(); - EntityTreePointer entityTree = DependencyManager::get()->getTree(); - if (!entityTree) { - return; - } - EntitySimulationPointer entitySimulation = entityTree->getSimulation(); - PhysicalEntitySimulationPointer physicalEntitySimulation = std::static_pointer_cast(entitySimulation); - EntityEditPacketSender* entityPacketSender = physicalEntitySimulation->getPacketSender(); - QScriptEngine scriptEngine; - for (int index = 0; index < avatarEntities.count(); index++) { - const QVariantMap& avatarEntityProperties = avatarEntities.at(index).toMap(); - QVariant variantProperties = avatarEntityProperties["properties"]; - QVariantMap asMap = variantProperties.toMap(); - QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine); - EntityItemProperties entityProperties; - EntityItemPropertiesFromScriptValueHonorReadOnly(scriptProperties, entityProperties); - - entityProperties.setParentID(myNodeID); - entityProperties.setClientOnly(true); - entityProperties.setOwningAvatarID(myNodeID); - entityProperties.setSimulationOwner(myNodeID, AVATAR_ENTITY_SIMULATION_PRIORITY); - entityProperties.markAllChanged(); - - EntityItemID id = EntityItemID(QUuid::createUuid()); - bool success = true; - entityTree->withWriteLock([&] { - EntityItemPointer entity = entityTree->addEntity(id, entityProperties); - if (entity) { - if (entityProperties.queryAACubeRelatedPropertyChanged()) { - // due to parenting, the server may not know where something is in world-space, so include the bounding cube. - bool success; - AACube queryAACube = entity->getQueryAACube(success); - if (success) { - entityProperties.setQueryAACube(queryAACube); - } - } - - entity->setLastBroadcast(usecTimestampNow()); - // since we're creating this object we will immediately volunteer to own its simulation - entity->flagForOwnershipBid(VOLUNTEER_SIMULATION_PRIORITY); - entityProperties.setLastEdited(entity->getLastEdited()); - } else { - qCDebug(entities) << "AvatarEntitiesBookmark failed to add new Entity to local Octree"; - success = false; - } - }); - - if (success) { - entityPacketSender->queueEditEntityMessage(PacketType::EntityAdd, entityTree, id, entityProperties); - } - } -} - -AvatarEntitiesBookmarks::AvatarEntitiesBookmarks() { - _bookmarksFilename = PathUtils::getAppDataPath() + "/" + AVATAR_ENTITIES_BOOKMARKS_FILENAME; - Bookmarks::readFromFile(); -} - -void AvatarEntitiesBookmarks::setupMenus(Menu* menubar, MenuWrapper* menu) { - auto bookmarkAction = menubar->addActionToQMenuAndActionHash(menu, MenuOption::BookmarkAvatarEntities); - QObject::connect(bookmarkAction, SIGNAL(triggered()), this, SLOT(addBookmark()), Qt::QueuedConnection); - _bookmarksMenu = menu->addMenu(MenuOption::AvatarEntitiesBookmarks); - _deleteBookmarksAction = menubar->addActionToQMenuAndActionHash(menu, MenuOption::DeleteAvatarEntitiesBookmark); - QObject::connect(_deleteBookmarksAction, SIGNAL(triggered()), this, SLOT(deleteBookmark()), Qt::QueuedConnection); - - for (auto it = _bookmarks.begin(); it != _bookmarks.end(); ++it) { - addBookmarkToMenu(menubar, it.key(), it.value()); - } - - Bookmarks::sortActions(menubar, _bookmarksMenu); -} - -void AvatarEntitiesBookmarks::applyBookmarkedAvatarEntities() { - QAction* action = qobject_cast(sender()); - auto myAvatar = DependencyManager::get()->getMyAvatar(); - - const QMap bookmark = action->data().toMap(); - - if (bookmark.value(ENTRY_VERSION) == AVATAR_ENTITIES_BOOKMARK_VERSION) { - myAvatar->removeAvatarEntities(); - const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString(); - myAvatar->useFullAvatarURL(avatarUrl); - const QVariantList& avatarEntities = bookmark.value(ENTRY_AVATAR_ENTITIES, QVariantList()).toList(); - addAvatarEntities(avatarEntities); - const float& avatarScale = bookmark.value(ENTRY_AVATAR_SCALE, 1.0f).toFloat(); - myAvatar->setAvatarScale(avatarScale); - } else { - qCDebug(interfaceapp) << " Bookmark entry does not match client version, make sure client has a handler for the new AvatarEntitiesBookmark"; - } -} - -void AvatarEntitiesBookmarks::addBookmark() { - ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Avatar Entities", "Name", QString()); - connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { - disconnect(dlg, &ModalDialogListener::response, this, nullptr); - auto bookmarkName = response.toString(); - bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " "); - if (bookmarkName.length() == 0) { - return; - } - - auto myAvatar = DependencyManager::get()->getMyAvatar(); - - const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString(); - const QVariant& avatarScale = myAvatar->getAvatarScale(); - - QVariantMap bookmark; - bookmark.insert(ENTRY_VERSION, AVATAR_ENTITIES_BOOKMARK_VERSION); - bookmark.insert(ENTRY_AVATAR_URL, avatarUrl); - bookmark.insert(ENTRY_AVATAR_SCALE, avatarScale); - bookmark.insert(ENTRY_AVATAR_ENTITIES, myAvatar->getAvatarEntitiesVariant()); - - Bookmarks::addBookmarkToFile(bookmarkName, bookmark); - }); -} - -void AvatarEntitiesBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) { - QAction* changeAction = _bookmarksMenu->newAction(); - changeAction->setData(bookmark); - connect(changeAction, SIGNAL(triggered()), this, SLOT(applyBookmarkedAvatarEntities())); - if (!_isMenuSorted) { - menubar->addActionToQMenuAndActionHash(_bookmarksMenu, changeAction, name, 0, QAction::NoRole); - } else { - // TODO: this is aggressive but other alternatives have proved less fruitful so far. - menubar->addActionToQMenuAndActionHash(_bookmarksMenu, changeAction, name, 0, QAction::NoRole); - Bookmarks::sortActions(menubar, _bookmarksMenu); - } -} diff --git a/interface/src/AvatarEntitiesBookmarks.h b/interface/src/AvatarEntitiesBookmarks.h deleted file mode 100644 index 0c70e4dbc0..0000000000 --- a/interface/src/AvatarEntitiesBookmarks.h +++ /dev/null @@ -1,45 +0,0 @@ -// -// AvatarEntitiesBookmarks.h -// interface/src -// -// Created by Dante Ruiz on 15/01/18. -// Copyright 2018 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_AvatarEntitiesBookmarks_h -#define hifi_AvatarEntitiesBookmarks_h - -#include -#include "Bookmarks.h" - -class AvatarEntitiesBookmarks: public Bookmarks, public Dependency { - Q_OBJECT - SINGLETON_DEPENDENCY - -public: - AvatarEntitiesBookmarks(); - void setupMenus(Menu* menubar, MenuWrapper* menu) override; - -public slots: - void addBookmark(); - -protected: - void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) override; - -private: - const QString AVATAR_ENTITIES_BOOKMARKS_FILENAME = "AvatarEntitiesBookmarks.json"; - const QString ENTRY_AVATAR_URL = "AvatarUrl"; - const QString ENTRY_AVATAR_SCALE = "AvatarScale"; - const QString ENTRY_AVATAR_ENTITIES = "AvatarEntities"; - const QString ENTRY_VERSION = "version"; - - const int AVATAR_ENTITIES_BOOKMARK_VERSION = 1; - -private slots: - void applyBookmarkedAvatarEntities(); -}; - -#endif diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index f3d8ea2344..8c0ac584c5 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -34,7 +34,6 @@ #include "audio/AudioScope.h" #include "avatar/AvatarManager.h" #include "AvatarBookmarks.h" -#include "AvatarEntitiesBookmarks.h" #include "devices/DdeFaceTracker.h" #include "MainWindow.h" #include "render/DrawStatus.h" @@ -207,9 +206,6 @@ Menu::Menu() { auto avatarBookmarks = DependencyManager::get(); avatarBookmarks->setupMenus(this, avatarMenu); - auto avatarEntitiesBookmarks = DependencyManager::get(); - avatarEntitiesBookmarks->setupMenus(this, avatarMenu); - // Display menu ---------------------------------- // FIXME - this is not yet matching Alan's spec because it doesn't have // menus for "2D"/"3D" - we need to add support for detecting the appropriate @@ -712,6 +708,7 @@ Menu::Menu() { MenuWrapper* crashMenu = developerMenu->addMenu("Crash"); addActionToQMenuAndActionHash(crashMenu, MenuOption::DeadlockInterface, 0, qApp, SLOT(deadlockApplication())); + addActionToQMenuAndActionHash(crashMenu, MenuOption::UnresponsiveInterface, 0, qApp, SLOT(unresponsiveApplication())); action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunction); connect(action, &QAction::triggered, qApp, []() { crash::pureVirtualCall(); }); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 8cb1804fd4..34588535ec 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -46,7 +46,6 @@ namespace MenuOption { const QString AutoMuteAudio = "Auto Mute Microphone"; const QString AvatarReceiveStats = "Show Receive Stats"; const QString AvatarBookmarks = "Avatar Bookmarks"; - const QString AvatarEntitiesBookmarks = "Avatar Entities Bookmarks"; const QString Back = "Back"; const QString BinaryEyelidControl = "Binary Eyelid Control"; const QString BookmarkAvatar = "Bookmark Avatar"; @@ -77,6 +76,7 @@ namespace MenuOption { const QString CrashNewFault = "New Fault"; const QString CrashNewFaultThreaded = "New Fault (threaded)"; const QString DeadlockInterface = "Deadlock Interface"; + const QString UnresponsiveInterface = "Unresponsive Interface"; const QString DecreaseAvatarSize = "Decrease Avatar Size"; const QString DefaultSkybox = "Default Skybox"; const QString DeleteAvatarBookmark = "Delete Avatar Bookmark..."; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c25aaeeecd..b6fa3fde96 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -79,6 +79,8 @@ float DEFAULT_SCRIPTED_MOTOR_TIMESCALE = 1.0e6f; const int SCRIPTED_MOTOR_CAMERA_FRAME = 0; const int SCRIPTED_MOTOR_AVATAR_FRAME = 1; const int SCRIPTED_MOTOR_WORLD_FRAME = 2; +const int SCRIPTED_MOTOR_SIMPLE_MODE = 0; +const int SCRIPTED_MOTOR_DYNAMIC_MODE = 1; const QString& DEFAULT_AVATAR_COLLISION_SOUND_URL = "https://hifi-public.s3.amazonaws.com/sounds/Collisions-otherorganic/Body_Hits_Impact.wav"; const float MyAvatar::ZOOM_MIN = 0.5f; @@ -92,6 +94,7 @@ MyAvatar::MyAvatar(QThread* thread) : _pitchSpeed(PITCH_SPEED_DEFAULT), _scriptedMotorTimescale(DEFAULT_SCRIPTED_MOTOR_TIMESCALE), _scriptedMotorFrame(SCRIPTED_MOTOR_CAMERA_FRAME), + _scriptedMotorMode(SCRIPTED_MOTOR_SIMPLE_MODE), _motionBehaviors(AVATAR_MOTION_DEFAULTS), _characterController(this), _eyeContactTarget(LEFT_EYE), @@ -1479,7 +1482,7 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { }); saveAvatarUrl(); emit skeletonChanged(); - + emit skeletonModelURLChanged(); } void MyAvatar::removeAvatarEntities() { @@ -1623,20 +1626,27 @@ controller::Pose MyAvatar::getControllerPoseInAvatarFrame(controller::Action act void MyAvatar::updateMotors() { _characterController.clearMotors(); glm::quat motorRotation; + + const float FLYING_MOTOR_TIMESCALE = 0.05f; + const float WALKING_MOTOR_TIMESCALE = 0.2f; + const float INVALID_MOTOR_TIMESCALE = 1.0e6f; + + float horizontalMotorTimescale; + float verticalMotorTimescale; + + if (_characterController.getState() == CharacterController::State::Hover || + _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { + horizontalMotorTimescale = FLYING_MOTOR_TIMESCALE; + verticalMotorTimescale = FLYING_MOTOR_TIMESCALE; + } else { + horizontalMotorTimescale = WALKING_MOTOR_TIMESCALE; + verticalMotorTimescale = INVALID_MOTOR_TIMESCALE; + } + if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) { - - const float FLYING_MOTOR_TIMESCALE = 0.05f; - const float WALKING_MOTOR_TIMESCALE = 0.2f; - const float INVALID_MOTOR_TIMESCALE = 1.0e6f; - - float horizontalMotorTimescale; - float verticalMotorTimescale; - if (_characterController.getState() == CharacterController::State::Hover || _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { motorRotation = getMyHead()->getHeadOrientation(); - horizontalMotorTimescale = FLYING_MOTOR_TIMESCALE; - verticalMotorTimescale = FLYING_MOTOR_TIMESCALE; } else { // non-hovering = walking: follow camera twist about vertical but not lift // we decompose camera's rotation and store the twist part in motorRotation @@ -1647,8 +1657,6 @@ void MyAvatar::updateMotors() { glm::quat liftRotation; swingTwistDecomposition(headOrientation, Vectors::UNIT_Y, liftRotation, motorRotation); motorRotation = orientation * motorRotation; - horizontalMotorTimescale = WALKING_MOTOR_TIMESCALE; - verticalMotorTimescale = INVALID_MOTOR_TIMESCALE; } if (_isPushing || _isBraking || !_isBeingPushed) { @@ -1668,7 +1676,12 @@ void MyAvatar::updateMotors() { // world-frame motorRotation = glm::quat(); } - _characterController.addMotor(_scriptedMotorVelocity, motorRotation, _scriptedMotorTimescale); + if (_scriptedMotorMode == SCRIPTED_MOTOR_SIMPLE_MODE) { + _characterController.addMotor(_scriptedMotorVelocity, motorRotation, _scriptedMotorTimescale); + } else { + // dynamic mode + _characterController.addMotor(_scriptedMotorVelocity, motorRotation, horizontalMotorTimescale, verticalMotorTimescale); + } } // legacy support for 'MyAvatar::applyThrust()', which has always been implemented as a @@ -1752,6 +1765,14 @@ QString MyAvatar::getScriptedMotorFrame() const { return frame; } +QString MyAvatar::getScriptedMotorMode() const { + QString mode = "simple"; + if (_scriptedMotorMode == SCRIPTED_MOTOR_DYNAMIC_MODE) { + mode = "dynamic"; + } + return mode; +} + void MyAvatar::setScriptedMotorVelocity(const glm::vec3& velocity) { float MAX_SCRIPTED_MOTOR_SPEED = 500.0f; _scriptedMotorVelocity = velocity; @@ -1778,6 +1799,14 @@ void MyAvatar::setScriptedMotorFrame(QString frame) { } } +void MyAvatar::setScriptedMotorMode(QString mode) { + if (mode.toLower() == "simple") { + _scriptedMotorMode = SCRIPTED_MOTOR_SIMPLE_MODE; + } else if (mode.toLower() == "dynamic") { + _scriptedMotorMode = SCRIPTED_MOTOR_DYNAMIC_MODE; + } +} + void MyAvatar::clearScriptableSettings() { _scriptedMotorVelocity = Vectors::ZERO; _scriptedMotorTimescale = DEFAULT_SCRIPTED_MOTOR_TIMESCALE; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 28af8b62fd..fa5206e128 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -69,6 +69,7 @@ class MyAvatar : public Avatar { * @property motorTimescale {float} Specifies how quickly the avatar should accelerate to meet the motorVelocity, * smaller values will result in higher acceleration. * @property motorReferenceFrame {string} Reference frame of the motorVelocity, must be one of the following: "avatar", "camera", "world" + * @property motorMode {string} Type of scripted motor behavior, "simple" = use motorTimescale property (default mode) and "dynamic" = use action motor's timescales * @property collisionSoundURL {string} Specifies the sound to play when the avatar experiences a collision. * You can provide a mono or stereo 16-bit WAV file running at either 24 Khz or 48 Khz. * The latter is downsampled by the audio mixer, so all audio effectively plays back at a 24 Khz sample rate. @@ -124,6 +125,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(glm::vec3 motorVelocity READ getScriptedMotorVelocity WRITE setScriptedMotorVelocity) Q_PROPERTY(float motorTimescale READ getScriptedMotorTimescale WRITE setScriptedMotorTimescale) Q_PROPERTY(QString motorReferenceFrame READ getScriptedMotorFrame WRITE setScriptedMotorFrame) + Q_PROPERTY(QString motorMode READ getScriptedMotorMode WRITE setScriptedMotorMode) Q_PROPERTY(QString collisionSoundURL READ getCollisionSoundURL WRITE setCollisionSoundURL) Q_PROPERTY(AudioListenerMode audioListenerMode READ getAudioListenerMode WRITE setAudioListenerMode) Q_PROPERTY(glm::vec3 customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition) @@ -662,9 +664,11 @@ private: glm::vec3 getScriptedMotorVelocity() const { return _scriptedMotorVelocity; } float getScriptedMotorTimescale() const { return _scriptedMotorTimescale; } QString getScriptedMotorFrame() const; + QString getScriptedMotorMode() const; void setScriptedMotorVelocity(const glm::vec3& velocity); void setScriptedMotorTimescale(float timescale); void setScriptedMotorFrame(QString frame); + void setScriptedMotorMode(QString mode); virtual void attach(const QString& modelURL, const QString& jointName = QString(), const glm::vec3& translation = glm::vec3(), const glm::quat& rotation = glm::quat(), float scale = 1.0f, bool isSoft = false, @@ -706,6 +710,7 @@ private: glm::vec3 _scriptedMotorVelocity; // target local-frame velocity of avatar (analog script) float _scriptedMotorTimescale; // timescale for avatar to achieve its target velocity int _scriptedMotorFrame; + int _scriptedMotorMode; quint32 _motionBehaviors; QString _collisionSoundURL; diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 10ddd4c110..712c505e8a 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -22,6 +22,8 @@ // inventory answers {status: 'success', data: {assets: [{id: "guid", title: "name", preview: "url"}....]}} // balance answers {status: 'success', data: {balance: integer}} // buy and receive_at answer {status: 'success'} +// account synthesizes a result {status: 'success', data: {keyStatus: "preexisting"|"conflicting"|"ok"}} + QJsonObject Ledger::apiResponse(const QString& label, QNetworkReply& reply) { QByteArray response = reply.readAll(); @@ -49,6 +51,7 @@ Handler(balance) Handler(inventory) Handler(transferHfcToNode) Handler(transferHfcToUsername) +Handler(alreadyOwned) void Ledger::send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request) { auto accountManager = DependencyManager::get(); @@ -98,7 +101,7 @@ void Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, cons signedSend("transaction", transactionString, hfc_key, "buy", "buySuccess", "buyFailure", controlled_failure); } -bool Ledger::receiveAt(const QString& hfc_key, const QString& old_key) { +bool Ledger::receiveAt(const QString& hfc_key, const QString& signing_key) { auto accountManager = DependencyManager::get(); if (!accountManager->isLoggedIn()) { qCWarning(commerce) << "Cannot set receiveAt when not logged in."; @@ -107,7 +110,7 @@ bool Ledger::receiveAt(const QString& hfc_key, const QString& old_key) { return false; // We know right away that we will fail, so tell the caller. } - signedSend("public_key", hfc_key.toUtf8(), old_key, "receive_at", "receiveAtSuccess", "receiveAtFailure"); + signedSend("public_key", hfc_key.toUtf8(), signing_key, "receive_at", "receiveAtSuccess", "receiveAtFailure"); return true; // Note that there may still be an asynchronous signal of failure that callers might be interested in. } @@ -178,7 +181,7 @@ QString transactionString(const QJsonObject& valueObject) { } else { result += valueObject["message"].toString(); } - + // no matter what we append a smaller date to the bottom of this... result += QString("
    %1").arg(createdAt.toLocalTime().toString(Qt::DefaultLocaleShortDate)); return result; @@ -245,18 +248,33 @@ void Ledger::accountSuccess(QNetworkReply& reply) { auto iv = QByteArray::fromBase64(data["iv"].toString().toUtf8()); auto ckey = QByteArray::fromBase64(data["ckey"].toString().toUtf8()); QString remotePublicKey = data["public_key"].toString(); + bool isOverride = wallet->wasSoftReset(); wallet->setSalt(salt); wallet->setIv(iv); wallet->setCKey(ckey); + QString keyStatus = "ok"; QStringList localPublicKeys = wallet->listPublicKeys(); - if (remotePublicKey.isEmpty() && !localPublicKeys.isEmpty()) { - receiveAt(localPublicKeys.first(), ""); + if (remotePublicKey.isEmpty() || isOverride) { + if (!localPublicKeys.isEmpty()) { + QString key = localPublicKeys.first(); + receiveAt(key, key); + } + } else { + if (localPublicKeys.isEmpty()) { + keyStatus = "preexisting"; + } else if (localPublicKeys.first() != remotePublicKey) { + keyStatus = "conflicting"; + } } // none of the hfc account info should be emitted - emit accountResult(QJsonObject{ {"status", "success"} }); + QJsonObject json; + QJsonObject responseData{ { "status", "success"} }; + json["keyStatus"] = keyStatus; + responseData["data"] = json; + emit accountResult(responseData); } void Ledger::accountFailure(QNetworkReply& reply) { @@ -336,3 +354,12 @@ void Ledger::transferHfcToUsername(const QString& hfc_key, const QString& userna auto transactionString = transactionDoc.toJson(QJsonDocument::Compact); signedSend("transaction", transactionString, hfc_key, "transfer_hfc_to_user", "transferHfcToUsernameSuccess", "transferHfcToUsernameFailure"); } + +void Ledger::alreadyOwned(const QString& marketplaceId) { + auto wallet = DependencyManager::get(); + QString endpoint = "already_owned"; + QJsonObject request; + request["public_keys"] = QJsonArray::fromStringList(wallet->listPublicKeys()); + request["marketplace_item_id"] = marketplaceId; + send(endpoint, "alreadyOwnedSuccess", "alreadyOwnedFailure", QNetworkAccessManager::PutOperation, AccountManagerAuth::Required, request); +} diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 1b56c893ab..703ebda2dc 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -26,7 +26,7 @@ class Ledger : public QObject, public Dependency { public: void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const bool controlled_failure = false); - bool receiveAt(const QString& hfc_key, const QString& old_key); + bool receiveAt(const QString& hfc_key, const QString& signing_key); void balance(const QStringList& keys); void inventory(const QStringList& keys); void history(const QStringList& keys, const int& pageNumber); @@ -35,6 +35,7 @@ public: void certificateInfo(const QString& certificateId); void transferHfcToNode(const QString& hfc_key, const QString& nodeID, const int& amount, const QString& optionalMessage); void transferHfcToUsername(const QString& hfc_key, const QString& username, const int& amount, const QString& optionalMessage); + void alreadyOwned(const QString& marketplaceId); enum CertificateStatus { CERTIFICATE_STATUS_UNKNOWN = 0, @@ -55,6 +56,7 @@ signals: void certificateInfoResult(QJsonObject result); void transferHfcToNodeResult(QJsonObject result); void transferHfcToUsernameResult(QJsonObject result); + void alreadyOwnedResult(QJsonObject result); void updateCertificateStatus(const QString& certID, uint certStatus); @@ -79,6 +81,8 @@ public slots: void transferHfcToNodeFailure(QNetworkReply& reply); void transferHfcToUsernameSuccess(QNetworkReply& reply); void transferHfcToUsernameFailure(QNetworkReply& reply); + void alreadyOwnedSuccess(QNetworkReply& reply); + void alreadyOwnedFailure(QNetworkReply& reply); private: QJsonObject apiResponse(const QString& label, QNetworkReply& reply); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index c9caa393ce..557193c074 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -10,11 +10,17 @@ // #include "QmlCommerce.h" +#include "CommerceLogging.h" #include "Application.h" #include "DependencyManager.h" #include "Ledger.h" #include "Wallet.h" #include +#include +#include +#include +#include +#include "scripting/HMDScriptingInterface.h" QmlCommerce::QmlCommerce() { auto ledger = DependencyManager::get(); @@ -28,14 +34,18 @@ QmlCommerce::QmlCommerce() { connect(ledger.data(), &Ledger::accountResult, this, &QmlCommerce::accountResult); connect(wallet.data(), &Wallet::walletStatusResult, this, &QmlCommerce::walletStatusResult); connect(ledger.data(), &Ledger::certificateInfoResult, this, &QmlCommerce::certificateInfoResult); + connect(ledger.data(), &Ledger::alreadyOwnedResult, this, &QmlCommerce::alreadyOwnedResult); connect(ledger.data(), &Ledger::updateCertificateStatus, this, &QmlCommerce::updateCertificateStatus); connect(ledger.data(), &Ledger::transferHfcToNodeResult, this, &QmlCommerce::transferHfcToNodeResult); connect(ledger.data(), &Ledger::transferHfcToUsernameResult, this, &QmlCommerce::transferHfcToUsernameResult); + connect(ledger.data(), &Ledger::transferHfcToUsernameResult, this, &QmlCommerce::transferHfcToUsernameResult); auto accountManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::usernameChanged, this, [&]() { setPassphrase(""); }); + + _appsPath = PathUtils::getAppDataPath() + "Apps/"; } void QmlCommerce::getWalletStatus() { @@ -52,6 +62,11 @@ void QmlCommerce::getKeyFilePathIfExists() { emit keyFilePathIfExistsResult(wallet->getKeyFilePath()); } +bool QmlCommerce::copyKeyFileFrom(const QString& pathname) { + auto wallet = DependencyManager::get(); + return wallet->copyKeyFileFrom(pathname); +} + void QmlCommerce::getWalletAuthenticatedStatus() { auto wallet = DependencyManager::get(); emit walletAuthenticatedStatusResult(wallet->walletIsAuthenticatedWithPassphrase()); @@ -118,6 +133,11 @@ void QmlCommerce::changePassphrase(const QString& oldPassphrase, const QString& } } +void QmlCommerce::setSoftReset() { + auto wallet = DependencyManager::get(); + wallet->setSoftReset(); +} + void QmlCommerce::setPassphrase(const QString& passphrase) { auto wallet = DependencyManager::get(); wallet->setPassphrase(passphrase); @@ -163,3 +183,164 @@ void QmlCommerce::transferHfcToUsername(const QString& username, const int& amou QString key = keys[0]; ledger->transferHfcToUsername(key, username, amount, optionalMessage); } + +void QmlCommerce::replaceContentSet(const QString& itemHref) { + qApp->replaceDomainContent(itemHref); + QJsonObject messageProperties = { + { "status", "SuccessfulRequestToReplaceContent" }, + { "content_set_url", itemHref } + }; + UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); + + emit contentSetChanged(itemHref); +} + +void QmlCommerce::alreadyOwned(const QString& marketplaceId) { + auto ledger = DependencyManager::get(); + ledger->alreadyOwned(marketplaceId); +} + +QString QmlCommerce::getInstalledApps() { + QString installedAppsFromMarketplace; + QStringList runningScripts = DependencyManager::get()->getRunningScripts(); + + QDir directory(_appsPath); + QStringList apps = directory.entryList(QStringList("*.app.json")); + foreach(QString appFileName, apps) { + installedAppsFromMarketplace += appFileName; + installedAppsFromMarketplace += ","; + QFile appFile(_appsPath + appFileName); + if (appFile.open(QIODevice::ReadOnly)) { + QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(appFile.readAll()); + + appFile.close(); + + QJsonObject appFileJsonObject = appFileJsonDocument.object(); + QString scriptURL = appFileJsonObject["scriptURL"].toString(); + + // If the script .app.json is on the user's local disk but the associated script isn't running + // for some reason, start that script again. + if (!runningScripts.contains(scriptURL)) { + if ((DependencyManager::get()->loadScript(scriptURL.trimmed())).isNull()) { + qCDebug(commerce) << "Couldn't start script while checking installed apps."; + } + } + } else { + qCDebug(commerce) << "Couldn't open local .app.json file for reading."; + } + } + + return installedAppsFromMarketplace; +} + +bool QmlCommerce::installApp(const QString& itemHref) { + if (!QDir(_appsPath).exists()) { + if (!QDir().mkdir(_appsPath)) { + qCDebug(commerce) << "Couldn't make _appsPath directory."; + return false; + } + } + + QUrl appHref(itemHref); + + auto request = DependencyManager::get()->createResourceRequest(this, appHref); + + if (!request) { + qCDebug(commerce) << "Couldn't create resource request for app."; + return false; + } + + connect(request, &ResourceRequest::finished, this, [=]() { + if (request->getResult() != ResourceRequest::Success) { + qCDebug(commerce) << "Failed to get .app.json file from remote."; + return false; + } + + // Copy the .app.json to the apps directory inside %AppData%/High Fidelity/Interface + auto requestData = request->getData(); + QFile appFile(_appsPath + "/" + appHref.fileName()); + if (!appFile.open(QIODevice::WriteOnly)) { + qCDebug(commerce) << "Couldn't open local .app.json file for creation."; + return false; + } + if (appFile.write(requestData) == -1) { + qCDebug(commerce) << "Couldn't write to local .app.json file."; + return false; + } + // Close the file + appFile.close(); + + // Read from the returned datastream to know what .js to add to Running Scripts + QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(requestData); + QJsonObject appFileJsonObject = appFileJsonDocument.object(); + QString scriptUrl = appFileJsonObject["scriptURL"].toString(); + + if ((DependencyManager::get()->loadScript(scriptUrl.trimmed())).isNull()) { + qCDebug(commerce) << "Couldn't load script."; + return false; + } + + emit appInstalled(itemHref); + return true; + }); + request->send(); + return true; +} + +bool QmlCommerce::uninstallApp(const QString& itemHref) { + QUrl appHref(itemHref); + + // Read from the file to know what .js script to stop + QFile appFile(_appsPath + "/" + appHref.fileName()); + if (!appFile.open(QIODevice::ReadOnly)) { + qCDebug(commerce) << "Couldn't open local .app.json file for deletion."; + return false; + } + QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(appFile.readAll()); + QJsonObject appFileJsonObject = appFileJsonDocument.object(); + QString scriptUrl = appFileJsonObject["scriptURL"].toString(); + + if (!DependencyManager::get()->stopScript(scriptUrl.trimmed(), false)) { + qCDebug(commerce) << "Couldn't stop script."; + return false; + } + + // Delete the .app.json from the filesystem + // remove() closes the file first. + if (!appFile.remove()) { + qCDebug(commerce) << "Couldn't delete local .app.json file."; + return false; + } + + emit appUninstalled(itemHref); + return true; +} + +bool QmlCommerce::openApp(const QString& itemHref) { + QUrl appHref(itemHref); + + // Read from the file to know what .html or .qml document to open + QFile appFile(_appsPath + "/" + appHref.fileName()); + if (!appFile.open(QIODevice::ReadOnly)) { + qCDebug(commerce) << "Couldn't open local .app.json file."; + return false; + } + QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(appFile.readAll()); + QJsonObject appFileJsonObject = appFileJsonDocument.object(); + QString homeUrl = appFileJsonObject["homeURL"].toString(); + + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (homeUrl.contains(".qml", Qt::CaseInsensitive)) { + tablet->loadQMLSource(homeUrl); + } else if (homeUrl.contains(".html", Qt::CaseInsensitive)) { + tablet->gotoWebScreen(homeUrl); + } else { + qCDebug(commerce) << "Attempted to open unknown type of homeURL!"; + return false; + } + + DependencyManager::get()->openTablet(); + + return true; +} diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index b261ee6de6..6a4eaa2be2 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -42,12 +42,18 @@ signals: void historyResult(QJsonObject result); void accountResult(QJsonObject result); void certificateInfoResult(QJsonObject result); + void alreadyOwnedResult(QJsonObject result); void updateCertificateStatus(const QString& certID, uint certStatus); void transferHfcToNodeResult(QJsonObject result); void transferHfcToUsernameResult(QJsonObject result); + void contentSetChanged(const QString& contentSetHref); + + void appInstalled(const QString& appHref); + void appUninstalled(const QString& appHref); + protected: Q_INVOKABLE void getWalletStatus(); @@ -55,10 +61,12 @@ protected: Q_INVOKABLE void getKeyFilePathIfExists(); Q_INVOKABLE void getSecurityImage(); Q_INVOKABLE void getWalletAuthenticatedStatus(); + Q_INVOKABLE bool copyKeyFileFrom(const QString& pathname); Q_INVOKABLE void chooseSecurityImage(const QString& imageFile); Q_INVOKABLE void setPassphrase(const QString& passphrase); Q_INVOKABLE void changePassphrase(const QString& oldPassphrase, const QString& newPassphrase); + Q_INVOKABLE void setSoftReset(); Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); @@ -68,9 +76,20 @@ protected: Q_INVOKABLE void account(); Q_INVOKABLE void certificateInfo(const QString& certificateId); + Q_INVOKABLE void alreadyOwned(const QString& marketplaceId); Q_INVOKABLE void transferHfcToNode(const QString& nodeID, const int& amount, const QString& optionalMessage); Q_INVOKABLE void transferHfcToUsername(const QString& username, const int& amount, const QString& optionalMessage); + + Q_INVOKABLE void replaceContentSet(const QString& itemHref); + + Q_INVOKABLE QString getInstalledApps(); + Q_INVOKABLE bool installApp(const QString& appHref); + Q_INVOKABLE bool uninstallApp(const QString& appHref); + Q_INVOKABLE bool openApp(const QString& appHref); + +private: + QString _appsPath; }; #endif // hifi_QmlCommerce_h diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 9599af827f..fad82115d6 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -59,6 +59,23 @@ QString keyFilePath() { auto accountManager = DependencyManager::get(); return PathUtils::getAppDataFilePath(QString("%1.%2").arg(accountManager->getAccountInfo().getUsername(), KEY_FILE)); } +bool Wallet::copyKeyFileFrom(const QString& pathname) { + QString existing = getKeyFilePath(); + qCDebug(commerce) << "Old keyfile" << existing; + if (!existing.isEmpty()) { + QString backup = QString(existing).insert(existing.indexOf(KEY_FILE) - 1, + QDateTime::currentDateTime().toString(Qt::ISODate).replace(":", "")); + qCDebug(commerce) << "Renaming old keyfile to" << backup; + if (!QFile::rename(existing, backup)) { + qCCritical(commerce) << "Unable to backup" << existing << "to" << backup; + return false; + } + } + QString destination = keyFilePath(); + bool result = QFile::copy(pathname, destination); + qCDebug(commerce) << "copy" << pathname << "to" << destination << "=>" << result; + return result; +} // use the cached _passphrase if it exists, otherwise we need to prompt int passwordCallback(char* password, int maxPasswordSize, int rwFlag, void* u) { @@ -300,17 +317,24 @@ Wallet::Wallet() { packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "handleChallengeOwnershipPacket"); packetReceiver.registerListener(PacketType::ChallengeOwnershipRequest, this, "handleChallengeOwnershipPacket"); - connect(ledger.data(), &Ledger::accountResult, this, [&]() { + connect(ledger.data(), &Ledger::accountResult, this, [&](QJsonObject result) { auto wallet = DependencyManager::get(); auto walletScriptingInterface = DependencyManager::get(); uint status; + QString keyStatus = result.contains("data") ? result["data"].toObject()["keyStatus"].toString() : ""; if (wallet->getKeyFilePath() == "" || !wallet->getSecurityImage()) { - status = (uint)WalletStatus::WALLET_STATUS_NOT_SET_UP; + if (keyStatus == "preexisting") { + status = (uint) WalletStatus::WALLET_STATUS_PREEXISTING; + } else{ + status = (uint) WalletStatus::WALLET_STATUS_NOT_SET_UP; + } } else if (!wallet->walletIsAuthenticatedWithPassphrase()) { - status = (uint)WalletStatus::WALLET_STATUS_NOT_AUTHENTICATED; + status = (uint) WalletStatus::WALLET_STATUS_NOT_AUTHENTICATED; + } else if (keyStatus == "conflicting") { + status = (uint) WalletStatus::WALLET_STATUS_CONFLICTING; } else { - status = (uint)WalletStatus::WALLET_STATUS_READY; + status = (uint) WalletStatus::WALLET_STATUS_READY; } walletScriptingInterface->setWalletStatus(status); @@ -524,17 +548,17 @@ bool Wallet::generateKeyPair() { // TODO: redo this soon -- need error checking and so on writeSecurityImage(_securityImage, keyFilePath()); - QString oldKey = _publicKeys.count() == 0 ? "" : _publicKeys.last(); QString key = keyPair.first->toBase64(); _publicKeys.push_back(key); qCDebug(commerce) << "public key:" << key; + _isOverridingServer = false; // It's arguable whether we want to change the receiveAt every time, but: // 1. It's certainly needed the first time, when createIfNeeded answers true. // 2. It is maximally private, and we can step back from that later if desired. // 3. It maximally exercises all the machinery, so we are most likely to surface issues now. auto ledger = DependencyManager::get(); - return ledger->receiveAt(key, oldKey); + return ledger->receiveAt(key, key); } QStringList Wallet::listPublicKeys() { diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index fe3a9f1d5f..d771f404e5 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -35,6 +35,7 @@ public: void chooseSecurityImage(const QString& imageFile); bool getSecurityImage(); QString getKeyFilePath(); + bool copyKeyFileFrom(const QString& pathname); void setSalt(const QByteArray& salt) { _salt = salt; } QByteArray getSalt() { return _salt; } @@ -48,11 +49,15 @@ public: bool getPassphraseIsCached() { return !(_passphrase->isEmpty()); } bool walletIsAuthenticatedWithPassphrase(); bool changePassphrase(const QString& newPassphrase); + void setSoftReset() { _isOverridingServer = true; } + bool wasSoftReset() { bool was = _isOverridingServer; _isOverridingServer = false; return was; } void getWalletStatus(); enum WalletStatus { WALLET_STATUS_NOT_LOGGED_IN = 0, WALLET_STATUS_NOT_SET_UP, + WALLET_STATUS_PREEXISTING, + WALLET_STATUS_CONFLICTING, WALLET_STATUS_NOT_AUTHENTICATED, WALLET_STATUS_READY }; @@ -73,6 +78,7 @@ private: QByteArray _iv; QByteArray _ckey; QString* _passphrase { new QString("") }; + bool _isOverridingServer { false }; bool writeWallet(const QString& newPassphrase = QString("")); void updateImageProvider(); diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 30e8439985..e80dc1d213 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -38,6 +38,7 @@ extern "C" { #endif int main(int argc, const char* argv[]) { + setupHifiApplication(BuildInfo::INTERFACE_NAME); #ifdef Q_OS_LINUX QApplication::setAttribute(Qt::AA_DontUseNativeMenuBar); @@ -51,17 +52,9 @@ int main(int argc, const char* argv[]) { QCoreApplication::setAttribute(Qt::AA_UseOpenGLES); #endif - disableQtBearerPoll(); // Fixes wifi ping spikes - QElapsedTimer startupTime; startupTime.start(); - // Set application infos - QCoreApplication::setApplicationName(BuildInfo::INTERFACE_NAME); - QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); - QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); - QCoreApplication::setApplicationVersion(BuildInfo::VERSION); - Setting::init(); // Instance UserActivityLogger now that the settings are loaded diff --git a/interface/src/octree/OctreePacketProcessor.cpp b/interface/src/octree/OctreePacketProcessor.cpp index c792834d9c..122b58c057 100644 --- a/interface/src/octree/OctreePacketProcessor.cpp +++ b/interface/src/octree/OctreePacketProcessor.cpp @@ -17,6 +17,8 @@ #include "SceneScriptingInterface.h" OctreePacketProcessor::OctreePacketProcessor() { + setObjectName("Octree Packet Processor"); + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerDirectListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase }, diff --git a/interface/src/scripting/AccountServicesScriptingInterface.h b/interface/src/scripting/AccountServicesScriptingInterface.h index 012c37305d..cfa51697af 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.h +++ b/interface/src/scripting/AccountServicesScriptingInterface.h @@ -38,7 +38,7 @@ class AccountServicesScriptingInterface : public QObject { Q_PROPERTY(QString username READ getUsername NOTIFY myUsernameChanged) Q_PROPERTY(bool loggedIn READ loggedIn NOTIFY loggedInChanged) Q_PROPERTY(QString findableBy READ getFindableBy WRITE setFindableBy NOTIFY findableByChanged) - Q_PROPERTY(QUrl metaverseServerURL READ getMetaverseServerURL) + Q_PROPERTY(QUrl metaverseServerURL READ getMetaverseServerURL CONSTANT) public: static AccountServicesScriptingInterface* getInstance(); diff --git a/interface/src/scripting/MenuScriptingInterface.cpp b/interface/src/scripting/MenuScriptingInterface.cpp index d9372978e8..d6dc2fa703 100644 --- a/interface/src/scripting/MenuScriptingInterface.cpp +++ b/interface/src/scripting/MenuScriptingInterface.cpp @@ -94,22 +94,6 @@ bool MenuScriptingInterface::menuItemExists(const QString& menu, const QString& return result; } -void MenuScriptingInterface::addActionGroup(const QString& groupName, const QStringList& actionList, - const QString& selected) { - static const char* slot = SLOT(menuItemTriggered()); - QMetaObject::invokeMethod(Menu::getInstance(), "addActionGroup", - Q_ARG(const QString&, groupName), - Q_ARG(const QStringList&, actionList), - Q_ARG(const QString&, selected), - Q_ARG(QObject*, this), - Q_ARG(const char*, slot)); -} - -void MenuScriptingInterface::removeActionGroup(const QString& groupName) { - QMetaObject::invokeMethod(Menu::getInstance(), "removeActionGroup", - Q_ARG(const QString&, groupName)); -} - bool MenuScriptingInterface::isOptionChecked(const QString& menuOption) { if (QThread::currentThread() == qApp->thread()) { return Menu::getInstance()->isOptionChecked(menuOption); @@ -147,19 +131,3 @@ void MenuScriptingInterface::setMenuEnabled(const QString& menuOption, bool isCh void MenuScriptingInterface::triggerOption(const QString& menuOption) { QMetaObject::invokeMethod(Menu::getInstance(), "triggerOption", Q_ARG(const QString&, menuOption)); } - -void MenuScriptingInterface::closeInfoView(const QString& path) { - QMetaObject::invokeMethod(Menu::getInstance(), "closeInfoView", Q_ARG(const QString&, path)); -} - -bool MenuScriptingInterface::isInfoViewVisible(const QString& path) { - if (QThread::currentThread() == qApp->thread()) { - return Menu::getInstance()->isInfoViewVisible(path); - } - - bool result; - BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isInfoViewVisible", - Q_RETURN_ARG(bool, result), Q_ARG(const QString&, path)); - return result; -} - diff --git a/interface/src/scripting/MenuScriptingInterface.h b/interface/src/scripting/MenuScriptingInterface.h index 59cfc76d21..649c444eaf 100644 --- a/interface/src/scripting/MenuScriptingInterface.h +++ b/interface/src/scripting/MenuScriptingInterface.h @@ -163,13 +163,6 @@ public slots: */ bool menuItemExists(const QString& menuName, const QString& menuitem); - /** - * TODO: Not working; don't document until fixed. - */ - void addActionGroup(const QString& groupName, const QStringList& actionList, - const QString& selected = QString()); - void removeActionGroup(const QString& groupName); - /**jsdoc * Check whether a checkable menu item is checked. * @function Menu.isOptionChecked @@ -222,12 +215,6 @@ public slots: */ void setMenuEnabled(const QString& menuName, bool isEnabled); - /** - * TODO: Not used or useful; will not document until used. - */ - void closeInfoView(const QString& path); - bool isInfoViewVisible(const QString& path); - signals: /**jsdoc * Triggered when a menu item is clicked (or triggered by {@link Menu.triggerOption}). diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 529b8f796c..12b20566ed 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -32,20 +32,6 @@ static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation"; static const QString LAST_BROWSE_ASSETS_LOCATION_SETTING = "LastBrowseAssetsLocation"; -QScriptValue CustomPromptResultToScriptValue(QScriptEngine* engine, const CustomPromptResult& result) { - if (!result.value.isValid()) { - return QScriptValue::UndefinedValue; - } - - Q_ASSERT(result.value.userType() == qMetaTypeId()); - return engine->toScriptValue(result.value.toMap()); -} - -void CustomPromptResultFromScriptValue(const QScriptValue& object, CustomPromptResult& result) { - result.value = object.toVariant(); -} - - WindowScriptingInterface::WindowScriptingInterface() { const DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::connectedToDomain, this, &WindowScriptingInterface::domainChanged); @@ -143,14 +129,6 @@ void WindowScriptingInterface::disconnectedFromDomain() { emit domainChanged(""); } -CustomPromptResult WindowScriptingInterface::customPrompt(const QVariant& config) { - CustomPromptResult result; - bool ok = false; - auto configMap = config.toMap(); - result.value = OffscreenUi::getCustomInfo(OffscreenUi::ICON_NONE, "", configMap, &ok); - return ok ? result : CustomPromptResult(); -} - QString fixupPathForMac(const QString& directory) { // On OS X `directory` does not work as expected unless a file is included in the path, so we append a bogus // filename if the directory is valid. diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 76decf4362..67fdea0bc5 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -22,17 +22,6 @@ #include -class CustomPromptResult { -public: - QVariant value; -}; - -Q_DECLARE_METATYPE(CustomPromptResult); - -QScriptValue CustomPromptResultToScriptValue(QScriptEngine* engine, const CustomPromptResult& result); -void CustomPromptResultFromScriptValue(const QScriptValue& object, CustomPromptResult& result); - - /**jsdoc * The Window API provides various facilities not covered elsewhere: window dimensions, window focus, normal or entity camera * view, clipboard, announcements, user connections, common dialog boxes, snapshots, file import, domain changes, domain @@ -142,15 +131,6 @@ public slots: */ void promptAsync(const QString& message = "", const QString& defaultText = ""); - /**jsdoc - * Prompt the user for input in a custom, modal dialog. - * @deprecated This function is deprecated and will soon be removed. - * @function Window.customPrompt - * @param {object} config - Configures the modal dialog. - * @returns {object} The user's response. - */ - CustomPromptResult customPrompt(const QVariant& config); - /**jsdoc * Prompt the user to choose a directory. Displays a modal dialog that navigates the directory tree. * @function Window.browseDir @@ -325,6 +305,7 @@ public slots: * {@link Window.stillSnapshotTaken|stillSnapshotTaken} is emitted; when a still image plus moving images are captured, * {@link Window.processingGifStarted|processingGifStarted} and {@link Window.processingGifCompleted|processingGifCompleted} * are emitted. The path to store the snapshots and the length of the animated GIF to capture are specified in Settings > + * NOTE: to provide a non-default value - all previous parameters must be provided. * General > Snapshots. * @function Window.takeSnapshot * @param {boolean} notify=true - This value is passed on through the {@link Window.stillSnapshotTaken|stillSnapshotTaken} @@ -334,8 +315,10 @@ public slots: * @param {number} aspectRatio=0 - The width/height ratio of the snapshot required. If the value is 0 the * full resolution is used (window dimensions in desktop mode; HMD display dimensions in HMD mode), otherwise one of the * dimensions is adjusted in order to match the aspect ratio. - * @param {string} filename=QString() - If this value is not null then the image will be saved to this filename, with an appended ",jpg". - * otherwise, the image will be saved as 'hifi-snap-by--YYYY-MM-DD_HH-MM-SS' + * @param {string} filename="" - If this parameter is not given, the image will be saved as 'hifi-snap-by--YYYY-MM-DD_HH-MM-SS'. + * If this parameter is "" then the image will be saved as ".jpg". + * Otherwise, the image will be saved to this filename, with an appended ".jpg". + * * @example Using the snapshot function and signals. * function onStillSnapshotTaken(path, notify) { * print("Still snapshot taken: " + path); @@ -357,15 +340,18 @@ public slots: * var notify = true; * var animated = true; * var aspect = 1920 / 1080; - * var filename = QString(); + * var filename = ""; * Window.takeSnapshot(notify, animated, aspect, filename); */ void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f, const QString& filename = QString()); /**jsdoc * Takes a still snapshot of the current view from the secondary camera that can be set up through the {@link Render} API. + * NOTE: to provide a non-default value - all previous parameters must be provided. * @function Window.takeSecondaryCameraSnapshot - * @param {string} filename=QString() - If this value is not null then the image will be saved to this filename, with an appended ".jpg" + * @param {string} filename="" - If this parameter is not given, the image will be saved as 'hifi-snap-by--YYYY-MM-DD_HH-MM-SS'. + * If this parameter is "" then the image will be saved as ".jpg". + * Otherwise, the image will be saved to this filename, with an appended ".jpg". * * var filename = QString(); */ diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index b36b4f02df..23055cf246 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -42,8 +42,6 @@ private: int _domainStatusBorder; int _magnifierBorder; - ivec2 _previousBorderSize{ -1 }; - gpu::TexturePointer _uiTexture; gpu::TexturePointer _overlayDepthTexture; gpu::TexturePointer _overlayColorTexture; diff --git a/interface/src/ui/DomainConnectionModel.cpp b/interface/src/ui/DomainConnectionModel.cpp index b9e4c1348e..83aa18420c 100644 --- a/interface/src/ui/DomainConnectionModel.cpp +++ b/interface/src/ui/DomainConnectionModel.cpp @@ -98,4 +98,4 @@ void DomainConnectionModel::refresh() { //inform view that we want refresh data beginResetModel(); endResetModel(); -} \ No newline at end of file +} diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index df0f3c4728..bbf064fddd 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -13,10 +13,10 @@ #include #include - +#include #include "Overlay.h" -class Base3DOverlay : public Overlay, public SpatiallyNestable { +class Base3DOverlay : public Overlay, public SpatiallyNestable, public scriptable::ModelProvider { Q_OBJECT using Parent = Overlay; @@ -36,6 +36,7 @@ public: virtual bool is3D() const override { return true; } virtual uint32_t fetchMetaSubItems(render::ItemIDs& subItems) const override { subItems.push_back(getRenderItemID()); return (uint32_t) subItems.size(); } + virtual scriptable::ScriptableModelBase getScriptableModel() override { return scriptable::ScriptableModelBase(); } // TODO: consider implementing registration points in this class glm::vec3 getCenter() const { return getWorldPosition(); } diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index f13f782482..c98d9330df 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -125,13 +125,6 @@ Cube3DOverlay* Cube3DOverlay::createClone() const { void Cube3DOverlay::setProperties(const QVariantMap& properties) { Volume3DOverlay::setProperties(properties); - - auto borderSize = properties["borderSize"]; - - if (borderSize.isValid()) { - float value = borderSize.toFloat(); - setBorderSize(value); - } } /**jsdoc @@ -177,14 +170,8 @@ void Cube3DOverlay::setProperties(const QVariantMap& properties) { * parentID is an avatar skeleton. A value of 65535 means "no joint". * * @property {Vec3} dimensions - The dimensions of the overlay. Synonyms: scale, size. - * - * @property {number} borderSize - Not used. */ QVariant Cube3DOverlay::getProperty(const QString& property) { - if (property == "borderSize") { - return _borderSize; - } - return Volume3DOverlay::getProperty(property); } @@ -200,3 +187,14 @@ Transform Cube3DOverlay::evalRenderTransform() { transform.setRotation(rotation); return transform; } + +scriptable::ScriptableModelBase Cube3DOverlay::getScriptableModel() { + auto geometryCache = DependencyManager::get(); + auto vertexColor = ColorUtils::toVec3(_color); + scriptable::ScriptableModelBase result; + if (auto mesh = geometryCache->meshFromShape(GeometryCache::Cube, vertexColor)) { + result.objectID = getID(); + result.append(mesh); + } + return result; +} diff --git a/interface/src/ui/overlays/Cube3DOverlay.h b/interface/src/ui/overlays/Cube3DOverlay.h index e7b58ad911..d28d11920a 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.h +++ b/interface/src/ui/overlays/Cube3DOverlay.h @@ -29,18 +29,14 @@ public: virtual Cube3DOverlay* createClone() const override; - float getBorderSize() const { return _borderSize; } - - void setBorderSize(float value) { _borderSize = value; } - void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; + virtual scriptable::ScriptableModelBase getScriptableModel() override; protected: Transform evalRenderTransform() override; private: - float _borderSize; // edges on a cube std::array _geometryIds; }; diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index e95f13f8ff..6603a44d46 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -75,6 +75,7 @@ void ModelOverlay::update(float deltatime) { render::ScenePointer scene = qApp->getMain3DScene(); render::Transaction transaction; if (_model->needsFixupInScene()) { + emit DependencyManager::get()->modelRemovedFromScene(getID(), NestableType::Overlay, _model); _model->removeFromScene(scene, transaction); _model->addToScene(scene, transaction); @@ -84,6 +85,7 @@ void ModelOverlay::update(float deltatime) { modelOverlay->setSubRenderItemIDs(newRenderItemIDs); }); processMaterials(); + emit DependencyManager::get()->modelAddedToScene(getID(), NestableType::Overlay, _model); } if (_visibleDirty) { _visibleDirty = false; @@ -110,12 +112,14 @@ bool ModelOverlay::addToScene(Overlay::Pointer overlay, const render::ScenePoint Volume3DOverlay::addToScene(overlay, scene, transaction); _model->addToScene(scene, transaction); processMaterials(); + emit DependencyManager::get()->modelAddedToScene(getID(), NestableType::Overlay, _model); return true; } void ModelOverlay::removeFromScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction) { Volume3DOverlay::removeFromScene(overlay, scene, transaction); _model->removeFromScene(scene, transaction); + emit DependencyManager::get()->modelRemovedFromScene(getID(), NestableType::Overlay, _model); transaction.updateItem(getRenderItemID(), [](Overlay& data) { auto modelOverlay = static_cast(&data); modelOverlay->clearSubRenderItemIDs(); @@ -659,4 +663,23 @@ void ModelOverlay::processMaterials() { material.pop(); } } -} \ No newline at end of file +} + +bool ModelOverlay::canReplaceModelMeshPart(int meshIndex, int partIndex) { + // TODO: bounds checking; for now just used to indicate provider generally supports mesh updates + return _model && _model->isLoaded(); +} + +bool ModelOverlay::replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointer newModel, int meshIndex, int partIndex) { + return canReplaceModelMeshPart(meshIndex, partIndex) && + _model->replaceScriptableModelMeshPart(newModel, meshIndex, partIndex); +} + +scriptable::ScriptableModelBase ModelOverlay::getScriptableModel() { + if (!_model || !_model->isLoaded()) { + return Base3DOverlay::getScriptableModel(); + } + auto result = _model->getScriptableModel(); + result.objectID = getID(); + return result; +} diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index b38d5cd6d9..88a1729d68 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -62,6 +62,10 @@ public: void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) override; void removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName) override; + virtual scriptable::ScriptableModelBase getScriptableModel() override; + virtual bool canReplaceModelMeshPart(int meshIndex, int partIndex) override; + virtual bool replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointer model, int meshIndex, int partIndex) override; + protected: Transform evalRenderTransform() override; diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 6898b5ed2b..35274e4fbe 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -388,21 +388,6 @@ QString Overlays::getOverlayType(OverlayID overlayId) { return ""; } -QObject* Overlays::getOverlayObject(OverlayID id) { - if (QThread::currentThread() != thread()) { - QObject* result; - PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "getOverlayObject", Q_RETURN_ARG(QObject*, result), Q_ARG(OverlayID, id)); - return result; - } - - Overlay::Pointer thisOverlay = getOverlay(id); - if (thisOverlay) { - return qobject_cast(&(*thisOverlay)); - } - return nullptr; -} - OverlayID Overlays::getOverlayAtPoint(const glm::vec2& point) { if (!_enabled) { return UNKNOWN_OVERLAY_ID; diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 113ee92b80..3efe94c206 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -98,6 +98,11 @@ public: OverlayID addOverlay(Overlay* overlay) { return addOverlay(Overlay::Pointer(overlay)); } OverlayID addOverlay(const Overlay::Pointer& overlay); + RayToOverlayIntersectionResult findRayIntersectionVector(const PickRay& ray, bool precisionPicking, + const QVector& overlaysToInclude, + const QVector& overlaysToDiscard, + bool visibleOnly = false, bool collidableOnly = false); + bool mousePressEvent(QMouseEvent* event); bool mouseDoublePressEvent(QMouseEvent* event); bool mouseReleaseEvent(QMouseEvent* event); @@ -227,15 +232,6 @@ public slots: */ QString getOverlayType(OverlayID overlayId); - /**jsdoc - * Get the overlay script object. - * @function Overlays.getOverlayObject - * @deprecated This function is deprecated and will soon be removed. - * @param {Uuid} overlayID - The ID of the overlay to get the script object of. - * @returns {object} The script object for the overlay if found. - */ - QObject* getOverlayObject(OverlayID id); - /**jsdoc * Get the ID of the 2D overlay at a particular point on the screen or HUD. * @function Overlays.getOverlayAtPoint @@ -356,28 +352,6 @@ public slots: bool visibleOnly = false, bool collidableOnly = false); - // TODO: Apart from the name, this function signature on JavaScript is identical to that of findRayIntersection() and should - // probably be removed from the JavaScript API so as not to cause confusion. - /**jsdoc - * Find the closest 3D overlay intersected by a {@link PickRay}. - * @function Overlays.findRayIntersectionVector - * @deprecated Use {@link Overlays.findRayIntersection} instead; it has identical parameters and results. - * @param {PickRay} pickRay - The PickRay to use for finding overlays. - * @param {boolean} [precisionPicking=false] - Unused; exists to match Entity API. - * @param {Array.} [overlayIDsToInclude=[]] - Whitelist for intersection test. If empty then the result isn't limited - * to overlays in the list. - * @param {Array.} [overlayIDsToExclude=[]] - Blacklist for intersection test. If empty then the result doesn't - * exclude overlays in the list. - * @param {boolean} [visibleOnly=false] - Unused; exists to match Entity API. - * @param {boolean} [collidableOnly=false] - Unused; exists to match Entity API. - * @returns {Overlays.RayToOverlayIntersectionResult} The closest 3D overlay intersected by pickRay, taking - * into account overlayIDsToInclude and overlayIDsToExclude if they're not empty. - */ - RayToOverlayIntersectionResult findRayIntersectionVector(const PickRay& ray, bool precisionPicking, - const QVector& overlaysToInclude, - const QVector& overlaysToDiscard, - bool visibleOnly = false, bool collidableOnly = false); - /**jsdoc * Return a list of 3D overlays with bounding boxes that touch a search sphere. * @function Overlays.findOverlays diff --git a/interface/src/ui/overlays/Shape3DOverlay.cpp b/interface/src/ui/overlays/Shape3DOverlay.cpp index 97342a80ab..e9ec0d6cf4 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.cpp +++ b/interface/src/ui/overlays/Shape3DOverlay.cpp @@ -99,13 +99,6 @@ void Shape3DOverlay::setProperties(const QVariantMap& properties) { } } } - - auto borderSize = properties["borderSize"]; - - if (borderSize.isValid()) { - float value = borderSize.toFloat(); - setBorderSize(value); - } } /**jsdoc @@ -153,13 +146,8 @@ void Shape3DOverlay::setProperties(const QVariantMap& properties) { * @property {Vec3} dimensions - The dimensions of the overlay. Synonyms: scale, size. * * @property {Shape} shape=Hexagon - The geometrical shape of the overlay. - * @property {number} borderSize - Not used. */ QVariant Shape3DOverlay::getProperty(const QString& property) { - if (property == "borderSize") { - return _borderSize; - } - if (property == "shape") { return shapeStrings[_shape]; } @@ -179,3 +167,14 @@ Transform Shape3DOverlay::evalRenderTransform() { transform.setRotation(rotation); return transform; } + +scriptable::ScriptableModelBase Shape3DOverlay::getScriptableModel() { + auto geometryCache = DependencyManager::get(); + auto vertexColor = ColorUtils::toVec3(_color); + scriptable::ScriptableModelBase result; + result.objectID = getID(); + if (auto mesh = geometryCache->meshFromShape(_shape, vertexColor)) { + result.append(mesh); + } + return result; +} diff --git a/interface/src/ui/overlays/Shape3DOverlay.h b/interface/src/ui/overlays/Shape3DOverlay.h index 7fc95ec981..60af287af0 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.h +++ b/interface/src/ui/overlays/Shape3DOverlay.h @@ -30,18 +30,14 @@ public: virtual Shape3DOverlay* createClone() const override; - float getBorderSize() const { return _borderSize; } - - void setBorderSize(float value) { _borderSize = value; } - void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; + virtual scriptable::ScriptableModelBase getScriptableModel() override; protected: Transform evalRenderTransform() override; private: - float _borderSize; GeometryCache::Shape _shape { GeometryCache::Hexagon }; }; diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index 3021aa4404..4743e1ed3a 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -123,3 +123,15 @@ Transform Sphere3DOverlay::evalRenderTransform() { return transform; } + + +scriptable::ScriptableModelBase Sphere3DOverlay::getScriptableModel() { + auto geometryCache = DependencyManager::get(); + auto vertexColor = ColorUtils::toVec3(_color); + scriptable::ScriptableModelBase result; + if (auto mesh = geometryCache->meshFromShape(GeometryCache::Sphere, vertexColor)) { + result.objectID = getID(); + result.append(mesh); + } + return result; +} diff --git a/interface/src/ui/overlays/Sphere3DOverlay.h b/interface/src/ui/overlays/Sphere3DOverlay.h index ebe6dc8d83..9a434e7182 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.h +++ b/interface/src/ui/overlays/Sphere3DOverlay.h @@ -28,6 +28,7 @@ public: virtual Sphere3DOverlay* createClone() const override; + virtual scriptable::ScriptableModelBase getScriptableModel() override; protected: Transform evalRenderTransform() override; }; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index d1a2ff3943..b324bf39c4 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -233,6 +234,7 @@ void Web3DOverlay::setupQmlSurface() { _webSurface->getSurfaceContext()->setContextProperty("Controller", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("Pointers", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("Web3DOverlay", this); + _webSurface->getSurfaceContext()->setContextProperty("Window", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("pathToFonts", "../../"); diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 01b7dfb0de..d6791ab0b8 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -434,9 +434,13 @@ void Rig::setJointRotation(int index, bool valid, const glm::quat& rotation, flo bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const { bool success { false }; - if (QThread::currentThread() == thread()) { + glm::vec3 originalPosition = position; + bool onOwnerThread = (QThread::currentThread() == thread()); + glm::vec3 poseSetTrans; + if (onOwnerThread) { if (isIndexValid(jointIndex)) { - position = (rotation * _internalPoseSet._absolutePoses[jointIndex].trans()) + translation; + poseSetTrans = _internalPoseSet._absolutePoses[jointIndex].trans(); + position = (rotation * poseSetTrans) + translation; success = true; } else { success = false; @@ -444,7 +448,8 @@ bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm: } else { QReadLocker readLock(&_externalPoseSetLock); if (jointIndex >= 0 && jointIndex < (int)_externalPoseSet._absolutePoses.size()) { - position = (rotation * _externalPoseSet._absolutePoses[jointIndex].trans()) + translation; + poseSetTrans = _externalPoseSet._absolutePoses[jointIndex].trans(); + position = (rotation * poseSetTrans) + translation; success = true; } else { success = false; @@ -452,7 +457,14 @@ bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm: } if (isNaN(position)) { - qCWarning(animation) << "Rig::getJointPositionInWorldFrame produces NaN"; + qCWarning(animation) << "Rig::getJointPositionInWorldFrame produced NaN." + << " is owner thread = " << onOwnerThread + << " position = " << originalPosition + << " translation = " << translation + << " rotation = " << rotation + << " poseSetTrans = " << poseSetTrans + << " success = " << success + << " jointIndex = " << jointIndex; success = false; position = glm::vec3(0.0f); } diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 579910d9f7..8379151d1c 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1824,16 +1824,9 @@ float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { float AudioClient::gainForSource(float distance, float volume) { - const float ATTENUATION_BEGINS_AT_DISTANCE = 1.0f; - - // I'm assuming that the AudioMixer's getting of the stream's attenuation - // factor is basically same as getting volume - float gain = volume; - - // attenuate based on distance - if (distance >= ATTENUATION_BEGINS_AT_DISTANCE) { - gain /= (distance/ATTENUATION_BEGINS_AT_DISTANCE); // attenuation = -6dB * log2(distance) - } + // attenuation = -6dB * log2(distance) + // reference attenuation of 0dB at distance = 1.0m + float gain = volume / std::max(distance, HRTF_NEARFIELD_MIN); return gain; } diff --git a/libraries/audio/src/AudioHRTF.cpp b/libraries/audio/src/AudioHRTF.cpp index f2f0235ccb..aa951210a6 100644 --- a/libraries/audio/src/AudioHRTF.cpp +++ b/libraries/audio/src/AudioHRTF.cpp @@ -71,6 +71,47 @@ ALIGN32 static const float crossfadeTable[HRTF_BLOCK] = { 0.0029059408f, 0.0022253666f, 0.0016352853f, 0.0011358041f, 0.0007270137f, 0.0004089886f, 0.0001817865f, 0.0000454487f, }; +// +// Fast approximation of the azimuth parallax correction, +// for azimuth = [-pi,pi] and distance = [0.125,2]. +// +// Correction becomes 0 at distance = 2. +// Correction is continuous and smooth. +// +static const int NAZIMUTH = 8; +static const float azimuthTable[NAZIMUTH][3] = { + { 0.018719007f, 0.097263971f, 0.080748954f }, // [-4pi/4,-3pi/4] + { 0.066995833f, 0.319754290f, 0.336963269f }, // [-3pi/4,-2pi/4] + { -0.066989851f, -0.101178847f, 0.006359474f }, // [-2pi/4,-1pi/4] + { -0.018727343f, -0.020357568f, 0.040065626f }, // [-1pi/4,-0pi/4] + { -0.005519051f, -0.018744412f, 0.040065629f }, // [ 0pi/4, 1pi/4] + { -0.001201296f, -0.025103593f, 0.042396711f }, // [ 1pi/4, 2pi/4] + { 0.001198959f, -0.032642381f, 0.048316220f }, // [ 2pi/4, 3pi/4] + { 0.005519640f, -0.053424870f, 0.073296888f }, // [ 3pi/4, 4pi/4] +}; + +// +// Model the normalized near-field Distance Variation Filter. +// +// This version is parameterized by the DC gain correction, instead of directly by azimuth and distance. +// A first-order shelving filter is used to minimize the disturbance in ITD. +// +// Loosely based on data from S. Spagnol, "Distance rendering and perception of nearby virtual sound sources +// with a near-field filter model,” Applied Acoustics (2017) +// +static const int NNEARFIELD = 9; +static const float nearFieldTable[NNEARFIELD][3] = { // { b0, b1, a1 } + { 0.008410604f, -0.000262748f, -0.991852144f }, // gain = 1/256 + { 0.016758914f, -0.000529590f, -0.983770676f }, // gain = 1/128 + { 0.033270607f, -0.001075350f, -0.967804743f }, // gain = 1/64 + { 0.065567740f, -0.002213762f, -0.936646021f }, // gain = 1/32 + { 0.127361554f, -0.004667324f, -0.877305769f }, // gain = 1/16 + { 0.240536414f, -0.010201827f, -0.769665412f }, // gain = 1/8 + { 0.430858205f, -0.023243052f, -0.592384847f }, // gain = 1/4 + { 0.703238106f, -0.054157913f, -0.350919807f }, // gain = 1/2 + { 1.000000000f, -0.123144711f, -0.123144711f }, // gain = 1/1 +}; + // // Model the frequency-dependent attenuation of sound propogation in air. // @@ -168,6 +209,8 @@ static const float lowpassTable[NLOWPASS][5] = { // { b0, b1, b2, a1, a2 } { 0.000076480f, 0.000024509f, -0.000000991f, -1.985807957f, 0.985907955f }, }; +static const float HALFPI = 1.570796327f; +static const float PI = 3.141592654f; static const float TWOPI = 6.283185307f; // @@ -716,7 +759,7 @@ static void distanceBiquad(float distance, float& b0, float& b1, float& b2, floa // float x = distance; - x = MIN(MAX(x, 1.0f), 1<<30); + x = MIN(x, 1<<30); x *= x; x *= x; // x = distance^4 @@ -747,33 +790,181 @@ static void distanceBiquad(float distance, float& b0, float& b1, float& b2, floa a2 = lowpassTable[e+0][4] + frac * (lowpassTable[e+1][4] - lowpassTable[e+0][4]); } +// +// Geometric correction of the azimuth, to the angle at each ear. +// D. Brungart, "Auditory parallax effects in the HRTF for nearby sources," IEEE WASPAA (1999). +// +static void nearFieldAzimuthCorrection(float azimuth, float distance, float& azimuthL, float& azimuthR) { + +#ifdef HRTF_AZIMUTH_EXACT + float dx = distance * cosf(azimuth); + float dy = distance * sinf(azimuth); + + float dx0 = HRTF_AZIMUTH_REF * cosf(azimuth); + float dy0 = HRTF_AZIMUTH_REF * sinf(azimuth); + + azimuthL += atan2f(dy + HRTF_HEAD_RADIUS, dx) - atan2f(dy0 + HRTF_HEAD_RADIUS, dx0); + azimuthR += atan2f(dy - HRTF_HEAD_RADIUS, dx) - atan2f(dy0 - HRTF_HEAD_RADIUS, dx0); +#else + // at reference distance, the azimuth parallax is correct + float fy = (HRTF_AZIMUTH_REF - distance) / distance; + + float x0 = +azimuth; + float x1 = -azimuth; // compute using symmetry + + const float RAD_TO_INDEX = 1.2732394f; // 8/(2*pi), rounded down + int k0 = (int)(RAD_TO_INDEX * x0 + 4.0f); + int k1 = (NAZIMUTH-1) - k0; + assert(k0 >= 0); + assert(k1 >= 0); + assert(k0 < NAZIMUTH); + assert(k1 < NAZIMUTH); + + // piecewise polynomial approximation over azimuth=[-pi,pi] + float fx0 = (azimuthTable[k0][0] * x0 + azimuthTable[k0][1]) * x0 + azimuthTable[k0][2]; + float fx1 = (azimuthTable[k1][0] * x1 + azimuthTable[k1][1]) * x1 + azimuthTable[k1][2]; + + // approximate the azimuth correction + // NOTE: must converge to 0 when distance = HRTF_AZIMUTH_REF + azimuthL += fx0 * fy; + azimuthR -= fx1 * fy; +#endif +} + +// +// Approximate the near-field DC gain correction at each ear. +// +static void nearFieldGainCorrection(float azimuth, float distance, float& gainL, float& gainR) { + + // normalized distance factor = [0,1] as distance = [HRTF_NEARFIELD_MAX,HRTF_HEAD_RADIUS] + assert(distance < HRTF_NEARFIELD_MAX); + assert(distance > HRTF_HEAD_RADIUS); + float d = (HRTF_NEARFIELD_MAX - distance) * ( 1.0f / (HRTF_NEARFIELD_MAX - HRTF_HEAD_RADIUS)); + + // angle of incidence at each ear + float angleL = azimuth + HALFPI; + float angleR = azimuth - HALFPI; + if (angleL > +PI) { + angleL -= TWOPI; + } + if (angleR < -PI) { + angleR += TWOPI; + } + assert(angleL >= -PI); + assert(angleL <= +PI); + assert(angleR >= -PI); + assert(angleR <= +PI); + + // normalized occlusion factor = [0,1] as angle of incidence = [0,pi] + angleL *= angleL; + angleR *= angleR; + float cL = ((-0.000452339132f * angleL - 0.00173192444f) * angleL + 0.162476536f) * angleL; + float cR = ((-0.000452339132f * angleR - 0.00173192444f) * angleR + 0.162476536f) * angleR; + + // approximate the gain correction + // NOTE: must converge to 0 when distance = HRTF_NEARFIELD_MAX + gainL -= d * cL; + gainR -= d * cR; +} + +// +// Approximate the normalized near-field Distance Variation Function at each ear. +// A. Kan, "Distance Variation Function for simulation of near-field virtual auditory space," IEEE ICASSP (2006) +// +static void nearFieldFilter(float gain, float& b0, float& b1, float& a1) { + + // + // Computed from a lookup table quantized to gain = 2^(-N) + // and reconstructed by piecewise linear interpolation. + // + + // split gain into e and frac, such that gain = 2^(e+0) + frac * (2^(e+1) - 2^(e+0)) + int e; + float frac; + splitf(gain, e, frac); + + // clamp to table limits + e += NNEARFIELD-1; + if (e < 0) { + e = 0; + frac = 0.0f; + } + if (e > NNEARFIELD-2) { + e = NNEARFIELD-2; + frac = 1.0f; + } + assert(frac >= 0.0f); + assert(frac <= 1.0f); + assert(e+0 >= 0); + assert(e+1 < NNEARFIELD); + + // piecewise linear interpolation + b0 = nearFieldTable[e+0][0] + frac * (nearFieldTable[e+1][0] - nearFieldTable[e+0][0]); + b1 = nearFieldTable[e+0][1] + frac * (nearFieldTable[e+1][1] - nearFieldTable[e+0][1]); + a1 = nearFieldTable[e+0][2] + frac * (nearFieldTable[e+1][2] - nearFieldTable[e+0][2]); +} + +static void azimuthToIndex(float azimuth, int& index0, int& index1, float& frac) { + + // convert from radians to table units + azimuth *= (HRTF_AZIMUTHS / TWOPI); + + if (azimuth < 0.0f) { + azimuth += HRTF_AZIMUTHS; + } + + // table parameters + index0 = (int)azimuth; + index1 = index0 + 1; + frac = azimuth - (float)index0; + + if (index1 >= HRTF_AZIMUTHS) { + index1 -= HRTF_AZIMUTHS; + } + + assert((index0 >= 0) && (index0 < HRTF_AZIMUTHS)); + assert((index1 >= 0) && (index1 < HRTF_AZIMUTHS)); + assert((frac >= 0.0f) && (frac < 1.0f)); +} + // compute new filters for a given azimuth, distance and gain static void setFilters(float firCoef[4][HRTF_TAPS], float bqCoef[5][8], int delay[4], int index, float azimuth, float distance, float gain, int channel) { - // convert from radians to table units - azimuth *= HRTF_AZIMUTHS / TWOPI; - - // wrap to principle value - while (azimuth < 0.0f) { - azimuth += HRTF_AZIMUTHS; + if (azimuth > PI) { + azimuth -= TWOPI; } - while (azimuth >= HRTF_AZIMUTHS) { - azimuth -= HRTF_AZIMUTHS; + assert(azimuth >= -PI); + assert(azimuth <= +PI); + + distance = MAX(distance, HRTF_NEARFIELD_MIN); + + // compute the azimuth correction at each ear + float azimuthL = azimuth; + float azimuthR = azimuth; + if (distance < HRTF_AZIMUTH_REF) { + nearFieldAzimuthCorrection(azimuth, distance, azimuthL, azimuthR); } - // table parameters - int az0 = (int)azimuth; - int az1 = (az0 + 1) % HRTF_AZIMUTHS; - float frac = azimuth - (float)az0; + // compute the DC gain correction at each ear + float gainL = 1.0f; + float gainR = 1.0f; + if (distance < HRTF_NEARFIELD_MAX) { + nearFieldGainCorrection(azimuth, distance, gainL, gainR); + } - assert((az0 >= 0) && (az0 < HRTF_AZIMUTHS)); - assert((az1 >= 0) && (az1 < HRTF_AZIMUTHS)); - assert((frac >= 0.0f) && (frac < 1.0f)); + // parameters for table interpolation + int azL0, azR0, az0; + int azL1, azR1, az1; + float fracL, fracR, frac; + + azimuthToIndex(azimuthL, azL0, azL1, fracL); + azimuthToIndex(azimuthR, azR0, azR1, fracR); + azimuthToIndex(azimuth, az0, az1, frac); // interpolate FIR - interpolate(firCoef[channel+0], ir_table_table[index][az0][0], ir_table_table[index][az1][0], frac, gain); - interpolate(firCoef[channel+1], ir_table_table[index][az0][1], ir_table_table[index][az1][1], frac, gain); + interpolate(firCoef[channel+0], ir_table_table[index][azL0][0], ir_table_table[index][azL1][0], fracL, gain * gainL); + interpolate(firCoef[channel+1], ir_table_table[index][azR0][1], ir_table_table[index][azR1][1], fracR, gain * gainR); // interpolate ITD float itd = (1.0f - frac) * itd_table_table[index][az0] + frac * itd_table_table[index][az1]; @@ -832,21 +1023,42 @@ static void setFilters(float firCoef[4][HRTF_TAPS], float bqCoef[5][8], int dela } // - // Second biquad implements the distance filter. + // Second biquad implements the near-field or distance filter. // - distanceBiquad(distance, b0, b1, b2, a1, a2); + if (distance < HRTF_NEARFIELD_MAX) { - bqCoef[0][channel+4] = b0; - bqCoef[1][channel+4] = b1; - bqCoef[2][channel+4] = b2; - bqCoef[3][channel+4] = a1; - bqCoef[4][channel+4] = a2; + nearFieldFilter(gainL, b0, b1, a1); - bqCoef[0][channel+5] = b0; - bqCoef[1][channel+5] = b1; - bqCoef[2][channel+5] = b2; - bqCoef[3][channel+5] = a1; - bqCoef[4][channel+5] = a2; + bqCoef[0][channel+4] = b0; + bqCoef[1][channel+4] = b1; + bqCoef[2][channel+4] = 0.0f; + bqCoef[3][channel+4] = a1; + bqCoef[4][channel+4] = 0.0f; + + nearFieldFilter(gainR, b0, b1, a1); + + bqCoef[0][channel+5] = b0; + bqCoef[1][channel+5] = b1; + bqCoef[2][channel+5] = 0.0f; + bqCoef[3][channel+5] = a1; + bqCoef[4][channel+5] = 0.0f; + + } else { + + distanceBiquad(distance, b0, b1, b2, a1, a2); + + bqCoef[0][channel+4] = b0; + bqCoef[1][channel+4] = b1; + bqCoef[2][channel+4] = b2; + bqCoef[3][channel+4] = a1; + bqCoef[4][channel+4] = a2; + + bqCoef[0][channel+5] = b0; + bqCoef[1][channel+5] = b1; + bqCoef[2][channel+5] = b2; + bqCoef[3][channel+5] = a1; + bqCoef[4][channel+5] = a2; + } } void AudioHRTF::render(int16_t* input, float* output, int index, float azimuth, float distance, float gain, int numFrames) { diff --git a/libraries/audio/src/AudioHRTF.h b/libraries/audio/src/AudioHRTF.h index a197264994..8993842d6e 100644 --- a/libraries/audio/src/AudioHRTF.h +++ b/libraries/audio/src/AudioHRTF.h @@ -23,6 +23,12 @@ static const int HRTF_BLOCK = 240; // block processing size static const float HRTF_GAIN = 1.0f; // HRTF global gain adjustment +// Near-field HRTF +static const float HRTF_AZIMUTH_REF = 2.0f; // IRCAM Listen HRTF was recorded at 2 meters +static const float HRTF_NEARFIELD_MAX = 1.0f; // distance in meters +static const float HRTF_NEARFIELD_MIN = 0.125f; // distance in meters +static const float HRTF_HEAD_RADIUS = 0.0875f; // average human head in meters + class AudioHRTF { public: diff --git a/libraries/avatars-renderer/CMakeLists.txt b/libraries/avatars-renderer/CMakeLists.txt index d3d61ea3fa..a70c8294d5 100644 --- a/libraries/avatars-renderer/CMakeLists.txt +++ b/libraries/avatars-renderer/CMakeLists.txt @@ -15,5 +15,6 @@ include_hifi_library_headers(entities) include_hifi_library_headers(octree) include_hifi_library_headers(task) include_hifi_library_headers(workload) +include_hifi_library_headers(graphics-scripting) # for ScriptableModel.h target_bullet() diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 55a40f3ae3..f493bb14dd 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -35,6 +35,8 @@ #include "ModelEntityItem.h" #include "RenderableModelEntityItem.h" +#include + #include "Logging.h" using namespace std; @@ -576,6 +578,7 @@ void Avatar::addToScene(AvatarSharedPointer self, const render::ScenePointer& sc } _mustFadeIn = true; + emit DependencyManager::get()->modelAddedToScene(getSessionUUID(), NestableType::Avatar, _skeletonModel); } void Avatar::fadeIn(render::ScenePointer scene) { @@ -625,6 +628,7 @@ void Avatar::removeFromScene(AvatarSharedPointer self, const render::ScenePointe for (auto& attachmentModel : _attachmentModels) { attachmentModel->removeFromScene(scene, transaction); } + emit DependencyManager::get()->modelRemovedFromScene(getSessionUUID(), NestableType::Avatar, _skeletonModel); } void Avatar::updateRenderItem(render::Transaction& transaction) { @@ -1789,4 +1793,13 @@ void Avatar::processMaterials() { material.pop(); } } +} + +scriptable::ScriptableModelBase Avatar::getScriptableModel() { + if (!_skeletonModel || !_skeletonModel->isLoaded()) { + return scriptable::ScriptableModelBase(); + } + auto result = _skeletonModel->getScriptableModel(); + result.objectID = getSessionUUID().isNull() ? AVATAR_SELF_ID : getSessionUUID(); + return result; } \ No newline at end of file diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index b24fbeaef2..bf0adc5c05 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -20,6 +20,7 @@ #include #include #include +#include #include @@ -53,7 +54,7 @@ class Texture; using AvatarPhysicsCallback = std::function; -class Avatar : public AvatarData { +class Avatar : public AvatarData, public scriptable::ModelProvider { Q_OBJECT /**jsdoc @@ -275,6 +276,8 @@ public: void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) override; void removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName) override; + virtual scriptable::ScriptableModelBase getScriptableModel() override; + public slots: // FIXME - these should be migrated to use Pose data instead diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index 1112ccde60..b2a494230b 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -31,6 +31,8 @@ SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent) : _defaultEyeModelPosition(glm::vec3(0.0f, 0.0f, 0.0f)), _headClipDistance(DEFAULT_NEAR_CLIP) { + // SkeletonModels, and by extention Avatars, use Dual Quaternion skinning. + _useDualQuaternionSkinning = true; assert(_owningAvatar); } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 033756bfd2..e927120b07 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -373,7 +373,7 @@ class AvatarData : public QObject, public SpatiallyNestable { // The result is unique among all avatars present at the time. Q_PROPERTY(QString sessionDisplayName READ getSessionDisplayName WRITE setSessionDisplayName NOTIFY sessionDisplayNameChanged) Q_PROPERTY(bool lookAtSnappingEnabled MEMBER _lookAtSnappingEnabled NOTIFY lookAtSnappingChanged) - Q_PROPERTY(QString skeletonModelURL READ getSkeletonModelURLFromScript WRITE setSkeletonModelURLFromScript) + Q_PROPERTY(QString skeletonModelURL READ getSkeletonModelURLFromScript WRITE setSkeletonModelURLFromScript NOTIFY skeletonModelURLChanged) Q_PROPERTY(QVector attachmentData READ getAttachmentData WRITE setAttachmentData) Q_PROPERTY(QStringList jointNames READ getJointNames) @@ -702,6 +702,7 @@ public: signals: void displayNameChanged(); void sessionDisplayNameChanged(); + void skeletonModelURLChanged(); void lookAtSnappingChanged(bool enabled); void sessionUUIDChanged(); diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp index 1fd001e536..8491e5368b 100644 --- a/libraries/avatars/src/ScriptAvatarData.cpp +++ b/libraries/avatars/src/ScriptAvatarData.cpp @@ -16,6 +16,7 @@ ScriptAvatarData::ScriptAvatarData(AvatarSharedPointer avatarData) : { QObject::connect(avatarData.get(), &AvatarData::displayNameChanged, this, &ScriptAvatarData::displayNameChanged); QObject::connect(avatarData.get(), &AvatarData::sessionDisplayNameChanged, this, &ScriptAvatarData::sessionDisplayNameChanged); + QObject::connect(avatarData.get(), &AvatarData::skeletonModelURLChanged, this, &ScriptAvatarData::skeletonModelURLChanged); QObject::connect(avatarData.get(), &AvatarData::lookAtSnappingChanged, this, &ScriptAvatarData::lookAtSnappingChanged); } diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index 68074b838d..13713ff15f 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -51,7 +51,7 @@ class ScriptAvatarData : public QObject { // // ATTACHMENT AND JOINT PROPERTIES // - Q_PROPERTY(QString skeletonModelURL READ getSkeletonModelURLFromScript) + Q_PROPERTY(QString skeletonModelURL READ getSkeletonModelURLFromScript NOTIFY skeletonModelURLChanged) Q_PROPERTY(QVector attachmentData READ getAttachmentData) Q_PROPERTY(QStringList jointNames READ getJointNames) @@ -132,6 +132,7 @@ public: signals: void displayNameChanged(); void sessionDisplayNameChanged(); + void skeletonModelURLChanged(); void lookAtSnappingChanged(bool enabled); public slots: diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index a61bc95f8b..00879e1380 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -133,12 +133,56 @@ QList HTTPConnection::parseFormData() const { } void HTTPConnection::respond(const char* code, const QByteArray& content, const char* contentType, const Headers& headers) { + respondWithStatusAndHeaders(code, contentType, headers, content.size()); + + _socket->write(content); + + _socket->disconnectFromHost(); + + // make sure we receive no further read notifications + disconnect(_socket, &QTcpSocket::readyRead, this, nullptr); +} + +void HTTPConnection::respond(const char* code, std::unique_ptr device, const char* contentType, const Headers& headers) { + _responseDevice = std::move(device); + + if (_responseDevice->isSequential()) { + qWarning() << "Error responding to HTTPConnection: sequential IO devices not supported"; + respondWithStatusAndHeaders(StatusCode500, contentType, headers, 0); + _socket->disconnect(SIGNAL(readyRead()), this); + _socket->disconnectFromHost(); + return; + } + + int totalToBeWritten = _responseDevice->size(); + respondWithStatusAndHeaders(code, contentType, headers, totalToBeWritten); + + if (_responseDevice->atEnd()) { + _socket->disconnectFromHost(); + } else { + connect(_socket, &QTcpSocket::bytesWritten, this, [this, totalToBeWritten](size_t bytes) mutable { + constexpr size_t HTTP_RESPONSE_CHUNK_SIZE = 1024 * 10; + if (!_responseDevice->atEnd()) { + totalToBeWritten -= _socket->write(_responseDevice->read(HTTP_RESPONSE_CHUNK_SIZE)); + if (_responseDevice->atEnd()) { + _socket->disconnectFromHost(); + disconnect(_socket, &QTcpSocket::bytesWritten, this, nullptr); + } + } + }); + + } + + // make sure we receive no further read notifications + disconnect(_socket, &QTcpSocket::readyRead, this, nullptr); +} + +void HTTPConnection::respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, qint64 contentLength) { _socket->write("HTTP/1.1 "); + _socket->write(code); _socket->write("\r\n"); - int csize = content.size(); - for (Headers::const_iterator it = headers.constBegin(), end = headers.constEnd(); it != end; it++) { _socket->write(it.key()); @@ -146,9 +190,10 @@ void HTTPConnection::respond(const char* code, const QByteArray& content, const _socket->write(it.value()); _socket->write("\r\n"); } - if (csize > 0) { + + if (contentLength > 0) { _socket->write("Content-Length: "); - _socket->write(QByteArray::number(csize)); + _socket->write(QByteArray::number(contentLength)); _socket->write("\r\n"); _socket->write("Content-Type: "); @@ -156,21 +201,16 @@ void HTTPConnection::respond(const char* code, const QByteArray& content, const _socket->write("\r\n"); } _socket->write("Connection: close\r\n\r\n"); - - if (csize > 0) { - _socket->write(content); - } - - // make sure we receive no further read notifications - _socket->disconnect(SIGNAL(readyRead()), this); - - _socket->disconnectFromHost(); } void HTTPConnection::readRequest() { if (!_socket->canReadLine()) { return; } + if (!_requestUrl.isEmpty()) { + qDebug() << "Request URL was already set"; + return; + } // parse out the method and resource QByteArray line = _socket->readLine().trimmed(); if (line.startsWith("HEAD")) { diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index 966fc26949..60408d4325 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.h +++ b/libraries/embedded-webserver/src/HTTPConnection.h @@ -26,6 +26,8 @@ #include #include +#include + class QTcpSocket; class HTTPManager; class MaskFilter; @@ -87,6 +89,9 @@ public: void respond (const char* code, const QByteArray& content = QByteArray(), const char* contentType = DefaultContentType, const Headers& headers = Headers()); + void respond (const char* code, std::unique_ptr device, + const char* contentType = DefaultContentType, + const Headers& headers = Headers()); protected slots: @@ -100,6 +105,7 @@ protected slots: void readContent (); protected: + void respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, qint64 size); /// The parent HTTP manager HTTPManager* _parentManager; @@ -127,6 +133,9 @@ protected: /// The content of the request. QByteArray _requestContent; + + /// Response content + std::unique_ptr _responseDevice; }; #endif // hifi_HTTPConnection_h diff --git a/libraries/embedded-webserver/src/HTTPManager.cpp b/libraries/embedded-webserver/src/HTTPManager.cpp index fd127a2e92..bd1b545412 100644 --- a/libraries/embedded-webserver/src/HTTPManager.cpp +++ b/libraries/embedded-webserver/src/HTTPManager.cpp @@ -98,13 +98,14 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, // file exists, serve it static QMimeDatabase mimeDatabase; - QFile localFile(filePath); - localFile.open(QIODevice::ReadOnly); - QByteArray localFileData = localFile.readAll(); + auto localFile = std::unique_ptr(new QFile(filePath)); + localFile->open(QIODevice::ReadOnly); + QByteArray localFileData; QFileInfo localFileInfo(filePath); if (localFileInfo.completeSuffix() == "shtml") { + localFileData = localFile->readAll(); // this is a file that may have some SSI statements // the only thing we support is the include directive, but check the contents for that @@ -153,8 +154,12 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, ? QString { "text/html" } : mimeDatabase.mimeTypeForFile(filePath).name(); - connection->respond(HTTPConnection::StatusCode200, localFileData, qPrintable(mimeType)); - + if (localFileData.isNull()) { + connection->respond(HTTPConnection::StatusCode200, std::move(localFile), qPrintable(mimeType)); + } else { + connection->respond(HTTPConnection::StatusCode200, localFileData, qPrintable(mimeType)); + } + return true; } } diff --git a/libraries/entities-renderer/CMakeLists.txt b/libraries/entities-renderer/CMakeLists.txt index 083a1716ce..12b9b3dea5 100644 --- a/libraries/entities-renderer/CMakeLists.txt +++ b/libraries/entities-renderer/CMakeLists.txt @@ -14,6 +14,7 @@ include_hifi_library_headers(entities) include_hifi_library_headers(avatars) include_hifi_library_headers(controllers) include_hifi_library_headers(task) +include_hifi_library_headers(graphics-scripting) # for Forward.h target_bullet() target_polyvox() diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index ce652a758c..d34a1127ae 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -17,13 +17,14 @@ #include #include "AbstractViewStateInterface.h" #include "EntitiesRendererLogging.h" +#include class EntityTreeRenderer; namespace render { namespace entities { // Base class for all renderable entities -class EntityRenderer : public QObject, public std::enable_shared_from_this, public PayloadProxyInterface, protected ReadWriteLockable { +class EntityRenderer : public QObject, public std::enable_shared_from_this, public PayloadProxyInterface, protected ReadWriteLockable, public scriptable::ModelProvider { Q_OBJECT using Pointer = std::shared_ptr; @@ -37,7 +38,7 @@ public: virtual bool wantsKeyboardFocus() const { return false; } virtual void setProxyWindow(QWindow* proxyWindow) {} virtual QObject* getEventHandler() { return nullptr; } - const EntityItemPointer& getEntity() { return _entity; } + const EntityItemPointer& getEntity() const { return _entity; } const ItemID& getRenderItemID() const { return _renderItemID; } const SharedSoundPointer& getCollisionSound() { return _collisionSound; } @@ -57,6 +58,8 @@ public: virtual void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName); virtual void removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName); + virtual scriptable::ScriptableModelBase getScriptableModel() override { return scriptable::ScriptableModelBase(); } + protected: virtual bool needsRenderUpdateFromEntity() const final { return needsRenderUpdateFromEntity(_entity); } virtual void onAddToScene(const EntityItemPointer& entity); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 83ec675adf..5f926d1bb9 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -133,6 +133,9 @@ void RenderableModelEntityItem::doInitialModelSimulation() { model->setRotation(getWorldOrientation()); model->setTranslation(getWorldPosition()); + glm::vec3 scale = model->getScale(); + model->setUseDualQuaternionSkinning(!isNonUniformScale(scale)); + if (_needsInitialSimulation) { model->simulate(0.0f); _needsInitialSimulation = false; @@ -243,6 +246,8 @@ void RenderableModelEntityItem::updateModelBounds() { } if (updateRenderItems) { + glm::vec3 scale = model->getScale(); + model->setUseDualQuaternionSkinning(!isNonUniformScale(scale)); model->updateRenderItems(); } } @@ -950,6 +955,7 @@ QStringList RenderableModelEntityItem::getJointNames() const { return result; } +// FIXME: deprecated; remove >= RC67 bool RenderableModelEntityItem::getMeshes(MeshProxyList& result) { auto model = getModel(); if (!model || !model->isLoaded()) { @@ -959,6 +965,34 @@ bool RenderableModelEntityItem::getMeshes(MeshProxyList& result) { return !result.isEmpty(); } +scriptable::ScriptableModelBase render::entities::ModelEntityRenderer::getScriptableModel() { + auto model = resultWithReadLock([this]{ return _model; }); + + if (!model || !model->isLoaded()) { + return scriptable::ScriptableModelBase(); + } + + auto result = _model->getScriptableModel(); + result.objectID = getEntity()->getID(); + return result; +} + +bool render::entities::ModelEntityRenderer::canReplaceModelMeshPart(int meshIndex, int partIndex) { + // TODO: for now this method is just used to indicate that this provider generally supports mesh updates + auto model = resultWithReadLock([this]{ return _model; }); + return model && model->isLoaded(); +} + +bool render::entities::ModelEntityRenderer::replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointer newModel, int meshIndex, int partIndex) { + auto model = resultWithReadLock([this]{ return _model; }); + + if (!model || !model->isLoaded()) { + return false; + } + + return model->replaceScriptableModelMeshPart(newModel, meshIndex, partIndex); +} + void RenderableModelEntityItem::simulateRelayedJoints() { ModelPointer model = getModel(); if (model && model->isLoaded()) { @@ -1052,7 +1086,6 @@ void ModelEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entit entity->setModel({}); } - void ModelEntityRenderer::animate(const TypedEntityPointer& entity) { if (!_animation || !_animation->isLoaded()) { return; @@ -1276,6 +1309,8 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce auto entityRenderer = static_cast(&data); entityRenderer->clearSubRenderItemIDs(); }); + emit DependencyManager::get()-> + modelRemovedFromScene(entity->getEntityItemID(), NestableType::Entity, _model); } return; } @@ -1286,6 +1321,10 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce connect(model.get(), &Model::setURLFinished, this, [&](bool didVisualGeometryRequestSucceed) { setKey(didVisualGeometryRequestSucceed); emit requestRenderUpdate(); + if(didVisualGeometryRequestSucceed) { + emit DependencyManager::get()-> + modelAddedToScene(entity->getEntityItemID(), NestableType::Entity, _model); + } }); connect(model.get(), &Model::requestRenderUpdate, this, &ModelEntityRenderer::requestRenderUpdate); connect(entity.get(), &RenderableModelEntityItem::requestCollisionGeometryUpdate, this, &ModelEntityRenderer::flagForCollisionGeometryUpdate); @@ -1500,4 +1539,4 @@ void ModelEntityRenderer::processMaterials() { material.pop(); } } -} \ No newline at end of file +} diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 25ae668a8e..5d7d84b7bc 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -111,7 +111,7 @@ public: virtual int getJointIndex(const QString& name) const override; virtual QStringList getJointNames() const override; - bool getMeshes(MeshProxyList& result) override; + bool getMeshes(MeshProxyList& result) override; // deprecated const void* getCollisionMeshKey() const { return _collisionMeshKey; } signals: @@ -142,6 +142,9 @@ class ModelEntityRenderer : public TypedEntityRenderer PolyLineEntityRenderer::updateVertic return vertices; } +scriptable::ScriptableModelBase PolyLineEntityRenderer::getScriptableModel() { + // TODO: adapt polyline into a triangles mesh... + return EntityRenderer::getScriptableModel(); +} void PolyLineEntityRenderer::doRender(RenderArgs* args) { if (_empty) { @@ -319,4 +323,4 @@ void PolyLineEntityRenderer::doRender(RenderArgs* args) { #endif batch.draw(gpu::TRIANGLE_STRIP, _numVertices, 0); -} \ No newline at end of file +} diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h index 1e27ac9ae7..f460baac59 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h @@ -25,6 +25,7 @@ class PolyLineEntityRenderer : public TypedEntityRenderer { public: PolyLineEntityRenderer(const EntityItemPointer& entity); + virtual scriptable::ScriptableModelBase getScriptableModel() override; protected: virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 78a363d63c..2b1de8d11b 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1418,6 +1418,7 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() { } } +// deprecated bool RenderablePolyVoxEntityItem::getMeshes(MeshProxyList& result) { if (!updateDependents()) { return false; @@ -1450,6 +1451,37 @@ bool RenderablePolyVoxEntityItem::getMeshes(MeshProxyList& result) { return success; } +scriptable::ScriptableModelBase RenderablePolyVoxEntityItem::getScriptableModel() { + if (!updateDependents() || !_mesh) { + return scriptable::ScriptableModelBase(); + } + + bool success = false; + glm::mat4 transform = voxelToLocalMatrix(); + scriptable::ScriptableModelBase result; + result.objectID = getThisPointer()->getID(); + withReadLock([&] { + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)_mesh->getNumVertices(); + if (!_meshReady) { + // we aren't ready to return a mesh. the caller will have to try again later. + success = false; + } else if (numVertices == 0) { + // we are ready, but there are no triangles in the mesh. + success = true; + } else { + success = true; + // the mesh will be in voxel-space. transform it into object-space + result.append(_mesh->map( + [=](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, + [=](glm::vec3 color){ return color; }, + [=](glm::vec3 normal){ return glm::normalize(glm::vec3(transform * glm::vec4(normal, 0.0f))); }, + [&](uint32_t index){ return index; } + )); + } + }); + return result; +} + using namespace render; using namespace render::entities; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 1e97067795..0a00d1cb73 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -32,7 +32,7 @@ namespace render { namespace entities { class PolyVoxEntityRenderer; } } -class RenderablePolyVoxEntityItem : public PolyVoxEntityItem { +class RenderablePolyVoxEntityItem : public PolyVoxEntityItem, public scriptable::ModelProvider { friend class render::entities::PolyVoxEntityRenderer; public: @@ -113,7 +113,8 @@ public: void setVolDataDirty() { withWriteLock([&] { _volDataDirty = true; _meshReady = false; }); } - bool getMeshes(MeshProxyList& result) override; + bool getMeshes(MeshProxyList& result) override; // deprecated + virtual scriptable::ScriptableModelBase getScriptableModel() override; private: bool updateOnCount(const ivec3& v, uint8_t toValue); @@ -163,6 +164,9 @@ class PolyVoxEntityRenderer : public TypedEntityRenderer()->getScriptableModel(); + } protected: virtual ItemKey getKey() override { return ItemKey::Builder::opaqueShape().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1); } diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index d561e8d5e8..22cd98b08a 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -163,3 +163,18 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { const auto triCount = geometryCache->getShapeTriangleCount(geometryShape); args->_details._trianglesRendered += (int)triCount; } + +scriptable::ScriptableModelBase ShapeEntityRenderer::getScriptableModel() { + scriptable::ScriptableModelBase result; + auto geometryCache = DependencyManager::get(); + auto geometryShape = geometryCache->getShapeForEntityShape(_shape); + glm::vec3 vertexColor; + if (_materials["0"].top().material) { + vertexColor = _materials["0"].top().material->getAlbedo(); + } + if (auto mesh = geometryCache->meshFromShape(geometryShape, vertexColor)) { + result.objectID = getEntity()->getID(); + result.append(mesh); + } + return result; +} diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h index c913544255..de855ce0c6 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.h +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.h @@ -22,6 +22,8 @@ class ShapeEntityRenderer : public TypedEntityRenderer { public: ShapeEntityRenderer(const EntityItemPointer& entity); + virtual scriptable::ScriptableModelBase getScriptableModel() override; + private: virtual bool needsRenderUpdate() const override; virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; diff --git a/libraries/entities/src/EntityEditFilters.cpp b/libraries/entities/src/EntityEditFilters.cpp index 550c8f17c4..676b1ce518 100644 --- a/libraries/entities/src/EntityEditFilters.cpp +++ b/libraries/entities/src/EntityEditFilters.cpp @@ -41,8 +41,8 @@ QList EntityEditFilters::getZonesByPosition(glm::vec3& position) { return zones; } -bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, - EntityTree::FilterType filterType, EntityItemID& itemID) { +bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, + bool& wasChanged, EntityTree::FilterType filterType, EntityItemID& itemID, EntityItemPointer& existingEntity) { // get the ids of all the zones (plus the global entity edit filter) that the position // lies within @@ -61,6 +61,17 @@ bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& proper if (filterData.rejectAll) { return false; } + + // check to see if this filter wants to filter this message type + if ((!filterData.wantsToFilterEdit && filterType == EntityTree::FilterType::Edit) || + (!filterData.wantsToFilterPhysics && filterType == EntityTree::FilterType::Physics) || + (!filterData.wantsToFilterDelete && filterType == EntityTree::FilterType::Delete) || + (!filterData.wantsToFilterAdd && filterType == EntityTree::FilterType::Add)) { + + wasChanged = false; + return true; // accept the message + } + auto oldProperties = propertiesIn.getDesiredProperties(); auto specifiedProperties = propertiesIn.getChangedProperties(); propertiesIn.setDesiredProperties(specifiedProperties); @@ -68,16 +79,62 @@ bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& proper propertiesIn.setDesiredProperties(oldProperties); auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter. + QScriptValueList args; args << inputValues; args << filterType; + // get the current properties for then entity and include them for the filter call + if (existingEntity && filterData.wantsOriginalProperties) { + auto currentProperties = existingEntity->getProperties(filterData.includedOriginalProperties); + QScriptValue currentValues = currentProperties.copyToScriptValue(filterData.engine, false, true, true); + args << currentValues; + } + + + // get the zone properties + if (filterData.wantsZoneProperties) { + auto zoneEntity = _tree->findEntityByEntityItemID(id); + if (zoneEntity) { + auto zoneProperties = zoneEntity->getProperties(filterData.includedZoneProperties); + QScriptValue zoneValues = zoneProperties.copyToScriptValue(filterData.engine, false, true, true); + + if (filterData.wantsZoneBoundingBox) { + bool success = true; + AABox aaBox = zoneEntity->getAABox(success); + if (success) { + QScriptValue boundingBox = filterData.engine->newObject(); + QScriptValue bottomRightNear = vec3toScriptValue(filterData.engine, aaBox.getCorner()); + QScriptValue topFarLeft = vec3toScriptValue(filterData.engine, aaBox.calcTopFarLeft()); + QScriptValue center = vec3toScriptValue(filterData.engine, aaBox.calcCenter()); + QScriptValue boundingBoxDimensions = vec3toScriptValue(filterData.engine, aaBox.getDimensions()); + boundingBox.setProperty("brn", bottomRightNear); + boundingBox.setProperty("tfl", topFarLeft); + boundingBox.setProperty("center", center); + boundingBox.setProperty("dimensions", boundingBoxDimensions); + zoneValues.setProperty("boundingBox", boundingBox); + } + } + + // If this is an add or delete, or original properties weren't requested + // there won't be original properties in the args, but zone properties need + // to be the fourth parameter, so we need to pad the args accordingly + int EXPECTED_ARGS = 3; + if (args.length() < EXPECTED_ARGS) { + args << QScriptValue(); + } + assert(args.length() == EXPECTED_ARGS); // we MUST have 3 args by now! + args << zoneValues; + } + } + QScriptValue result = filterData.filterFn.call(_nullObjectForFilter, args); + if (filterData.uncaughtExceptions()) { return false; } - if (result.isObject()){ + if (result.isObject()) { // make propertiesIn reflect the changes, for next filter... propertiesIn.copyFromScriptValue(result, false); @@ -86,6 +143,17 @@ bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& proper // Javascript objects are == only if they are the same object. To compare arbitrary values, we need to use JSON. auto out = QJsonValue::fromVariant(result.toVariant()); wasChanged |= (in != out); + } else if (result.isBool()) { + + // if the filter returned false, then it's authoritative + if (!result.toBool()) { + return false; + } + + // otherwise, assume it wants to pass all properties + propertiesOut = propertiesIn; + wasChanged = false; + } else { return false; } @@ -182,8 +250,8 @@ static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) { qDebug() << "script request completed for entity " << entityID; auto scriptRequest = qobject_cast(sender()); - const QString urlString = scriptRequest->getUrl().toString(); if (scriptRequest && scriptRequest->getResult() == ResourceRequest::Success) { + const QString urlString = scriptRequest->getUrl().toString(); auto scriptContents = scriptRequest->getData(); qInfo() << "Downloaded script:" << scriptContents; QScriptProgram program(scriptContents, urlString); @@ -207,6 +275,7 @@ void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) { entitiesObject.setProperty("ADD_FILTER_TYPE", EntityTree::FilterType::Add); entitiesObject.setProperty("EDIT_FILTER_TYPE", EntityTree::FilterType::Edit); entitiesObject.setProperty("PHYSICS_FILTER_TYPE", EntityTree::FilterType::Physics); + entitiesObject.setProperty("DELETE_FILTER_TYPE", EntityTree::FilterType::Delete); global.setProperty("Entities", entitiesObject); filterData.filterFn = global.property("filter"); if (!filterData.filterFn.isFunction()) { @@ -214,8 +283,86 @@ void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) { delete engine; filterData.rejectAll=true; } - - + + // if the wantsToFilterEdit is a boolean evaluate as a boolean, otherwise assume true + QScriptValue wantsToFilterAddValue = filterData.filterFn.property("wantsToFilterAdd"); + filterData.wantsToFilterAdd = wantsToFilterAddValue.isBool() ? wantsToFilterAddValue.toBool() : true; + + // if the wantsToFilterEdit is a boolean evaluate as a boolean, otherwise assume true + QScriptValue wantsToFilterEditValue = filterData.filterFn.property("wantsToFilterEdit"); + filterData.wantsToFilterEdit = wantsToFilterEditValue.isBool() ? wantsToFilterEditValue.toBool() : true; + + // if the wantsToFilterPhysics is a boolean evaluate as a boolean, otherwise assume true + QScriptValue wantsToFilterPhysicsValue = filterData.filterFn.property("wantsToFilterPhysics"); + filterData.wantsToFilterPhysics = wantsToFilterPhysicsValue.isBool() ? wantsToFilterPhysicsValue.toBool() : true; + + // if the wantsToFilterDelete is a boolean evaluate as a boolean, otherwise assume false + QScriptValue wantsToFilterDeleteValue = filterData.filterFn.property("wantsToFilterDelete"); + filterData.wantsToFilterDelete = wantsToFilterDeleteValue.isBool() ? wantsToFilterDeleteValue.toBool() : false; + + // check to see if the filterFn has properties asking for Original props + QScriptValue wantsOriginalPropertiesValue = filterData.filterFn.property("wantsOriginalProperties"); + // if the wantsOriginalProperties is a boolean, or a string, or list of strings, then evaluate as follows: + // - boolean - true - include all original properties + // false - no properties at all + // - string - empty - no properties at all + // any valid property - include just that property in the Original properties + // - list of strings - include only those properties in the Original properties + if (wantsOriginalPropertiesValue.isBool()) { + filterData.wantsOriginalProperties = wantsOriginalPropertiesValue.toBool(); + } else if (wantsOriginalPropertiesValue.isString()) { + auto stringValue = wantsOriginalPropertiesValue.toString(); + filterData.wantsOriginalProperties = !stringValue.isEmpty(); + if (filterData.wantsOriginalProperties) { + EntityPropertyFlagsFromScriptValue(wantsOriginalPropertiesValue, filterData.includedOriginalProperties); + } + } else if (wantsOriginalPropertiesValue.isArray()) { + EntityPropertyFlagsFromScriptValue(wantsOriginalPropertiesValue, filterData.includedOriginalProperties); + filterData.wantsOriginalProperties = !filterData.includedOriginalProperties.isEmpty(); + } + + // check to see if the filterFn has properties asking for Zone props + QScriptValue wantsZonePropertiesValue = filterData.filterFn.property("wantsZoneProperties"); + // if the wantsZoneProperties is a boolean, or a string, or list of strings, then evaluate as follows: + // - boolean - true - include all Zone properties + // false - no properties at all + // - string - empty - no properties at all + // any valid property - include just that property in the Zone properties + // - list of strings - include only those properties in the Zone properties + if (wantsZonePropertiesValue.isBool()) { + filterData.wantsZoneProperties = wantsZonePropertiesValue.toBool(); + filterData.wantsZoneBoundingBox = filterData.wantsZoneProperties; // include this too + } else if (wantsZonePropertiesValue.isString()) { + auto stringValue = wantsZonePropertiesValue.toString(); + filterData.wantsZoneProperties = !stringValue.isEmpty(); + if (filterData.wantsZoneProperties) { + if (stringValue == "boundingBox") { + filterData.wantsZoneBoundingBox = true; + } else { + EntityPropertyFlagsFromScriptValue(wantsZonePropertiesValue, filterData.includedZoneProperties); + } + } + } else if (wantsZonePropertiesValue.isArray()) { + auto length = wantsZonePropertiesValue.property("length").toInteger(); + for (int i = 0; i < length; i++) { + auto stringValue = wantsZonePropertiesValue.property(i).toString(); + if (!stringValue.isEmpty()) { + filterData.wantsZoneProperties = true; + + // boundingBox is a special case since it's not a true EntityPropertyFlag, so we + // need to detect it here. + if (stringValue == "boundingBox") { + filterData.wantsZoneBoundingBox = true; + break; // we can break here, since there are no other special cases + } + + } + } + if (filterData.wantsZoneProperties) { + EntityPropertyFlagsFromScriptValue(wantsZonePropertiesValue, filterData.includedZoneProperties); + } + } + _lock.lockForWrite(); _filterDataMap.insert(entityID, filterData); _lock.unlock(); @@ -227,6 +374,7 @@ void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) { } } } else if (scriptRequest) { + const QString urlString = scriptRequest->getUrl().toString(); qCritical() << "Failed to download script at" << urlString; // See HTTPResourceRequest::onRequestFinished for interpretation of codes. For example, a 404 is code 6 and 403 is 3. A timeout is 2. Go figure. qCritical() << "ResourceRequest error was" << scriptRequest->getResult(); diff --git a/libraries/entities/src/EntityEditFilters.h b/libraries/entities/src/EntityEditFilters.h index 6aeb347603..cb99c97762 100644 --- a/libraries/entities/src/EntityEditFilters.h +++ b/libraries/entities/src/EntityEditFilters.h @@ -28,6 +28,18 @@ class EntityEditFilters : public QObject, public Dependency { public: struct FilterData { QScriptValue filterFn; + bool wantsOriginalProperties { false }; + bool wantsZoneProperties { false }; + + bool wantsToFilterAdd { true }; + bool wantsToFilterEdit { true }; + bool wantsToFilterPhysics { true }; + bool wantsToFilterDelete { true }; + + EntityPropertyFlags includedOriginalProperties; + EntityPropertyFlags includedZoneProperties; + bool wantsZoneBoundingBox { false }; + std::function uncaughtExceptions; QScriptEngine* engine; bool rejectAll; @@ -43,7 +55,7 @@ public: void removeFilter(EntityItemID entityID); bool filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, - EntityTree::FilterType filterType, EntityItemID& entityID); + EntityTree::FilterType filterType, EntityItemID& entityID, EntityItemPointer& existingEntity); signals: void filterAdded(EntityItemID id, bool success); diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h index 51a23b6867..3bbff6cfa6 100644 --- a/libraries/entities/src/EntityItemPropertiesMacros.h +++ b/libraries/entities/src/EntityItemPropertiesMacros.h @@ -105,6 +105,7 @@ inline QScriptValue convertScriptValue(QScriptEngine* e, const glm::vec2& v) { r inline QScriptValue convertScriptValue(QScriptEngine* e, const glm::vec3& v) { return vec3toScriptValue(e, v); } inline QScriptValue convertScriptValue(QScriptEngine* e, float v) { return QScriptValue(v); } inline QScriptValue convertScriptValue(QScriptEngine* e, int v) { return QScriptValue(v); } +inline QScriptValue convertScriptValue(QScriptEngine* e, bool v) { return QScriptValue(v); } inline QScriptValue convertScriptValue(QScriptEngine* e, quint16 v) { return QScriptValue(v); } inline QScriptValue convertScriptValue(QScriptEngine* e, quint32 v) { return QScriptValue(v); } inline QScriptValue convertScriptValue(QScriptEngine* e, quint64 v) { return QScriptValue((qsreal)v); } diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 06a141a95e..2b44b47128 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -101,6 +101,11 @@ bool EntityScriptingInterface::canWriteAssets() { return nodeList->getThisNodeCanWriteAssets(); } +bool EntityScriptingInterface::canReplaceContent() { + auto nodeList = DependencyManager::get(); + return nodeList->getThisNodeCanReplaceContent(); +} + void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) { if (_entityTree) { disconnect(_entityTree.get(), &EntityTree::addingEntity, this, &EntityScriptingInterface::addingEntity); @@ -588,7 +593,10 @@ void EntityScriptingInterface::deleteEntity(QUuid id) { if (entity->getLocked()) { shouldDelete = false; } else { - _entityTree->deleteEntity(entityID); + // only delete local entities, server entities will round trip through the server filters + if (entity->getClientOnly()) { + _entityTree->deleteEntity(entityID); + } } } }); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 919d0e3489..cdb784ffa8 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -144,6 +144,12 @@ public slots: */ Q_INVOKABLE bool canWriteAssets(); + /**jsdoc + * @function Entities.canReplaceContent + * @return {bool} `true` if the DomainServer will allow this Node/Avatar to replace the domain's content set + */ + Q_INVOKABLE bool canReplaceContent(); + /**jsdoc * Add a new entity with the specified properties. If `clientOnly` is true, the entity will * not be sent to the server and will only be visible/accessible on the local client. diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 6afe7cc574..27ec868fcf 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1111,7 +1111,7 @@ bool EntityTree::filterProperties(EntityItemPointer& existingEntity, EntityItemP if (entityEditFilters) { auto position = existingEntity ? existingEntity->getWorldPosition() : propertiesIn.getPosition(); auto entityID = existingEntity ? existingEntity->getEntityItemID() : EntityItemID(); - accepted = entityEditFilters->filter(position, propertiesIn, propertiesOut, wasChanged, filterType, entityID); + accepted = entityEditFilters->filter(position, propertiesIn, propertiesOut, wasChanged, filterType, entityID, existingEntity); } return accepted; @@ -1874,6 +1874,36 @@ void EntityTree::forgetEntitiesDeletedBefore(quint64 sinceTime) { } +bool EntityTree::shouldEraseEntity(EntityItemID entityID, const SharedNodePointer& sourceNode) { + EntityItemPointer existingEntity; + + auto startLookup = usecTimestampNow(); + existingEntity = findEntityByEntityItemID(entityID); + auto endLookup = usecTimestampNow(); + _totalLookupTime += endLookup - startLookup; + + auto startFilter = usecTimestampNow(); + FilterType filterType = FilterType::Delete; + EntityItemProperties dummyProperties; + bool wasChanged = false; + + bool allowed = (sourceNode->isAllowedEditor()) || filterProperties(existingEntity, dummyProperties, dummyProperties, wasChanged, filterType); + auto endFilter = usecTimestampNow(); + + _totalFilterTime += endFilter - startFilter; + + if (allowed) { + if (wantEditLogging() || wantTerseEditLogging()) { + qCDebug(entities) << "User [" << sourceNode->getUUID() << "] deleting entity. ID:" << entityID; + } + } else if (wantEditLogging() || wantTerseEditLogging()) { + qCDebug(entities) << "User [" << sourceNode->getUUID() << "] attempted to deleteentity. ID:" << entityID << " Filter rejected erase."; + } + + return allowed; +} + + // TODO: consider consolidating processEraseMessageDetails() and processEraseMessage() int EntityTree::processEraseMessage(ReceivedMessage& message, const SharedNodePointer& sourceNode) { #ifdef EXTRA_ERASE_DEBUGGING @@ -1901,12 +1931,10 @@ int EntityTree::processEraseMessage(ReceivedMessage& message, const SharedNodePo #endif EntityItemID entityItemID(entityID); - entityItemIDsToDelete << entityItemID; - if (wantEditLogging() || wantTerseEditLogging()) { - qCDebug(entities) << "User [" << sourceNode->getUUID() << "] deleting entity. ID:" << entityItemID; + if (shouldEraseEntity(entityID, sourceNode)) { + entityItemIDsToDelete << entityItemID; } - } deleteEntities(entityItemIDsToDelete, true, true); } @@ -1952,10 +1980,9 @@ int EntityTree::processEraseMessageDetails(const QByteArray& dataByteArray, cons #endif EntityItemID entityItemID(entityID); - entityItemIDsToDelete << entityItemID; - if (wantEditLogging() || wantTerseEditLogging()) { - qCDebug(entities) << "User [" << sourceNode->getUUID() << "] deleting entity. ID:" << entityItemID; + if (shouldEraseEntity(entityID, sourceNode)) { + entityItemIDsToDelete << entityItemID; } } @@ -2251,6 +2278,8 @@ bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer if (! entityDescription.contains("Entities")) { entityDescription["Entities"] = QVariantList(); } + entityDescription["DataVersion"] = _persistDataVersion; + entityDescription["Id"] = _persistID; QScriptEngine scriptEngine; RecurseOctreeToMapOperator theOperator(entityDescription, element, &scriptEngine, skipDefaultValues, skipThoseWithBadParents, _myAvatar); @@ -2263,6 +2292,14 @@ bool EntityTree::readFromMap(QVariantMap& map) { int contentVersion = map["Version"].toInt(); bool needsConversion = (contentVersion < (int)EntityVersion::ZoneLightInheritModes); + if (map.contains("Id")) { + _persistID = map["Id"].toUuid(); + } + + if (map.contains("DataVersion")) { + _persistDataVersion = map["DataVersion"].toInt(); + } + // map will have a top-level list keyed as "Entities". This will be extracted // and iterated over. Each member of this list is converted to a QVariantMap, then // to a QScriptValue, and then to EntityItemProperties. These properties are used diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index f6130430a2..17eb360f86 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -57,7 +57,8 @@ public: enum FilterType { Add, Edit, - Physics + Physics, + Delete }; EntityTree(bool shouldReaverage = false); virtual ~EntityTree(); @@ -193,6 +194,8 @@ public: int processEraseMessage(ReceivedMessage& message, const SharedNodePointer& sourceNode); int processEraseMessageDetails(const QByteArray& buffer, const SharedNodePointer& sourceNode); + bool shouldEraseEntity(EntityItemID entityID, const SharedNodePointer& sourceNode); + EntityTreeElementPointer getContainingElement(const EntityItemID& entityItemID) /*const*/; void addEntityMapEntry(EntityItemPointer entity); diff --git a/libraries/entities/src/EntityTypes.cpp b/libraries/entities/src/EntityTypes.cpp index 307371c649..694542b04e 100644 --- a/libraries/entities/src/EntityTypes.cpp +++ b/libraries/entities/src/EntityTypes.cpp @@ -97,6 +97,7 @@ EntityItemPointer EntityTypes::constructEntityItem(EntityType entityType, const auto mutableProperties = properties; mutableProperties.markAllChanged(); newEntityItem = factory(entityID, mutableProperties); + newEntityItem->moveToThread(qApp->thread()); } return newEntityItem; } diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index 224d19fe96..a609d85fc8 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -242,6 +242,9 @@ public: graphics::MeshPointer _mesh; bool wasCompressed { false }; + + void createMeshTangents(bool generateFromTexCoords); + void createBlendShapeTangents(bool generateTangents); }; class ExtractedMesh { diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 14f12b5d1b..1e59646795 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -408,7 +408,16 @@ static void createTangents(const FBXMesh& mesh, bool generateFromTexCoords, } } -static void createMeshTangents(FBXMesh& mesh, bool generateFromTexCoords) { +static void _createBlendShapeTangents(FBXMesh& mesh, bool generateFromTexCoords, FBXBlendshape& blendShape); + +void FBXMesh::createBlendShapeTangents(bool generateTangents) { + for (auto& blendShape : blendshapes) { + _createBlendShapeTangents(*this, generateTangents, blendShape); + } +} + +void FBXMesh::createMeshTangents(bool generateFromTexCoords) { + FBXMesh& mesh = *this; // This is the only workaround I've found to trick the compiler into understanding that mesh.tangents isn't // const in the lambda function. auto& tangents = mesh.tangents; @@ -421,7 +430,7 @@ static void createMeshTangents(FBXMesh& mesh, bool generateFromTexCoords) { }); } -static void createBlendShapeTangents(FBXMesh& mesh, bool generateFromTexCoords, FBXBlendshape& blendShape) { +static void _createBlendShapeTangents(FBXMesh& mesh, bool generateFromTexCoords, FBXBlendshape& blendShape) { // Create lookup to get index in blend shape from vertex index in mesh std::vector reverseIndices; reverseIndices.resize(mesh.vertices.size()); @@ -1718,10 +1727,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } } - createMeshTangents(extracted.mesh, generateTangents); - for (auto& blendShape : extracted.mesh.blendshapes) { - createBlendShapeTangents(extracted.mesh, generateTangents, blendShape); - } + extracted.mesh.createMeshTangents(generateTangents); + extracted.mesh.createBlendShapeTangents(generateTangents); // find the clusters with which the mesh is associated QVector clusterIDs; @@ -1901,7 +1908,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS geometry.meshes.append(extracted.mesh); int meshIndex = geometry.meshes.size() - 1; if (extracted.mesh._mesh) { - extracted.mesh._mesh->displayName = QString("%1#/mesh/%2").arg(url).arg(meshIndex); + extracted.mesh._mesh->displayName = QString("%1#/mesh/%2").arg(url).arg(meshIndex).toStdString(); + extracted.mesh._mesh->modelName = modelIDsToNames.value(modelID).toStdString(); } meshIDsToMeshIndices.insert(it.key(), meshIndex); } @@ -1977,7 +1985,10 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS auto name = geometry.getModelNameOfMesh(i++); if (!name.isEmpty()) { if (mesh._mesh) { - mesh._mesh->displayName += "#" + name; + mesh._mesh->modelName = name.toStdString(); + if (!mesh._mesh->displayName.size()) { + mesh._mesh->displayName = QString("#%1").arg(name).toStdString(); + } } else { qDebug() << "modelName but no mesh._mesh" << name; } diff --git a/libraries/fbx/src/OBJWriter.cpp b/libraries/fbx/src/OBJWriter.cpp index 4441ae6649..630ac95b9e 100644 --- a/libraries/fbx/src/OBJWriter.cpp +++ b/libraries/fbx/src/OBJWriter.cpp @@ -9,10 +9,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "OBJWriter.h" + #include #include -#include "graphics/Geometry.h" -#include "OBJWriter.h" +#include +#include #include "ModelFormatLogging.h" static QString formatFloat(double n) { @@ -46,59 +48,60 @@ bool writeOBJToTextStream(QTextStream& out, QList meshes) { QList meshNormalStartOffset; int currentVertexStartOffset = 0; int currentNormalStartOffset = 0; + int subMeshIndex = 0; + out << "# OBJWriter::writeOBJToTextStream\n"; // write out vertices (and maybe colors) foreach (const MeshPointer& mesh, meshes) { + out << "# vertices::subMeshIndex " << subMeshIndex++ << "\n"; meshVertexStartOffset.append(currentVertexStartOffset); - const gpu::BufferView& vertexBuffer = mesh->getVertexBuffer(); - const gpu::BufferView& colorsBufferView = mesh->getAttributeBuffer(gpu::Stream::COLOR); - gpu::BufferView::Index numColors = (gpu::BufferView::Index)colorsBufferView.getNumElements(); - gpu::BufferView::Index colorIndex = 0; + auto vertices = buffer_helpers::bufferToVector(mesh->getVertexBuffer(), "mesh.vertices"); + auto colors = buffer_helpers::mesh::attributeToVector(mesh, gpu::Stream::COLOR); - int vertexCount = 0; - gpu::BufferView::Iterator vertexItr = vertexBuffer.cbegin(); - while (vertexItr != vertexBuffer.cend()) { - glm::vec3 v = *vertexItr; + gpu::BufferView::Index numColors = colors.size(); + + int i = 0; + for (const auto& v : vertices) { out << "v "; out << formatFloat(v[0]) << " "; out << formatFloat(v[1]) << " "; out << formatFloat(v[2]); - if (colorIndex < numColors) { - glm::vec3 color = colorsBufferView.get(colorIndex); + if (i < numColors) { + const glm::vec3& color = colors[i]; out << " " << formatFloat(color[0]); out << " " << formatFloat(color[1]); out << " " << formatFloat(color[2]); - colorIndex++; } out << "\n"; - vertexItr++; - vertexCount++; + i++; } - currentVertexStartOffset += vertexCount; + currentVertexStartOffset += i; } out << "\n"; // write out normals bool haveNormals = true; + subMeshIndex = 0; foreach (const MeshPointer& mesh, meshes) { + out << "# normals::subMeshIndex " << subMeshIndex++ << "\n"; meshNormalStartOffset.append(currentNormalStartOffset); - const gpu::BufferView& normalsBufferView = mesh->getAttributeBuffer(gpu::Stream::InputSlot::NORMAL); - gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); - for (gpu::BufferView::Index i = 0; i < numNormals; i++) { - glm::vec3 normal = normalsBufferView.get(i); + auto normals = buffer_helpers::mesh::attributeToVector(mesh, gpu::Stream::NORMAL); + for (const auto& normal : normals) { out << "vn "; out << formatFloat(normal[0]) << " "; out << formatFloat(normal[1]) << " "; out << formatFloat(normal[2]) << "\n"; } - currentNormalStartOffset += numNormals; + currentNormalStartOffset += normals.size(); } out << "\n"; // write out faces int nth = 0; + subMeshIndex = 0; foreach (const MeshPointer& mesh, meshes) { + out << "# faces::subMeshIndex " << subMeshIndex++ << "\n"; currentVertexStartOffset = meshVertexStartOffset.takeFirst(); currentNormalStartOffset = meshNormalStartOffset.takeFirst(); @@ -106,45 +109,46 @@ bool writeOBJToTextStream(QTextStream& out, QList meshes) { const gpu::BufferView& indexBuffer = mesh->getIndexBuffer(); graphics::Index partCount = (graphics::Index)mesh->getNumParts(); + QString name = (!mesh->displayName.size() ? QString("mesh-%1-part").arg(nth) : QString::fromStdString(mesh->displayName)) + .replace(QRegExp("[^-_a-zA-Z0-9]"), "_"); for (int partIndex = 0; partIndex < partCount; partIndex++) { const graphics::Mesh::Part& part = partBuffer.get(partIndex); - out << "g part-" << nth++ << "\n"; - - // graphics::Mesh::TRIANGLES - // TODO -- handle other formats - gpu::BufferView::Iterator indexItr = indexBuffer.cbegin(); - indexItr += part._startIndex; - - int indexCount = 0; - while (indexItr != indexBuffer.cend() && indexCount < part._numIndices) { - uint32_t index0 = *indexItr; - indexItr++; - indexCount++; - if (indexItr == indexBuffer.cend() || indexCount >= part._numIndices) { - qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3"; - break; - } - uint32_t index1 = *indexItr; - indexItr++; - indexCount++; - if (indexItr == indexBuffer.cend() || indexCount >= part._numIndices) { - qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3"; - break; - } - uint32_t index2 = *indexItr; - indexItr++; - indexCount++; + out << QString("g %1-%2-%3\n").arg(subMeshIndex, 3, 10, QChar('0')).arg(name).arg(partIndex); + auto indices = buffer_helpers::bufferToVector(mesh->getIndexBuffer(), "mesh.indices"); + auto face = [&](uint32_t i0, uint32_t i1, uint32_t i2) { out << "f "; if (haveNormals) { - out << currentVertexStartOffset + index0 + 1 << "//" << currentVertexStartOffset + index0 + 1 << " "; - out << currentVertexStartOffset + index1 + 1 << "//" << currentVertexStartOffset + index1 + 1 << " "; - out << currentVertexStartOffset + index2 + 1 << "//" << currentVertexStartOffset + index2 + 1 << "\n"; + out << currentVertexStartOffset + indices[i0] + 1 << "//" << currentVertexStartOffset + indices[i0] + 1 << " "; + out << currentVertexStartOffset + indices[i1] + 1 << "//" << currentVertexStartOffset + indices[i1] + 1 << " "; + out << currentVertexStartOffset + indices[i2] + 1 << "//" << currentVertexStartOffset + indices[i2] + 1 << "\n"; } else { - out << currentVertexStartOffset + index0 + 1 << " "; - out << currentVertexStartOffset + index1 + 1 << " "; - out << currentVertexStartOffset + index2 + 1 << "\n"; + out << currentVertexStartOffset + indices[i0] + 1 << " "; + out << currentVertexStartOffset + indices[i1] + 1 << " "; + out << currentVertexStartOffset + indices[i2] + 1 << "\n"; + } + }; + + uint32_t len = part._startIndex + part._numIndices; + qCDebug(modelformat) << "OBJWriter -- part" << partIndex << "topo" << part._topology << "index elements"; + if (part._topology == graphics::Mesh::TRIANGLES && len % 3 != 0) { + qCDebug(modelformat) << "OBJWriter -- index buffer length isn't a multiple of 3" << len; + } + if (part._topology == graphics::Mesh::QUADS && len % 4 != 0) { + qCDebug(modelformat) << "OBJWriter -- index buffer length isn't a multiple of 4" << len; + } + if (len > indexBuffer.getNumElements()) { + qCDebug(modelformat) << "OBJWriter -- len > index size" << len << indexBuffer.getNumElements(); + } + if (part._topology == graphics::Mesh::QUADS) { + for (uint32_t idx = part._startIndex; idx+3 < len; idx += 4) { + face(idx+0, idx+1, idx+3); + face(idx+1, idx+2, idx+3); + } + } else if (part._topology == graphics::Mesh::TRIANGLES) { + for (uint32_t idx = part._startIndex; idx+2 < len; idx += 3) { + face(idx+0, idx+1, idx+2); } } out << "\n"; diff --git a/libraries/gpu/src/gpu/Stream.cpp b/libraries/gpu/src/gpu/Stream.cpp index caa1ecbc06..f83900be42 100644 --- a/libraries/gpu/src/gpu/Stream.cpp +++ b/libraries/gpu/src/gpu/Stream.cpp @@ -19,7 +19,7 @@ using namespace gpu; using ElementArray = std::array; -const ElementArray& getDefaultElements() { +const ElementArray& Stream::getDefaultElements() { static ElementArray defaultElements{{ //POSITION = 0, Element::VEC3F_XYZ, diff --git a/libraries/gpu/src/gpu/Stream.h b/libraries/gpu/src/gpu/Stream.h index 5562980a91..0def1ab201 100644 --- a/libraries/gpu/src/gpu/Stream.h +++ b/libraries/gpu/src/gpu/Stream.h @@ -23,6 +23,8 @@ namespace gpu { +class Element; + // Stream namespace class class Stream { public: @@ -49,6 +51,8 @@ public: typedef uint8 Slot; + static const std::array& getDefaultElements(); + // Frequency describer enum Frequency { PER_VERTEX = 0, diff --git a/libraries/graphics-scripting/CMakeLists.txt b/libraries/graphics-scripting/CMakeLists.txt new file mode 100644 index 0000000000..ad8055b647 --- /dev/null +++ b/libraries/graphics-scripting/CMakeLists.txt @@ -0,0 +1,4 @@ +set(TARGET_NAME graphics-scripting) +setup_hifi_library() +link_hifi_libraries(shared networking graphics fbx model-networking script-engine) +include_hifi_library_headers(gpu) diff --git a/libraries/graphics-scripting/src/graphics-scripting/Forward.h b/libraries/graphics-scripting/src/graphics-scripting/Forward.h new file mode 100644 index 0000000000..a0e186dda5 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/Forward.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +namespace graphics { + class Mesh; +} +class Model; +using ModelPointer = std::shared_ptr; +namespace gpu { + class BufferView; +} +class QScriptEngine; + +namespace scriptable { + using Mesh = graphics::Mesh; + using MeshPointer = std::shared_ptr; + using WeakMeshPointer = std::weak_ptr; + + class ScriptableModelBase; + using ScriptableModelBasePointer = QPointer; + + class ModelProvider; + using ModelProviderPointer = std::shared_ptr; + using WeakModelProviderPointer = std::weak_ptr; + + class ScriptableMeshBase : public QObject { + Q_OBJECT + public: + WeakModelProviderPointer provider; + ScriptableModelBasePointer model; + WeakMeshPointer weakMesh; + MeshPointer strongMesh; + ScriptableMeshBase(WeakModelProviderPointer provider, ScriptableModelBasePointer model, WeakMeshPointer weakMesh, QObject* parent); + ScriptableMeshBase(WeakMeshPointer weakMesh = WeakMeshPointer(), QObject* parent = nullptr); + ScriptableMeshBase(const ScriptableMeshBase& other, QObject* parent = nullptr) : QObject(parent) { *this = other; } + ScriptableMeshBase& operator=(const ScriptableMeshBase& view); + virtual ~ScriptableMeshBase(); + Q_INVOKABLE const scriptable::MeshPointer getMeshPointer() const { return weakMesh.lock(); } + Q_INVOKABLE const scriptable::ModelProviderPointer getModelProviderPointer() const { return provider.lock(); } + Q_INVOKABLE const scriptable::ScriptableModelBasePointer getModelBasePointer() const { return model; } + }; + + // abstract container for holding one or more references to mesh pointers + class ScriptableModelBase : public QObject { + Q_OBJECT + public: + WeakModelProviderPointer provider; + QUuid objectID; // spatially nestable ID + QVector meshes; + + ScriptableModelBase(QObject* parent = nullptr) : QObject(parent) {} + ScriptableModelBase(const ScriptableModelBase& other) : QObject(other.parent()) { *this = other; } + ScriptableModelBase& operator=(const ScriptableModelBase& other); + virtual ~ScriptableModelBase(); + + void append(const ScriptableMeshBase& mesh); + void append(scriptable::WeakMeshPointer mesh); + // TODO: in future containers for these could go here + // QVariantMap shapes; + // QVariantMap materials; + // QVariantMap armature; + }; + + // mixin class for Avatar/Entity/Overlay Rendering that expose their in-memory graphics::Meshes + class ModelProvider { + public: + NestableType modelProviderType; + virtual scriptable::ScriptableModelBase getScriptableModel() = 0; + virtual bool canReplaceModelMeshPart(int meshIndex, int partIndex) { return false; } + virtual bool replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointer model, int meshIndex, int partIndex) { return false; } + }; + + // mixin class for resolving UUIDs into a corresponding ModelProvider + class ModelProviderFactory : public QObject, public Dependency { + Q_OBJECT + public: + virtual scriptable::ModelProviderPointer lookupModelProvider(const QUuid& uuid) = 0; + signals: + void modelAddedToScene(const QUuid& objectID, NestableType nestableType, const ModelPointer& sender); + void modelRemovedFromScene(const QUuid& objectID, NestableType nestableType, const ModelPointer& sender); + }; + + class ScriptableModel; + using ScriptableModelPointer = QPointer; + class ScriptableMesh; + using ScriptableMeshPointer = QPointer; + class ScriptableMeshPart; + using ScriptableMeshPartPointer = QPointer; +} diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp new file mode 100644 index 0000000000..f3ba5e8406 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp @@ -0,0 +1,367 @@ +// +// GraphicsScriptingInterface.cpp +// libraries/graphics-scripting/src +// +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GraphicsScriptingInterface.h" +#include "GraphicsScriptingUtil.h" +#include "OBJWriter.h" +#include "RegisteredMetaTypes.h" +#include "ScriptEngineLogging.h" +#include "ScriptableMesh.h" +#include "ScriptableMeshPart.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +GraphicsScriptingInterface::GraphicsScriptingInterface(QObject* parent) : QObject(parent), QScriptable() { +} + +void GraphicsScriptingInterface::jsThrowError(const QString& error) { + if (context()) { + context()->throwError(error); + } else { + qCWarning(graphics_scripting) << "GraphicsScriptingInterface::jsThrowError (without valid JS context):" << error; + } +} + +bool GraphicsScriptingInterface::canUpdateModel(QUuid uuid, int meshIndex, int partNumber) { + auto provider = getModelProvider(uuid); + return provider && provider->canReplaceModelMeshPart(meshIndex, partNumber); +} + +bool GraphicsScriptingInterface::updateModel(QUuid uuid, const scriptable::ScriptableModelPointer& model) { + if (!model) { + jsThrowError("null model argument"); + } + + auto base = model->operator scriptable::ScriptableModelBasePointer(); + if (!base) { + jsThrowError("could not get base model pointer"); + return false; + } + + auto provider = getModelProvider(uuid); + if (!provider) { + jsThrowError("provider unavailable"); + return false; + } + + if (!provider->canReplaceModelMeshPart(-1, -1)) { + jsThrowError("provider does not support updating mesh parts"); + return false; + } + +#ifdef SCRIPTABLE_MESH_DEBUG + qDebug() << "replaceScriptableModelMeshPart" << model->toString() << -1 << -1; +#endif + return provider->replaceScriptableModelMeshPart(base, -1, -1); +} + +scriptable::ModelProviderPointer GraphicsScriptingInterface::getModelProvider(QUuid uuid) { + QString error; + if (auto appProvider = DependencyManager::get()) { + if (auto provider = appProvider->lookupModelProvider(uuid)) { + return provider; + } else { + error = "provider unavailable for " + uuid.toString(); + } + } else { + error = "appProvider unavailable"; + } + jsThrowError(error); + return nullptr; +} + +scriptable::ScriptableModelPointer GraphicsScriptingInterface::newModel(const scriptable::ScriptableMeshes& meshes) { + auto modelWrapper = scriptable::make_scriptowned(); + modelWrapper->setObjectName("js::model"); + if (meshes.isEmpty()) { + jsThrowError("expected [meshes] array as first argument"); + } else { + int i = 0; + for (const auto& mesh : meshes) { +#ifdef SCRIPTABLE_MESH_DEBUG + qDebug() << "newModel" << i << meshes.size() << mesh; +#endif + if (mesh) { + modelWrapper->append(*mesh); + } else { + jsThrowError(QString("invalid mesh at index: %1").arg(i)); + break; + } + i++; + } + } + return modelWrapper; +} + +scriptable::ScriptableModelPointer GraphicsScriptingInterface::getModel(QUuid uuid) { + QString error; + bool success; + QString providerType = "unknown"; + if (auto nestable = DependencyManager::get()->find(uuid, success).lock()) { + providerType = SpatiallyNestable::nestableTypeToString(nestable->getNestableType()); + if (auto provider = getModelProvider(uuid)) { + auto modelObject = provider->getScriptableModel(); + const bool found = !modelObject.objectID.isNull(); + if (found && uuid == AVATAR_SELF_ID) { + // special case override so that scripts can rely on matching intput/output UUIDs + modelObject.objectID = AVATAR_SELF_ID; + } + if (modelObject.objectID == uuid) { + if (modelObject.meshes.size()) { + auto modelWrapper = scriptable::make_scriptowned(modelObject); + modelWrapper->setObjectName(providerType+"::"+uuid.toString()+"::model"); + return modelWrapper; + } else { + error = "no meshes available: " + modelObject.objectID.toString(); + } + } else { + error = QString("objectID mismatch: %1 (result contained %2 meshes)").arg(modelObject.objectID.toString()).arg(modelObject.meshes.size()); + } + } else { + error = "model provider unavailable"; + } + } else { + error = "model object not found"; + } + jsThrowError(QString("failed to get meshes from %1 provider for uuid %2 (%3)").arg(providerType).arg(uuid.toString()).arg(error)); + return nullptr; +} + +#ifdef SCRIPTABLE_MESH_TODO +bool GraphicsScriptingInterface::updateMeshPart(scriptable::ScriptableMeshPointer mesh, scriptable::ScriptableMeshPartPointer part) { + Q_ASSERT(mesh); + Q_ASSERT(part); + Q_ASSERT(part->parentMesh); + auto tmp = exportMeshPart(mesh, part->partIndex); + if (part->parentMesh == mesh) { +#ifdef SCRIPTABLE_MESH_DEBUG + qCInfo(graphics_scripting) << "updateMeshPart -- update via clone" << mesh << part; +#endif + tmp->replaceMeshData(part->cloneMeshPart()); + return false; + } else { +#ifdef SCRIPTABLE_MESH_DEBUG + qCInfo(graphics_scripting) << "updateMeshPart -- update via inplace" << mesh << part; +#endif + tmp->replaceMeshData(part); + return true; + } +} +#endif + +scriptable::ScriptableMeshPointer GraphicsScriptingInterface::newMesh(const QVariantMap& ifsMeshData) { + // TODO: this is bare-bones way for now to improvise a new mesh from the scripting side + // in the future we want to support a formal C++ structure data type here instead + QString meshName = ifsMeshData.value("name").toString(); + QString topologyName = ifsMeshData.value("topology").toString(); + QVector indices = buffer_helpers::variantToVector(ifsMeshData.value("indices")); + QVector vertices = buffer_helpers::variantToVector(ifsMeshData.value("positions")); + QVector normals = buffer_helpers::variantToVector(ifsMeshData.value("normals")); + QVector colors = buffer_helpers::variantToVector(ifsMeshData.value("colors")); + QVector texCoords0 = buffer_helpers::variantToVector(ifsMeshData.value("texCoords0")); + + const auto numVertices = vertices.size(); + const auto numIndices = indices.size(); + const auto topology = graphics::TOPOLOGIES.key(topologyName); + + // TODO: support additional topologies (POINTS and LINES ought to "just work" -- + // if MeshPartPayload::drawCall is updated to actually check the Mesh::Part::_topology value + // (TRIANGLE_STRIP, TRIANGLE_FAN, QUADS, QUAD_STRIP may need additional conversion code though) + static const QStringList acceptableTopologies{ "triangles" }; + + // sanity checks + QString error; + if (!topologyName.isEmpty() && !acceptableTopologies.contains(topologyName)) { + error = QString("expected .topology to be %1").arg(acceptableTopologies.join(" | ")); + } else if (!numIndices) { + error = QString("expected non-empty [uint32,...] array for .indices (got type=%1)").arg(ifsMeshData.value("indices").typeName()); + } else if (numIndices % 3 != 0) { + error = QString("expected 'triangle faces' for .indices (ie: length to be divisible by 3) length=%1").arg(numIndices); + } else if (!numVertices) { + error = "expected non-empty [glm::vec3(),...] array for .positions"; + } else { + const gpu::uint32 maxVertexIndex = numVertices; + int i = 0; + for (const auto& ind : indices) { + if (ind >= maxVertexIndex) { + error = QString("index out of .indices[%1] index=%2 >= maxVertexIndex=%3").arg(i).arg(ind).arg(maxVertexIndex); + break; + } + i++; + } + } + if (!error.isEmpty()) { + jsThrowError(error); + return nullptr; + } + + if (ifsMeshData.contains("normals") && normals.size() < numVertices) { + qCInfo(graphics_scripting) << "newMesh -- expanding .normals to #" << numVertices; + normals.resize(numVertices); + } + if (ifsMeshData.contains("colors") && colors.size() < numVertices) { + qCInfo(graphics_scripting) << "newMesh -- expanding .colors to #" << numVertices; + colors.resize(numVertices); + } + if (ifsMeshData.contains("texCoords0") && texCoords0.size() < numVertices) { + qCInfo(graphics_scripting) << "newMesh -- expanding .texCoords0 to #" << numVertices; + texCoords0.resize(numVertices); + } + if (ifsMeshData.contains("texCoords1")) { + qCWarning(graphics_scripting) << "newMesh - texCoords1 not yet supported; ignoring"; + } + + graphics::MeshPointer mesh(new graphics::Mesh()); + mesh->modelName = "graphics::newMesh"; + mesh->displayName = meshName.toStdString(); + + // TODO: newFromVector does inbound type conversion, but not compression or packing + // (later we should autodetect if fitting into gpu::INDEX_UINT16 and reduce / pack normals etc.) + mesh->setIndexBuffer(buffer_helpers::newFromVector(indices, gpu::Format::INDEX_INT32)); + mesh->setVertexBuffer(buffer_helpers::newFromVector(vertices, gpu::Format::VEC3F_XYZ)); + if (normals.size()) { + mesh->addAttribute(gpu::Stream::NORMAL, buffer_helpers::newFromVector(normals, gpu::Format::VEC3F_XYZ)); + } + if (colors.size()) { + mesh->addAttribute(gpu::Stream::COLOR, buffer_helpers::newFromVector(colors, gpu::Format::VEC3F_XYZ)); + } + if (texCoords0.size()) { + mesh->addAttribute(gpu::Stream::TEXCOORD0, buffer_helpers::newFromVector(texCoords0, gpu::Format::VEC2F_UV)); + } + QVector parts = {{ 0, indices.size(), 0, topology }}; + mesh->setPartBuffer(buffer_helpers::newFromVector(parts, gpu::Element::PART_DRAWCALL)); + return scriptable::make_scriptowned(mesh, nullptr); +} + +QString GraphicsScriptingInterface::exportModelToOBJ(const scriptable::ScriptableModel& _in) { + const auto& in = _in.getConstMeshes(); + if (in.size()) { + QList meshes; + foreach (auto meshProxy, in) { + if (meshProxy) { + meshes.append(getMeshPointer(meshProxy)); + } + } + if (meshes.size()) { + return writeOBJToString(meshes); + } + } + jsThrowError("null mesh"); + return QString(); +} + +MeshPointer GraphicsScriptingInterface::getMeshPointer(const scriptable::ScriptableMesh& meshProxy) { + return meshProxy.getMeshPointer(); +} +MeshPointer GraphicsScriptingInterface::getMeshPointer(scriptable::ScriptableMesh& meshProxy) { + return getMeshPointer(&meshProxy); +} +MeshPointer GraphicsScriptingInterface::getMeshPointer(scriptable::ScriptableMeshPointer meshProxy) { + MeshPointer result; + if (!meshProxy) { + jsThrowError("expected meshProxy as first parameter"); + return result; + } + auto mesh = meshProxy->getMeshPointer(); + if (!mesh) { + jsThrowError("expected valid meshProxy as first parameter"); + return result; + } + return mesh; +} + +namespace { + QVector metaTypeIds{ + qRegisterMetaType("uint32"), + qRegisterMetaType("glm::uint32"), + qRegisterMetaType>(), + qRegisterMetaType>("QVector"), + qRegisterMetaType(), + qRegisterMetaType("ScriptableMeshes"), + qRegisterMetaType("scriptable::ScriptableMeshes"), + qRegisterMetaType>("QVector"), + qRegisterMetaType(), + qRegisterMetaType(), + qRegisterMetaType(), + qRegisterMetaType(), + }; +} + +namespace scriptable { + template int registerQPointerMetaType(QScriptEngine* engine) { + qScriptRegisterSequenceMetaType>>(engine); + return qScriptRegisterMetaType>( + engine, + [](QScriptEngine* engine, const QPointer& object) -> QScriptValue { + if (!object) { + return QScriptValue::NullValue; + } + return engine->newQObject(object, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::AutoCreateDynamicProperties); + }, + [](const QScriptValue& value, QPointer& out) { + auto obj = value.toQObject(); +#ifdef SCRIPTABLE_MESH_DEBUG + qCInfo(graphics_scripting) << "qpointer_qobject_cast" << obj << value.toString(); +#endif + if (auto tmp = qobject_cast(obj)) { + out = QPointer(tmp); + return; + } +#if 0 + if (auto tmp = static_cast(obj)) { +#ifdef SCRIPTABLE_MESH_DEBUG + qCInfo(graphics_scripting) << "qpointer_qobject_cast -- via static_cast" << obj << tmp << value.toString(); +#endif + out = QPointer(tmp); + return; + } +#endif + out = nullptr; + } + ); + } + + template int registerDebugEnum(QScriptEngine* engine, const DebugEnums& debugEnums) { + static const DebugEnums& instance = debugEnums; + return qScriptRegisterMetaType( + engine, + [](QScriptEngine* engine, const T& topology) -> QScriptValue { + return instance.value(topology); + }, + [](const QScriptValue& value, T& topology) { + topology = instance.key(value.toString()); + } + ); + } +} + +void GraphicsScriptingInterface::registerMetaTypes(QScriptEngine* engine) { + qScriptRegisterSequenceMetaType>(engine); + + scriptable::registerQPointerMetaType(engine); + scriptable::registerQPointerMetaType(engine); + scriptable::registerQPointerMetaType(engine); + + scriptable::registerDebugEnum(engine, graphics::TOPOLOGIES); + scriptable::registerDebugEnum(engine, gpu::TYPES); + scriptable::registerDebugEnum(engine, gpu::SEMANTICS); + scriptable::registerDebugEnum(engine, gpu::DIMENSIONS); + + Q_UNUSED(metaTypeIds); +} + +#include "GraphicsScriptingInterface.moc" diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h new file mode 100644 index 0000000000..84c6cb6fa8 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h @@ -0,0 +1,92 @@ +// +// GraphicsScriptingInterface.h +// libraries/graphics-scripting/src +// +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_GraphicsScriptingInterface_h +#define hifi_GraphicsScriptingInterface_h + +#include +#include + +#include +#include + +#include "ScriptableMesh.h" +#include + + +/**jsdoc + * The experimental Graphics API (experimental) lets you query and manage certain graphics-related structures (like underlying meshes and textures) from scripting. + * @namespace Graphics + */ + +class GraphicsScriptingInterface : public QObject, public QScriptable, public Dependency { + Q_OBJECT + +public: + static void registerMetaTypes(QScriptEngine* engine); + GraphicsScriptingInterface(QObject* parent = nullptr); + +public slots: + /**jsdoc + * Returns a model reference object associated with the specified UUID ({@link EntityID}, {@link OverlayID}, or {@link AvatarID}). + * + * @function Graphics.getModel + * @param {UUID} The objectID of the model whose meshes are to be retrieved. + * @return {Graphics.Model} the resulting Model object + */ + scriptable::ScriptableModelPointer getModel(QUuid uuid); + + bool updateModel(QUuid uuid, const scriptable::ScriptableModelPointer& model); + + bool canUpdateModel(QUuid uuid, int meshIndex = -1, int partNumber = -1); + + scriptable::ScriptableModelPointer newModel(const scriptable::ScriptableMeshes& meshes); + + /**jsdoc + * Create a new Mesh / Mesh Part with the specified data buffers. + * + * @function Graphics.newMesh + * @param {Graphics.IFSData} ifsMeshData Index-Faced Set (IFS) arrays used to create the new mesh. + * @return {Graphics.Mesh} the resulting Mesh / Mesh Part object + */ + /**jsdoc + * @typedef {object} Graphics.IFSData + * @property {string} [name] - mesh name (useful for debugging / debug prints). + * @property {number[]} indices - vertex indices to use for the mesh faces. + * @property {Vec3[]} vertices - vertex positions (model space) + * @property {Vec3[]} [normals] - vertex normals (normalized) + * @property {Vec3[]} [colors] - vertex colors (normalized) + * @property {Vec2[]} [texCoords0] - vertex texture coordinates (normalized) + */ + scriptable::ScriptableMeshPointer newMesh(const QVariantMap& ifsMeshData); + +#ifdef SCRIPTABLE_MESH_TODO + scriptable::ScriptableMeshPartPointer exportMeshPart(scriptable::ScriptableMeshPointer mesh, int partNumber = -1) { + return scriptable::make_scriptowned(mesh, part); + } + bool updateMeshPart(scriptable::ScriptableMeshPointer mesh, scriptable::ScriptableMeshPartPointer part); +#endif + + QString exportModelToOBJ(const scriptable::ScriptableModel& in); + +private: + scriptable::ModelProviderPointer getModelProvider(QUuid uuid); + void jsThrowError(const QString& error); + scriptable::MeshPointer getMeshPointer(scriptable::ScriptableMeshPointer meshProxy); + scriptable::MeshPointer getMeshPointer(scriptable::ScriptableMesh& meshProxy); + scriptable::MeshPointer getMeshPointer(const scriptable::ScriptableMesh& meshProxy); + +}; + +Q_DECLARE_METATYPE(glm::uint32) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(NestableType) + +#endif // hifi_GraphicsScriptingInterface_h diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.cpp new file mode 100644 index 0000000000..da582b2d21 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.cpp @@ -0,0 +1,114 @@ +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GraphicsScriptingUtil.h" + +#include + +#include +#include +#include + +using buffer_helpers::glmVecToVariant; + +Q_LOGGING_CATEGORY(graphics_scripting, "hifi.scripting.graphics") + +namespace scriptable { + +QVariant toVariant(const glm::mat4& mat4) { + QVector floats; + floats.resize(16); + memcpy(floats.data(), &mat4, sizeof(glm::mat4)); + QVariant v; + v.setValue>(floats); + return v; +}; + +QVariant toVariant(const Extents& box) { + return QVariantMap{ + { "center", glmVecToVariant(box.minimum + (box.size() / 2.0f)) }, + { "minimum", glmVecToVariant(box.minimum) }, + { "maximum", glmVecToVariant(box.maximum) }, + { "dimensions", glmVecToVariant(box.size()) }, + }; +} + +QVariant toVariant(const AABox& box) { + return QVariantMap{ + { "brn", glmVecToVariant(box.getCorner()) }, + { "tfl", glmVecToVariant(box.calcTopFarLeft()) }, + { "center", glmVecToVariant(box.calcCenter()) }, + { "minimum", glmVecToVariant(box.getMinimumPoint()) }, + { "maximum", glmVecToVariant(box.getMaximumPoint()) }, + { "dimensions", glmVecToVariant(box.getDimensions()) }, + }; +} + +QVariant toVariant(const gpu::Element& element) { + return QVariantMap{ + { "type", gpu::toString(element.getType()) }, + { "semantic", gpu::toString(element.getSemantic()) }, + { "dimension", gpu::toString(element.getDimension()) }, + { "scalarCount", element.getScalarCount() }, + { "byteSize", element.getSize() }, + { "BYTES_PER_ELEMENT", element.getSize() / element.getScalarCount() }, + }; +} + +QScriptValue jsBindCallback(QScriptValue value) { + if (value.isObject() && value.property("callback").isFunction()) { + // value is already a bound callback + return value; + } + auto engine = value.engine(); + auto context = engine ? engine->currentContext() : nullptr; + auto length = context ? context->argumentCount() : 0; + QScriptValue scope = context ? context->thisObject() : QScriptValue::NullValue; + QScriptValue method; +#ifdef SCRIPTABLE_MESH_DEBUG + qCInfo(graphics_scripting) << "jsBindCallback" << engine << length << scope.toQObject() << method.toString(); +#endif + + // find position in the incoming JS Function.arguments array (so we can test for the two-argument case) + for (int i = 0; context && i < length; i++) { + if (context->argument(i).strictlyEquals(value)) { + method = context->argument(i+1); + } + } + if (method.isFunction() || method.isString()) { + // interpret as `API.func(..., scope, function callback(){})` or `API.func(..., scope, "methodName")` + scope = value; + } else { + // interpret as `API.func(..., function callback(){})` + method = value; + } +#ifdef SCRIPTABLE_MESH_DEBUG + qCInfo(graphics_scripting) << "scope:" << scope.toQObject() << "method:" << method.toString(); +#endif + return ::makeScopedHandlerObject(scope, method); +} + +template +T this_qobject_cast(QScriptEngine* engine) { + auto context = engine ? engine->currentContext() : nullptr; + return qscriptvalue_cast(context ? context->thisObject() : QScriptValue::NullValue); +} +QString toDebugString(QObject* tmp) { + QString s; + QTextStream out(&s); + out << tmp; + return s; + // return QString("%0 (0x%1%2)") + // .arg(tmp ? tmp->metaObject()->className() : "QObject") + // .arg(qulonglong(tmp), 16, 16, QChar('0')) + // .arg(tmp && tmp->objectName().size() ? " name=" + tmp->objectName() : ""); +} +template QString toDebugString(std::shared_ptr tmp) { + return toDebugString(qobject_cast(tmp.get())); +} + +} diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.h b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.h new file mode 100644 index 0000000000..1ca62277ff --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class Extents; +class AABox; +namespace gpu { + class Element; +} +Q_DECLARE_LOGGING_CATEGORY(graphics_scripting) + +namespace scriptable { + QVariant toVariant(const Extents& box); + QVariant toVariant(const AABox& box); + QVariant toVariant(const gpu::Element& element); + QVariant toVariant(const glm::mat4& mat4); + + // helper that automatically resolves Qt-signal-like scoped callbacks + // ... C++ side: `void MyClass::asyncMethod(..., QScriptValue callback)` + // ... JS side: + // * `API.asyncMethod(..., function(){})` + // * `API.asyncMethod(..., scope, function(){})` + // * `API.asyncMethod(..., scope, "methodName")` + QScriptValue jsBindCallback(QScriptValue callback); + + // cast engine->thisObject() => C++ class instance + template T this_qobject_cast(QScriptEngine* engine); + + QString toDebugString(QObject* tmp); + template QString toDebugString(std::shared_ptr tmp); + + // Helper for creating C++ > ScriptOwned JS instances + // (NOTE: this also helps track in the code where we need to update later if switching to + // std::shared_ptr's -- something currently non-trivial given mixed JS/C++ object ownership) + template inline QPointer make_scriptowned(Rest... rest) { + auto instance = QPointer(new T(rest...)); + Q_ASSERT(instance && instance->parent()); + return instance; + } +} diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp new file mode 100644 index 0000000000..bc31d45229 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp @@ -0,0 +1,405 @@ +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Forward.h" + +#include "ScriptableMesh.h" +#include "ScriptableMeshPart.h" + +#include "GraphicsScriptingUtil.h" +#include "OBJWriter.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// #define SCRIPTABLE_MESH_DEBUG 1 + +scriptable::ScriptableMesh::ScriptableMesh(const ScriptableMeshBase& other) + : ScriptableMeshBase(other), QScriptable() { + auto mesh = getMeshPointer(); + QString name = mesh ? QString::fromStdString(mesh->modelName) : ""; + if (name.isEmpty()) { + name = mesh ? QString::fromStdString(mesh->displayName) : ""; + } + auto parentModel = getParentModel(); + setObjectName(QString("%1#%2").arg(parentModel ? parentModel->objectName() : "").arg(name)); +} + +QVector scriptable::ScriptableMesh::getMeshParts() const { + QVector out; + for (glm::uint32 i = 0; i < getNumParts(); i++) { + out << scriptable::make_scriptowned(getSelf(), i); + } + return out; +} + +glm::uint32 scriptable::ScriptableMesh::getNumIndices() const { + if (auto mesh = getMeshPointer()) { + return (glm::uint32)mesh->getNumIndices(); + } + return 0; +} + +glm::uint32 scriptable::ScriptableMesh::getNumVertices() const { + if (auto mesh = getMeshPointer()) { + return (glm::uint32)mesh->getNumVertices(); + } + return 0; +} + +QVector scriptable::ScriptableMesh::findNearbyVertexIndices(const glm::vec3& origin, float epsilon) const { + QVector result; + if (!isValid()) { + return result; + } + const auto epsilon2 = epsilon*epsilon; + buffer_helpers::forEach(buffer_helpers::mesh::getBufferView(getMeshPointer(), gpu::Stream::POSITION), [&](glm::uint32 index, const glm::vec3& position) { + if (glm::length2(position - origin) <= epsilon2) { + result << index; + } + return true; + }); + return result; +} + +QVector scriptable::ScriptableMesh::getIndices() const { + if (auto mesh = getMeshPointer()) { +#ifdef SCRIPTABLE_MESH_DEBUG + qCDebug(graphics_scripting, "getIndices mesh %p", mesh.get()); +#endif + return buffer_helpers::bufferToVector(mesh->getIndexBuffer()); + } + return QVector(); +} + + +glm::uint32 scriptable::ScriptableMesh::getNumAttributes() const { + if (auto mesh = getMeshPointer()) { + return (glm::uint32)mesh->getNumAttributes() + 1; + } + return 0; +} +QVector scriptable::ScriptableMesh::getAttributeNames() const { + QVector result; + if (auto mesh = getMeshPointer()) { + for (const auto& a : buffer_helpers::ATTRIBUTES.toStdMap()) { + auto bufferView = buffer_helpers::mesh::getBufferView(mesh, a.second); + if (bufferView.getNumElements() > 0) { + result << a.first; + } + } + } + return result; +} + +QVariantMap scriptable::ScriptableMesh::getVertexAttributes(glm::uint32 vertexIndex) const { + if (!isValidIndex(vertexIndex)) { + return QVariantMap(); + } + return buffer_helpers::mesh::getVertexAttributes(getMeshPointer(), vertexIndex).toMap(); +} + +bool scriptable::ScriptableMesh::setVertexAttributes(glm::uint32 vertexIndex, const QVariantMap& attributes) { + for (const auto& name : attributes.keys()) { + if (!isValidIndex(vertexIndex, name)) { + return false; + } + } + return buffer_helpers::mesh::setVertexAttributes(getMeshPointer(), vertexIndex, attributes); +} + +int scriptable::ScriptableMesh::getSlotNumber(const QString& attributeName) const { + if (auto mesh = getMeshPointer()) { + return buffer_helpers::ATTRIBUTES.value(attributeName, -1); + } + return -1; +} + +QVariantMap scriptable::ScriptableMesh::getBufferFormats() const { + QVariantMap result; + for (const auto& a : buffer_helpers::ATTRIBUTES.toStdMap()) { + auto bufferView = buffer_helpers::mesh::getBufferView(getMeshPointer(), a.second); + result[a.first] = QVariantMap{ + { "slot", a.second }, + { "length", (glm::uint32)bufferView.getNumElements() }, + { "byteLength", (glm::uint32)bufferView._size }, + { "offset", (glm::uint32) bufferView._offset }, + { "stride", (glm::uint32)bufferView._stride }, + { "element", scriptable::toVariant(bufferView._element) }, + }; + } + return result; +} + +bool scriptable::ScriptableMesh::removeAttribute(const QString& attributeName) { + auto slot = isValid() ? getSlotNumber(attributeName) : -1; + if (slot < 0) { + return false; + } + if (slot == gpu::Stream::POSITION) { + context()->throwError("cannot remove .position attribute"); + return false; + } + if (buffer_helpers::mesh::getBufferView(getMeshPointer(), slot).getNumElements()) { + getMeshPointer()->removeAttribute(slot); + return true; + } + return false; +} + +glm::uint32 scriptable::ScriptableMesh::addAttribute(const QString& attributeName, const QVariant& defaultValue) { + auto slot = isValid() ? getSlotNumber(attributeName) : -1; + if (slot < 0) { + return 0; + } + auto mesh = getMeshPointer(); + auto numVertices = getNumVertices(); + if (!getAttributeNames().contains(attributeName)) { + QVector values; + values.fill(defaultValue, numVertices); + mesh->addAttribute(slot, buffer_helpers::newFromVector(values, gpu::Stream::getDefaultElements()[slot])); + return values.size(); + } else { + auto bufferView = buffer_helpers::mesh::getBufferView(mesh, slot); + auto current = (glm::uint32)bufferView.getNumElements(); + if (current < numVertices) { + bufferView = buffer_helpers::resized(bufferView, numVertices); + for (glm::uint32 i = current; i < numVertices; i++) { + buffer_helpers::setValue(bufferView, i, defaultValue); + } + return numVertices - current; + } else if (current > numVertices) { + qCDebug(graphics_scripting) << QString("current=%1 > numVertices=%2").arg(current).arg(numVertices); + return 0; + } + } + return 0; +} + +glm::uint32 scriptable::ScriptableMesh::fillAttribute(const QString& attributeName, const QVariant& value) { + auto slot = isValid() ? getSlotNumber(attributeName) : -1; + if (slot < 0) { + return 0; + } + auto mesh = getMeshPointer(); + auto numVertices = getNumVertices(); + QVector values; + values.fill(value, numVertices); + mesh->addAttribute(slot, buffer_helpers::newFromVector(values, gpu::Stream::getDefaultElements()[slot])); + return true; +} + +QVariantMap scriptable::ScriptableMesh::getMeshExtents() const { + auto mesh = getMeshPointer(); + auto box = mesh ? mesh->evalPartsBound(0, (int)mesh->getNumParts()) : AABox(); + return scriptable::toVariant(box).toMap(); +} + +glm::uint32 scriptable::ScriptableMesh::getNumParts() const { + if (auto mesh = getMeshPointer()) { + return (glm::uint32)mesh->getNumParts(); + } + return 0; +} + +QVariantList scriptable::ScriptableMesh::queryVertexAttributes(QVariant selector) const { + QVariantList result; + const auto& attributeName = selector.toString(); + if (!isValidIndex(0, attributeName)) { + return result; + } + auto slotNum = getSlotNumber(attributeName); + const auto& bufferView = buffer_helpers::mesh::getBufferView(getMeshPointer(), static_cast(slotNum)); + glm::uint32 numElements = (glm::uint32)bufferView.getNumElements(); + for (glm::uint32 i = 0; i < numElements; i++) { + result << buffer_helpers::getValue(bufferView, i, qUtf8Printable(attributeName)); + } + return result; +} + +QVariant scriptable::ScriptableMesh::getVertexProperty(glm::uint32 vertexIndex, const QString& attributeName) const { + if (!isValidIndex(vertexIndex, attributeName)) { + return QVariant(); + } + auto slotNum = getSlotNumber(attributeName); + const auto& bufferView = buffer_helpers::mesh::getBufferView(getMeshPointer(), static_cast(slotNum)); + return buffer_helpers::getValue(bufferView, vertexIndex, qUtf8Printable(attributeName)); +} + +bool scriptable::ScriptableMesh::setVertexProperty(glm::uint32 vertexIndex, const QString& attributeName, const QVariant& value) { + if (!isValidIndex(vertexIndex, attributeName)) { + return false; + } + auto slotNum = getSlotNumber(attributeName); + const auto& bufferView = buffer_helpers::mesh::getBufferView(getMeshPointer(), static_cast(slotNum)); + return buffer_helpers::setValue(bufferView, vertexIndex, value); +} + +glm::uint32 scriptable::ScriptableMesh::forEachVertex(QScriptValue _callback) { + auto mesh = getMeshPointer(); + if (!mesh) { + return 0; + } + auto scopedHandler = jsBindCallback(_callback); + + // destructure so we can still invoke callback scoped, but with a custom signature (obj, i, jsMesh) + auto scope = scopedHandler.property("scope"); + auto callback = scopedHandler.property("callback"); + auto js = engine() ? engine() : scopedHandler.engine(); // cache value to avoid resolving each iteration + if (!js) { + return 0; + } + auto meshPart = js ? js->toScriptValue(getSelf()) : QScriptValue::NullValue; + int numProcessed = 0; + buffer_helpers::mesh::forEachVertex(mesh, [&](glm::uint32 index, const QVariantMap& values) { + auto result = callback.call(scope, { js->toScriptValue(values), index, meshPart }); + if (js->hasUncaughtException()) { + js->currentContext()->throwValue(js->uncaughtException()); + return false; + } + numProcessed++; + return true; + }); + return numProcessed; +} + + +glm::uint32 scriptable::ScriptableMesh::updateVertexAttributes(QScriptValue _callback) { + auto mesh = getMeshPointer(); + if (!mesh) { + return 0; + } + auto scopedHandler = jsBindCallback(_callback); + + // destructure so we can still invoke callback scoped, but with a custom signature (obj, i, jsMesh) + auto scope = scopedHandler.property("scope"); + auto callback = scopedHandler.property("callback"); + auto js = engine() ? engine() : scopedHandler.engine(); // cache value to avoid resolving each iteration + if (!js) { + return 0; + } + auto meshPart = js ? js->toScriptValue(getSelf()) : QScriptValue::NullValue; + int numProcessed = 0; + auto attributeViews = buffer_helpers::mesh::getAllBufferViews(mesh); + buffer_helpers::mesh::forEachVertex(mesh, [&](glm::uint32 index, const QVariantMap& values) { + auto obj = js->toScriptValue(values); + auto result = callback.call(scope, { obj, index, meshPart }); + if (js->hasUncaughtException()) { + js->currentContext()->throwValue(js->uncaughtException()); + return false; + } + if (result.isBool() && !result.toBool()) { + // bail without modifying data if user explicitly returns false + return true; + } + if (result.isObject() && !result.strictlyEquals(obj)) { + // user returned a new object (ie: instead of modifying input properties) + obj = result; + } + for (const auto& a : attributeViews) { + const auto& attribute = obj.property(a.first); + if (attribute.isValid()) { + buffer_helpers::setValue(a.second, index, attribute.toVariant()); + } + } + numProcessed++; + return true; + }); + return numProcessed; +} + +// protect against user scripts sending bogus values +bool scriptable::ScriptableMesh::isValidIndex(glm::uint32 vertexIndex, const QString& attributeName) const { + if (!isValid()) { + return false; + } + const auto last = getNumVertices() - 1; + if (vertexIndex > last) { + if (context()) { + context()->throwError(QString("vertexIndex=%1 out of range (firstVertexIndex=%2, lastVertexIndex=%3)").arg(vertexIndex).arg(0).arg(last)); + } + return false; + } + if (!attributeName.isEmpty()) { + auto slotNum = getSlotNumber(attributeName); + if (slotNum < 0) { + if (context()) { + context()->throwError(QString("invalid attributeName=%1").arg(attributeName)); + } + return false; + } + auto view = buffer_helpers::mesh::getBufferView(getMeshPointer(), static_cast(slotNum)); + if (vertexIndex >= (glm::uint32)view.getNumElements()) { + if (context()) { + context()->throwError(QString("vertexIndex=%1 out of range (attribute=%2, numElements=%3)").arg(vertexIndex).arg(attributeName).arg(view.getNumElements())); + } + return false; + } + } + return true; +} + + +scriptable::ScriptableMeshPointer scriptable::ScriptableMesh::cloneMesh() { + auto mesh = getMeshPointer(); + if (!mesh) { + qCInfo(graphics_scripting) << "ScriptableMesh::cloneMesh -- !meshPointer"; + return nullptr; + } + auto clone = buffer_helpers::mesh::clone(mesh); + + auto meshPointer = scriptable::make_scriptowned(provider, model, clone, nullptr); + return scriptable::ScriptableMeshPointer(meshPointer); +} + +// note: we don't always want the JS side to prevent mesh data from being freed (hence weak pointers unless parented QObject) +scriptable::ScriptableMeshBase::ScriptableMeshBase( + scriptable::WeakModelProviderPointer provider, scriptable::ScriptableModelBasePointer model, scriptable::WeakMeshPointer weakMesh, QObject* parent + ) : QObject(parent), provider(provider), model(model), weakMesh(weakMesh) { + if (parent) { +#ifdef SCRIPTABLE_MESH_DEBUG + qCDebug(graphics_scripting) << "ScriptableMeshBase -- have parent QObject, creating strong neshref" << weakMesh.lock().get() << parent; +#endif + strongMesh = weakMesh.lock(); + } +} + +scriptable::ScriptableMeshBase::ScriptableMeshBase(scriptable::WeakMeshPointer weakMesh, QObject* parent) : + scriptable::ScriptableMeshBase(scriptable::WeakModelProviderPointer(), nullptr, weakMesh, parent) { +} + +scriptable::ScriptableMeshBase& scriptable::ScriptableMeshBase::operator=(const scriptable::ScriptableMeshBase& view) { + provider = view.provider; + model = view.model; + weakMesh = view.weakMesh; + strongMesh = view.strongMesh; + return *this; +} + +scriptable::ScriptableMeshBase::~ScriptableMeshBase() { +#ifdef SCRIPTABLE_MESH_DEBUG + qCInfo(graphics_scripting) << "//~ScriptableMeshBase" << this << "strongMesh:" << strongMesh.use_count() << "weakMesh:" << weakMesh.use_count(); +#endif + strongMesh.reset(); +} + +scriptable::ScriptableMesh::~ScriptableMesh() { +#ifdef SCRIPTABLE_MESH_DEBUG + qCInfo(graphics_scripting) << "//~ScriptableMesh" << this << "strongMesh:" << strongMesh.use_count() << "weakMesh:" << weakMesh.use_count(); +#endif + strongMesh.reset(); +} + + +#include "ScriptableMesh.moc" + diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h new file mode 100644 index 0000000000..62a67aa5e6 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h @@ -0,0 +1,107 @@ +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include "ScriptableModel.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "GraphicsScriptingUtil.h" + +#include + +namespace scriptable { + /**jsdoc + * @typedef {object} Graphics.Mesh + * @property {Graphics.MeshPart[]} parts - Array of submesh part references. + * @property {string[]} attributeNames - Vertex attribute names (color, normal, etc.) + * @property {number} numParts - The number of parts contained in the mesh. + * @property {number} numIndices - Total number of vertex indices in the mesh. + * @property {number} numVertices - Total number of vertices in the Mesh. + * @property {number} numAttributes - Number of currently defined vertex attributes. + */ + class ScriptableMesh : public ScriptableMeshBase, QScriptable { + Q_OBJECT + public: + Q_PROPERTY(glm::uint32 numParts READ getNumParts) + Q_PROPERTY(glm::uint32 numAttributes READ getNumAttributes) + Q_PROPERTY(glm::uint32 numVertices READ getNumVertices) + Q_PROPERTY(glm::uint32 numIndices READ getNumIndices) + Q_PROPERTY(QVector attributeNames READ getAttributeNames) + Q_PROPERTY(QVector parts READ getMeshParts) + Q_PROPERTY(bool valid READ isValid) + Q_PROPERTY(bool strong READ hasValidStrongMesh) + Q_PROPERTY(QVariantMap extents READ getMeshExtents) + Q_PROPERTY(QVariantMap bufferFormats READ getBufferFormats) + QVariantMap getBufferFormats() const; + + operator const ScriptableMeshBase*() const { return (qobject_cast(this)); } + + ScriptableMesh(WeakModelProviderPointer provider, ScriptableModelBasePointer model, MeshPointer mesh, QObject* parent) + : ScriptableMeshBase(provider, model, mesh, parent), QScriptable() { strongMesh = mesh; } + ScriptableMesh(MeshPointer mesh, QObject* parent) + : ScriptableMeshBase(WeakModelProviderPointer(), nullptr, mesh, parent), QScriptable() { strongMesh = mesh; } + ScriptableMesh(const ScriptableMeshBase& other); + ScriptableMesh(const ScriptableMesh& other) : ScriptableMeshBase(other), QScriptable() {}; + virtual ~ScriptableMesh(); + + const scriptable::MeshPointer getOwnedMeshPointer() const { return strongMesh; } + scriptable::ScriptableMeshPointer getSelf() const { return const_cast(this); } + bool isValid() const { return !weakMesh.expired(); } + bool hasValidStrongMesh() const { return (bool)strongMesh; } + glm::uint32 getNumParts() const; + glm::uint32 getNumVertices() const; + glm::uint32 getNumAttributes() const; + glm::uint32 getNumIndices() const; + QVector getAttributeNames() const; + QVector getMeshParts() const; + QVariantMap getMeshExtents() const; + + operator bool() const { return !weakMesh.expired(); } + int getSlotNumber(const QString& attributeName) const; + + public slots: + const scriptable::ScriptableModelPointer getParentModel() const { return qobject_cast(model); } + QVector getIndices() const; + QVector findNearbyVertexIndices(const glm::vec3& origin, float epsilon = 1e-6) const; + + glm::uint32 addAttribute(const QString& attributeName, const QVariant& defaultValue = QVariant()); + glm::uint32 fillAttribute(const QString& attributeName, const QVariant& value); + bool removeAttribute(const QString& attributeName); + + QVariantList queryVertexAttributes(QVariant selector) const; + QVariantMap getVertexAttributes(glm::uint32 vertexIndex) const; + bool setVertexAttributes(glm::uint32 vertexIndex, const QVariantMap& attributeValues); + + QVariant getVertexProperty(glm::uint32 vertexIndex, const QString& attributeName) const; + bool setVertexProperty(glm::uint32 vertexIndex, const QString& attributeName, const QVariant& value); + + scriptable::ScriptableMeshPointer cloneMesh(); + + // QScriptEngine-specific wrappers + glm::uint32 updateVertexAttributes(QScriptValue callback); + glm::uint32 forEachVertex(QScriptValue callback); + bool isValidIndex(glm::uint32 vertexIndex, const QString& attributeName = QString()) const; + }; + +} + +Q_DECLARE_METATYPE(scriptable::ScriptableMeshPointer) +Q_DECLARE_METATYPE(QVector) diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.cpp new file mode 100644 index 0000000000..4414b0ad7e --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.cpp @@ -0,0 +1,452 @@ +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Forward.h" + +#include "ScriptableMeshPart.h" + +#include "GraphicsScriptingUtil.h" +#include "OBJWriter.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +QString scriptable::ScriptableMeshPart::toOBJ() { + if (!getMeshPointer()) { + if (context()) { + context()->throwError(QString("null mesh")); + } else { + qCWarning(graphics_scripting) << "null mesh"; + } + return QString(); + } + return writeOBJToString({ getMeshPointer() }); +} + + +bool scriptable::ScriptableMeshPart::isValidIndex(glm::uint32 vertexIndex, const QString& attributeName) const { + return isValid() && parentMesh->isValidIndex(vertexIndex, attributeName); +} + +bool scriptable::ScriptableMeshPart::setVertexAttributes(glm::uint32 vertexIndex, const QVariantMap& attributes) { + if (!isValidIndex(vertexIndex)) { + return false; + } + return buffer_helpers::mesh::setVertexAttributes(getMeshPointer(), vertexIndex, attributes); +} + +QVariantMap scriptable::ScriptableMeshPart::getVertexAttributes(glm::uint32 vertexIndex) const { + if (!isValidIndex(vertexIndex)) { + return QVariantMap(); + } + return parentMesh->getVertexAttributes(vertexIndex); +} + +bool scriptable::ScriptableMeshPart::setVertexProperty(glm::uint32 vertexIndex, const QString& attributeName, const QVariant& value) { + if (!isValidIndex(vertexIndex, attributeName)) { + return false; + } + auto slotNum = parentMesh->getSlotNumber(attributeName); + const auto& bufferView = buffer_helpers::mesh::getBufferView(getMeshPointer(), static_cast(slotNum)); + return buffer_helpers::setValue(bufferView, vertexIndex, value); +} + +QVariant scriptable::ScriptableMeshPart::getVertexProperty(glm::uint32 vertexIndex, const QString& attributeName) const { + if (!isValidIndex(vertexIndex, attributeName)) { + return false; + } + return parentMesh->getVertexProperty(vertexIndex, attributeName); +} + +QVariantList scriptable::ScriptableMeshPart::queryVertexAttributes(QVariant selector) const { + QVariantList result; + if (!isValid()) { + return result; + } + return parentMesh->queryVertexAttributes(selector); +} + +glm::uint32 scriptable::ScriptableMeshPart::forEachVertex(QScriptValue _callback) { + // TODO: limit to vertices within the part's indexed range? + return isValid() ? parentMesh->forEachVertex(_callback) : 0; +} + +glm::uint32 scriptable::ScriptableMeshPart::updateVertexAttributes(QScriptValue _callback) { + // TODO: limit to vertices within the part's indexed range? + return isValid() ? parentMesh->updateVertexAttributes(_callback) : 0; +} + +bool scriptable::ScriptableMeshPart::replaceMeshPartData(scriptable::ScriptableMeshPartPointer src, const QVector& attributeNames) { + auto target = getMeshPointer(); + auto source = src ? src->getMeshPointer() : nullptr; + if (!target || !source) { + if (context()) { + context()->throwError("ScriptableMeshPart::replaceMeshData -- expected dest and src to be valid mesh proxy pointers"); + } else { + qCWarning(graphics_scripting) << "ScriptableMeshPart::replaceMeshData -- expected dest and src to be valid mesh proxy pointers"; + } + return false; + } + + QVector attributes = attributeNames.isEmpty() ? src->parentMesh->getAttributeNames() : attributeNames; + + qCInfo(graphics_scripting) << "ScriptableMeshPart::replaceMeshData -- " << + "source:" << QString::fromStdString(source->displayName) << + "target:" << QString::fromStdString(target->displayName) << + "attributes:" << attributes; + + // remove attributes only found on target mesh, unless user has explicitly specified the relevant attribute names + if (attributeNames.isEmpty()) { + auto attributeViews = buffer_helpers::mesh::getAllBufferViews(target); + for (const auto& a : attributeViews) { + auto slot = buffer_helpers::ATTRIBUTES[a.first]; + if (!attributes.contains(a.first)) { +#ifdef SCRIPTABLE_MESH_DEBUG + qCInfo(graphics_scripting) << "ScriptableMesh::replaceMeshData -- pruning target attribute" << a.first << slot; +#endif + target->removeAttribute(slot); + } + } + } + + target->setVertexBuffer(buffer_helpers::clone(source->getVertexBuffer())); + target->setIndexBuffer(buffer_helpers::clone(source->getIndexBuffer())); + target->setPartBuffer(buffer_helpers::clone(source->getPartBuffer())); + + for (const auto& a : attributes) { + auto slot = buffer_helpers::ATTRIBUTES[a]; + if (slot == gpu::Stream::POSITION) { + continue; + } +#ifdef SCRIPTABLE_MESH_DEBUG + auto& before = target->getAttributeBuffer(slot); +#endif + auto& input = source->getAttributeBuffer(slot); + if (input.getNumElements() == 0) { +#ifdef SCRIPTABLE_MESH_DEBUG + qCInfo(graphics_scripting) << "ScriptableMeshPart::replaceMeshData buffer is empty -- pruning" << a << slot; +#endif + target->removeAttribute(slot); + } else { +#ifdef SCRIPTABLE_MESH_DEBUG + if (before.getNumElements() == 0) { + qCInfo(graphics_scripting) << "ScriptableMeshPart::replaceMeshData target buffer is empty -- adding" << a << slot; + } else { + qCInfo(graphics_scripting) << "ScriptableMeshPart::replaceMeshData target buffer exists -- updating" << a << slot; + } +#endif + target->addAttribute(slot, buffer_helpers::clone(input)); + } +#ifdef SCRIPTABLE_MESH_DEBUG + auto& after = target->getAttributeBuffer(slot); + qCInfo(graphics_scripting) << "ScriptableMeshPart::replaceMeshData" << a << slot << before.getNumElements() << " -> " << after.getNumElements(); +#endif + } + + + return true; +} + +bool scriptable::ScriptableMeshPart::dedupeVertices(float epsilon) { + auto mesh = getMeshPointer(); + if (!mesh) { + return false; + } + auto positions = mesh->getVertexBuffer(); + auto numPositions = positions.getNumElements(); + const auto epsilon2 = epsilon*epsilon; + + QVector uniqueVerts; + uniqueVerts.reserve((int)numPositions); + QMap remapIndices; + + for (glm::uint32 i = 0; i < numPositions; i++) { + const glm::uint32 numUnique = uniqueVerts.size(); + const auto& position = positions.get(i); + bool unique = true; + for (glm::uint32 j = 0; j < numUnique; j++) { + if (glm::length2(uniqueVerts[j] - position) <= epsilon2) { + remapIndices[i] = j; + unique = false; + break; + } + } + if (unique) { + uniqueVerts << position; + remapIndices[i] = numUnique; + } + } + + qCInfo(graphics_scripting) << "//VERTS before" << numPositions << "after" << uniqueVerts.size(); + + auto indices = mesh->getIndexBuffer(); + auto numIndices = indices.getNumElements(); + auto esize = indices._element.getSize(); + QVector newIndices; + newIndices.reserve((int)numIndices); + for (glm::uint32 i = 0; i < numIndices; i++) { + glm::uint32 index = esize == 4 ? indices.get(i) : indices.get(i); + if (remapIndices.contains(index)) { + newIndices << remapIndices[index]; + } else { + qCInfo(graphics_scripting) << i << index << "!remapIndices[index]"; + } + } + + mesh->setIndexBuffer(buffer_helpers::newFromVector(newIndices, { gpu::SCALAR, gpu::UINT32, gpu::INDEX })); + mesh->setVertexBuffer(buffer_helpers::newFromVector(uniqueVerts, gpu::Element::VEC3F_XYZ)); + + auto attributeViews = buffer_helpers::mesh::getAllBufferViews(mesh); + glm::uint32 numUniqueVerts = uniqueVerts.size(); + for (const auto& a : attributeViews) { + auto& view = a.second; + auto slot = buffer_helpers::ATTRIBUTES[a.first]; + if (slot == gpu::Stream::POSITION) { + continue; + } + auto newView = buffer_helpers::resized(view, numUniqueVerts); +#ifdef SCRIPTABLE_MESH_DEBUG + qCInfo(graphics_scripting) << "ScriptableMeshPart::dedupeVertices" << a.first << slot << view.getNumElements(); + qCInfo(graphics_scripting) << a.first << "before: #" << view.getNumElements() << "after: #" << newView.getNumElements(); +#endif + glm::uint32 numElements = (glm::uint32)view.getNumElements(); + for (glm::uint32 i = 0; i < numElements; i++) { + glm::uint32 fromVertexIndex = i; + glm::uint32 toVertexIndex = remapIndices.contains(fromVertexIndex) ? remapIndices[fromVertexIndex] : fromVertexIndex; + buffer_helpers::setValue(newView, toVertexIndex, buffer_helpers::getValue(view, fromVertexIndex, "dedupe")); + } + mesh->addAttribute(slot, newView); + } + return true; +} + +bool scriptable::ScriptableMeshPart::removeAttribute(const QString& attributeName) { + return isValid() && parentMesh->removeAttribute(attributeName); +} + +glm::uint32 scriptable::ScriptableMeshPart::addAttribute(const QString& attributeName, const QVariant& defaultValue) { + return isValid() ? parentMesh->addAttribute(attributeName, defaultValue): 0; +} + +glm::uint32 scriptable::ScriptableMeshPart::fillAttribute(const QString& attributeName, const QVariant& value) { + return isValid() ? parentMesh->fillAttribute(attributeName, value) : 0; +} + +QVector scriptable::ScriptableMeshPart::findNearbyPartVertexIndices(const glm::vec3& origin, float epsilon) const { + QSet result; + if (!isValid()) { + return result.toList().toVector(); + } + auto mesh = getMeshPointer(); + auto offset = getFirstVertexIndex(); + auto numIndices = getNumIndices(); + auto vertexBuffer = mesh->getVertexBuffer(); + auto indexBuffer = mesh->getIndexBuffer(); + const auto epsilon2 = epsilon*epsilon; + + for (glm::uint32 i = 0; i < numIndices; i++) { + auto vertexIndex = buffer_helpers::getValue(indexBuffer, offset + i); + if (result.contains(vertexIndex)) { + continue; + } + const auto& position = buffer_helpers::getValue(vertexBuffer, vertexIndex); + if (glm::length2(position - origin) <= epsilon2) { + result << vertexIndex; + } + } + return result.toList().toVector(); +} + +scriptable::ScriptableMeshPartPointer scriptable::ScriptableMeshPart::cloneMeshPart() { + if (parentMesh) { + if (auto clone = parentMesh->cloneMesh()) { + return clone->getMeshParts().value(partIndex); + } + } + return nullptr; +} + +QVariantMap scriptable::ScriptableMeshPart::scaleToFit(float unitScale) { + if (auto mesh = getMeshPointer()) { + auto box = mesh->evalPartsBound(0, (int)mesh->getNumParts()); + auto center = box.calcCenter(); + float maxDimension = glm::distance(box.getMaximumPoint(), box.getMinimumPoint()); + return scale(glm::vec3(unitScale / maxDimension), center); + } + return {}; +} +QVariantMap scriptable::ScriptableMeshPart::translate(const glm::vec3& translation) { + return transform(glm::translate(translation)); +} +QVariantMap scriptable::ScriptableMeshPart::scale(const glm::vec3& scale, const glm::vec3& origin) { + if (auto mesh = getMeshPointer()) { + auto box = mesh->evalPartsBound(0, (int)mesh->getNumParts()); + glm::vec3 center = glm::isnan(origin.x) ? box.calcCenter() : origin; + return transform(glm::translate(center) * glm::scale(scale)); + } + return {}; +} +QVariantMap scriptable::ScriptableMeshPart::rotateDegrees(const glm::vec3& eulerAngles, const glm::vec3& origin) { + return rotate(glm::quat(glm::radians(eulerAngles)), origin); +} +QVariantMap scriptable::ScriptableMeshPart::rotate(const glm::quat& rotation, const glm::vec3& origin) { + if (auto mesh = getMeshPointer()) { + auto box = mesh->evalPartsBound(0, (int)mesh->getNumParts()); + glm::vec3 center = glm::isnan(origin.x) ? box.calcCenter() : origin; + return transform(glm::translate(center) * glm::toMat4(rotation)); + } + return {}; +} +QVariantMap scriptable::ScriptableMeshPart::transform(const glm::mat4& transform) { + if (auto mesh = getMeshPointer()) { + const auto& pos = buffer_helpers::mesh::getBufferView(mesh, gpu::Stream::POSITION); + const glm::uint32 num = (glm::uint32)pos.getNumElements(); + for (glm::uint32 i = 0; i < num; i++) { + auto& position = pos.edit(i); + position = transform * glm::vec4(position, 1.0f); + } + return parentMesh->getMeshExtents(); + } + return {}; +} + + +scriptable::ScriptableMeshPart::ScriptableMeshPart(scriptable::ScriptableMeshPointer parentMesh, int partIndex) + : QObject(), parentMesh(parentMesh), partIndex(partIndex) { + setObjectName(QString("%1.part[%2]").arg(parentMesh ? parentMesh->objectName() : "").arg(partIndex)); +} + +QVector scriptable::ScriptableMeshPart::getIndices() const { + if (auto mesh = getMeshPointer()) { +#ifdef SCRIPTABLE_MESH_DEBUG + qCDebug(graphics_scripting, "getIndices mesh %p", mesh.get()); +#endif + return buffer_helpers::bufferToVector(mesh->getIndexBuffer()); + } + return QVector(); +} + +bool scriptable::ScriptableMeshPart::setFirstVertexIndex( glm::uint32 vertexIndex) { + if (!isValidIndex(vertexIndex)) { + return false; + } + auto& part = getMeshPointer()->getPartBuffer().edit(partIndex); + part._startIndex = vertexIndex; + return true; +} + +bool scriptable::ScriptableMeshPart::setBaseVertexIndex( glm::uint32 vertexIndex) { + if (!isValidIndex(vertexIndex)) { + return false; + } + auto& part = getMeshPointer()->getPartBuffer().edit(partIndex); + part._baseVertex = vertexIndex; + return true; +} + +bool scriptable::ScriptableMeshPart::setLastVertexIndex( glm::uint32 vertexIndex) { + if (!isValidIndex(vertexIndex) || vertexIndex <= getFirstVertexIndex()) { + return false; + } + auto& part = getMeshPointer()->getPartBuffer().edit(partIndex); + part._numIndices = vertexIndex - part._startIndex; + return true; +} + +bool scriptable::ScriptableMeshPart::setIndices(const QVector& indices) { + if (!isValid()) { + return false; + } + glm::uint32 len = indices.size(); + if (len != getNumIndices()) { + context()->throwError(QString("setIndices: currently new indicies must be assign 1:1 across old indicies (indicies.size()=%1, numIndices=%2)") + .arg(len).arg(getNumIndices())); + return false; + } + auto mesh = getMeshPointer(); + auto indexBuffer = mesh->getIndexBuffer(); + + // first loop to validate all indices are valid + for (glm::uint32 i = 0; i < len; i++) { + if (!isValidIndex(indices.at(i))) { + return false; + } + } + const auto first = getFirstVertexIndex(); + // now actually apply them + for (glm::uint32 i = 0; i < len; i++) { + buffer_helpers::setValue(indexBuffer, first + i, indices.at(i)); + } + return true; +} + +const graphics::Mesh::Part& scriptable::ScriptableMeshPart::getMeshPart() const { + static const graphics::Mesh::Part invalidPart; + if (!isValid()) { + return invalidPart; + } + return getMeshPointer()->getPartBuffer().get(partIndex); +} + +// FIXME: how we handle topology will need to be reworked if wanting to support TRIANGLE_STRIP, QUADS and QUAD_STRIP +bool scriptable::ScriptableMeshPart::setTopology(graphics::Mesh::Topology topology) { + if (!isValid()) { + return false; + } + auto& part = getMeshPointer()->getPartBuffer().edit(partIndex); + switch (topology) { +#ifdef DEV_BUILD + case graphics::Mesh::Topology::POINTS: + case graphics::Mesh::Topology::LINES: +#endif + case graphics::Mesh::Topology::TRIANGLES: + part._topology = topology; + return true; + default: + context()->throwError("changing topology to " + graphics::toString(topology) + " is not yet supported"); + return false; + } +} + +glm::uint32 scriptable::ScriptableMeshPart::getTopologyLength() const { + switch(getTopology()) { + case graphics::Mesh::Topology::POINTS: return 1; + case graphics::Mesh::Topology::LINES: return 2; + case graphics::Mesh::Topology::TRIANGLES: return 3; + case graphics::Mesh::Topology::QUADS: return 4; + default: qCDebug(graphics_scripting) << "getTopologyLength -- unrecognized topology" << getTopology(); + } + return 0; +} + +QVector scriptable::ScriptableMeshPart::getFace(glm::uint32 faceIndex) const { + switch (getTopology()) { + case graphics::Mesh::Topology::POINTS: + case graphics::Mesh::Topology::LINES: + case graphics::Mesh::Topology::TRIANGLES: + case graphics::Mesh::Topology::QUADS: + if (faceIndex < getNumFaces()) { + return getIndices().mid(faceIndex * getTopologyLength(), getTopologyLength()); + } + default: return QVector(); + } +} + +QVariantMap scriptable::ScriptableMeshPart::getPartExtents() const { + graphics::Box box; + if (auto mesh = getMeshPointer()) { + box = mesh->evalPartBound(partIndex); + } + return scriptable::toVariant(box).toMap(); +} diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h new file mode 100644 index 0000000000..dd71d9b998 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h @@ -0,0 +1,119 @@ +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include "ScriptableMesh.h" + +namespace scriptable { + /**jsdoc + * @typedef {object} Graphics.MeshPart + * @property {number} partIndex - The part index (within the containing Mesh). + * @property {Graphics.Topology} topology - element interpretation (currently only 'triangles' is supported). + * @property {string[]} attributeNames - Vertex attribute names (color, normal, etc.) + * @property {number} numIndices - Number of vertex indices that this mesh part refers to. + * @property {number} numVerticesPerFace - Number of vertices per face (eg: 3 when topology is 'triangles'). + * @property {number} numFaces - Number of faces represented by the mesh part (numIndices / numVerticesPerFace). + * @property {number} numVertices - Total number of vertices in the containing Mesh. + * @property {number} numAttributes - Number of currently defined vertex attributes. + */ + + class ScriptableMeshPart : public QObject, QScriptable { + Q_OBJECT + Q_PROPERTY(bool valid READ isValid) + Q_PROPERTY(glm::uint32 partIndex MEMBER partIndex CONSTANT) + Q_PROPERTY(glm::uint32 firstVertexIndex READ getFirstVertexIndex WRITE setFirstVertexIndex) + Q_PROPERTY(glm::uint32 baseVertexIndex READ getBaseVertexIndex WRITE setBaseVertexIndex) + Q_PROPERTY(glm::uint32 lastVertexIndex READ getLastVertexIndex WRITE setLastVertexIndex) + Q_PROPERTY(int numVerticesPerFace READ getTopologyLength) + // NOTE: making read-only for now (see also GraphicsScriptingInterface::newMesh and MeshPartPayload::drawCall) + Q_PROPERTY(graphics::Mesh::Topology topology READ getTopology) + + Q_PROPERTY(glm::uint32 numFaces READ getNumFaces) + Q_PROPERTY(glm::uint32 numAttributes READ getNumAttributes) + Q_PROPERTY(glm::uint32 numVertices READ getNumVertices) + Q_PROPERTY(glm::uint32 numIndices READ getNumIndices WRITE setNumIndices) + + Q_PROPERTY(QVariantMap extents READ getPartExtents) + Q_PROPERTY(QVector attributeNames READ getAttributeNames) + Q_PROPERTY(QVariantMap bufferFormats READ getBufferFormats) + + public: + ScriptableMeshPart(scriptable::ScriptableMeshPointer parentMesh, int partIndex); + ScriptableMeshPart& operator=(const ScriptableMeshPart& view) { parentMesh=view.parentMesh; return *this; }; + ScriptableMeshPart(const ScriptableMeshPart& other) : QObject(other.parent()), QScriptable(), parentMesh(other.parentMesh), partIndex(other.partIndex) {} + bool isValid() const { auto mesh = getMeshPointer(); return mesh && partIndex < mesh->getNumParts(); } + + public slots: + QVector getIndices() const; + bool setIndices(const QVector& indices); + QVector findNearbyPartVertexIndices(const glm::vec3& origin, float epsilon = 1e-6) const; + QVariantList queryVertexAttributes(QVariant selector) const; + QVariantMap getVertexAttributes(glm::uint32 vertexIndex) const; + bool setVertexAttributes(glm::uint32 vertexIndex, const QVariantMap& attributeValues); + + QVariant getVertexProperty(glm::uint32 vertexIndex, const QString& attributeName) const; + bool setVertexProperty(glm::uint32 vertexIndex, const QString& attributeName, const QVariant& attributeValues); + + QVector getFace(glm::uint32 faceIndex) const; + + QVariantMap scaleToFit(float unitScale); + QVariantMap translate(const glm::vec3& translation); + QVariantMap scale(const glm::vec3& scale, const glm::vec3& origin = glm::vec3(NAN)); + QVariantMap rotateDegrees(const glm::vec3& eulerAngles, const glm::vec3& origin = glm::vec3(NAN)); + QVariantMap rotate(const glm::quat& rotation, const glm::vec3& origin = glm::vec3(NAN)); + QVariantMap transform(const glm::mat4& transform); + + glm::uint32 addAttribute(const QString& attributeName, const QVariant& defaultValue = QVariant()); + glm::uint32 fillAttribute(const QString& attributeName, const QVariant& value); + bool removeAttribute(const QString& attributeName); + bool dedupeVertices(float epsilon = 1e-6); + + scriptable::ScriptableMeshPointer getParentMesh() const { return parentMesh; } + + bool replaceMeshPartData(scriptable::ScriptableMeshPartPointer source, const QVector& attributeNames = QVector()); + scriptable::ScriptableMeshPartPointer cloneMeshPart(); + + QString toOBJ(); + + // QScriptEngine-specific wrappers + glm::uint32 updateVertexAttributes(QScriptValue callback); + glm::uint32 forEachVertex(QScriptValue callback); + + bool isValidIndex(glm::uint32 vertexIndex, const QString& attributeName = QString()) const; + public: + scriptable::ScriptableMeshPointer parentMesh; + glm::uint32 partIndex; + + protected: + const graphics::Mesh::Part& getMeshPart() const; + scriptable::MeshPointer getMeshPointer() const { return parentMesh ? parentMesh->getMeshPointer() : nullptr; } + QVariantMap getBufferFormats() { return isValid() ? parentMesh->getBufferFormats() : QVariantMap(); } + glm::uint32 getNumAttributes() const { return isValid() ? parentMesh->getNumAttributes() : 0; } + + bool setTopology(graphics::Mesh::Topology topology); + graphics::Mesh::Topology getTopology() const { return isValid() ? getMeshPart()._topology : graphics::Mesh::Topology(); } + glm::uint32 getTopologyLength() const; + glm::uint32 getNumIndices() const { return isValid() ? getMeshPart()._numIndices : 0; } + bool setNumIndices(glm::uint32 numIndices) { return setLastVertexIndex(getFirstVertexIndex() + numIndices); } + glm::uint32 getNumVertices() const { return isValid() ? parentMesh->getNumVertices() : 0; } + + bool setFirstVertexIndex(glm::uint32 vertexIndex); + glm::uint32 getFirstVertexIndex() const { return isValid() ? getMeshPart()._startIndex : 0; } + bool setLastVertexIndex(glm::uint32 vertexIndex); + glm::uint32 getLastVertexIndex() const { return isValid() ? getFirstVertexIndex() + getNumIndices() - 1 : 0; } + bool setBaseVertexIndex(glm::uint32 vertexIndex); + glm::uint32 getBaseVertexIndex() const { return isValid() ? getMeshPart()._baseVertex : 0; } + + glm::uint32 getNumFaces() const { return getNumIndices() / getTopologyLength(); } + QVector getAttributeNames() const { return isValid() ? parentMesh->getAttributeNames() : QVector(); } + QVariantMap getPartExtents() const; + }; +} + +Q_DECLARE_METATYPE(scriptable::ScriptableMeshPartPointer) +Q_DECLARE_METATYPE(QVector) diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp new file mode 100644 index 0000000000..36322d170d --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp @@ -0,0 +1,116 @@ +// +// ScriptableModel.cpp +// libraries/graphics-scripting +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GraphicsScriptingUtil.h" +#include "ScriptableModel.h" +#include "ScriptableMesh.h" + +#include + +// #define SCRIPTABLE_MESH_DEBUG 1 + +scriptable::ScriptableModelBase& scriptable::ScriptableModelBase::operator=(const scriptable::ScriptableModelBase& other) { + provider = other.provider; + objectID = other.objectID; + for (const auto& mesh : other.meshes) { + append(mesh); + } + return *this; +} + +scriptable::ScriptableModelBase::~ScriptableModelBase() { +#ifdef SCRIPTABLE_MESH_DEBUG + qCDebug(graphics_scripting) << "~ScriptableModelBase" << this; +#endif + // makes cleanup order more deterministic to help with debugging + for (auto& m : meshes) { + m.strongMesh.reset(); + } + meshes.clear(); +} + +void scriptable::ScriptableModelBase::append(scriptable::WeakMeshPointer mesh) { + meshes << ScriptableMeshBase{ provider, this, mesh, this /*parent*/ }; +} + +void scriptable::ScriptableModelBase::append(const ScriptableMeshBase& mesh) { + if (mesh.provider.lock().get() != provider.lock().get()) { + qCDebug(graphics_scripting) << "warning: appending mesh from different provider..." << mesh.provider.lock().get() << " != " << provider.lock().get(); + } + meshes << mesh; +} + +QString scriptable::ScriptableModel::toString() const { + return QString("[ScriptableModel%1%2 numMeshes=%3]") + .arg(objectID.isNull() ? "" : " objectID="+objectID.toString()) + .arg(objectName().isEmpty() ? "" : " name=" +objectName()) + .arg(meshes.size()); +} + +scriptable::ScriptableModelPointer scriptable::ScriptableModel::cloneModel(const QVariantMap& options) { + scriptable::ScriptableModelPointer clone = scriptable::ScriptableModelPointer(new scriptable::ScriptableModel(*this)); + clone->meshes.clear(); + for (const auto &mesh : getConstMeshes()) { + auto cloned = mesh->cloneMesh(); + if (auto tmp = qobject_cast(cloned)) { + clone->meshes << *tmp; + tmp->deleteLater(); // schedule our copy for cleanup + } else { + qCDebug(graphics_scripting) << "error cloning mesh" << cloned; + } + } + return clone; +} + + +const scriptable::ScriptableMeshes scriptable::ScriptableModel::getConstMeshes() const { + scriptable::ScriptableMeshes out; + for (const auto& mesh : meshes) { + const scriptable::ScriptableMesh* m = qobject_cast(&mesh); + if (!m) { + m = scriptable::make_scriptowned(mesh); + } else { + qCDebug(graphics_scripting) << "reusing scriptable mesh" << m; + } + const scriptable::ScriptableMeshPointer mp = scriptable::ScriptableMeshPointer(const_cast(m)); + out << mp; + } + return out; +} + +scriptable::ScriptableMeshes scriptable::ScriptableModel::getMeshes() { + scriptable::ScriptableMeshes out; + for (auto& mesh : meshes) { + scriptable::ScriptableMesh* m = qobject_cast(&mesh); + if (!m) { + m = scriptable::make_scriptowned(mesh); + } else { + qCDebug(graphics_scripting) << "reusing scriptable mesh" << m; + } + scriptable::ScriptableMeshPointer mp = scriptable::ScriptableMeshPointer(m); + out << mp; + } + return out; +} + +#if 0 +glm::uint32 scriptable::ScriptableModel::forEachVertexAttribute(QScriptValue callback) { + glm::uint32 result = 0; + scriptable::ScriptableMeshes in = getMeshes(); + if (in.size()) { + foreach (scriptable::ScriptableMeshPointer meshProxy, in) { + result += meshProxy->mapAttributeValues(callback); + } + } + return result; +} +#endif + +#include "ScriptableModel.moc" diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h new file mode 100644 index 0000000000..9a4c4f695b --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h @@ -0,0 +1,55 @@ +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include "Forward.h" +#include "GraphicsScriptingUtil.h" + +class QScriptValue; + +namespace scriptable { + + using ScriptableMeshes = QVector; + + /**jsdoc + * @typedef {object} Graphics.Model + * @property {Uuid} objectID - UUID of corresponding inworld object (if model is associated) + * @property {number} numMeshes - The number of submeshes contained in the model. + * @property {Graphics.Mesh[]} meshes - Array of submesh references. + */ + + class ScriptableModel : public ScriptableModelBase { + Q_OBJECT + Q_PROPERTY(QUuid objectID MEMBER objectID CONSTANT) + Q_PROPERTY(glm::uint32 numMeshes READ getNumMeshes) + Q_PROPERTY(ScriptableMeshes meshes READ getMeshes) + + public: + ScriptableModel(QObject* parent = nullptr) : ScriptableModelBase(parent) {} + ScriptableModel(const ScriptableModel& other) : ScriptableModelBase(other) {} + ScriptableModel(const ScriptableModelBase& other) : ScriptableModelBase(other) {} + ScriptableModel& operator=(const ScriptableModelBase& view) { ScriptableModelBase::operator=(view); return *this; } + operator scriptable::ScriptableModelBasePointer() { + return QPointer(qobject_cast(this)); + } + ScriptableMeshes getMeshes(); + const ScriptableMeshes getConstMeshes() const; + + public slots: + scriptable::ScriptableModelPointer cloneModel(const QVariantMap& options = QVariantMap()); + QString toString() const; + + protected: + glm::uint32 getNumMeshes() { return meshes.size(); } + + }; + +} + +Q_DECLARE_METATYPE(scriptable::ScriptableModelPointer) +Q_DECLARE_METATYPE(QVector) diff --git a/libraries/graphics/src/graphics/BufferViewHelpers.cpp b/libraries/graphics/src/graphics/BufferViewHelpers.cpp new file mode 100644 index 0000000000..5c7e6ff892 --- /dev/null +++ b/libraries/graphics/src/graphics/BufferViewHelpers.cpp @@ -0,0 +1,592 @@ +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "BufferViewHelpers.h" + +#include +#include + +#include +#include +#include + +#include "Geometry.h" +#include "GpuHelpers.h" + +#include +#include + +#include +#include + +namespace glm { + using hvec2 = glm::tvec2; + using hvec4 = glm::tvec4; +} + +namespace { + QLoggingCategory bufferhelper_logging{ "hifi.bufferview" }; +} + +namespace buffer_helpers { + +const std::array XYZW = { { "x", "y", "z", "w" } }; +const std::array ZERO123 = { { "0", "1", "2", "3" } }; + +QMap ATTRIBUTES{ + {"position", gpu::Stream::POSITION }, + {"normal", gpu::Stream::NORMAL }, + {"color", gpu::Stream::COLOR }, + {"tangent", gpu::Stream::TEXCOORD0 }, + {"skin_cluster_index", gpu::Stream::SKIN_CLUSTER_INDEX }, + {"skin_cluster_weight", gpu::Stream::SKIN_CLUSTER_WEIGHT }, + {"texcoord0", gpu::Stream::TEXCOORD0 }, + {"texcoord1", gpu::Stream::TEXCOORD1 }, + {"texcoord2", gpu::Stream::TEXCOORD2 }, + {"texcoord3", gpu::Stream::TEXCOORD3 }, + {"texcoord4", gpu::Stream::TEXCOORD4 }, +}; + + +namespace { + bool boundsCheck(const gpu::BufferView& view, glm::uint32 index) { + const auto byteLength = view._element.getSize(); + return ( + index < view.getNumElements() && + index * byteLength < (view._size - 1) * byteLength + ); + } +} + +void packNormalAndTangent(glm::vec3 normal, glm::vec3 tangent, glm::uint32& packedNormal, glm::uint32& packedTangent) { + auto absNormal = glm::abs(normal); + auto absTangent = glm::abs(tangent); + normal /= glm::max(1e-6f, glm::max(glm::max(absNormal.x, absNormal.y), absNormal.z)); + tangent /= glm::max(1e-6f, glm::max(glm::max(absTangent.x, absTangent.y), absTangent.z)); + normal = glm::clamp(normal, -1.0f, 1.0f); + tangent = glm::clamp(tangent, -1.0f, 1.0f); + normal *= 511.0f; + tangent *= 511.0f; + normal = glm::round(normal); + tangent = glm::round(tangent); + + glm::detail::i10i10i10i2 normalStruct; + glm::detail::i10i10i10i2 tangentStruct; + normalStruct.data.x = int(normal.x); + normalStruct.data.y = int(normal.y); + normalStruct.data.z = int(normal.z); + normalStruct.data.w = 0; + tangentStruct.data.x = int(tangent.x); + tangentStruct.data.y = int(tangent.y); + tangentStruct.data.z = int(tangent.z); + tangentStruct.data.w = 0; + packedNormal = normalStruct.pack; + packedTangent = tangentStruct.pack; +} + +template +glm::uint32 forEachGlmVec(const gpu::BufferView& view, std::function func) { + QVector result; + const glm::uint32 num = (glm::uint32)view.getNumElements(); + glm::uint32 i = 0; + for (; i < num; i++) { + if (!func(i, view.get(i))) { + break; + } + } + return i; +} + +template<> glm::uint32 forEach(const gpu::BufferView& view, std::function func) { + return forEachGlmVec(view, func); +} + +template +QVariant glmVecToVariant(const T& v, bool asArray /*= false*/) { + static const auto len = T().length(); + if (asArray) { + QVariantList list; + for (int i = 0; i < len ; i++) { + list << v[i]; + } + return list; + } else { + QVariantMap obj; + for (int i = 0; i < len ; i++) { + obj[XYZW[i]] = v[i]; + } + return obj; + } +} + +template +const T glmVecFromVariant(const QVariant& v) { + auto isMap = v.type() == (QVariant::Type)QMetaType::QVariantMap; + static const auto len = T().length(); + const auto& components = isMap ? XYZW : ZERO123; + T result; + QVariantMap map; + QVariantList list; + if (isMap) map = v.toMap(); else list = v.toList(); + for (int i = 0; i < len ; i++) { + float value; + if (isMap) { + value = map.value(components[i]).toFloat(); + } else { + value = list.value(i).toFloat(); + } +#ifdef DEBUG_BUFFERVIEW_HELPERS + if (value != value) { // NAN + qWarning().nospace()<< "vec" << len << "." << components[i] << " NAN received from script.... " << v.toString(); + } +#endif + result[i] = value; + } + return result; +} + +// QVector => BufferView +template +gpu::BufferView newFromVector(const QVector& elements, const gpu::Element& elementType) { + auto vertexBuffer = std::make_shared(elements.size() * sizeof(T), (gpu::Byte*)elements.data()); + return { vertexBuffer, 0, vertexBuffer->getSize(),sizeof(T), elementType }; +} + +template +gpu::BufferView bufferViewFromVector(const QVector& elements, const gpu::Element& elementType) { + auto vertexBuffer = std::make_shared(elements.size() * sizeof(T), (gpu::Byte*)elements.data()); + return { vertexBuffer, 0, vertexBuffer->getSize(),sizeof(T), elementType }; +} +template<> gpu::BufferView newFromVector(const QVector& elements, const gpu::Element& elementType) { return bufferViewFromVector(elements, elementType); } +template<> gpu::BufferView newFromVector(const QVector& elements, const gpu::Element& elementType) { return bufferViewFromVector(elements, elementType); } +template<> gpu::BufferView newFromVector( const QVector& elements, const gpu::Element& elementType) { return bufferViewFromVector(elements, elementType); } +template<> gpu::BufferView newFromVector(const QVector& elements, const gpu::Element& elementType) { return bufferViewFromVector(elements, elementType); } +template<> gpu::BufferView newFromVector(const QVector& elements, const gpu::Element& elementType) { return bufferViewFromVector(elements, elementType); } + +struct GpuToGlmAdapter { + static float error(const QString& name, const gpu::BufferView& view, glm::uint32 index, const char *hint) { + qDebug() << QString("GpuToGlmAdapter:: unhandled type=%1(element=%2) size=%3(location=%4,per=%5) vec%6 hint=%7 #%8 %9 %10") + .arg(name) + .arg(gpu::toString(view._element.getType())) + .arg(view._element.getSize()) + .arg(view._element.getLocationSize()) + .arg(view._element.getSize() / view._element.getScalarCount()) + .arg(view._element.getScalarCount()) + .arg(hint) + .arg(view.getNumElements()) + .arg(gpu::toString(view._element.getSemantic())) + .arg(gpu::toString(view._element.getDimension())); + Q_ASSERT(false); + assert(false); + return NAN; + } +}; + +#define CHECK_SIZE(T) if (view._element.getSize() != sizeof(T)) { qDebug() << "invalid elementSize" << hint << view._element.getSize() << "expected:" << sizeof(T); break; } + +template struct GpuScalarToGlm : GpuToGlmAdapter { + static T get(const gpu::BufferView& view, glm::uint32 index, const char *hint) { +#ifdef DEBUG_BUFFERVIEW_HELPERS + if(!boundsCheck(view, index))return T(error("GpuScalarToGlm::get::out of bounds", view, index, hint)); +#endif + switch(view._element.getType()) { + case gpu::UINT32: return view.get(index); + case gpu::UINT16: return view.get(index); + case gpu::UINT8: return view.get(index); + case gpu::INT32: return view.get(index); + case gpu::INT16: return view.get(index); + case gpu::INT8: return view.get(index); + case gpu::FLOAT: return view.get(index); + case gpu::HALF: return T(glm::unpackHalf1x16(view.get(index))); + case gpu::NUINT8: return T(glm::unpackUnorm1x8(view.get(index))); + default: break; + } return T(error("GpuScalarToGlm::get", view, index, hint)); + } + static bool set(const gpu::BufferView& view, glm::uint32 index, const T& value, const char *hint) { +#ifdef DEBUG_BUFFERVIEW_HELPERS + if(!boundsCheck(view, index))return T(error("GpuScalarToGlm::set::out of bounds", view, index, hint)); +#endif + switch(view._element.getType()) { + case gpu::UINT32: view.edit(index) = value; return true; + case gpu::UINT16: view.edit(index) = value; return true; + case gpu::UINT8: view.edit(index) = value; return true; + case gpu::INT32: view.edit(index) = value; return true; + case gpu::INT16: view.edit(index) = value; return true; + case gpu::INT8: view.edit(index) = value; return true; + case gpu::FLOAT: view.edit(index) = value; return true; + case gpu::HALF: view.edit(index) = glm::packHalf1x16(value); return true; + case gpu::NUINT8: view.edit(index) = glm::packUnorm1x8(value); return true; + default: break; + } error("GpuScalarToGlm::set", view, index, hint); return false; + } +}; + +template struct GpuVec2ToGlm : GpuToGlmAdapter { static T get(const gpu::BufferView& view, glm::uint32 index, const char *hint) { +#ifdef DEBUG_BUFFERVIEW_HELPERS + if(!boundsCheck(view, index))return T(error("GpuVec2ToGlm::get::out of bounds", view, index, hint)); +#endif + switch(view._element.getType()) { + case gpu::UINT32: return view.get(index); + case gpu::UINT16: return view.get(index); + case gpu::UINT8: return view.get(index); + case gpu::INT32: return view.get(index); + case gpu::INT16: return view.get(index); + case gpu::INT8: return view.get(index); + case gpu::FLOAT: return view.get(index); + case gpu::HALF: CHECK_SIZE(glm::uint32); return glm::unpackHalf2x16(view.get(index)); + case gpu::NUINT16: CHECK_SIZE(glm::uint32); return glm::unpackUnorm2x16(view.get(index)); + case gpu::NUINT8: CHECK_SIZE(glm::uint16); return glm::unpackUnorm2x8(view.get(index)); + default: break; + } return T(error("GpuVec2ToGlm::get", view, index, hint)); } + + static bool set(const gpu::BufferView& view, glm::uint32 index, const T& value, const char *hint) { +#ifdef DEBUG_BUFFERVIEW_HELPERS + if(!boundsCheck(view, index))return T(error("GpuVec2ToGlm::set::out of bounds", view, index, hint)); +#endif + switch(view._element.getType()) { + // TODO: flush out GpuVec2ToGlm::set(value) + case gpu::FLOAT: view.edit(index) = value; return true; + case gpu::HALF: view.edit(index) = glm::packHalf2x16(value); return true; + case gpu::NUINT16: view.edit(index) = glm::packUnorm2x16(value); return true; + case gpu::NUINT8: view.edit(index) = glm::packUnorm2x8(value); return true; + default: break; + } error("GpuVec2ToGlm::set", view, index, hint); return false; + } +}; + +template struct GpuVec4ToGlm; + +template struct GpuVec3ToGlm : GpuToGlmAdapter { static T get(const gpu::BufferView& view, glm::uint32 index, const char *hint) { +#ifdef DEBUG_BUFFERVIEW_HELPERS + if(!boundsCheck(view, index))return T(error("GpuVec3ToGlm::get::out of bounds", view, index, hint)); +#endif + switch(view._element.getType()) { + case gpu::UINT32: return view.get(index); + case gpu::UINT16: return view.get(index); + case gpu::UINT8: return view.get(index); + case gpu::INT32: return view.get(index); + case gpu::INT16: return view.get(index); + case gpu::INT8: return view.get(index); + case gpu::FLOAT: return view.get(index); + case gpu::HALF: CHECK_SIZE(glm::uint64); return T(glm::unpackHalf4x16(view.get(index))); + case gpu::NUINT8: CHECK_SIZE(glm::uint32); return T(glm::unpackUnorm4x8(view.get(index))); + case gpu::NINT2_10_10_10: return T(glm::unpackSnorm3x10_1x2(view.get(index))); + default: break; + } return T(error("GpuVec3ToGlm::get", view, index, hint)); } + static bool set(const gpu::BufferView& view, glm::uint32 index, const T& value, const char *hint) { +#ifdef DEBUG_BUFFERVIEW_HELPERS + if(!boundsCheck(view, index))return T(error("GpuVec3ToGlm::set::out of bounds", view, index, hint)); +#endif + switch(view._element.getType()) { + // TODO: flush out GpuVec3ToGlm::set(value) + case gpu::FLOAT: view.edit(index) = value; return true; + case gpu::NUINT8: CHECK_SIZE(glm::uint32); view.edit(index) = glm::packUnorm4x8(glm::fvec4(value,0.0f)); return true; + case gpu::UINT8: view.edit(index) = value; return true; + case gpu::NINT2_10_10_10: view.edit(index) = glm::packSnorm3x10_1x2(glm::fvec4(value,0.0f)); return true; + default: break; + } error("GpuVec3ToGlm::set", view, index, hint); return false; + } +}; + +template struct GpuVec4ToGlm : GpuToGlmAdapter { static T get(const gpu::BufferView& view, glm::uint32 index, const char *hint) { +#ifdef DEBUG_BUFFERVIEW_HELPERS + if(!boundsCheck(view, index))return T(error("GpuVec4ToGlm::get::out of bounds", view, index, hint)); +#endif + switch(view._element.getType()) { + case gpu::UINT32: return view.get(index); + case gpu::UINT16: return view.get(index); + case gpu::UINT8: return view.get(index); + case gpu::INT32: return view.get(index); + case gpu::INT16: return view.get(index); + case gpu::INT8: return view.get(index); + case gpu::NUINT32: break; + case gpu::NUINT16: CHECK_SIZE(glm::uint64); return glm::unpackUnorm4x16(view.get(index)); + case gpu::NUINT8: CHECK_SIZE(glm::uint32); return glm::unpackUnorm4x8(view.get(index)); + case gpu::NUINT2: break; + case gpu::NINT32: break; + case gpu::NINT16: break; + case gpu::NINT8: break; + case gpu::COMPRESSED: break; + case gpu::NUM_TYPES: break; + case gpu::FLOAT: return view.get(index); + case gpu::HALF: CHECK_SIZE(glm::uint64); return glm::unpackHalf4x16(view.get(index)); + case gpu::NINT2_10_10_10: return glm::unpackSnorm3x10_1x2(view.get(index)); + } return T(error("GpuVec4ToGlm::get", view, index, hint)); } + static bool set(const gpu::BufferView& view, glm::uint32 index, const T& value, const char *hint) { +#ifdef DEBUG_BUFFERVIEW_HELPERS + if(!boundsCheck(view, index))return T(error("GpuVec4ToGlm::set::out of bounds", view, index, hint)); +#endif + switch(view._element.getType()) { + case gpu::FLOAT: view.edit(index) = value; return true; + case gpu::HALF: CHECK_SIZE(glm::uint64); view.edit(index) = glm::packHalf4x16(value); return true; + case gpu::UINT8: view.edit(index) = value; return true; + case gpu::NINT2_10_10_10: view.edit(index) = glm::packSnorm3x10_1x2(value); return true; + case gpu::NUINT16: CHECK_SIZE(glm::uint64); view.edit(index) = glm::packUnorm4x16(value); return true; + case gpu::NUINT8: CHECK_SIZE(glm::uint32); view.edit(index) = glm::packUnorm4x8(value); return true; + default: break; + } error("GpuVec4ToGlm::set", view, index, hint); return false; + } +}; +#undef CHECK_SIZE + +template +struct GpuValueResolver { + static QVector toVector(const gpu::BufferView& view, const char *hint) { + QVector result; + const glm::uint32 count = (glm::uint32)view.getNumElements(); + result.resize(count); + for (glm::uint32 i = 0; i < count; i++) { + result[i] = FUNC::get(view, i, hint); + } + return result; + } + static T toValue(const gpu::BufferView& view, glm::uint32 index, const char *hint) { + return FUNC::get(view, index, hint); + } +}; + +// BufferView => QVector +template QVector bufferToVector(const gpu::BufferView& view, const char *hint) { return GpuValueResolver,U>::toVector(view, hint); } + +template<> QVector bufferToVector(const gpu::BufferView& view, const char *hint) { return GpuValueResolver,int>::toVector(view, hint); } +template<> QVector bufferToVector(const gpu::BufferView& view, const char *hint) { return GpuValueResolver,glm::uint16>::toVector(view, hint); } +template<> QVector bufferToVector(const gpu::BufferView& view, const char *hint) { return GpuValueResolver,glm::uint32>::toVector(view, hint); } +template<> QVector bufferToVector(const gpu::BufferView& view, const char *hint) { return GpuValueResolver,glm::vec2>::toVector(view, hint); } +template<> QVector bufferToVector(const gpu::BufferView& view, const char *hint) { return GpuValueResolver,glm::vec3>::toVector(view, hint); } +template<> QVector bufferToVector(const gpu::BufferView& view, const char *hint) { return GpuValueResolver,glm::vec4>::toVector(view, hint); } + +// view.get with conversion between types +template<> int getValue(const gpu::BufferView& view, glm::uint32 index, const char *hint) { return GpuScalarToGlm::get(view, index, hint); } +template<> glm::uint32 getValue(const gpu::BufferView& view, glm::uint32 index, const char *hint) { return GpuScalarToGlm::get(view, index, hint); } +template<> glm::vec2 getValue(const gpu::BufferView& view, glm::uint32 index, const char *hint) { return GpuVec2ToGlm::get(view, index, hint); } +template<> glm::vec3 getValue(const gpu::BufferView& view, glm::uint32 index, const char *hint) { return GpuVec3ToGlm::get(view, index, hint); } +template<> glm::vec4 getValue(const gpu::BufferView& view, glm::uint32 index, const char *hint) { return GpuVec4ToGlm::get(view, index, hint); } + +// bufferView => QVariant +template<> QVariant getValue(const gpu::BufferView& view, glm::uint32 index, const char* hint) { + if (!boundsCheck(view, index)) { + qDebug() << "getValue -- out of bounds" << index << hint; + return false; + } + const auto dataType = view._element.getType(); + switch(view._element.getScalarCount()) { + case 1: + if (dataType == gpu::Type::FLOAT) { + return GpuScalarToGlm::get(view, index, hint); + } else { + switch(dataType) { + case gpu::INT8: case gpu::INT16: case gpu::INT32: + case gpu::NINT8: case gpu::NINT16: case gpu::NINT32: + case gpu::NINT2_10_10_10: + // signed + return GpuScalarToGlm::get(view, index, hint); + default: + // unsigned + return GpuScalarToGlm::get(view, index, hint); + } + } + case 2: return glmVecToVariant(GpuVec2ToGlm::get(view, index, hint)); + case 3: return glmVecToVariant(GpuVec3ToGlm::get(view, index, hint)); + case 4: return glmVecToVariant(GpuVec4ToGlm::get(view, index, hint)); + } + return QVariant(); +} + +// view.edit with conversion between types +template<> bool setValue(const gpu::BufferView& view, glm::uint32 index, const QVariant& v, const char* hint) { + if (!boundsCheck(view, index)) { + qDebug() << "setValue -- out of bounds" << index << hint; + return false; + } + const auto dataType = view._element.getType(); + + switch(view._element.getScalarCount()) { + case 1: + if (dataType == gpu::Type::FLOAT) { + return GpuScalarToGlm::set(view, index, v.toFloat(), hint); + } else { + switch(dataType) { + case gpu::INT8: case gpu::INT16: case gpu::INT32: + case gpu::NINT8: case gpu::NINT16: case gpu::NINT32: + case gpu::NINT2_10_10_10: + // signed + return GpuScalarToGlm::set(view, index, v.toInt(), hint); + default: + // unsigned + return GpuScalarToGlm::set(view, index, v.toUInt(), hint); + } + } + return false; + case 2: return GpuVec2ToGlm::set(view, index, glmVecFromVariant(v), hint); + case 3: return GpuVec3ToGlm::set(view, index, glmVecFromVariant(v), hint); + case 4: return GpuVec4ToGlm::set(view, index, glmVecFromVariant(v), hint); + } + return false; +} + +template<> bool setValue(const gpu::BufferView& view, glm::uint32 index, const glm::uint32& value, const char* hint) { + return GpuScalarToGlm::set(view, index, value, hint); +} +template<> bool setValue(const gpu::BufferView& view, glm::uint32 index, const glm::uint16& value, const char* hint) { + return GpuScalarToGlm::set(view, index, value, hint); +} +template<> bool setValue(const gpu::BufferView& view, glm::uint32 index, const glm::vec2& value, const char* hint) { + return GpuVec2ToGlm::set(view, index, value, hint); +} +template<> bool setValue(const gpu::BufferView& view, glm::uint32 index, const glm::vec3& value, const char* hint) { + return GpuVec3ToGlm::set(view, index, value, hint); +} +template<> bool setValue(const gpu::BufferView& view, glm::uint32 index, const glm::vec4& value, const char* hint) { + return GpuVec4ToGlm::set(view, index, value, hint); +} + +// QVariantList => QVector +template QVector qVariantListToGlmVector(const QVariantList& list) { + QVector output; + output.resize(list.size()); + int i = 0; + for (const auto& v : list) { + output[i++] = glmVecFromVariant(v); + } + return output; +} +template QVector qVariantListToScalarVector(const QVariantList& list) { + QVector output; + output.resize(list.size()); + int i = 0; + for (const auto& v : list) { + output[i++] = v.value(); + } + return output; +} + +template QVector variantToVector(const QVariant& value) { qDebug() << "variantToVector[class]"; return qVariantListToGlmVector(value.toList()); } +template<> QVector variantToVector(const QVariant& value) { return qVariantListToScalarVector(value.toList()); } +template<> QVector variantToVector(const QVariant& value) { return qVariantListToScalarVector(value.toList()); } +template<> QVector variantToVector(const QVariant& value) { return qVariantListToScalarVector(value.toList()); } +template<> QVector variantToVector(const QVariant& value) { return qVariantListToGlmVector(value.toList()); } +template<> QVector variantToVector(const QVariant& value) { return qVariantListToGlmVector(value.toList()); } +template<> QVector variantToVector(const QVariant& value) { return qVariantListToGlmVector(value.toList()); } + +template<> gpu::BufferView newFromVector(const QVector& _elements, const gpu::Element& elementType) { + glm::uint32 numElements = _elements.size(); + auto buffer = new gpu::Buffer(); + buffer->resize(elementType.getSize() * numElements); + auto bufferView = gpu::BufferView(buffer, elementType); + for (glm::uint32 i = 0; i < numElements; i++) { + setValue(bufferView, i, _elements[i]); + } + return bufferView; +} + + +gpu::BufferView clone(const gpu::BufferView& input) { + return gpu::BufferView( + std::make_shared(input._buffer->getSize(), input._buffer->getData()), + input._offset, input._size, input._stride, input._element + ); +} + +gpu::BufferView resized(const gpu::BufferView& input, glm::uint32 numElements) { +#ifdef DEBUG_BUFFERVIEW_HELPERS + auto effectiveSize = input._buffer->getSize() / input.getNumElements(); + qCDebug(bufferhelper_logging) << "resize input" << input.getNumElements() << input._buffer->getSize() << "effectiveSize" << effectiveSize; +#endif + auto vsize = input._element.getSize() * numElements; + std::unique_ptr data{ new gpu::Byte[vsize] }; + memset(data.get(), 0, vsize); + auto buffer = new gpu::Buffer(vsize, data.get()); + memcpy(data.get(), input._buffer->getData(), std::min(vsize, (glm::uint32)input._buffer->getSize())); + auto output = gpu::BufferView(buffer, input._element); +#ifdef DEBUG_BUFFERVIEW_HELPERS + qCDebug(bufferhelper_logging) << "resized output" << output.getNumElements() << output._buffer->getSize(); +#endif + return output; +} + +// mesh helpers +namespace mesh { + gpu::BufferView getBufferView(const graphics::MeshPointer& mesh, gpu::Stream::Slot slot) { + return slot == gpu::Stream::POSITION ? mesh->getVertexBuffer() : mesh->getAttributeBuffer(slot); + } + + glm::uint32 forEachVertex(const graphics::MeshPointer& mesh, std::function func) { + glm::uint32 i = 0; + auto attributeViews = getAllBufferViews(mesh); + auto nPositions = mesh->getNumVertices(); + for (; i < nPositions; i++) { + QVariantMap values; + for (const auto& a : attributeViews) { + values[a.first] = getValue(a.second, i, qUtf8Printable(a.first)); + } + if (!func(i, values)) { + break; + } + } + return i; + } + bool setVertexAttributes(const graphics::MeshPointer& mesh, glm::uint32 index, const QVariantMap& attributes) { + bool ok = true; + for (auto& a : getAllBufferViews(mesh)) { + const auto& name = a.first; + if (attributes.contains(name)) { + const auto& value = attributes.value(name); + if (value.isValid()) { + auto& view = a.second; + setValue(view, index, value); + } else { + ok = false; + //qCDebug(graphics_scripting) << "(skipping) setVertexAttributes" << vertexIndex << name; + } + } + } + return ok; + } + + QVariant getVertexAttributes(const graphics::MeshPointer& mesh, glm::uint32 vertexIndex) { + auto attributeViews = getAllBufferViews(mesh); + QVariantMap values; + for (const auto& a : attributeViews) { + values[a.first] = getValue(a.second, vertexIndex, qUtf8Printable(a.first)); + } + return values; + } + + graphics::MeshPointer clone(const graphics::MeshPointer& mesh) { + auto clonedMesh = std::make_shared(); + clonedMesh->displayName = (QString::fromStdString(mesh->displayName) + "-clone").toStdString(); + clonedMesh->setIndexBuffer(buffer_helpers::clone(mesh->getIndexBuffer())); + clonedMesh->setPartBuffer(buffer_helpers::clone(mesh->getPartBuffer())); + auto attributeViews = getAllBufferViews(mesh); + for (const auto& a : attributeViews) { + auto& view = a.second; + auto slot = ATTRIBUTES[a.first]; + auto points = buffer_helpers::clone(view); + if (slot == gpu::Stream::POSITION) { + clonedMesh->setVertexBuffer(points); + } else { + clonedMesh->addAttribute(slot, points); + } + } + return clonedMesh; + } + + std::map getAllBufferViews(const graphics::MeshPointer& mesh) { + std::map attributeViews; + if (!mesh) { + return attributeViews; + } + for (const auto& a : ATTRIBUTES.toStdMap()) { + auto bufferView = getBufferView(mesh, a.second); + if (bufferView.getNumElements()) { + attributeViews[a.first] = bufferView; + } + } + return attributeViews; + } +} // mesh +} // buffer_helpers diff --git a/libraries/graphics/src/graphics/BufferViewHelpers.h b/libraries/graphics/src/graphics/BufferViewHelpers.h new file mode 100644 index 0000000000..f877341d50 --- /dev/null +++ b/libraries/graphics/src/graphics/BufferViewHelpers.h @@ -0,0 +1,60 @@ +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once + +#include +#include +#include + +#include "GpuHelpers.h" + +namespace graphics { + class Mesh; + using MeshPointer = std::shared_ptr; +} + +class Extents; +class AABox; + +namespace buffer_helpers { + extern QMap ATTRIBUTES; + extern const std::array XYZW; + extern const std::array ZERO123; + + template QVariant glmVecToVariant(const T& v, bool asArray = false); + template const T glmVecFromVariant(const QVariant& v); + + glm::uint32 forEachVariant(const gpu::BufferView& view, std::function func, const char* hint = ""); + template glm::uint32 forEach(const gpu::BufferView& view, std::function func); + + template gpu::BufferView newFromVector(const QVector& elements, const gpu::Element& elementType); + template gpu::BufferView newFromVariantList(const QVariantList& list, const gpu::Element& elementType); + + template QVector variantToVector(const QVariant& list); + template QVector bufferToVector(const gpu::BufferView& view, const char *hint = ""); + + // note: these do value conversions from the underlying buffer type into the template type + template T getValue(const gpu::BufferView& view, glm::uint32 index, const char* hint = ""); + template bool setValue(const gpu::BufferView& view, glm::uint32 index, const T& value, const char* hint = ""); + + gpu::BufferView clone(const gpu::BufferView& input); + gpu::BufferView resized(const gpu::BufferView& input, glm::uint32 numElements); + + void packNormalAndTangent(glm::vec3 normal, glm::vec3 tangent, glm::uint32& packedNormal, glm::uint32& packedTangent); + + namespace mesh { + glm::uint32 forEachVertex(const graphics::MeshPointer& mesh, std::function func); + bool setVertexAttributes(const graphics::MeshPointer& mesh, glm::uint32 index, const QVariantMap& attributes); + QVariant getVertexAttributes(const graphics::MeshPointer& mesh, glm::uint32 index); + graphics::MeshPointer clone(const graphics::MeshPointer& mesh); + gpu::BufferView getBufferView(const graphics::MeshPointer& mesh, quint8 slot); + std::map getAllBufferViews(const graphics::MeshPointer& mesh); + template QVector attributeToVector(const graphics::MeshPointer& mesh, gpu::Stream::InputSlot slot) { + return bufferToVector(getBufferView(mesh, slot), qUtf8Printable(gpu::toString(slot))); + } + } +} diff --git a/libraries/graphics/src/graphics/Geometry.cpp b/libraries/graphics/src/graphics/Geometry.cpp index ba5afcbc62..d43c773249 100755 --- a/libraries/graphics/src/graphics/Geometry.cpp +++ b/libraries/graphics/src/graphics/Geometry.cpp @@ -42,6 +42,11 @@ void Mesh::addAttribute(Slot slot, const BufferView& buffer) { evalVertexFormat(); } +void Mesh::removeAttribute(Slot slot) { + _attributeBuffers.erase(slot); + evalVertexFormat(); +} + const BufferView Mesh::getAttributeBuffer(int attrib) const { auto attribBuffer = _attributeBuffers.find(attrib); if (attribBuffer != _attributeBuffers.end()) { @@ -224,6 +229,7 @@ graphics::MeshPointer Mesh::map(std::function vertexFunc, } graphics::MeshPointer result(new graphics::Mesh()); + result->displayName = displayName; gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); gpu::Buffer* resultVertexBuffer = new gpu::Buffer(vertexSize, resultVertexData.get()); diff --git a/libraries/graphics/src/graphics/Geometry.h b/libraries/graphics/src/graphics/Geometry.h index 642aa9e38d..a75fb1bf62 100755 --- a/libraries/graphics/src/graphics/Geometry.h +++ b/libraries/graphics/src/graphics/Geometry.h @@ -56,6 +56,7 @@ public: // Attribute Buffers size_t getNumAttributes() const { return _attributeBuffers.size(); } void addAttribute(Slot slot, const BufferView& buffer); + void removeAttribute(Slot slot); const BufferView getAttributeBuffer(int attrib) const; // Stream format @@ -135,7 +136,8 @@ public: static MeshPointer createIndexedTriangles_P3F(uint32_t numVertices, uint32_t numTriangles, const glm::vec3* vertices = nullptr, const uint32_t* indices = nullptr); - QString displayName; + std::string modelName; + std::string displayName; protected: diff --git a/libraries/graphics/src/graphics/GpuHelpers.cpp b/libraries/graphics/src/graphics/GpuHelpers.cpp new file mode 100644 index 0000000000..0c3bd945e1 --- /dev/null +++ b/libraries/graphics/src/graphics/GpuHelpers.cpp @@ -0,0 +1,121 @@ +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// + +#include "GpuHelpers.h" + +namespace graphics { + DebugEnums TOPOLOGIES{ + { Mesh::Topology::POINTS, "points" }, + { Mesh::Topology::LINES, "lines" }, + { Mesh::Topology::LINE_STRIP, "line_strip" }, + { Mesh::Topology::TRIANGLES, "triangles" }, + { Mesh::Topology::TRIANGLE_STRIP, "triangle_strip" }, + { Mesh::Topology::QUADS, "quads" }, + { Mesh::Topology::QUAD_STRIP, "quad_strip" }, + { Mesh::Topology::NUM_TOPOLOGIES, "num_topologies" }, + }; +} +namespace gpu { + DebugEnums TYPES{ + { Type::FLOAT, "float" }, + { Type::INT32, "int32" }, + { Type::UINT32, "uint32" }, + { Type::HALF, "half" }, + { Type::INT16, "int16" }, + { Type::UINT16, "uint16" }, + { Type::INT8, "int8" }, + { Type::UINT8, "uint8" }, + { Type::NINT32, "nint32" }, + { Type::NUINT32, "nuint32" }, + { Type::NINT16, "nint16" }, + { Type::NUINT16, "nuint16" }, + { Type::NINT8, "nint8" }, + { Type::NUINT8, "nuint8" }, + { Type::NUINT2, "nuint2" }, + { Type::NINT2_10_10_10, "nint2_10_10_10" }, + { Type::COMPRESSED, "compressed" }, + { Type::NUM_TYPES, "num_types" }, + }; + DebugEnums DIMENSIONS{ + { Dimension::SCALAR, "scalar" }, + { Dimension::VEC2, "vec2" }, + { Dimension::VEC3, "vec3" }, + { Dimension::VEC4, "vec4" }, + { Dimension::MAT2, "mat2" }, + { Dimension::MAT3, "mat3" }, + { Dimension::MAT4, "mat4" }, + { Dimension::TILE4x4, "tile4x4" }, + { Dimension::NUM_DIMENSIONS, "num_dimensions" }, + }; + DebugEnums SEMANTICS{ + { Semantic::RAW, "raw" }, + + { Semantic::RED, "red" }, + { Semantic::RGB, "rgb" }, + { Semantic::RGBA, "rgba" }, + { Semantic::BGRA, "bgra" }, + + { Semantic::XY, "xy" }, + { Semantic::XYZ, "xyz" }, + { Semantic::XYZW, "xyzw" }, + { Semantic::QUAT, "quat" }, + { Semantic::UV, "uv" }, + { Semantic::INDEX, "index" }, + { Semantic::PART, "part" }, + + { Semantic::DEPTH, "depth" }, + { Semantic::STENCIL, "stencil" }, + { Semantic::DEPTH_STENCIL, "depth_stencil" }, + + { Semantic::SRED, "sred" }, + { Semantic::SRGB, "srgb" }, + { Semantic::SRGBA, "srgba" }, + { Semantic::SBGRA, "sbgra" }, + + { Semantic::_FIRST_COMPRESSED, "_first_compressed" }, + + { Semantic::COMPRESSED_BC1_SRGB, "compressed_bc1_srgb" }, + { Semantic::COMPRESSED_BC1_SRGBA, "compressed_bc1_srgba" }, + { Semantic::COMPRESSED_BC3_SRGBA, "compressed_bc3_srgba" }, + { Semantic::COMPRESSED_BC4_RED, "compressed_bc4_red" }, + { Semantic::COMPRESSED_BC5_XY, "compressed_bc5_xy" }, + { Semantic::COMPRESSED_BC6_RGB, "compressed_bc6_rgb" }, + { Semantic::COMPRESSED_BC7_SRGBA, "compressed_bc7_srgba" }, + + { Semantic::_LAST_COMPRESSED, "_last_compressed" }, + + { Semantic::R11G11B10, "r11g11b10" }, + { Semantic::RGB9E5, "rgb9e5" }, + + { Semantic::UNIFORM, "uniform" }, + { Semantic::UNIFORM_BUFFER, "uniform_buffer" }, + { Semantic::RESOURCE_BUFFER, "resource_buffer" }, + { Semantic::SAMPLER, "sampler" }, + { Semantic::SAMPLER_MULTISAMPLE, "sampler_multisample" }, + { Semantic::SAMPLER_SHADOW, "sampler_shadow" }, + + + { Semantic::NUM_SEMANTICS, "num_semantics" }, + }; + DebugEnums SLOTS{ + { Stream::InputSlot::POSITION, "position" }, + { Stream::InputSlot::NORMAL, "normal" }, + { Stream::InputSlot::COLOR, "color" }, + { Stream::InputSlot::TEXCOORD0, "texcoord0" }, + { Stream::InputSlot::TEXCOORD, "texcoord" }, + { Stream::InputSlot::TANGENT, "tangent" }, + { Stream::InputSlot::SKIN_CLUSTER_INDEX, "skin_cluster_index" }, + { Stream::InputSlot::SKIN_CLUSTER_WEIGHT, "skin_cluster_weight" }, + { Stream::InputSlot::TEXCOORD1, "texcoord1" }, + { Stream::InputSlot::TEXCOORD2, "texcoord2" }, + { Stream::InputSlot::TEXCOORD3, "texcoord3" }, + { Stream::InputSlot::TEXCOORD4, "texcoord4" }, + { Stream::InputSlot::NUM_INPUT_SLOTS, "num_input_slots" }, + { Stream::InputSlot::DRAW_CALL_INFO, "draw_call_info" }, + }; +} diff --git a/libraries/graphics/src/graphics/GpuHelpers.h b/libraries/graphics/src/graphics/GpuHelpers.h new file mode 100644 index 0000000000..ceae823f83 --- /dev/null +++ b/libraries/graphics/src/graphics/GpuHelpers.h @@ -0,0 +1,47 @@ +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once + +#include +#include +#include +#include "Geometry.h" + +template +using DebugEnums = QMap; + +namespace graphics { + extern DebugEnums TOPOLOGIES; + inline QDebug operator<<(QDebug dbg, Mesh::Topology type) { return dbg << TOPOLOGIES.value(type);} + inline const QString toString(Mesh::Topology v) { return TOPOLOGIES.value(v); } +} + +namespace gpu { + extern DebugEnums TYPES; + extern DebugEnums DIMENSIONS; + extern DebugEnums SEMANTICS; + extern DebugEnums SLOTS; + inline QDebug operator<<(QDebug dbg, gpu::Type type) { return dbg << TYPES.value(type); } + inline QDebug operator<<(QDebug dbg, gpu::Dimension type) { return dbg << DIMENSIONS.value(type); } + inline QDebug operator<<(QDebug dbg, gpu::Semantic type) { return dbg << SEMANTICS.value(type); } + inline QDebug operator<<(QDebug dbg, gpu::Stream::InputSlot type) { return dbg << SLOTS.value(type); } + inline const QString toString(gpu::Type v) { return TYPES.value(v); } + inline const QString toString(gpu::Dimension v) { return DIMENSIONS.value(v); } + inline const QString toString(gpu::Semantic v) { return SEMANTICS.value(v); } + inline const QString toString(gpu::Stream::InputSlot v) { return SLOTS.value(v); } + inline const QString toString(gpu::Element v) { + return QString("[Element semantic=%1 type=%1 dimension=%2]") + .arg(toString(v.getSemantic())) + .arg(toString(v.getType())) + .arg(toString(v.getDimension())); + } +} + +Q_DECLARE_METATYPE(gpu::Type) +Q_DECLARE_METATYPE(gpu::Dimension) +Q_DECLARE_METATYPE(gpu::Semantic) +Q_DECLARE_METATYPE(graphics::Mesh::Topology) diff --git a/libraries/image/CMakeLists.txt b/libraries/image/CMakeLists.txt index 442fa714b3..e6a1856327 100644 --- a/libraries/image/CMakeLists.txt +++ b/libraries/image/CMakeLists.txt @@ -5,7 +5,8 @@ link_hifi_libraries(shared gpu) if (NOT ANDROID) add_dependency_external_projects(nvtt) find_package(NVTT REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${NVTT_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${NVTT_LIBRARIES}) add_paths_to_fixup_libs(${NVTT_DLL_PATH}) -endif() \ No newline at end of file +endif() diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 41d988eca4..0a5c2c46c6 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -53,7 +53,7 @@ AssetClient::AssetClient() { this, &AssetClient::handleNodeClientConnectionReset); } -void AssetClient::init() { +void AssetClient::initCaching() { Q_ASSERT(QThread::currentThread() == thread()); // Setup disk cache if not already diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index 3ec96c3dd4..1860a1744a 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -64,7 +64,7 @@ public: Q_INVOKABLE AssetUpload* createUpload(const QByteArray& data); public slots: - void init(); + void initCaching(); void cacheInfoRequest(QObject* reciever, QString slot); MiniPromise::Promise cacheInfoRequestAsync(MiniPromise::Promise deferred = nullptr); diff --git a/libraries/networking/src/AssetUtils.cpp b/libraries/networking/src/AssetUtils.cpp index 117274eab8..d302c6fac6 100644 --- a/libraries/networking/src/AssetUtils.cpp +++ b/libraries/networking/src/AssetUtils.cpp @@ -72,9 +72,8 @@ QByteArray loadFromCache(const QUrl& url) { qCDebug(asset_client) << url.toDisplayString() << "not in disk cache"; } - } else { - qCWarning(asset_client) << "No disk cache to load assets from."; } + return QByteArray(); } @@ -96,9 +95,8 @@ bool saveToCache(const QUrl& url, const QByteArray& file) { } qCWarning(asset_client) << "Could not save" << url.toDisplayString() << "to disk cache."; } - } else { - qCWarning(asset_client) << "No disk cache to save assets to."; } + return false; } diff --git a/libraries/networking/src/BaseAssetScriptingInterface.cpp b/libraries/networking/src/BaseAssetScriptingInterface.cpp index 3149bbc768..decb796fa4 100644 --- a/libraries/networking/src/BaseAssetScriptingInterface.cpp +++ b/libraries/networking/src/BaseAssetScriptingInterface.cpp @@ -47,7 +47,7 @@ bool BaseAssetScriptingInterface::initializeCache() { } // attempt to initialize the cache - QMetaObject::invokeMethod(assetClient().data(), "init"); + QMetaObject::invokeMethod(assetClient().data(), "initCaching"); Promise deferred = makePromise("BaseAssetScriptingInterface--queryCacheStatus"); deferred->then([this](QVariantMap result) { diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 2343695914..9dbbc570dd 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -315,8 +315,10 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe } if (sourceNode) { - if (!PacketTypeEnum::getNonVerifiedPackets().contains(headerType) && - !isDomainServer()) { + bool verifiedPacket = !PacketTypeEnum::getNonVerifiedPackets().contains(headerType); + bool ignoreVerification = isDomainServer() && PacketTypeEnum::getDomainIgnoredVerificationPackets().contains(headerType); + + if (verifiedPacket && !ignoreVerification) { QByteArray packetHeaderHash = NLPacket::verificationHashInHeader(packet); QByteArray expectedHash = NLPacket::hashForPacketAndSecret(packet, sourceNode->getConnectionSecret()); @@ -326,6 +328,7 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe static QMultiMap hashDebugSuppressMap; if (!hashDebugSuppressMap.contains(sourceID, headerType)) { + qCDebug(networking) << packetHeaderHash << expectedHash; qCDebug(networking) << "Packet hash mismatch on" << headerType << "- Sender" << sourceID; hashDebugSuppressMap.insert(sourceID, headerType); diff --git a/libraries/networking/src/ResourceManager.cpp b/libraries/networking/src/ResourceManager.cpp index 17dcd9728d..317cabdc61 100644 --- a/libraries/networking/src/ResourceManager.cpp +++ b/libraries/networking/src/ResourceManager.cpp @@ -31,7 +31,7 @@ ResourceManager::ResourceManager() { auto assetClient = DependencyManager::set(); assetClient->moveToThread(&_thread); - QObject::connect(&_thread, &QThread::started, assetClient.data(), &AssetClient::init); + QObject::connect(&_thread, &QThread::started, assetClient.data(), &AssetClient::initCaching); _thread.start(); } diff --git a/libraries/networking/src/ThreadedAssignment.h b/libraries/networking/src/ThreadedAssignment.h index 8b35acaac5..007e41a543 100644 --- a/libraries/networking/src/ThreadedAssignment.h +++ b/libraries/networking/src/ThreadedAssignment.h @@ -18,8 +18,6 @@ #include "Assignment.h" -using DownstreamNodeFoundCallback = std::function; - class ThreadedAssignment : public Assignment { Q_OBJECT public: @@ -47,10 +45,10 @@ protected: QTimer _domainServerTimer; QTimer _statsTimer; int _numQueuedCheckIns { 0 }; - + protected slots: void domainSettingsRequestFailed(); - + private slots: void checkInWithDomainServerOrExit(); }; diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 670324c4b7..e9fe232335 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -126,6 +126,11 @@ public: EntityScriptCallMethod, ChallengeOwnershipRequest, ChallengeOwnershipReply, + + OctreeDataFileRequest, + OctreeDataFileReply, + OctreeDataPersist, + NUM_PACKET_TYPE }; @@ -165,6 +170,8 @@ public: << PacketTypeEnum::Value::DomainConnectionDenied << PacketTypeEnum::Value::DomainServerPathQuery << PacketTypeEnum::Value::DomainServerPathResponse << PacketTypeEnum::Value::DomainServerAddedNode << PacketTypeEnum::Value::DomainServerConnectionToken << PacketTypeEnum::Value::DomainSettingsRequest + << PacketTypeEnum::Value::OctreeDataFileRequest << PacketTypeEnum::Value::OctreeDataFileReply + << PacketTypeEnum::Value::OctreeDataPersist << PacketTypeEnum::Value::OctreeFileReplacementFromUrl << PacketTypeEnum::Value::DomainSettings << PacketTypeEnum::Value::ICEServerPeerInformation << PacketTypeEnum::Value::ICEServerQuery << PacketTypeEnum::Value::ICEServerHeartbeat << PacketTypeEnum::Value::ICEServerHeartbeatACK << PacketTypeEnum::Value::ICEPing @@ -180,14 +187,19 @@ public: const static QSet getDomainSourcedPackets() { const static QSet DOMAIN_SOURCED_PACKETS = QSet() - << PacketTypeEnum::Value::AssetMappingOperation - << PacketTypeEnum::Value::AssetMappingOperationReply - << PacketTypeEnum::Value::AssetGet - << PacketTypeEnum::Value::AssetGetReply - << PacketTypeEnum::Value::AssetUpload - << PacketTypeEnum::Value::AssetUploadReply; + << PacketTypeEnum::Value::AssetMappingOperation + << PacketTypeEnum::Value::AssetGet + << PacketTypeEnum::Value::AssetUpload; return DOMAIN_SOURCED_PACKETS; } + + const static QSet getDomainIgnoredVerificationPackets() { + const static QSet DOMAIN_IGNORED_VERIFICATION_PACKETS = QSet() + << PacketTypeEnum::Value::AssetMappingOperationReply + << PacketTypeEnum::Value::AssetGetReply + << PacketTypeEnum::Value::AssetUploadReply; + return DOMAIN_IGNORED_VERIFICATION_PACKETS; + } }; using PacketType = PacketTypeEnum::Value; diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 55643985c8..8b93a05130 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -328,14 +328,14 @@ void Socket::checkForReadyReadBackup() { void Socket::readPendingDatagrams() { int packetSizeWithHeader = -1; - while ((packetSizeWithHeader = _udpSocket.pendingDatagramSize()) != -1) { + while (_udpSocket.hasPendingDatagrams() && (packetSizeWithHeader = _udpSocket.pendingDatagramSize()) != -1) { // we're reading a packet so re-start the readyRead backup timer _readyReadBackupTimer->start(); // grab a time point we can mark as the receive time of this packet auto receiveTime = p_high_resolution_clock::now(); - + // setup a HifiSockAddr to read into HifiSockAddr senderSockAddr; @@ -517,7 +517,7 @@ void Socket::handleSocketError(QAbstractSocket::SocketError socketError) { static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex(SOCKET_REGEX); - qCDebug(networking) << "udt::Socket error - " << socketError; + qCDebug(networking) << "udt::Socket error - " << socketError << _udpSocket.errorString(); } void Socket::handleStateChanged(QAbstractSocket::SocketState socketState) { diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index c63ff2f560..334299185e 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1778,11 +1778,9 @@ bool Octree::writeToFile(const char* fileName, const OctreeElementPointer& eleme return success; } -bool Octree::writeToJSONFile(const char* fileName, const OctreeElementPointer& element, bool doGzip) { +bool Octree::toJSON(QJsonDocument* doc, const OctreeElementPointer& element) { QVariantMap entityDescription; - qCDebug(octree, "Saving JSON SVO to file %s...", fileName); - OctreeElementPointer top; if (element) { top = element; @@ -1802,17 +1800,33 @@ bool Octree::writeToJSONFile(const char* fileName, const OctreeElementPointer& e return false; } - // convert the QVariantMap to JSON - QByteArray jsonData = QJsonDocument::fromVariant(entityDescription).toJson(); - QByteArray jsonDataForFile; + *doc = QJsonDocument::fromVariant(entityDescription); + return true; +} - if (doGzip) { - if (!gzip(jsonData, jsonDataForFile, -1)) { - qCritical("unable to gzip data while saving to json."); - return false; - } - } else { - jsonDataForFile = jsonData; +bool Octree::toGzippedJSON(QByteArray* data, const OctreeElementPointer& element) { + QJsonDocument doc; + if (!toJSON(&doc, element)) { + qCritical("Failed to convert Entities to QVariantMap while converting to json."); + return false; + } + + QByteArray jsonData = doc.toJson(); + + if (!gzip(jsonData, *data, -1)) { + qCritical("Unable to gzip data while saving to json."); + return false; + } + + return true; +} + +bool Octree::writeToJSONFile(const char* fileName, const OctreeElementPointer& element, bool doGzip) { + qCDebug(octree, "Saving JSON SVO to file %s...", fileName); + + QByteArray jsonDataForFile; + if (!toGzippedJSON(&jsonDataForFile)) { + return false; } QFile persistFile(fileName); @@ -1823,6 +1837,7 @@ bool Octree::writeToJSONFile(const char* fileName, const OctreeElementPointer& e qCritical("Could not write to JSON description of entities."); } + return success; } diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 1648cb0f47..cb281593b1 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -28,6 +28,7 @@ #include "OctreeElementBag.h" #include "OctreePacketData.h" #include "OctreeSceneStats.h" +#include "OctreeUtils.h" class ReadBitstreamToTreeParams; class Octree; @@ -283,8 +284,10 @@ public: void loadOctreeFile(const char* fileName); // Octree exporters - bool writeToFile(const char* filename, const OctreeElementPointer& element = NULL, QString persistAsFileType = "json.gz"); - bool writeToJSONFile(const char* filename, const OctreeElementPointer& element = NULL, bool doGzip = false); + bool toJSON(QJsonDocument* doc, const OctreeElementPointer& element = nullptr); + bool toGzippedJSON(QByteArray* data, const OctreeElementPointer& element = nullptr); + bool writeToFile(const char* filename, const OctreeElementPointer& element = nullptr, QString persistAsFileType = "json.gz"); + bool writeToJSONFile(const char* filename, const OctreeElementPointer& element = nullptr, bool doGzip = false); virtual bool writeToMap(QVariantMap& entityDescription, OctreeElementPointer element, bool skipDefaultValues, bool skipThoseWithBadParents) = 0; @@ -326,6 +329,11 @@ public: virtual void dumpTree() { } virtual void pruneTree() { } + void setOctreeVersionInfo(QUuid id, int64_t dataVersion) { + _persistID = id; + _persistDataVersion = dataVersion; + } + virtual void resetEditStats() { } virtual quint64 getAverageDecodeTime() const { return 0; } virtual quint64 getAverageLookupTime() const { return 0; } @@ -334,6 +342,8 @@ public: virtual quint64 getAverageLoggingTime() const { return 0; } virtual quint64 getAverageFilterTime() const { return 0; } + void incrementPersistDataVersion() { _persistDataVersion++; } + signals: void importSize(float x, float y, float z); void importProgress(int progress); @@ -359,6 +369,9 @@ protected: OctreeElementPointer _rootElement = nullptr; + QUuid _persistID { QUuid::createUuid() }; + int _persistDataVersion { 0 }; + bool _isDirty; bool _shouldReaverage; bool _stopImport; diff --git a/libraries/octree/src/OctreeDataUtils.cpp b/libraries/octree/src/OctreeDataUtils.cpp new file mode 100644 index 0000000000..b57ab8db31 --- /dev/null +++ b/libraries/octree/src/OctreeDataUtils.cpp @@ -0,0 +1,128 @@ +// +// OctreeDataUtils.cpp +// libraries/octree/src +// +// Created by Ryan Huffman 2018-02-26 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#include "OctreeDataUtils.h" + +#include +#include + +#include +#include +#include +#include + +// Reads octree file and parses it into a QJsonDocument. Handles both gzipped and non-gzipped files. +// Returns true if the file was successfully opened and parsed, otherwise false. +// Example failures: file does not exist, gzipped file cannot be unzipped, invalid JSON. +bool readOctreeFile(QString path, QJsonDocument* doc) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Cannot open json file for reading: " << path; + return false; + } + + QByteArray data = file.readAll(); + QByteArray jsonData; + + if (!gunzip(data, jsonData)) { + jsonData = data; + } + + *doc = QJsonDocument::fromJson(jsonData); + return !doc->isNull(); +} + +bool OctreeUtils::RawOctreeData::readOctreeDataInfoFromJSON(QJsonObject root) { + if (root.contains("Id") && root.contains("DataVersion")) { + id = root["Id"].toVariant().toUuid(); + version = root["DataVersion"].toInt(); + } + readSubclassData(root); + return true; +} + +bool OctreeUtils::RawOctreeData::readOctreeDataInfoFromData(QByteArray data) { + QByteArray jsonData; + if (gunzip(data, jsonData)) { + data = jsonData; + } + + auto doc = QJsonDocument::fromJson(data); + if (doc.isNull()) { + return false; + } + + auto root = doc.object(); + return readOctreeDataInfoFromJSON(root); +} + +// Reads octree file and parses it into a RawOctreeData object. +// Returns false if readOctreeFile fails. +bool OctreeUtils::RawOctreeData::readOctreeDataInfoFromFile(QString path) { + QJsonDocument doc; + if (!readOctreeFile(path, &doc)) { + return false; + } + + auto root = doc.object(); + return readOctreeDataInfoFromJSON(root); +} + +QByteArray OctreeUtils::RawOctreeData::toByteArray() { + const auto protocolVersion = (int)versionForPacketType((PacketTypeEnum::Value)dataPacketType()); + QJsonObject obj { + { "DataVersion", QJsonValue((qint64)version) }, + { "Id", QJsonValue(id.toString()) }, + { "Version", protocolVersion }, + }; + + writeSubclassData(obj); + + QJsonDocument doc; + doc.setObject(obj); + + return doc.toJson(); +} + +QByteArray OctreeUtils::RawOctreeData::toGzippedByteArray() { + auto data = toByteArray(); + QByteArray gzData; + + if (!gzip(data, gzData, -1)) { + qCritical("Unable to gzip data while converting json."); + return QByteArray(); + } + + return gzData; +} + +PacketType OctreeUtils::RawOctreeData::dataPacketType() const { + Q_ASSERT(false); + qCritical() << "Attemping to read packet type for incomplete base type 'RawOctreeData'"; + return (PacketType)0; +} + +void OctreeUtils::RawOctreeData::resetIdAndVersion() { + id = QUuid::createUuid(); + version = OctreeUtils::INITIAL_VERSION; + qDebug() << "Reset octree data to: " << id << version; +} + +void OctreeUtils::RawEntityData::readSubclassData(const QJsonObject& root) { + if (root.contains("Entities")) { + entityData = root["Entities"].toArray(); + } +} + +void OctreeUtils::RawEntityData::writeSubclassData(QJsonObject& root) const { + root["Entities"] = entityData; +} + +PacketType OctreeUtils::RawEntityData::dataPacketType() const { return PacketType::EntityData; } \ No newline at end of file diff --git a/libraries/octree/src/OctreeDataUtils.h b/libraries/octree/src/OctreeDataUtils.h new file mode 100644 index 0000000000..485599096d --- /dev/null +++ b/libraries/octree/src/OctreeDataUtils.h @@ -0,0 +1,57 @@ +// +// OctreeDataUtils.h +// libraries/octree/src +// +// Created by Ryan Huffman 2018-02-26 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#ifndef hifi_OctreeDataUtils_h +#define hifi_OctreeDataUtils_h + +#include + +#include +#include +#include + +namespace OctreeUtils { + +using Version = int64_t; +constexpr Version INITIAL_VERSION = 0; + +//using PacketType = uint8_t; + +// RawOctreeData is an intermediate format between JSON and a fully deserialized Octree. +class RawOctreeData { +public: + QUuid id { QUuid() }; + Version version { -1 }; + + virtual PacketType dataPacketType() const; + + virtual void readSubclassData(const QJsonObject& root) { } + virtual void writeSubclassData(QJsonObject& root) const { } + + void resetIdAndVersion(); + QByteArray toByteArray(); + QByteArray toGzippedByteArray(); + + bool readOctreeDataInfoFromData(QByteArray data); + bool readOctreeDataInfoFromFile(QString path); + bool readOctreeDataInfoFromJSON(QJsonObject root); +}; + +class RawEntityData : public RawOctreeData { + PacketType dataPacketType() const override; + void readSubclassData(const QJsonObject& root) override; + void writeSubclassData(QJsonObject& root) const override; + + QJsonArray entityData; +}; + +} + +#endif // hifi_OctreeDataUtils_h \ No newline at end of file diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index ea6bd28fc4..23d6b6c2aa 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -31,18 +31,20 @@ #include "OctreeLogging.h" #include "OctreePersistThread.h" +#include "OctreeUtils.h" +#include "OctreeDataUtils.h" const int OctreePersistThread::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds -const QString OctreePersistThread::REPLACEMENT_FILE_EXTENSION = ".replace"; OctreePersistThread::OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory, int persistInterval, bool wantBackup, const QJsonObject& settings, bool debugTimestampNow, - QString persistAsFileType) : + QString persistAsFileType, const QByteArray& replacementData) : _tree(tree), _filename(filename), _backupDirectory(backupDirectory), _persistInterval(persistInterval), _initialLoadComplete(false), + _replacementData(replacementData), _loadTimeUSecs(0), _lastCheck(0), _wantBackup(wantBackup), @@ -52,6 +54,7 @@ OctreePersistThread::OctreePersistThread(OctreePointer tree, const QString& file { parseSettings(settings); + // in case the persist filename has an extension that doesn't match the file type QString sansExt = fileNameWithoutExtension(_filename, PERSIST_EXTENSIONS); _filename = sansExt + "." + _persistAsFileType; @@ -132,51 +135,54 @@ quint64 OctreePersistThread::getMostRecentBackupTimeInUsecs(const QString& forma return mostRecentBackupInUsecs; } -void OctreePersistThread::possiblyReplaceContent() { - // before we load the normal file, check if there's a pending replacement file - auto replacementFileName = _filename + REPLACEMENT_FILE_EXTENSION; +void OctreePersistThread::replaceData(QByteArray data) { + backupCurrentFile(); - QFile replacementFile { replacementFileName }; - if (replacementFile.exists()) { - // we have a replacement file to process - qDebug() << "Replacing models file with" << replacementFileName; - - // first take the current models file and move it to a different filename, appended with the timestamp - QFile currentFile { _filename }; - if (currentFile.exists()) { - static const QString FILENAME_TIMESTAMP_FORMAT = "yyyyMMdd-hhmmss"; - auto backupFileName = _filename + ".backup." + QDateTime::currentDateTime().toString(FILENAME_TIMESTAMP_FORMAT); - - if (currentFile.rename(backupFileName)) { - qDebug() << "Moved previous models file to" << backupFileName; - } else { - qWarning() << "Could not backup previous models file to" << backupFileName << "- removing replacement models file"; - - if (!replacementFile.remove()) { - qWarning() << "Could not remove replacement models file from" << replacementFileName - << "- replacement will be re-attempted on next server restart"; - return; - } - } - } - - // rename the replacement file to match what the persist thread is just about to read - if (!replacementFile.rename(_filename)) { - qWarning() << "Could not replace models file with" << replacementFileName << "- starting with empty models file"; - } + QFile currentFile { _filename }; + if (currentFile.open(QIODevice::WriteOnly)) { + currentFile.write(data); + qDebug() << "Wrote replacement data"; + } else { + qWarning() << "Failed to write replacement data"; } } +// Return true if current file is backed up successfully or doesn't exist. +bool OctreePersistThread::backupCurrentFile() { + // first take the current models file and move it to a different filename, appended with the timestamp + QFile currentFile { _filename }; + if (currentFile.exists()) { + static const QString FILENAME_TIMESTAMP_FORMAT = "yyyyMMdd-hhmmss"; + auto backupFileName = _filename + ".backup." + QDateTime::currentDateTime().toString(FILENAME_TIMESTAMP_FORMAT); + + if (currentFile.rename(backupFileName)) { + qDebug() << "Moved previous models file to" << backupFileName; + return true; + } else { + qWarning() << "Could not backup previous models file to" << backupFileName << "- removing replacement models file"; + return false; + } + } + return true; +} bool OctreePersistThread::process() { if (!_initialLoadComplete) { - possiblyReplaceContent(); - quint64 loadStarted = usecTimestampNow(); qCDebug(octree) << "loading Octrees from file: " << _filename << "..."; - bool persistantFileRead; + if (!_replacementData.isNull()) { + replaceData(_replacementData); + } + + OctreeUtils::RawOctreeData data; + if (data.readOctreeDataInfoFromFile(_filename)) { + qDebug() << "Setting entity version info to: " << data.id << data.version; + _tree->setOctreeVersionInfo(data.id, data.version); + } + + bool persistentFileRead; _tree->withWriteLock([&] { PerformanceWarning warn(true, "Loading Octree File", true); @@ -199,7 +205,7 @@ bool OctreePersistThread::process() { qCDebug(octree) << "Loading Octree... lock file removed:" << lockFileName; } - persistantFileRead = _tree->readFromFile(qPrintable(_filename.toLocal8Bit())); + persistentFileRead = _tree->readFromFile(qPrintable(_filename.toLocal8Bit())); _tree->pruneTree(); }); @@ -207,7 +213,7 @@ bool OctreePersistThread::process() { _loadTimeUSecs = loadDone - loadStarted; _tree->clearDirtyBit(); // the tree is clean since we just loaded it - qCDebug(octree, "DONE loading Octrees from file... fileRead=%s", debug::valueOf(persistantFileRead)); + qCDebug(octree, "DONE loading Octrees from file... fileRead=%s", debug::valueOf(persistentFileRead)); unsigned long nodeCount = OctreeElement::getNodeCount(); unsigned long internalNodeCount = OctreeElement::getInternalNodeCount(); @@ -237,6 +243,11 @@ bool OctreePersistThread::process() { // want an uninitialized value for this, so we set it to the current time (startup of the server) time(&_lastPersistTime); + if (_replacementData.isNull()) { + sendLatestEntityDataToDS(); + } + _replacementData.clear(); + emit loadCompleted(); } @@ -272,7 +283,6 @@ bool OctreePersistThread::process() { return isStillRunning(); // keep running till they terminate us } - void OctreePersistThread::aboutToFinish() { qCDebug(octree) << "Persist thread about to finish..."; persist(); @@ -302,6 +312,7 @@ void OctreePersistThread::persist() { backup(); // handle backup if requested qCDebug(octree) << "persist operation DONE with backup..."; + _tree->incrementPersistDataVersion(); // create our "lock" file to indicate we're saving. QString lockFileName = _filename + ".lock"; @@ -319,6 +330,23 @@ void OctreePersistThread::persist() { remove(qPrintable(lockFileName)); qCDebug(octree) << "saving Octree lock file removed:" << lockFileName; } + + sendLatestEntityDataToDS(); + } +} + +void OctreePersistThread::sendLatestEntityDataToDS() { + qDebug() << "Sending latest entity data to DS"; + auto nodeList = DependencyManager::get(); + const DomainHandler& domainHandler = nodeList->getDomainHandler(); + + QByteArray data; + if (_tree->toGzippedJSON(&data)) { + auto message = NLPacketList::create(PacketType::OctreeDataPersist, QByteArray(), true, true); + message->write(data); + nodeList->sendPacketList(std::move(message), domainHandler.getSockAddr()); + } else { + qCWarning(octree) << "Failed to persist octree to DS"; } } @@ -453,7 +481,6 @@ void OctreePersistThread::rollOldBackupVersions(const BackupRule& rule) { } } - void OctreePersistThread::backup() { qCDebug(octree) << "backup operation wantBackup:" << _wantBackup; if (_wantBackup) { diff --git a/libraries/octree/src/OctreePersistThread.h b/libraries/octree/src/OctreePersistThread.h index 2441223467..bde207001f 100644 --- a/libraries/octree/src/OctreePersistThread.h +++ b/libraries/octree/src/OctreePersistThread.h @@ -18,7 +18,6 @@ #include #include "Octree.h" -/// Generalized threaded processor for handling received inbound packets. class OctreePersistThread : public GenericThread { Q_OBJECT public: @@ -32,11 +31,11 @@ public: }; static const int DEFAULT_PERSIST_INTERVAL; - static const QString REPLACEMENT_FILE_EXTENSION; OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory, int persistInterval = DEFAULT_PERSIST_INTERVAL, bool wantBackup = false, - const QJsonObject& settings = QJsonObject(), bool debugTimestampNow = false, QString persistAsFileType="json.gz"); + const QJsonObject& settings = QJsonObject(), bool debugTimestampNow = false, + QString persistAsFileType = "json.gz", const QByteArray& replacementData = QByteArray()); bool isInitialLoadComplete() const { return _initialLoadComplete; } quint64 getLoadElapsedTime() const { return _loadTimeUSecs; } @@ -61,7 +60,10 @@ protected: bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); quint64 getMostRecentBackupTimeInUsecs(const QString& format); void parseSettings(const QJsonObject& settings); - void possiblyReplaceContent(); + bool backupCurrentFile(); + + void replaceData(QByteArray data); + void sendLatestEntityDataToDS(); private: OctreePointer _tree; @@ -69,6 +71,7 @@ private: QString _backupDirectory; int _persistInterval; bool _initialLoadComplete; + QByteArray _replacementData; quint64 _loadTimeUSecs; diff --git a/libraries/octree/src/OctreeUtils.cpp b/libraries/octree/src/OctreeUtils.cpp index ca15324d4e..8980504431 100644 --- a/libraries/octree/src/OctreeUtils.cpp +++ b/libraries/octree/src/OctreeUtils.cpp @@ -17,7 +17,6 @@ #include - float calculateRenderAccuracy(const glm::vec3& position, const AABox& bounds, float octreeSizeScale, @@ -74,4 +73,4 @@ float getOrthographicAccuracySize(float octreeSizeScale, int boundaryLevelAdjust // Smallest visible element is 1cm const float smallestSize = 0.01f; return (smallestSize * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT) / boundaryDistanceForRenderLevel(boundaryLevelAdjust, octreeSizeScale); -} +} \ No newline at end of file diff --git a/libraries/octree/src/OctreeUtils.h b/libraries/octree/src/OctreeUtils.h index 0f87bb6f68..d5008376ea 100644 --- a/libraries/octree/src/OctreeUtils.h +++ b/libraries/octree/src/OctreeUtils.h @@ -15,6 +15,7 @@ #include "OctreeConstants.h" class AABox; +class QJsonDocument; /// renderAccuracy represents a floating point "visibility" of an object based on it's view from the camera. At a simple /// level it returns 0.0f for things that are so small for the current settings that they could not be visible. diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp index 87fc8a3025..a84f3feb4d 100644 --- a/libraries/qml/src/qml/OffscreenSurface.cpp +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -331,9 +331,9 @@ void OffscreenSurface::finishQmlLoad(QQmlComponent* qmlComponent, qmlComponent->deleteLater(); onItemCreated(qmlContext, newItem); - connect(newItem, SIGNAL(sendToScript(QVariant)), this, SIGNAL(fromQml(QVariant))); if (!rootCreated) { + connect(newItem, SIGNAL(sendToScript(QVariant)), this, SIGNAL(fromQml(QVariant))); onRootCreated(); emit rootItemCreated(newItem); // Call this callback after rootitem is set, otherwise VrMenu wont work diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 3e01fd2643..7fece45b2f 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -3,7 +3,7 @@ AUTOSCRIBE_SHADER_LIB(gpu graphics render) # pull in the resources.qrc file qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Gui Network Qml Quick Script) -link_hifi_libraries(shared task ktx gpu graphics model-networking render animation fbx image procedural) +link_hifi_libraries(shared task ktx gpu graphics graphics-scripting model-networking render animation fbx image procedural) include_hifi_library_headers(audio) include_hifi_library_headers(networking) include_hifi_library_headers(octree) diff --git a/libraries/render-utils/src/CauterizedMeshPartPayload.cpp b/libraries/render-utils/src/CauterizedMeshPartPayload.cpp index 41a5bf5faf..a4dc56e696 100644 --- a/libraries/render-utils/src/CauterizedMeshPartPayload.cpp +++ b/libraries/render-utils/src/CauterizedMeshPartPayload.cpp @@ -20,16 +20,32 @@ using namespace render; CauterizedMeshPartPayload::CauterizedMeshPartPayload(ModelPointer model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform) : ModelMeshPartPayload(model, meshIndex, partIndex, shapeIndex, transform, offsetTransform) {} -void CauterizedMeshPartPayload::updateClusterBuffer(const std::vector& clusterTransforms, const std::vector& cauterizedClusterTransforms) { - ModelMeshPartPayload::updateClusterBuffer(clusterTransforms); +void CauterizedMeshPartPayload::updateClusterBuffer(const std::vector& clusterMatrices, + const std::vector& cauterizedClusterMatrices) { + ModelMeshPartPayload::updateClusterBuffer(clusterMatrices); - if (cauterizedClusterTransforms.size() > 1) { + if (cauterizedClusterMatrices.size() > 1) { if (!_cauterizedClusterBuffer) { - _cauterizedClusterBuffer = std::make_shared(cauterizedClusterTransforms.size() * sizeof(TransformType), - (const gpu::Byte*) cauterizedClusterTransforms.data()); + _cauterizedClusterBuffer = std::make_shared(cauterizedClusterMatrices.size() * sizeof(glm::mat4), + (const gpu::Byte*) cauterizedClusterMatrices.data()); } else { - _cauterizedClusterBuffer->setSubData(0, cauterizedClusterTransforms.size() * sizeof(TransformType), - (const gpu::Byte*) cauterizedClusterTransforms.data()); + _cauterizedClusterBuffer->setSubData(0, cauterizedClusterMatrices.size() * sizeof(glm::mat4), + (const gpu::Byte*) cauterizedClusterMatrices.data()); + } + } +} + +void CauterizedMeshPartPayload::updateClusterBuffer(const std::vector& clusterDualQuaternions, + const std::vector& cauterizedClusterDualQuaternions) { + ModelMeshPartPayload::updateClusterBuffer(clusterDualQuaternions); + + if (cauterizedClusterDualQuaternions.size() > 1) { + if (!_cauterizedClusterBuffer) { + _cauterizedClusterBuffer = std::make_shared(cauterizedClusterDualQuaternions.size() * sizeof(Model::TransformDualQuaternion), + (const gpu::Byte*) cauterizedClusterDualQuaternions.data()); + } else { + _cauterizedClusterBuffer->setSubData(0, cauterizedClusterDualQuaternions.size() * sizeof(Model::TransformDualQuaternion), + (const gpu::Byte*) cauterizedClusterDualQuaternions.data()); } } } diff --git a/libraries/render-utils/src/CauterizedMeshPartPayload.h b/libraries/render-utils/src/CauterizedMeshPartPayload.h index 3c0f90fcb5..9a6cea8b9f 100644 --- a/libraries/render-utils/src/CauterizedMeshPartPayload.h +++ b/libraries/render-utils/src/CauterizedMeshPartPayload.h @@ -15,13 +15,13 @@ class CauterizedMeshPartPayload : public ModelMeshPartPayload { public: CauterizedMeshPartPayload(ModelPointer model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform); -#if defined(SKIN_DQ) - using TransformType = Model::TransformDualQuaternion; -#else - using TransformType = glm::mat4; -#endif + // matrix palette skinning + void updateClusterBuffer(const std::vector& clusterMatrices, + const std::vector& cauterizedClusterMatrices); - void updateClusterBuffer(const std::vector& clusterTransforms, const std::vector& cauterizedClusterTransforms); + // dual quaternion skinning + void updateClusterBuffer(const std::vector& clusterDualQuaternions, + const std::vector& cauterizedClusterQuaternions); void updateTransformForCauterizedMesh(const Transform& renderTransform); diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 54dfd96a00..6806b41647 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -35,8 +35,13 @@ bool CauterizedModel::updateGeometry() { const FBXGeometry& fbxGeometry = getFBXGeometry(); foreach (const FBXMesh& mesh, fbxGeometry.meshes) { Model::MeshState state; - state.clusterTransforms.resize(mesh.clusters.size()); - _cauterizeMeshStates.append(state); + if (_useDualQuaternionSkinning) { + state.clusterDualQuaternions.resize(mesh.clusters.size()); + _cauterizeMeshStates.append(state); + } else { + state.clusterMatrices.resize(mesh.clusters.size()); + _cauterizeMeshStates.append(state); + } } } return needsFullUpdate; @@ -109,33 +114,33 @@ void CauterizedModel::updateClusterMatrices() { const FBXMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); -#if defined(SKIN_DQ) - auto jointPose = _rig.getJointPose(cluster.jointIndex); - Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); - Transform clusterTransform; - Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform); - state.clusterTransforms[j] = Model::TransformDualQuaternion(clusterTransform); - state.clusterTransforms[j].setCauterizationParameters(0.0f, jointPose.trans()); -#else - auto jointMatrix = _rig.getJointTransform(cluster.jointIndex); - glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterTransforms[j]); -#endif + if (_useDualQuaternionSkinning) { + auto jointPose = _rig.getJointPose(cluster.jointIndex); + Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); + Transform clusterTransform; + Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform); + state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform); + state.clusterDualQuaternions[j].setCauterizationParameters(0.0f, jointPose.trans()); + } else { + auto jointMatrix = _rig.getJointTransform(cluster.jointIndex); + glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); + } } } // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. if (!_cauterizeBoneSet.empty()) { -#if defined(SKIN_DQ) + AnimPose cauterizePose = _rig.getJointPose(geometry.neckJointIndex); cauterizePose.scale() = glm::vec3(0.0001f, 0.0001f, 0.0001f); -#else + static const glm::mat4 zeroScale( glm::vec4(0.0001f, 0.0f, 0.0f, 0.0f), glm::vec4(0.0f, 0.0001f, 0.0f, 0.0f), glm::vec4(0.0f, 0.0f, 0.0001f, 0.0f), glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); auto cauterizeMatrix = _rig.getJointTransform(geometry.neckJointIndex) * zeroScale; -#endif + for (int i = 0; i < _cauterizeMeshStates.size(); i++) { Model::MeshState& state = _cauterizeMeshStates[i]; const FBXMesh& mesh = geometry.meshes.at(i); @@ -143,19 +148,24 @@ void CauterizedModel::updateClusterMatrices() { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - if (_cauterizeBoneSet.find(cluster.jointIndex) == _cauterizeBoneSet.end()) { - // not cauterized so just copy the value from the non-cauterized version. - state.clusterTransforms[j] = _meshStates[i].clusterTransforms[j]; + if (_useDualQuaternionSkinning) { + if (_cauterizeBoneSet.find(cluster.jointIndex) == _cauterizeBoneSet.end()) { + // not cauterized so just copy the value from the non-cauterized version. + state.clusterDualQuaternions[j] = _meshStates[i].clusterDualQuaternions[j]; + } else { + Transform jointTransform(cauterizePose.rot(), cauterizePose.scale(), cauterizePose.trans()); + Transform clusterTransform; + Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform); + state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform); + state.clusterDualQuaternions[j].setCauterizationParameters(1.0f, cauterizePose.trans()); + } } else { -#if defined(SKIN_DQ) - Transform jointTransform(cauterizePose.rot(), cauterizePose.scale(), cauterizePose.trans()); - Transform clusterTransform; - Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform); - state.clusterTransforms[j] = Model::TransformDualQuaternion(clusterTransform); - state.clusterTransforms[j].setCauterizationParameters(1.0f, cauterizePose.trans()); -#else - glm_mat4u_mul(cauterizeMatrix, cluster.inverseBindMatrix, state.clusterTransforms[j]); -#endif + if (_cauterizeBoneSet.find(cluster.jointIndex) == _cauterizeBoneSet.end()) { + // not cauterized so just copy the value from the non-cauterized version. + state.clusterMatrices[j] = _meshStates[i].clusterMatrices[j]; + } else { + glm_mat4u_mul(cauterizeMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); + } } } } @@ -213,45 +223,59 @@ void CauterizedModel::updateRenderItems() { auto itemID = self->_modelMeshRenderItemIDs[i]; auto meshIndex = self->_modelMeshRenderItemShapes[i].meshIndex; - auto clusterTransforms(self->getMeshState(meshIndex).clusterTransforms); - auto clusterTransformsCauterized(self->getCauterizeMeshState(meshIndex).clusterTransforms); + + const auto& meshState = self->getMeshState(meshIndex); + const auto& cauterizedMeshState = self->getCauterizeMeshState(meshIndex); bool invalidatePayloadShapeKey = self->shouldInvalidatePayloadShapeKey(meshIndex); + bool useDualQuaternionSkinning = self->getUseDualQuaternionSkinning(); - transaction.updateItem(itemID, [modelTransform, clusterTransforms, clusterTransformsCauterized, invalidatePayloadShapeKey, + transaction.updateItem(itemID, [modelTransform, meshState, useDualQuaternionSkinning, cauterizedMeshState, invalidatePayloadShapeKey, isWireframe, isVisible, isLayeredInFront, isLayeredInHUD, enableCauterization](CauterizedMeshPartPayload& data) { - data.updateClusterBuffer(clusterTransforms, clusterTransformsCauterized); + if (useDualQuaternionSkinning) { + data.updateClusterBuffer(meshState.clusterDualQuaternions, + cauterizedMeshState.clusterDualQuaternions); + } else { + data.updateClusterBuffer(meshState.clusterMatrices, + cauterizedMeshState.clusterMatrices); + } Transform renderTransform = modelTransform; - if (clusterTransforms.size() == 1) { -#if defined(SKIN_DQ) - Transform transform(clusterTransforms[0].getRotation(), - clusterTransforms[0].getScale(), - clusterTransforms[0].getTranslation()); - renderTransform = modelTransform.worldTransform(transform); -#else - renderTransform = modelTransform.worldTransform(Transform(clusterTransforms[0])); -#endif + if (useDualQuaternionSkinning) { + if (meshState.clusterDualQuaternions.size() == 1) { + const auto& dq = meshState.clusterDualQuaternions[0]; + Transform transform(dq.getRotation(), + dq.getScale(), + dq.getTranslation()); + renderTransform = modelTransform.worldTransform(transform); + } + } else { + if (meshState.clusterMatrices.size() == 1) { + renderTransform = modelTransform.worldTransform(Transform(meshState.clusterMatrices[0])); + } } data.updateTransformForSkinnedMesh(renderTransform, modelTransform); renderTransform = modelTransform; - if (clusterTransformsCauterized.size() == 1) { -#if defined(SKIN_DQ) - Transform transform(clusterTransformsCauterized[0].getRotation(), - clusterTransformsCauterized[0].getScale(), - clusterTransformsCauterized[0].getTranslation()); - renderTransform = modelTransform.worldTransform(Transform(transform)); -#else - renderTransform = modelTransform.worldTransform(Transform(clusterTransformsCauterized[0])); -#endif + if (useDualQuaternionSkinning) { + if (cauterizedMeshState.clusterDualQuaternions.size() == 1) { + const auto& dq = cauterizedMeshState.clusterDualQuaternions[0]; + Transform transform(dq.getRotation(), + dq.getScale(), + dq.getTranslation()); + renderTransform = modelTransform.worldTransform(Transform(transform)); + } + } else { + if (cauterizedMeshState.clusterMatrices.size() == 1) { + renderTransform = modelTransform.worldTransform(Transform(cauterizedMeshState.clusterMatrices[0])); + } } data.updateTransformForCauterizedMesh(renderTransform); data.setEnableCauterization(enableCauterization); data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, render::ItemKey::TAG_BITS_ALL); data.setLayer(isLayeredInFront, isLayeredInHUD); - data.setShapeKey(invalidatePayloadShapeKey, isWireframe); + data.setShapeKey(invalidatePayloadShapeKey, isWireframe, useDualQuaternionSkinning); }); } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index c57bb23f0a..7455da13b6 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -32,6 +32,7 @@ #include "gpu/StandardShaderLib.h" #include "graphics/TextureMap.h" +#include "graphics/BufferViewHelpers.h" #include "render/Args.h" #include "standardTransformPNTC_vert.h" @@ -2403,3 +2404,38 @@ void GeometryCache::renderWireCubeInstance(RenderArgs* args, gpu::Batch& batch, assert(pipeline != nullptr); renderInstances(args, batch, color, true, pipeline, GeometryCache::Cube); } + +graphics::MeshPointer GeometryCache::meshFromShape(Shape geometryShape, glm::vec3 color) { + auto shapeData = getShapeData(geometryShape); + + qDebug() << "GeometryCache::getMeshProxyListFromShape" << shapeData << stringFromShape(geometryShape); + + auto positionsBufferView = buffer_helpers::clone(shapeData->_positionView); + auto normalsBufferView = buffer_helpers::clone(shapeData->_normalView); + auto indexBufferView = buffer_helpers::clone(shapeData->_indicesView); + + gpu::BufferView::Size numVertices = positionsBufferView.getNumElements(); + Q_ASSERT(numVertices == normalsBufferView.getNumElements()); + + // apply input color across all vertices + auto colorsBufferView = buffer_helpers::clone(shapeData->_normalView); + for (gpu::BufferView::Size i = 0; i < numVertices; i++) { + colorsBufferView.edit((gpu::BufferView::Index)i) = color; + } + + graphics::MeshPointer mesh(new graphics::Mesh()); + mesh->setVertexBuffer(positionsBufferView); + mesh->setIndexBuffer(indexBufferView); + mesh->addAttribute(gpu::Stream::NORMAL, normalsBufferView); + mesh->addAttribute(gpu::Stream::COLOR, colorsBufferView); + + const auto startIndex = 0, baseVertex = 0; + graphics::Mesh::Part part(startIndex, (graphics::Index)indexBufferView.getNumElements(), baseVertex, graphics::Mesh::TRIANGLES); + auto partBuffer = new gpu::Buffer(sizeof(graphics::Mesh::Part), (gpu::Byte*)&part); + mesh->setPartBuffer(gpu::BufferView(partBuffer, gpu::Element::PART_DRAWCALL)); + + mesh->modelName = GeometryCache::stringFromShape(geometryShape).toStdString(); + mesh->displayName = QString("GeometryCache/shape::%1").arg(GeometryCache::stringFromShape(geometryShape)).toStdString(); + + return mesh; +} diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 63af30bb79..998043b80e 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -375,6 +375,7 @@ public: /// otherwise nullptr in the event of an error. const ShapeData * getShapeData(Shape shape) const; + graphics::MeshPointer meshFromShape(Shape geometryShape, glm::vec3 color); private: GeometryCache(); diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 07822c6103..2637d24d67 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -222,25 +222,35 @@ ModelMeshPartPayload::ModelMeshPartPayload(ModelPointer model, int meshIndex, in _shapeID(shapeIndex) { assert(model && model->isLoaded()); + + bool useDualQuaternionSkinning = model->getUseDualQuaternionSkinning(); + _blendedVertexBuffer = model->_blendedVertexBuffers[_meshIndex]; auto& modelMesh = model->getGeometry()->getMeshes().at(_meshIndex); const Model::MeshState& state = model->getMeshState(_meshIndex); updateMeshPart(modelMesh, partIndex); - computeAdjustedLocalBound(state.clusterTransforms); + + if (useDualQuaternionSkinning) { + computeAdjustedLocalBound(state.clusterDualQuaternions); + } else { + computeAdjustedLocalBound(state.clusterMatrices); + } updateTransform(transform, offsetTransform); Transform renderTransform = transform; - if (state.clusterTransforms.size() == 1) { -#if defined(SKIN_DQ) - Transform transform(state.clusterTransforms[0].getRotation(), - state.clusterTransforms[0].getScale(), - state.clusterTransforms[0].getTranslation()); - renderTransform = transform.worldTransform(Transform(transform)); -#else - renderTransform = transform.worldTransform(Transform(state.clusterTransforms[0])); -#endif - + if (useDualQuaternionSkinning) { + if (state.clusterDualQuaternions.size() == 1) { + const auto& dq = state.clusterDualQuaternions[0]; + Transform transform(dq.getRotation(), + dq.getScale(), + dq.getTranslation()); + renderTransform = transform.worldTransform(Transform(transform)); + } + } else { + if (state.clusterMatrices.size() == 1) { + renderTransform = transform.worldTransform(Transform(state.clusterMatrices[0])); + } } updateTransformForSkinnedMesh(renderTransform, transform); @@ -270,16 +280,44 @@ void ModelMeshPartPayload::notifyLocationChanged() { } -void ModelMeshPartPayload::updateClusterBuffer(const std::vector& clusterTransforms) { +void ModelMeshPartPayload::updateClusterBuffer(const std::vector& clusterMatrices) { + + // reset cluster buffer if we change the cluster buffer type + if (_clusterBufferType != ClusterBufferType::Matrices) { + _clusterBuffer.reset(); + } + _clusterBufferType = ClusterBufferType::Matrices; + // Once computed the cluster matrices, update the buffer(s) - if (clusterTransforms.size() > 1) { + if (clusterMatrices.size() > 1) { if (!_clusterBuffer) { - _clusterBuffer = std::make_shared(clusterTransforms.size() * sizeof(TransformType), - (const gpu::Byte*) clusterTransforms.data()); + _clusterBuffer = std::make_shared(clusterMatrices.size() * sizeof(glm::mat4), + (const gpu::Byte*) clusterMatrices.data()); } else { - _clusterBuffer->setSubData(0, clusterTransforms.size() * sizeof(TransformType), - (const gpu::Byte*) clusterTransforms.data()); + _clusterBuffer->setSubData(0, clusterMatrices.size() * sizeof(glm::mat4), + (const gpu::Byte*) clusterMatrices.data()); + } + } +} + +void ModelMeshPartPayload::updateClusterBuffer(const std::vector& clusterDualQuaternions) { + + // reset cluster buffer if we change the cluster buffer type + if (_clusterBufferType != ClusterBufferType::DualQuaternions) { + _clusterBuffer.reset(); + } + _clusterBufferType = ClusterBufferType::DualQuaternions; + + // Once computed the cluster matrices, update the buffer(s) + if (clusterDualQuaternions.size() > 1) { + if (!_clusterBuffer) { + _clusterBuffer = std::make_shared(clusterDualQuaternions.size() * sizeof(Model::TransformDualQuaternion), + (const gpu::Byte*) clusterDualQuaternions.data()); + } + else { + _clusterBuffer->setSubData(0, clusterDualQuaternions.size() * sizeof(Model::TransformDualQuaternion), + (const gpu::Byte*) clusterDualQuaternions.data()); } } } @@ -336,7 +374,7 @@ int ModelMeshPartPayload::getLayer() const { return _layer; } -void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, bool isWireframe) { +void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, bool isWireframe, bool useDualQuaternionSkinning) { if (invalidateShapeKey) { _shapeKey = ShapeKey::Builder::invalid(); return; @@ -383,6 +421,10 @@ void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, bool isWireframe if (isWireframe) { builder.withWireframe(); } + if (isSkinned && useDualQuaternionSkinning) { + builder.withDualQuatSkinned(); + } + _shapeKey = builder.build(); } @@ -438,29 +480,33 @@ void ModelMeshPartPayload::render(RenderArgs* args) { args->_details._trianglesRendered += _drawPart._numIndices / INDICES_PER_TRIANGLE; } - -void ModelMeshPartPayload::computeAdjustedLocalBound(const std::vector& clusterTransforms) { +void ModelMeshPartPayload::computeAdjustedLocalBound(const std::vector& clusterMatrices) { _adjustedLocalBound = _localBound; - if (clusterTransforms.size() > 0) { -#if defined(SKIN_DQ) - Transform rootTransform(clusterTransforms[0].getRotation(), - clusterTransforms[0].getScale(), - clusterTransforms[0].getTranslation()); - _adjustedLocalBound.transform(rootTransform); -#else - _adjustedLocalBound.transform(clusterTransforms[0]); -#endif + if (clusterMatrices.size() > 0) { + _adjustedLocalBound.transform(clusterMatrices[0]); - for (int i = 1; i < (int)clusterTransforms.size(); ++i) { + for (int i = 1; i < (int)clusterMatrices.size(); ++i) { AABox clusterBound = _localBound; -#if defined(SKIN_DQ) - Transform transform(clusterTransforms[i].getRotation(), - clusterTransforms[i].getScale(), - clusterTransforms[i].getTranslation()); - clusterBound.transform(transform); -#else - clusterBound.transform(clusterTransforms[i]); -#endif + clusterBound.transform(clusterMatrices[i]); + _adjustedLocalBound += clusterBound; + } + } +} + +void ModelMeshPartPayload::computeAdjustedLocalBound(const std::vector& clusterDualQuaternions) { + _adjustedLocalBound = _localBound; + if (clusterDualQuaternions.size() > 0) { + Transform rootTransform(clusterDualQuaternions[0].getRotation(), + clusterDualQuaternions[0].getScale(), + clusterDualQuaternions[0].getTranslation()); + _adjustedLocalBound.transform(rootTransform); + + for (int i = 1; i < (int)clusterDualQuaternions.size(); ++i) { + AABox clusterBound = _localBound; + Transform transform(clusterDualQuaternions[i].getRotation(), + clusterDualQuaternions[i].getScale(), + clusterDualQuaternions[i].getTranslation()); + clusterBound.transform(transform); _adjustedLocalBound += clusterBound; } } diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 7d7b834fc1..3ad222d90c 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -92,14 +92,13 @@ public: void notifyLocationChanged() override; -#if defined(SKIN_DQ) - using TransformType = Model::TransformDualQuaternion; -#else - using TransformType = glm::mat4; -#endif - void updateKey(bool isVisible, bool isLayered, uint8_t tagBits, bool isGroupCulled = false) override; - void updateClusterBuffer(const std::vector& clusterTransforms); + + // matrix palette skinning + void updateClusterBuffer(const std::vector& clusterMatrices); + + // dual quaternion skinning + void updateClusterBuffer(const std::vector& clusterDualQuaternions); void updateTransformForSkinnedMesh(const Transform& renderTransform, const Transform& boundTransform); // Render Item interface @@ -108,16 +107,23 @@ public: void render(RenderArgs* args) override; void setLayer(bool isLayeredInFront, bool isLayeredInHUD); - void setShapeKey(bool invalidateShapeKey, bool isWireframe); + void setShapeKey(bool invalidateShapeKey, bool isWireframe, bool useDualQuaternionSkinning); // ModelMeshPartPayload functions to perform render void bindMesh(gpu::Batch& batch) override; void bindTransform(gpu::Batch& batch, RenderArgs::RenderMode renderMode) const override; - void computeAdjustedLocalBound(const std::vector& clusterTransforms); + // matrix palette skinning + void computeAdjustedLocalBound(const std::vector& clusterMatrices); + + // dual quaternion skinning + void computeAdjustedLocalBound(const std::vector& clusterDualQuaternions); gpu::BufferPointer _clusterBuffer; + enum class ClusterBufferType { Matrices, DualQuaternions }; + ClusterBufferType _clusterBufferType { ClusterBufferType::Matrices }; + int _meshIndex; int _shapeID; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 18308d8df5..12d6659849 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -27,6 +27,8 @@ #include #include +#include +#include #include #include @@ -75,7 +77,7 @@ void initCollisionMaterials() { graphics::MaterialPointer material; material = std::make_shared(); int index = j * sectionWidth + i; - float red = component[index]; + float red = component[index % NUM_COLLISION_HULL_COLORS]; float green = component[(index + greenPhase) % NUM_COLLISION_HULL_COLORS]; float blue = component[(index + bluePhase) % NUM_COLLISION_HULL_COLORS]; material->setAlbedo(glm::vec3(red, green, blue)); @@ -278,32 +280,42 @@ void Model::updateRenderItems() { auto itemID = self->_modelMeshRenderItemIDs[i]; auto meshIndex = self->_modelMeshRenderItemShapes[i].meshIndex; - auto clusterTransforms(self->getMeshState(meshIndex).clusterTransforms); + + const auto& meshState = self->getMeshState(meshIndex); bool invalidatePayloadShapeKey = self->shouldInvalidatePayloadShapeKey(meshIndex); + bool useDualQuaternionSkinning = self->getUseDualQuaternionSkinning(); - transaction.updateItem(itemID, [modelTransform, clusterTransforms, + transaction.updateItem(itemID, [modelTransform, meshState, useDualQuaternionSkinning, invalidatePayloadShapeKey, isWireframe, isVisible, viewTagBits, isLayeredInFront, isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateClusterBuffer(clusterTransforms); + if (useDualQuaternionSkinning) { + data.updateClusterBuffer(meshState.clusterDualQuaternions); + } else { + data.updateClusterBuffer(meshState.clusterMatrices); + } Transform renderTransform = modelTransform; - if (clusterTransforms.size() == 1) { -#if defined(SKIN_DQ) - Transform transform(clusterTransforms[0].getRotation(), - clusterTransforms[0].getScale(), - clusterTransforms[0].getTranslation()); - renderTransform = modelTransform.worldTransform(Transform(transform)); -#else - renderTransform = modelTransform.worldTransform(Transform(clusterTransforms[0])); -#endif + + if (useDualQuaternionSkinning) { + if (meshState.clusterDualQuaternions.size() == 1) { + const auto& dq = meshState.clusterDualQuaternions[0]; + Transform transform(dq.getRotation(), + dq.getScale(), + dq.getTranslation()); + renderTransform = modelTransform.worldTransform(Transform(transform)); + } + } else { + if (meshState.clusterMatrices.size() == 1) { + renderTransform = modelTransform.worldTransform(Transform(meshState.clusterMatrices[0])); + } } data.updateTransformForSkinnedMesh(renderTransform, modelTransform); data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled); data.setLayer(isLayeredInFront, isLayeredInHUD); - data.setShapeKey(invalidatePayloadShapeKey, isWireframe); + data.setShapeKey(invalidatePayloadShapeKey, isWireframe, useDualQuaternionSkinning); }); } @@ -333,34 +345,6 @@ void Model::reset() { } } -#if FBX_PACK_NORMALS -static void packNormalAndTangent(glm::vec3 normal, glm::vec3 tangent, glm::uint32& packedNormal, glm::uint32& packedTangent) { - auto absNormal = glm::abs(normal); - auto absTangent = glm::abs(tangent); - normal /= glm::max(1e-6f, glm::max(glm::max(absNormal.x, absNormal.y), absNormal.z)); - tangent /= glm::max(1e-6f, glm::max(glm::max(absTangent.x, absTangent.y), absTangent.z)); - normal = glm::clamp(normal, -1.0f, 1.0f); - tangent = glm::clamp(tangent, -1.0f, 1.0f); - normal *= 511.0f; - tangent *= 511.0f; - normal = glm::round(normal); - tangent = glm::round(tangent); - - glm::detail::i10i10i10i2 normalStruct; - glm::detail::i10i10i10i2 tangentStruct; - normalStruct.data.x = int(normal.x); - normalStruct.data.y = int(normal.y); - normalStruct.data.z = int(normal.z); - normalStruct.data.w = 0; - tangentStruct.data.x = int(tangent.x); - tangentStruct.data.y = int(tangent.y); - tangentStruct.data.z = int(tangent.z); - tangentStruct.data.w = 0; - packedNormal = normalStruct.pack; - packedTangent = tangentStruct.pack; -} -#endif - bool Model::updateGeometry() { bool needFullUpdate = false; @@ -378,7 +362,8 @@ bool Model::updateGeometry() { const FBXGeometry& fbxGeometry = getFBXGeometry(); foreach (const FBXMesh& mesh, fbxGeometry.meshes) { MeshState state; - state.clusterTransforms.resize(mesh.clusters.size()); + state.clusterDualQuaternions.resize(mesh.clusters.size()); + state.clusterMatrices.resize(mesh.clusters.size()); _meshStates.push_back(state); // Note: we add empty buffers for meshes that lack blendshapes so we can access the buffers by index @@ -395,7 +380,7 @@ bool Model::updateGeometry() { #if FBX_PACK_NORMALS glm::uint32 finalNormal; glm::uint32 finalTangent; - packNormalAndTangent(*normalIt, *tangentIt, finalNormal, finalTangent); + buffer_helpers::packNormalAndTangent(*normalIt, *tangentIt, finalNormal, finalTangent); #else const auto finalNormal = *normalIt; const auto finalTangent = *tangentIt; @@ -466,7 +451,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g const FBXGeometry& geometry = getFBXGeometry(); if (!_triangleSetsValid) { - calculateTriangleSets(); + calculateTriangleSets(geometry); } glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset); @@ -518,7 +503,6 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g { "v2", vec3toVariant(bestModelTriangle.v2) }, }; } - } } @@ -552,7 +536,7 @@ bool Model::convexHullContains(glm::vec3 point) { QMutexLocker locker(&_mutex); if (!_triangleSetsValid) { - calculateTriangleSets(); + calculateTriangleSets(getFBXGeometry()); } // If we are inside the models box, then consider the submeshes... @@ -577,6 +561,7 @@ bool Model::convexHullContains(glm::vec3 point) { return false; } +// TODO: deprecate and remove MeshProxyList Model::getMeshes() const { MeshProxyList result; const Geometry::Pointer& renderGeometry = getGeometry(); @@ -606,16 +591,111 @@ MeshProxyList Model::getMeshes() const { return glm::normalize(glm::vec3(offsetMat * glm::vec4(normal, 0.0f))); }, [&](uint32_t index) { return index; })); + meshProxy->setObjectName(mesh->displayName.c_str()); result << meshProxy; } return result; } -void Model::calculateTriangleSets() { - PROFILE_RANGE(render, __FUNCTION__); +bool Model::replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointer newModel, int meshIndex, int partIndex) { + QMutexLocker lock(&_mutex); + + if (!isLoaded()) { + qDebug() << "!isLoaded" << this; + return false; + } + + if (!newModel || !newModel->meshes.size()) { + qDebug() << "!newModel.meshes.size()" << this; + return false; + } + + const auto& meshes = newModel->meshes; + render::Transaction transaction; + const render::ScenePointer& scene = AbstractViewStateInterface::instance()->getMain3DScene(); + + meshIndex = max(meshIndex, 0); + partIndex = max(partIndex, 0); + + if (meshIndex >= (int)meshes.size()) { + qDebug() << meshIndex << "meshIndex >= newModel.meshes.size()" << meshes.size(); + return false; + } + + auto mesh = meshes[meshIndex].getMeshPointer(); + + if (partIndex >= (int)mesh->getNumParts()) { + qDebug() << partIndex << "partIndex >= mesh->getNumParts()" << mesh->getNumParts(); + return false; + } + { + // update visual geometry + render::Transaction transaction; + for (int i = 0; i < (int) _modelMeshRenderItemIDs.size(); i++) { + auto itemID = _modelMeshRenderItemIDs[i]; + auto shape = _modelMeshRenderItemShapes[i]; + // TODO: check to see if .partIndex matches too + if (shape.meshIndex == meshIndex) { + transaction.updateItem(itemID, [=](ModelMeshPartPayload& data) { + data.updateMeshPart(mesh, partIndex); + }); + } + } + scene->enqueueTransaction(transaction); + } + // update triangles for ray picking + { + FBXGeometry geometry; + for (const auto& newMesh : meshes) { + FBXMesh mesh; + mesh._mesh = newMesh.getMeshPointer(); + mesh.vertices = buffer_helpers::mesh::attributeToVector(mesh._mesh, gpu::Stream::POSITION); + int numParts = (int)newMesh.getMeshPointer()->getNumParts(); + for (int partID = 0; partID < numParts; partID++) { + FBXMeshPart part; + part.triangleIndices = buffer_helpers::bufferToVector(mesh._mesh->getIndexBuffer(), "part.triangleIndices"); + mesh.parts << part; + } + { + foreach (const glm::vec3& vertex, mesh.vertices) { + glm::vec3 transformedVertex = glm::vec3(mesh.modelTransform * glm::vec4(vertex, 1.0f)); + geometry.meshExtents.minimum = glm::min(geometry.meshExtents.minimum, transformedVertex); + geometry.meshExtents.maximum = glm::max(geometry.meshExtents.maximum, transformedVertex); + mesh.meshExtents.minimum = glm::min(mesh.meshExtents.minimum, transformedVertex); + mesh.meshExtents.maximum = glm::max(mesh.meshExtents.maximum, transformedVertex); + } + } + geometry.meshes << mesh; + } + calculateTriangleSets(geometry); + } + return true; +} + +scriptable::ScriptableModelBase Model::getScriptableModel() { + QMutexLocker lock(&_mutex); + scriptable::ScriptableModelBase result; + + if (!isLoaded()) { + qCDebug(renderutils) << "Model::getScriptableModel -- !isLoaded"; + return result; + } const FBXGeometry& geometry = getFBXGeometry(); + int numberOfMeshes = geometry.meshes.size(); + for (int i = 0; i < numberOfMeshes; i++) { + const FBXMesh& fbxMesh = geometry.meshes.at(i); + if (auto mesh = fbxMesh._mesh) { + result.append(mesh); + } + } + return result; +} + +void Model::calculateTriangleSets(const FBXGeometry& geometry) { + PROFILE_RANGE(render, __FUNCTION__); + int numberOfMeshes = geometry.meshes.size(); _triangleSetsValid = true; @@ -638,7 +718,7 @@ void Model::calculateTriangleSets() { int totalTriangles = (numberOfQuads * TRIANGLES_PER_QUAD) + numberOfTris; _modelSpaceMeshTriangleSets[i].reserve(totalTriangles); - auto meshTransform = getFBXGeometry().offset * mesh.modelTransform; + auto meshTransform = geometry.offset * mesh.modelTransform; if (part.quadIndices.size() > 0) { int vIndex = 0; @@ -859,7 +939,7 @@ void Model::renderDebugMeshBoxes(gpu::Batch& batch) { DependencyManager::get()->bindSimpleProgram(batch, false, false, false, true, true); - for(const auto& triangleSet : _modelSpaceMeshTriangleSets) { + for (const auto& triangleSet : _modelSpaceMeshTriangleSets) { auto box = triangleSet.getBounds(); if (_debugMeshBoxesID == GeometryCache::UNKNOWN_ID) { @@ -1234,6 +1314,10 @@ void Model::snapToRegistrationPoint() { _snappedToRegistrationPoint = true; } +void Model::setUseDualQuaternionSkinning(bool value) { + _useDualQuaternionSkinning = value; +} + void Model::simulate(float deltaTime, bool fullUpdate) { DETAILED_PROFILE_RANGE(simulation_detail, __FUNCTION__); fullUpdate = updateGeometry() || fullUpdate || (_scaleToFit && !_scaledToFit) @@ -1267,7 +1351,11 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { void Model::computeMeshPartLocalBounds() { for (auto& part : _modelMeshRenderItems) { const Model::MeshState& state = _meshStates.at(part->_meshIndex); - part->computeAdjustedLocalBound(state.clusterTransforms); + if (_useDualQuaternionSkinning) { + part->computeAdjustedLocalBound(state.clusterDualQuaternions); + } else { + part->computeAdjustedLocalBound(state.clusterMatrices); + } } } @@ -1286,16 +1374,16 @@ void Model::updateClusterMatrices() { const FBXMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); -#if defined(SKIN_DQ) - auto jointPose = _rig.getJointPose(cluster.jointIndex); - Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); - Transform clusterTransform; - Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform); - state.clusterTransforms[j] = Model::TransformDualQuaternion(clusterTransform); -#else - auto jointMatrix = _rig.getJointTransform(cluster.jointIndex); - glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterTransforms[j]); -#endif + if (_useDualQuaternionSkinning) { + auto jointPose = _rig.getJointPose(cluster.jointIndex); + Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); + Transform clusterTransform; + Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform); + state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform); + } else { + auto jointMatrix = _rig.getJointTransform(cluster.jointIndex); + glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); + } } } @@ -1356,7 +1444,7 @@ void Model::setBlendedVertices(int blendNumber, const Geometry::WeakPointer& geo #if FBX_PACK_NORMALS glm::uint32 finalNormal; glm::uint32 finalTangent; - packNormalAndTangent(*normalIt, *tangentIt, finalNormal, finalTangent); + buffer_helpers::packNormalAndTangent(*normalIt, *tangentIt, finalNormal, finalTangent); #else const auto finalNormal = *normalIt; const auto finalTangent = *tangentIt; @@ -1379,7 +1467,7 @@ void Model::setBlendedVertices(int blendNumber, const Geometry::WeakPointer& geo #if FBX_PACK_NORMALS glm::uint32 finalNormal; glm::uint32 finalTangent; - packNormalAndTangent(*normalIt, *tangentIt, finalNormal, finalTangent); + buffer_helpers::packNormalAndTangent(*normalIt, *tangentIt, finalNormal, finalTangent); #else const auto finalNormal = *normalIt; const auto finalTangent = *tangentIt; @@ -1453,7 +1541,7 @@ void Model::createVisibleRenderItemSet() { // all of our mesh vectors must match in size if (meshes.size() != _meshStates.size()) { - qCDebug(renderutils) << "WARNING!!!! Mesh Sizes don't match! We will not segregate mesh groups yet."; + qCDebug(renderutils) << "WARNING!!!! Mesh Sizes don't match! " << meshes.size() << _meshStates.size() << " We will not segregate mesh groups yet."; return; } @@ -1565,12 +1653,13 @@ void Model::addMaterial(graphics::MaterialLayer material, const std::string& par bool wireframe = isWireframe(); auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); + bool useDualQuaternionSkinning = _useDualQuaternionSkinning; transaction.updateItem(itemID, [material, visible, layeredInFront, layeredInHUD, viewTagBits, - invalidatePayloadShapeKey, wireframe](ModelMeshPartPayload& data) { + invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning](ModelMeshPartPayload& data) { data.addMaterial(material); // if the material changed, we might need to update our item key or shape key data.updateKey(visible, layeredInFront || layeredInHUD, viewTagBits); - data.setShapeKey(invalidatePayloadShapeKey, wireframe); + data.setShapeKey(invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning); }); } } @@ -1590,12 +1679,13 @@ void Model::removeMaterial(graphics::MaterialPointer material, const std::string bool wireframe = isWireframe(); auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); + bool useDualQuaternionSkinning = _useDualQuaternionSkinning; transaction.updateItem(itemID, [material, visible, layeredInFront, layeredInHUD, viewTagBits, - invalidatePayloadShapeKey, wireframe](ModelMeshPartPayload& data) { + invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning](ModelMeshPartPayload& data) { data.removeMaterial(material); // if the material changed, we might need to update our item key or shape key data.updateKey(visible, layeredInFront || layeredInHUD, viewTagBits); - data.setShapeKey(invalidatePayloadShapeKey, wireframe); + data.setShapeKey(invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning); }); } } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index cfcf0a2ce6..9c98ac2b8b 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -64,7 +65,7 @@ using ModelWeakPointer = std::weak_ptr; /// A generic 3D model displaying geometry loaded from a URL. -class Model : public QObject, public std::enable_shared_from_this { +class Model : public QObject, public std::enable_shared_from_this, public scriptable::ModelProvider { Q_OBJECT public: @@ -256,8 +257,6 @@ public: int getRenderInfoDrawCalls() const { return _renderInfoDrawCalls; } bool getRenderInfoHasTransparent() const { return _renderInfoHasTransparent; } - -#if defined(SKIN_DQ) class TransformDualQuaternion { public: TransformDualQuaternion() {} @@ -295,15 +294,11 @@ public: DualQuaternion _dq; glm::vec4 _cauterizedPosition { 0.0f, 0.0f, 0.0f, 1.0f }; }; -#endif class MeshState { public: -#if defined(SKIN_DQ) - std::vector clusterTransforms; -#else - std::vector clusterTransforms; -#endif + std::vector clusterDualQuaternions; + std::vector clusterMatrices; }; const MeshState& getMeshState(int index) { return _meshStates.at(index); } @@ -317,8 +312,12 @@ public: int getResourceDownloadAttemptsRemaining() { return _renderWatcher.getResourceDownloadAttemptsRemaining(); } Q_INVOKABLE MeshProxyList getMeshes() const; + virtual scriptable::ScriptableModelBase getScriptableModel() override; + virtual bool replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointer model, int meshIndex, int partIndex) override; void scaleToFit(); + bool getUseDualQuaternionSkinning() const { return _useDualQuaternionSkinning; } + void setUseDualQuaternionSkinning(bool value); void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName); void removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName); @@ -412,11 +411,11 @@ protected: int _blendNumber; int _appliedBlendNumber; - QMutex _mutex; + mutable QMutex _mutex{ QMutex::Recursive }; bool _overrideModelTransform { false }; bool _triangleSetsValid { false }; - void calculateTriangleSets(); + void calculateTriangleSets(const FBXGeometry& geometry); QVector _modelSpaceMeshTriangleSets; // model space triangles for all sub meshes @@ -425,6 +424,7 @@ protected: virtual void createCollisionRenderItemSet(); bool _isWireframe; + bool _useDualQuaternionSkinning { false }; // debug rendering support int _debugMeshBoxesID = GeometryCache::UNKNOWN_ID; diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index 3c80a2d14c..3fedae1778 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -26,6 +26,8 @@ #include "model_lightmap_normal_map_vert.h" #include "skin_model_vert.h" #include "skin_model_normal_map_vert.h" +#include "skin_model_dq_vert.h" +#include "skin_model_normal_map_dq_vert.h" #include "model_lightmap_fade_vert.h" #include "model_lightmap_normal_map_fade_vert.h" @@ -33,6 +35,8 @@ #include "model_translucent_normal_map_vert.h" #include "skin_model_fade_vert.h" #include "skin_model_normal_map_fade_vert.h" +#include "skin_model_fade_dq_vert.h" +#include "skin_model_normal_map_fade_dq_vert.h" #include "simple_vert.h" #include "simple_textured_frag.h" @@ -95,6 +99,7 @@ #include "model_shadow_vert.h" #include "skin_model_shadow_vert.h" +#include "skin_model_shadow_dq_vert.h" #include "model_shadow_frag.h" #include "skin_model_shadow_frag.h" @@ -195,16 +200,28 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip auto modelTranslucentVertex = model_translucent_vert::getShader(); auto modelTranslucentNormalMapVertex = model_translucent_normal_map_vert::getShader(); auto modelShadowVertex = model_shadow_vert::getShader(); + + auto modelLightmapFadeVertex = model_lightmap_fade_vert::getShader(); + auto modelLightmapNormalMapFadeVertex = model_lightmap_normal_map_fade_vert::getShader(); + + // matrix palette skinned auto skinModelVertex = skin_model_vert::getShader(); auto skinModelNormalMapVertex = skin_model_normal_map_vert::getShader(); auto skinModelShadowVertex = skin_model_shadow_vert::getShader(); - auto modelLightmapFadeVertex = model_lightmap_fade_vert::getShader(); - auto modelLightmapNormalMapFadeVertex = model_lightmap_normal_map_fade_vert::getShader(); auto skinModelFadeVertex = skin_model_fade_vert::getShader(); auto skinModelNormalMapFadeVertex = skin_model_normal_map_fade_vert::getShader(); auto skinModelTranslucentVertex = skinModelFadeVertex; // We use the same because it ouputs world position per vertex auto skinModelNormalMapTranslucentVertex = skinModelNormalMapFadeVertex; // We use the same because it ouputs world position per vertex + // dual quaternion skinned + auto skinModelDualQuatVertex = skin_model_dq_vert::getShader(); + auto skinModelNormalMapDualQuatVertex = skin_model_normal_map_dq_vert::getShader(); + auto skinModelShadowDualQuatVertex = skin_model_shadow_dq_vert::getShader(); + auto skinModelFadeDualQuatVertex = skin_model_fade_dq_vert::getShader(); + auto skinModelNormalMapFadeDualQuatVertex = skin_model_normal_map_fade_dq_vert::getShader(); + auto skinModelTranslucentDualQuatVertex = skinModelFadeDualQuatVertex; // We use the same because it ouputs world position per vertex + auto skinModelNormalMapTranslucentDualQuatVertex = skinModelNormalMapFadeDualQuatVertex; // We use the same because it ouputs world position per vertex + auto modelFadeVertex = model_fade_vert::getShader(); auto modelNormalMapFadeVertex = model_normal_map_fade_vert::getShader(); auto simpleFadeVertex = simple_fade_vert::getShader(); @@ -376,7 +393,7 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip Key::Builder().withMaterial().withLightmap().withTangents().withSpecular().withFade(), modelLightmapNormalMapFadeVertex, modelLightmapNormalSpecularMapFadePixel, batchSetter, itemSetter); - // Skinned + // matrix palette skinned addPipeline( Key::Builder().withMaterial().withSkinned(), skinModelVertex, modelPixel, nullptr, nullptr); @@ -403,7 +420,7 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip Key::Builder().withMaterial().withSkinned().withTangents().withSpecular().withFade(), skinModelNormalMapFadeVertex, modelNormalSpecularMapFadePixel, batchSetter, itemSetter); - // Skinned and Translucent + // matrix palette skinned and translucent addPipeline( Key::Builder().withMaterial().withSkinned().withTranslucent(), skinModelTranslucentVertex, modelTranslucentPixel, nullptr, nullptr); @@ -430,6 +447,60 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip Key::Builder().withMaterial().withSkinned().withTranslucent().withTangents().withSpecular().withFade(), skinModelNormalMapFadeVertex, modelTranslucentNormalMapFadePixel, batchSetter, itemSetter); + // dual quaternion skinned + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned(), + skinModelDualQuatVertex, modelPixel, nullptr, nullptr); + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withTangents(), + skinModelNormalMapDualQuatVertex, modelNormalMapPixel, nullptr, nullptr); + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withSpecular(), + skinModelDualQuatVertex, modelSpecularMapPixel, nullptr, nullptr); + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withTangents().withSpecular(), + skinModelNormalMapDualQuatVertex, modelNormalSpecularMapPixel, nullptr, nullptr); + // Same thing but with Fade on + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withFade(), + skinModelFadeDualQuatVertex, modelFadePixel, batchSetter, itemSetter); + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withTangents().withFade(), + skinModelNormalMapFadeDualQuatVertex, modelNormalMapFadePixel, batchSetter, itemSetter); + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withSpecular().withFade(), + skinModelFadeDualQuatVertex, modelSpecularMapFadePixel, batchSetter, itemSetter); + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withTangents().withSpecular().withFade(), + skinModelNormalMapFadeDualQuatVertex, modelNormalSpecularMapFadePixel, batchSetter, itemSetter); + + // dual quaternion skinned and translucent + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withTranslucent(), + skinModelTranslucentDualQuatVertex, modelTranslucentPixel, nullptr, nullptr); + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withTranslucent().withTangents(), + skinModelNormalMapTranslucentDualQuatVertex, modelTranslucentNormalMapPixel, nullptr, nullptr); + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withTranslucent().withSpecular(), + skinModelTranslucentDualQuatVertex, modelTranslucentPixel, nullptr, nullptr); + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withTranslucent().withTangents().withSpecular(), + skinModelNormalMapTranslucentDualQuatVertex, modelTranslucentNormalMapPixel, nullptr, nullptr); + // Same thing but with Fade on + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withTranslucent().withFade(), + skinModelFadeVertex, modelTranslucentFadePixel, batchSetter, itemSetter); + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withTranslucent().withTangents().withFade(), + skinModelNormalMapFadeDualQuatVertex, modelTranslucentNormalMapFadePixel, batchSetter, itemSetter); + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withTranslucent().withSpecular().withFade(), + skinModelFadeDualQuatVertex, modelTranslucentFadePixel, batchSetter, itemSetter); + addPipeline( + Key::Builder().withMaterial().withSkinned().withDualQuatSkinned().withTranslucent().withTangents().withSpecular().withFade(), + skinModelNormalMapFadeDualQuatVertex, modelTranslucentNormalMapFadePixel, batchSetter, itemSetter); + // Depth-only addPipeline( Key::Builder().withDepthOnly(), diff --git a/libraries/render-utils/src/Shadow.slh b/libraries/render-utils/src/Shadow.slh index abb04a4498..36eb35c757 100644 --- a/libraries/render-utils/src/Shadow.slh +++ b/libraries/render-utils/src/Shadow.slh @@ -89,7 +89,9 @@ float evalShadowCascadeAttenuation(int cascadeIndex, ShadowSampleOffsets offsets float evalShadowAttenuation(vec3 worldLightDir, vec4 worldPosition, float viewDepth, vec3 worldNormal) { ShadowSampleOffsets offsets = evalShadowFilterOffsets(worldPosition); - vec4 cascadeShadowCoords[2] = { vec4(0), vec4(0) }; + vec4 cascadeShadowCoords[2]; + cascadeShadowCoords[0] = vec4(0); + cascadeShadowCoords[1] = vec4(0); ivec2 cascadeIndices; float cascadeMix = determineShadowCascadesOnPixel(worldPosition, viewDepth, cascadeShadowCoords, cascadeIndices); diff --git a/libraries/render-utils/src/Skinning.slh b/libraries/render-utils/src/Skinning.slh index 6048ba4ade..fbfe6b7185 100644 --- a/libraries/render-utils/src/Skinning.slh +++ b/libraries/render-utils/src/Skinning.slh @@ -11,18 +11,16 @@ <@if not SKINNING_SLH@> <@def SKINNING_SLH@> -// Use dual quaternion skinning -// Must match #define SKIN_DQ in Model.h -<@def SKIN_DQ@> - const int MAX_CLUSTERS = 128; const int INDICES_PER_VERTEX = 4; +<@func declareUseDualQuaternionSkinning(USE_DUAL_QUATERNION_SKINNING)@> + layout(std140) uniform skinClusterBuffer { mat4 clusterMatrices[MAX_CLUSTERS]; }; -<@if SKIN_DQ@> +<@if USE_DUAL_QUATERNION_SKINNING@> mat4 dualQuatToMat4(vec4 real, vec4 dual) { float twoRealXSq = 2.0 * real.x * real.x; @@ -211,7 +209,7 @@ void skinPositionNormalTangent(ivec4 skinClusterIndex, vec4 skinClusterWeight, v skinnedTangent = vec3(m * vec4(inTangent, 0)); } -<@else@> // SKIN_DQ +<@else@> // USE_DUAL_QUATERNION_SKINNING void skinPosition(ivec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPosition, out vec4 skinnedPosition) { vec4 newPosition = vec4(0.0, 0.0, 0.0, 0.0); @@ -260,6 +258,8 @@ void skinPositionNormalTangent(ivec4 skinClusterIndex, vec4 skinClusterWeight, v skinnedTangent = newTangent.xyz; } -<@endif@> // if SKIN_DQ +<@endif@> // if USE_DUAL_QUATERNION_SKINNING + +<@endfunc@> // func declareUseDualQuaternionSkinning(USE_DUAL_QUATERNION_SKINNING) <@endif@> // if not SKINNING_SLH diff --git a/libraries/render-utils/src/SoftAttachmentModel.cpp b/libraries/render-utils/src/SoftAttachmentModel.cpp index 0d0db7cbe3..079e6f75ef 100644 --- a/libraries/render-utils/src/SoftAttachmentModel.cpp +++ b/libraries/render-utils/src/SoftAttachmentModel.cpp @@ -52,27 +52,27 @@ void SoftAttachmentModel::updateClusterMatrices() { // TODO: cache these look-ups as an optimization int jointIndexOverride = getJointIndexOverride(cluster.jointIndex); -#if defined(SKIN_DQ) - glm::mat4 jointMatrix; - if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride.getJointStateCount()) { - jointMatrix = _rigOverride.getJointTransform(jointIndexOverride); - } else { - jointMatrix = _rig.getJointTransform(cluster.jointIndex); - } + if (_useDualQuaternionSkinning) { + glm::mat4 jointMatrix; + if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride.getJointStateCount()) { + jointMatrix = _rigOverride.getJointTransform(jointIndexOverride); + } else { + jointMatrix = _rig.getJointTransform(cluster.jointIndex); + } - glm::mat4 m; - glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, m); - state.clusterTransforms[j] = Model::TransformDualQuaternion(m); -#else - glm::mat4 jointMatrix; - if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride.getJointStateCount()) { - jointMatrix = _rigOverride.getJointTransform(jointIndexOverride); + glm::mat4 m; + glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, m); + state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(m); } else { - jointMatrix = _rig.getJointTransform(cluster.jointIndex); - } + glm::mat4 jointMatrix; + if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride.getJointStateCount()) { + jointMatrix = _rigOverride.getJointTransform(jointIndexOverride); + } else { + jointMatrix = _rig.getJointTransform(cluster.jointIndex); + } - glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterTransforms[j]); -#endif + glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); + } } } diff --git a/libraries/render-utils/src/skin_model.slv b/libraries/render-utils/src/skin_model.slv index 4236508edb..bd1655fc40 100644 --- a/libraries/render-utils/src/skin_model.slv +++ b/libraries/render-utils/src/skin_model.slv @@ -18,6 +18,7 @@ <$declareStandardTransform()$> <@include Skinning.slh@> +<$declareUseDualQuaternionSkinning()$> <@include MaterialTextures.slh@> <$declareMaterialTexMapArrayBuffer()$> diff --git a/libraries/render-utils/src/skin_model_dq.slv b/libraries/render-utils/src/skin_model_dq.slv new file mode 100644 index 0000000000..96f9b4a713 --- /dev/null +++ b/libraries/render-utils/src/skin_model_dq.slv @@ -0,0 +1,52 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// skin_model.vert +// vertex shader +// +// Created by Andrzej Kapolka on 10/14/13. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> +<@include gpu/Color.slh@> +<@include gpu/Transform.slh@> +<$declareStandardTransform()$> + +<@include Skinning.slh@> +<$declareUseDualQuaternionSkinning(1)$> + +<@include MaterialTextures.slh@> +<$declareMaterialTexMapArrayBuffer()$> + +out vec4 _position; +out vec2 _texCoord0; +out vec2 _texCoord1; +out vec3 _normal; +out vec3 _color; +out float _alpha; + +void main(void) { + vec4 position = vec4(0.0, 0.0, 0.0, 0.0); + vec3 interpolatedNormal = vec3(0.0, 0.0, 0.0); + + skinPositionNormal(inSkinClusterIndex, inSkinClusterWeight, inPosition, inNormal.xyz, position, interpolatedNormal); + + // pass along the color + _color = colorToLinearRGB(inColor.rgb); + _alpha = inColor.a; + + TexMapArray texMapArray = getTexMapArray(); + <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _texCoord0)$> + <$evalTexMapArrayTexcoord1(texMapArray, inTexCoord0, _texCoord1)$> + + // standard transform + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToEyeAndClipPos(cam, obj, position, _position, gl_Position)$> + <$transformModelToWorldDir(cam, obj, interpolatedNormal.xyz, _normal.xyz)$> +} diff --git a/libraries/render-utils/src/skin_model_fade.slv b/libraries/render-utils/src/skin_model_fade.slv index fa8e1f8991..b14bf1532e 100644 --- a/libraries/render-utils/src/skin_model_fade.slv +++ b/libraries/render-utils/src/skin_model_fade.slv @@ -18,6 +18,7 @@ <$declareStandardTransform()$> <@include Skinning.slh@> +<$declareUseDualQuaternionSkinning()$> <@include MaterialTextures.slh@> <$declareMaterialTexMapArrayBuffer()$> diff --git a/libraries/render-utils/src/skin_model_fade_dq.slv b/libraries/render-utils/src/skin_model_fade_dq.slv new file mode 100644 index 0000000000..4f8a923a03 --- /dev/null +++ b/libraries/render-utils/src/skin_model_fade_dq.slv @@ -0,0 +1,54 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// skin_model_fade.vert +// vertex shader +// +// Created by Olivier Prat on 06/045/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> +<@include gpu/Color.slh@> +<@include gpu/Transform.slh@> +<$declareStandardTransform()$> + +<@include Skinning.slh@> +<$declareUseDualQuaternionSkinning(1)$> + +<@include MaterialTextures.slh@> +<$declareMaterialTexMapArrayBuffer()$> + +out vec4 _position; +out vec2 _texCoord0; +out vec2 _texCoord1; +out vec3 _normal; +out vec3 _color; +out float _alpha; +out vec4 _worldPosition; + +void main(void) { + vec4 position = vec4(0.0, 0.0, 0.0, 0.0); + vec3 interpolatedNormal = vec3(0.0, 0.0, 0.0); + + skinPositionNormal(inSkinClusterIndex, inSkinClusterWeight, inPosition, inNormal.xyz, position, interpolatedNormal); + + // pass along the color + _color = colorToLinearRGB(inColor.rgb); + _alpha = inColor.a; + + TexMapArray texMapArray = getTexMapArray(); + <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _texCoord0)$> + <$evalTexMapArrayTexcoord1(texMapArray, inTexCoord0, _texCoord1)$> + + // standard transform + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToEyeAndClipPos(cam, obj, position, _position, gl_Position)$> + <$transformModelToWorldPos(obj, position, _worldPosition)$> + <$transformModelToWorldDir(cam, obj, interpolatedNormal.xyz, _normal.xyz)$> +} diff --git a/libraries/render-utils/src/skin_model_normal_map.slv b/libraries/render-utils/src/skin_model_normal_map.slv index 9f1087f87a..666bdf865f 100644 --- a/libraries/render-utils/src/skin_model_normal_map.slv +++ b/libraries/render-utils/src/skin_model_normal_map.slv @@ -18,6 +18,7 @@ <$declareStandardTransform()$> <@include Skinning.slh@> +<$declareUseDualQuaternionSkinning()$> <@include MaterialTextures.slh@> <$declareMaterialTexMapArrayBuffer()$> diff --git a/libraries/render-utils/src/skin_model_normal_map_dq.slv b/libraries/render-utils/src/skin_model_normal_map_dq.slv new file mode 100644 index 0000000000..02b3742f6f --- /dev/null +++ b/libraries/render-utils/src/skin_model_normal_map_dq.slv @@ -0,0 +1,61 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// skin_model_normal_map.vert +// vertex shader +// +// Created by Andrzej Kapolka on 10/29/13. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> +<@include gpu/Color.slh@> +<@include gpu/Transform.slh@> +<$declareStandardTransform()$> + +<@include Skinning.slh@> +<$declareUseDualQuaternionSkinning(1)$> + +<@include MaterialTextures.slh@> +<$declareMaterialTexMapArrayBuffer()$> + +out vec4 _position; +out vec2 _texCoord0; +out vec2 _texCoord1; +out vec3 _normal; +out vec3 _tangent; +out vec3 _color; +out float _alpha; + +void main(void) { + vec4 position = vec4(0.0, 0.0, 0.0, 0.0); + vec4 interpolatedNormal = vec4(0.0, 0.0, 0.0, 0.0); + vec4 interpolatedTangent = vec4(0.0, 0.0, 0.0, 0.0); + + skinPositionNormalTangent(inSkinClusterIndex, inSkinClusterWeight, inPosition, inNormal.xyz, inTangent.xyz, position, interpolatedNormal.xyz, interpolatedTangent.xyz); + + // pass along the color + _color = colorToLinearRGB(inColor.rgb); + _alpha = inColor.a; + + TexMapArray texMapArray = getTexMapArray(); + <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _texCoord0)$> + <$evalTexMapArrayTexcoord1(texMapArray, inTexCoord0, _texCoord1)$> + + interpolatedNormal = vec4(normalize(interpolatedNormal.xyz), 0.0); + interpolatedTangent = vec4(normalize(interpolatedTangent.xyz), 0.0); + + // standard transform + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToEyeAndClipPos(cam, obj, position, _position, gl_Position)$> + <$transformModelToWorldDir(cam, obj, interpolatedNormal.xyz, interpolatedNormal.xyz)$> + <$transformModelToWorldDir(cam, obj, interpolatedTangent.xyz, interpolatedTangent.xyz)$> + + _normal = interpolatedNormal.xyz; + _tangent = interpolatedTangent.xyz; +} diff --git a/libraries/render-utils/src/skin_model_normal_map_fade.slv b/libraries/render-utils/src/skin_model_normal_map_fade.slv index 4e638866fc..d72e47702d 100644 --- a/libraries/render-utils/src/skin_model_normal_map_fade.slv +++ b/libraries/render-utils/src/skin_model_normal_map_fade.slv @@ -18,6 +18,7 @@ <$declareStandardTransform()$> <@include Skinning.slh@> +<$declareUseDualQuaternionSkinning()$> <@include MaterialTextures.slh@> <$declareMaterialTexMapArrayBuffer()$> diff --git a/libraries/render-utils/src/skin_model_normal_map_fade_dq.slv b/libraries/render-utils/src/skin_model_normal_map_fade_dq.slv new file mode 100644 index 0000000000..02b3742f6f --- /dev/null +++ b/libraries/render-utils/src/skin_model_normal_map_fade_dq.slv @@ -0,0 +1,61 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// skin_model_normal_map.vert +// vertex shader +// +// Created by Andrzej Kapolka on 10/29/13. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> +<@include gpu/Color.slh@> +<@include gpu/Transform.slh@> +<$declareStandardTransform()$> + +<@include Skinning.slh@> +<$declareUseDualQuaternionSkinning(1)$> + +<@include MaterialTextures.slh@> +<$declareMaterialTexMapArrayBuffer()$> + +out vec4 _position; +out vec2 _texCoord0; +out vec2 _texCoord1; +out vec3 _normal; +out vec3 _tangent; +out vec3 _color; +out float _alpha; + +void main(void) { + vec4 position = vec4(0.0, 0.0, 0.0, 0.0); + vec4 interpolatedNormal = vec4(0.0, 0.0, 0.0, 0.0); + vec4 interpolatedTangent = vec4(0.0, 0.0, 0.0, 0.0); + + skinPositionNormalTangent(inSkinClusterIndex, inSkinClusterWeight, inPosition, inNormal.xyz, inTangent.xyz, position, interpolatedNormal.xyz, interpolatedTangent.xyz); + + // pass along the color + _color = colorToLinearRGB(inColor.rgb); + _alpha = inColor.a; + + TexMapArray texMapArray = getTexMapArray(); + <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _texCoord0)$> + <$evalTexMapArrayTexcoord1(texMapArray, inTexCoord0, _texCoord1)$> + + interpolatedNormal = vec4(normalize(interpolatedNormal.xyz), 0.0); + interpolatedTangent = vec4(normalize(interpolatedTangent.xyz), 0.0); + + // standard transform + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToEyeAndClipPos(cam, obj, position, _position, gl_Position)$> + <$transformModelToWorldDir(cam, obj, interpolatedNormal.xyz, interpolatedNormal.xyz)$> + <$transformModelToWorldDir(cam, obj, interpolatedTangent.xyz, interpolatedTangent.xyz)$> + + _normal = interpolatedNormal.xyz; + _tangent = interpolatedTangent.xyz; +} diff --git a/libraries/render-utils/src/skin_model_shadow.slv b/libraries/render-utils/src/skin_model_shadow.slv index 6684cfea80..03da2e074e 100644 --- a/libraries/render-utils/src/skin_model_shadow.slv +++ b/libraries/render-utils/src/skin_model_shadow.slv @@ -17,6 +17,7 @@ <$declareStandardTransform()$> <@include Skinning.slh@> +<$declareUseDualQuaternionSkinning()$> void main(void) { vec4 position = vec4(0.0, 0.0, 0.0, 0.0); diff --git a/libraries/render-utils/src/skin_model_shadow_dq.slv b/libraries/render-utils/src/skin_model_shadow_dq.slv new file mode 100644 index 0000000000..74cd4076bc --- /dev/null +++ b/libraries/render-utils/src/skin_model_shadow_dq.slv @@ -0,0 +1,30 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// skin_model_shadow.vert +// vertex shader +// +// Created by Andrzej Kapolka on 3/24/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> +<@include gpu/Transform.slh@> +<$declareStandardTransform()$> + +<@include Skinning.slh@> +<$declareUseDualQuaternionSkinning(1)$> + +void main(void) { + vec4 position = vec4(0.0, 0.0, 0.0, 0.0); + skinPosition(inSkinClusterIndex, inSkinClusterWeight, inPosition, position); + + // standard transform + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToClipPos(cam, obj, position, gl_Position)$> +} diff --git a/libraries/render-utils/src/skin_model_shadow_fade.slv b/libraries/render-utils/src/skin_model_shadow_fade.slv index 7b27263569..d2e79f9d74 100644 --- a/libraries/render-utils/src/skin_model_shadow_fade.slv +++ b/libraries/render-utils/src/skin_model_shadow_fade.slv @@ -17,6 +17,7 @@ <$declareStandardTransform()$> <@include Skinning.slh@> +<$declareUseDualQuaternionSkinning()$> out vec4 _worldPosition; diff --git a/libraries/render-utils/src/skin_model_shadow_fade_dq.slv b/libraries/render-utils/src/skin_model_shadow_fade_dq.slv new file mode 100644 index 0000000000..fb9c60eefd --- /dev/null +++ b/libraries/render-utils/src/skin_model_shadow_fade_dq.slv @@ -0,0 +1,33 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// skin_model_shadow_fade.vert +// vertex shader +// +// Created by Olivier Prat on 06/045/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> +<@include gpu/Transform.slh@> +<$declareStandardTransform()$> + +<@include Skinning.slh@> +<$declareUseDualQuaternionSkinning(1)$> + +out vec4 _worldPosition; + +void main(void) { + vec4 position = vec4(0.0, 0.0, 0.0, 0.0); + skinPosition(inSkinClusterIndex, inSkinClusterWeight, inPosition, position); + + // standard transform + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToClipPos(cam, obj, position, gl_Position)$> + <$transformModelToWorldPos(obj, position, _worldPosition)$> +} diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index 1dd9f5da49..f175bab99a 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -32,6 +32,7 @@ public: SPECULAR, UNLIT, SKINNED, + DUAL_QUAT_SKINNED, DEPTH_ONLY, DEPTH_BIAS, WIREFRAME, @@ -80,6 +81,7 @@ public: Builder& withSpecular() { _flags.set(SPECULAR); return (*this); } Builder& withUnlit() { _flags.set(UNLIT); return (*this); } Builder& withSkinned() { _flags.set(SKINNED); return (*this); } + Builder& withDualQuatSkinned() { _flags.set(DUAL_QUAT_SKINNED); return (*this); } Builder& withDepthOnly() { _flags.set(DEPTH_ONLY); return (*this); } Builder& withDepthBias() { _flags.set(DEPTH_BIAS); return (*this); } Builder& withWireframe() { _flags.set(WIREFRAME); return (*this); } @@ -133,6 +135,9 @@ public: Builder& withSkinned() { _flags.set(SKINNED); _mask.set(SKINNED); return (*this); } Builder& withoutSkinned() { _flags.reset(SKINNED); _mask.set(SKINNED); return (*this); } + Builder& withDualQuatSkinned() { _flags.set(DUAL_QUAT_SKINNED); _mask.set(SKINNED); return (*this); } + Builder& withoutDualQuatSkinned() { _flags.reset(DUAL_QUAT_SKINNED); _mask.set(SKINNED); return (*this); } + Builder& withDepthOnly() { _flags.set(DEPTH_ONLY); _mask.set(DEPTH_ONLY); return (*this); } Builder& withoutDepthOnly() { _flags.reset(DEPTH_ONLY); _mask.set(DEPTH_ONLY); return (*this); } diff --git a/libraries/script-engine/src/AssetScriptingInterface.cpp b/libraries/script-engine/src/AssetScriptingInterface.cpp index 1c811573fb..68c24ecc33 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.cpp +++ b/libraries/script-engine/src/AssetScriptingInterface.cpp @@ -440,7 +440,7 @@ void AssetScriptingInterface::saveToCache(const QUrl& rawURL, const QByteArray& JS_VERIFY(url.scheme() == "atp" || url.scheme() == "cache", "only 'atp' and 'cache' URL schemes supported"); JS_VERIFY(hash.isEmpty() || hash == hashDataHex(data), QString("invalid checksum hash for atp:HASH style URL (%1 != %2)").arg(hash, hashDataHex(data))); - qCDebug(scriptengine) << "saveToCache" << url.toDisplayString() << data << hash << metadata; + // qCDebug(scriptengine) << "saveToCache" << url.toDisplayString() << data << hash << metadata; jsPromiseReady(Parent::saveToCache(url, data, metadata), scope, callback); } diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 78cb05fa0d..871705d74b 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -132,6 +132,20 @@ QUrl expandScriptUrl(const QUrl& rawScriptURL) { QObject* scriptsModel(); +bool NativeScriptInitializers::registerNativeScriptInitializer(NativeScriptInitializer initializer) { + return registerScriptInitializer([=](ScriptEnginePointer engine) { + initializer(qobject_cast(engine.data())); + }); +} + +bool NativeScriptInitializers::registerScriptInitializer(ScriptInitializer initializer) { + if (auto scriptEngines = DependencyManager::get().data()) { + scriptEngines->registerScriptInitializer(initializer); + return true; + } + return false; +} + void ScriptEngines::registerScriptInitializer(ScriptInitializer initializer) { _scriptInitializers.push_back(initializer); } @@ -520,6 +534,16 @@ void ScriptEngines::onScriptEngineLoaded(const QString& rawScriptURL) { emit scriptCountChanged(); } +int ScriptEngines::runScriptInitializers(ScriptEnginePointer scriptEngine) { + int ii=0; + for (auto initializer : _scriptInitializers) { + ii++; + qDebug() << "initializer" << ii; + initializer(scriptEngine); + } + return ii; +} + void ScriptEngines::launchScriptEngine(ScriptEnginePointer scriptEngine) { connect(scriptEngine.data(), &ScriptEngine::finished, this, &ScriptEngines::onScriptFinished, Qt::DirectConnection); connect(scriptEngine.data(), &ScriptEngine::loadScript, [&](const QString& scriptName, bool userLoaded) { @@ -530,9 +554,7 @@ void ScriptEngines::launchScriptEngine(ScriptEnginePointer scriptEngine) { }); // register our application services and set it off on its own thread - for (auto initializer : _scriptInitializers) { - initializer(scriptEngine); - } + runScriptInitializers(scriptEngine); // FIXME disabling 'shift key' debugging for now. If you start up the application with // the shift key held down, it triggers a deadlock because of script interfaces running diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 5a4b8f2f47..ea07ebe840 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -19,6 +19,7 @@ #include #include +#include #include "ScriptEngine.h" #include "ScriptsModel.h" @@ -26,6 +27,12 @@ class ScriptEngine; +class NativeScriptInitializers : public ScriptInitializerMixin { +public: + bool registerNativeScriptInitializer(NativeScriptInitializer initializer) override; + bool registerScriptInitializer(ScriptInitializer initializer) override; +}; + class ScriptEngines : public QObject, public Dependency { Q_OBJECT @@ -34,11 +41,11 @@ class ScriptEngines : public QObject, public Dependency { Q_PROPERTY(QString debugScriptUrl READ getDebugScriptUrl WRITE setDebugScriptUrl) public: - using ScriptInitializer = std::function; + using ScriptInitializer = ScriptInitializerMixin::ScriptInitializer; ScriptEngines(ScriptEngine::Context context); void registerScriptInitializer(ScriptInitializer initializer); - + int runScriptInitializers(ScriptEnginePointer engine); void loadScripts(); void saveScripts(); diff --git a/libraries/shared/src/DualQuaternion.h b/libraries/shared/src/DualQuaternion.h index 709c089fdc..af1011a6d8 100644 --- a/libraries/shared/src/DualQuaternion.h +++ b/libraries/shared/src/DualQuaternion.h @@ -55,7 +55,7 @@ protected: inline QDebug operator<<(QDebug debug, const DualQuaternion& dq) { - debug << "AnimPose, real = (" << dq._real.x << dq._real.y << dq._real.z << dq._real.w << "), dual = (" << dq._dual.x << dq._dual.y << dq._dual.z << dq._dual.w << ")"; + debug << "DualQuaternion, real = (" << dq._real.x << dq._real.y << dq._real.z << dq._real.w << "), dual = (" << dq._dual.x << dq._dual.y << dq._dual.z << dq._dual.w << ")"; return debug; } diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index ff1d29eed1..72710a6a7d 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -601,3 +601,6 @@ glm::vec3 randVector() { return glm::vec3(randFloat() - 0.5f, randFloat() - 0.5f, randFloat() - 0.5f) * 2.0f; } +bool isNonUniformScale(const glm::vec3& scale) { + return fabsf(scale.x - scale.y) > EPSILON || fabsf(scale.y - scale.z) > EPSILON || fabsf(scale.z - scale.x) > EPSILON; +} diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index 4f761a4aac..5c9a8b5ca1 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -260,6 +260,8 @@ glm::mat4 orthoInverse(const glm::mat4& m); // Return a random vector of average length 1 glm::vec3 randVector(); +bool isNonUniformScale(const glm::vec3& scale); + // // Safe replacement of glm_mat4_mul() for unaligned arguments instead of __m128 // diff --git a/libraries/shared/src/GenericThread.cpp b/libraries/shared/src/GenericThread.cpp index 2e126f12c9..230b9590f1 100644 --- a/libraries/shared/src/GenericThread.cpp +++ b/libraries/shared/src/GenericThread.cpp @@ -37,7 +37,7 @@ void GenericThread::initialize(bool isThreaded, QThread::Priority priority) { // match the thread name to our object name _thread->setObjectName(objectName()); - // when the worker thread is started, call our engine's run.. + connect(_thread, &QThread::started, this, &GenericThread::started); connect(_thread, &QThread::started, this, &GenericThread::threadRoutine); connect(_thread, &QThread::finished, this, &GenericThread::finished); diff --git a/libraries/shared/src/GenericThread.h b/libraries/shared/src/GenericThread.h index 09872b32cd..c1f946d6aa 100644 --- a/libraries/shared/src/GenericThread.h +++ b/libraries/shared/src/GenericThread.h @@ -47,6 +47,7 @@ public slots: void threadRoutine(); signals: + void started(); void finished(); protected: diff --git a/libraries/shared/src/LogHandler.cpp b/libraries/shared/src/LogHandler.cpp index aa67c14c4b..cb3c0d07b2 100644 --- a/libraries/shared/src/LogHandler.cpp +++ b/libraries/shared/src/LogHandler.cpp @@ -14,6 +14,10 @@ #include +#ifdef Q_OS_WIN +#include +#endif + #include #include #include @@ -184,8 +188,13 @@ QString LogHandler::printMessage(LogMsgType type, const QMessageLogContext& cont } } - QString logMessage = QString("%1 %2").arg(prefixString, message.split('\n').join('\n' + prefixString + " ")); - fprintf(stdout, "%s\n", qPrintable(logMessage)); + QString logMessage = QString("%1 %2\n").arg(prefixString, message.split('\n').join('\n' + prefixString + " ")); + + fprintf(stdout, "%s", qPrintable(logMessage)); +#ifdef Q_OS_WIN + // On windows, this will output log lines into the Visual Studio "output" tab + OutputDebugStringA(qPrintable(logMessage)); +#endif return logMessage; } diff --git a/libraries/shared/src/SettingInterface.cpp b/libraries/shared/src/SettingInterface.cpp index 327668574e..04da35656e 100644 --- a/libraries/shared/src/SettingInterface.cpp +++ b/libraries/shared/src/SettingInterface.cpp @@ -23,51 +23,69 @@ #include "SharedUtil.h" namespace Setting { - static QSharedPointer globalManager; + // This should only run as a post-routine in the QCoreApplication destructor + void cleanupSettingsSaveThread() { + auto globalManager = DependencyManager::get(); + Q_ASSERT(qApp && globalManager); - // cleans up the settings private instance. Should only be run once at closing down. - void cleanupPrivateInstance() { - // grab the thread before we nuke the instance - QThread* settingsManagerThread = DependencyManager::get()->thread(); + // Grab the settings thread to shut it down + QThread* settingsManagerThread = globalManager->thread(); - // tell the private instance to clean itself up on its thread - DependencyManager::destroy(); - - globalManager.reset(); - - // quit the settings manager thread and wait on it to make sure it's gone + // Quit the settings manager thread and wait for it so we + // don't get concurrent accesses when we save all settings below settingsManagerThread->quit(); settingsManagerThread->wait(); + + // [IMPORTANT] Save all settings when the QApplication goes down + globalManager->saveAll(); + + qCDebug(shared) << "Settings thread stopped."; } - void setupPrivateInstance() { - // Ensure Setting::init has already ran and qApp exists - if (qApp && globalManager) { - // Let's set up the settings Private instance on its own thread - QThread* thread = new QThread(); - Q_CHECK_PTR(thread); - thread->setObjectName("Settings Thread"); + // This should only run as a pre-routine in the QCoreApplication constructor + void setupSettingsSaveThread() { + auto globalManager = DependencyManager::get(); + Q_ASSERT(qApp && globalManager); - QObject::connect(thread, SIGNAL(started()), globalManager.data(), SLOT(startTimer())); - QObject::connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); - QObject::connect(thread, SIGNAL(finished()), globalManager.data(), SLOT(deleteLater())); - globalManager->moveToThread(thread); - thread->start(); - qCDebug(shared) << "Settings thread started."; + // Let's set up the settings private instance on its own thread + QThread* thread = new QThread(qApp); + Q_CHECK_PTR(thread); + thread->setObjectName("Settings Thread"); - // Register cleanupPrivateInstance to run inside QCoreApplication's destructor. - qAddPostRoutine(cleanupPrivateInstance); - } + // Setup setting periodical save timer + QObject::connect(thread, &QThread::started, globalManager.data(), &Manager::startTimer); + QObject::connect(thread, &QThread::finished, globalManager.data(), &Manager::stopTimer); + + // Setup manager threading affinity + // This makes the timer fire on the settings thread so we don't block the main + // thread with a lot of file I/O. + // We bring back the manager to the main thread when the QApplication goes down + globalManager->moveToThread(thread); + QObject::connect(thread, &QThread::finished, globalManager.data(), [] { + auto globalManager = DependencyManager::get(); + Q_ASSERT(qApp && globalManager); + + // Move manager back to the main thread (has to be done on owning thread) + globalManager->moveToThread(qApp->thread()); + }); + + // Start the settings save thread + thread->start(); + qCDebug(shared) << "Settings thread started."; + + // Register cleanupSettingsSaveThread to run inside QCoreApplication's destructor. + // This will cleanup the settings thread and save all settings before shut down. + qAddPostRoutine(cleanupSettingsSaveThread); } - FIXED_Q_COREAPP_STARTUP_FUNCTION(setupPrivateInstance) - // Sets up the settings private instance. Should only be run once at startup. preInit() must be run beforehand, + // Sets up the settings private instance. Should only be run once at startup. void init() { // Set settings format QSettings::setDefaultFormat(JSON_FORMAT); QSettings settings; qCDebug(shared) << "Settings file:" << settings.fileName(); + // Backward compatibility for old settings file if (settings.allKeys().isEmpty()) { loadOldINIFile(settings); } @@ -80,11 +98,13 @@ namespace Setting { qCDebug(shared) << (deleted ? "Deleted" : "Failed to delete") << "settings lock file" << settingsLockFilename; } - globalManager = DependencyManager::set(); + // Setup settings manager, the manager will live until the process shuts down + DependencyManager::set(); - setupPrivateInstance(); + // Add pre-routine to setup threading + qAddPreRoutine(setupSettingsSaveThread); } - + void Interface::init() { if (!DependencyManager::isSet()) { // WARNING: As long as we are using QSettings this should always be triggered for each Setting::Handle diff --git a/libraries/shared/src/SettingManager.cpp b/libraries/shared/src/SettingManager.cpp index 6c246d4cea..2e0850255a 100644 --- a/libraries/shared/src/SettingManager.cpp +++ b/libraries/shared/src/SettingManager.cpp @@ -23,11 +23,7 @@ namespace Setting { // Cleanup timer stopTimer(); delete _saveTimer; - - // Save all settings before exit - saveAll(); - - // sync will be called in the QSettings destructor + _saveTimer = nullptr; } // Custom deleter does nothing, because we need to shutdown later than the dependency manager diff --git a/libraries/shared/src/SettingManager.h b/libraries/shared/src/SettingManager.h index ffdd4ba42a..6696a1ecf4 100644 --- a/libraries/shared/src/SettingManager.h +++ b/libraries/shared/src/SettingManager.h @@ -50,8 +50,8 @@ namespace Setting { QHash _pendingChanges; friend class Interface; - friend void cleanupPrivateInstance(); - friend void setupPrivateInstance(); + friend void cleanupSettingsSaveThread(); + friend void setupSettingsSaveThread(); }; } diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 8e5c30711c..7c4a9b6d6c 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -59,16 +59,32 @@ extern "C" FILE * __cdecl __iob_func(void) { #include #include +#include + +#include "LogHandler.h" #include "NumericalConstants.h" #include "OctalCode.h" #include "SharedLogging.h" +// Global instances are stored inside the QApplication properties +// to provide a single instance across DLL boundaries. +// This is something we cannot do here since several DLLs +// and our main binaries statically link this "shared" library +// resulting in multiple static memory blocks in different constexts +// But we need to be able to use global instances before the QApplication +// is setup, so to accomplish that we stage the global instances in a local +// map and setup a pre routine (commitGlobalInstances) that will run in the +// QApplication constructor and commit all the staged instances to the +// QApplication properties. +// Note: One of the side effects of this, is that no DLL loaded before +// the QApplication is constructed, can expect to access the existing staged +// global instanced. For this reason, we advise all DLLs be loaded after +// the QApplication is instanced. +static std::mutex stagedGlobalInstancesMutex; static std::unordered_map stagedGlobalInstances; - std::mutex& globalInstancesMutex() { - static std::mutex mutex; - return mutex; + return stagedGlobalInstancesMutex; } static void commitGlobalInstances() { @@ -78,7 +94,13 @@ static void commitGlobalInstances() { } stagedGlobalInstances.clear(); } -FIXED_Q_COREAPP_STARTUP_FUNCTION(commitGlobalInstances) + +// This call is necessary for global instances to work across DLL boundaries +// Ideally, this founction would be called at the top of the main function. +// See description at the top of the file. +void setupGlobalInstances() { + qAddPreRoutine(commitGlobalInstances); +} QVariant getGlobalInstance(const char* propertyName) { if (qApp) { @@ -105,7 +127,7 @@ void usecTimestampNowForceClockSkew(qint64 clockSkew) { ::usecTimestampNowAdjust = clockSkew; } -static qint64 TIME_REFERENCE = 0; // in usec +static std::atomic TIME_REFERENCE { 0 }; // in usec static std::once_flag usecTimestampNowIsInitialized; static QElapsedTimer timestampTimer; @@ -771,6 +793,10 @@ QString formatUsecTime(double usecs) { return formatUsecTime(usecs); } +QString formatSecTime(qint64 secs) { + return formatUsecTime(secs * 1000000); +} + QString formatSecondsElapsed(float seconds) { QString result; @@ -813,8 +839,8 @@ bool similarStrings(const QString& stringA, const QString& stringB) { } void disableQtBearerPoll() { - // to disable the Qt constant wireless scanning, set the env for polling interval - qDebug() << "Disabling Qt wireless polling by using a negative value for QTimer::setInterval"; + // To disable the Qt constant wireless scanning, set the env for polling interval to -1 + // The constant polling causes ping spikes on windows every 10 seconds or so that affect the audio const QByteArray DISABLE_BEARER_POLL_TIMEOUT = QString::number(-1).toLocal8Bit(); qputenv("QT_BEARER_POLL_TIMEOUT", DISABLE_BEARER_POLL_TIMEOUT); } @@ -1176,6 +1202,32 @@ void watchParentProcess(int parentPID) { } #endif +void setupHifiApplication(QString applicationName) { + disableQtBearerPoll(); // Fixes wifi ping spikes + + // Those calls are necessary to format the log correctly + // and to direct the application to the correct location + // for read/writes into AppData and other platform equivalents. + QCoreApplication::setApplicationName(applicationName); + QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); + QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); + QCoreApplication::setApplicationVersion(BuildInfo::VERSION); + + // This ensures the global instances mechanism is correctly setup. + // You can find more details as to why this is important in the SharedUtil.h/cpp files + setupGlobalInstances(); + +#ifndef WIN32 + // Windows tends to hold onto log lines until it has a sizeable buffer + // This makes the log feel unresponsive and trap useful log data in the log buffer + // when a crash occurs. + //Force windows to flush the buffer on each new line character to avoid this. + setvbuf(stdout, NULL, _IOLBF, 0); +#endif + + // Install the standard hifi message handler so we get consistant log formatting + qInstallMessageHandler(LogHandler::verboseMessageHandler); +} #ifdef Q_OS_WIN QString getLastErrorAsString() { diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index 5a1e48d9c0..6e00e5c090 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -25,23 +25,6 @@ #include #include -// Workaround for https://bugreports.qt.io/browse/QTBUG-54479 -// Wrap target function inside another function that holds -// a unique string identifier and uses it to ensure it only runs once -// by storing a state within the qApp -// We cannot used std::call_once with a static once_flag because -// this is used in shared libraries that are linked by several DLLs -// (ie. plugins), meaning the static will be useless in that case -#define FIXED_Q_COREAPP_STARTUP_FUNCTION(AFUNC) \ - static void AFUNC ## _fixed() { \ - const auto propertyName = std::string(Q_FUNC_INFO) + __FILE__; \ - if (!qApp->property(propertyName.c_str()).toBool()) { \ - AFUNC(); \ - qApp->setProperty(propertyName.c_str(), QVariant(true)); \ - } \ - } \ - Q_COREAPP_STARTUP_FUNCTION(AFUNC ## _fixed) - // When writing out avatarEntities to a QByteArray, if the parentID is the ID of MyAvatar, use this ID instead. This allows // the value to be reset when the sessionID changes. const QUuid AVATAR_SELF_ID = QUuid("{00000000-0000-0000-0000-000000000001}"); @@ -53,20 +36,11 @@ std::unique_ptr& globalInstancePointer() { return instancePtr; } -template -void setGlobalInstance(const char* propertyName, T* instance) { - globalInstancePointer().reset(instance); -} - -template -bool destroyGlobalInstance() { - std::unique_ptr& instancePtr = globalInstancePointer(); - if (instancePtr.get()) { - instancePtr.reset(); - return true; - } - return false; -} +// Sets up the global instances for use +// This NEEDS to be called on startup +// for any binary planing on using global instances +// More details in cpp file +void setupGlobalInstances(); std::mutex& globalInstancesMutex(); QVariant getGlobalInstance(const char* propertyName); @@ -78,7 +52,6 @@ void setGlobalInstance(const char* propertyName, const QVariant& variant); template T* globalInstance(const char* propertyName, Args&&... args) { static T* resultInstance { nullptr }; - static std::mutex mutex; if (!resultInstance) { std::unique_lock lock(globalInstancesMutex()); if (!resultInstance) { @@ -216,6 +189,7 @@ QString formatUsecTime(float usecs); QString formatUsecTime(double usecs); QString formatUsecTime(quint64 usecs); QString formatUsecTime(qint64 usecs); +QString formatSecTime(qint64 secs); QString formatSecondsElapsed(float seconds); bool similarStrings(const QString& stringA, const QString& stringB); @@ -260,6 +234,7 @@ void watchParentProcess(int parentPID); bool processIsRunning(int64_t pid); +void setupHifiApplication(QString applicationName); #ifdef Q_OS_WIN void* createProcessGroup(); diff --git a/libraries/shared/src/shared/ScriptInitializerMixin.h b/libraries/shared/src/shared/ScriptInitializerMixin.h new file mode 100644 index 0000000000..50de553b0b --- /dev/null +++ b/libraries/shared/src/shared/ScriptInitializerMixin.h @@ -0,0 +1,38 @@ +// +// ScriptInitializerMixin.h +// libraries/shared/src/shared +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. + +#pragma once + +#include +#include +#include "../DependencyManager.h" + +class QScriptEngine; +class ScriptEngine; + +class ScriptInitializerMixin : public QObject, public Dependency { + Q_OBJECT +public: + // Lightweight `QScriptEngine*` initializer (only depends on built-in Qt components) + // example registration: + // eg: [&](QScriptEngine* engine) -> bool { + // engine->globalObject().setProperties("API", engine->newQObject(...instance...)) + // return true; + // } + using NativeScriptInitializer = std::function; + virtual bool registerNativeScriptInitializer(NativeScriptInitializer initializer) = 0; + + // Heavyweight `ScriptEngine*` initializer (tightly coupled to Interface and script-engine library internals) + // eg: [&](ScriptEnginePointer scriptEngine) -> bool { + // engine->registerGlobalObject("API", ...instance..); + // return true; + // } + using ScriptEnginePointer = QSharedPointer; + using ScriptInitializer = std::function; + virtual bool registerScriptInitializer(ScriptInitializer initializer) { return false; }; +}; diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index e45a35a0fc..7a938f39c8 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -389,19 +389,6 @@ QString OffscreenUi::getItem(const Icon icon, const QString& title, const QStrin return result.toString(); } -QVariant OffscreenUi::getCustomInfo(const Icon icon, const QString& title, const QVariantMap& config, bool* ok) { - if (ok) { - *ok = false; - } - - QVariant result = DependencyManager::get()->customInputDialog(icon, title, config); - if (ok && result.isValid()) { - *ok = true; - } - - return result; -} - ModalDialogListener* OffscreenUi::getTextAsync(const Icon icon, const QString& title, const QString& label, const QString& text) { return DependencyManager::get()->inputDialogAsync(icon, title, label, text); } @@ -423,10 +410,6 @@ ModalDialogListener* OffscreenUi::getItemAsync(const Icon icon, const QString& t return inputDialogListener; } -ModalDialogListener* OffscreenUi::getCustomInfoAsync(const Icon icon, const QString& title, const QVariantMap& config) { - return DependencyManager::get()->customInputDialogAsync(icon, title, config); -} - QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current) { if (QThread::currentThread() != thread()) { QVariant result; @@ -642,8 +625,7 @@ private: auto windows = qApp->topLevelWindows(); QWindow* result = nullptr; for (auto window : windows) { - QVariant isMainWindow = window->property("MainWindow"); - if (!qobject_cast(window)) { + if (window->objectName().contains("MainWindow")) { result = window; break; } diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index e507333840..cb8ee29068 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -230,10 +230,8 @@ public: static QString getText(const Icon icon, const QString & title, const QString & label, const QString & text = QString(), bool * ok = 0); static QString getItem(const Icon icon, const QString & title, const QString & label, const QStringList & items, int current = 0, bool editable = true, bool * ok = 0); - static QVariant getCustomInfo(const Icon icon, const QString& title, const QVariantMap& config, bool* ok = 0); static ModalDialogListener* getTextAsync(const Icon icon, const QString & title, const QString & label, const QString & text = QString()); static ModalDialogListener* getItemAsync(const Icon icon, const QString & title, const QString & label, const QStringList & items, int current = 0, bool editable = true); - static ModalDialogListener* getCustomInfoAsync(const Icon icon, const QString& title, const QVariantMap& config); unsigned int getMenuUserDataId() const; QList &getModalDialogListeners(); diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index b600fc7b29..16af862324 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -268,16 +268,6 @@ bool Menu::isOptionChecked(const QString& menuOption) const { return false; } -void Menu::closeInfoView(const QString& path) { - auto offscreenUi = DependencyManager::get(); - offscreenUi->hide(path); -} - -bool Menu::isInfoViewVisible(const QString& path) { - auto offscreenUi = DependencyManager::get(); - return offscreenUi->isVisible(path); -} - void Menu::triggerOption(const QString& menuOption) { QAction* action = _actionHash.value(menuOption); if (action) { @@ -538,24 +528,6 @@ void Menu::setGroupingIsVisible(const QString& grouping, bool isVisible) { QMenuBar::repaint(); } -void Menu::addActionGroup(const QString& groupName, const QStringList& actionList, const QString& selected, QObject* receiver, const char* slot) { - auto menu = addMenu(groupName); - - QActionGroup* actionGroup = new QActionGroup(menu); - actionGroup->setExclusive(true); - - for (auto action : actionList) { - auto item = addCheckableActionToQMenuAndActionHash(menu, action, 0, action == selected, receiver, slot); - actionGroup->addAction(item); - } - - QMenuBar::repaint(); -} - -void Menu::removeActionGroup(const QString& groupName) { - removeMenu(groupName); -} - MenuWrapper::MenuWrapper(ui::Menu& rootMenu, QMenu* menu) : _rootMenu(rootMenu), _realMenu(menu) { auto offscreenUi = DependencyManager::get(); offscreenUi->addMenuInitializer([=](VrMenu* vrMenu) { diff --git a/libraries/ui/src/ui/Menu.h b/libraries/ui/src/ui/Menu.h index 25f8f74063..2977a5330a 100644 --- a/libraries/ui/src/ui/Menu.h +++ b/libraries/ui/src/ui/Menu.h @@ -110,9 +110,6 @@ public slots: void removeSeparator(const QString& menuName, const QString& separatorName); void removeMenuItem(const QString& menuName, const QString& menuitem); bool menuItemExists(const QString& menuName, const QString& menuitem); - void addActionGroup(const QString& groupName, const QStringList& actionList, const QString& selected = QString(), - QObject* receiver = nullptr, const char* slot = nullptr); - void removeActionGroup(const QString& groupName); bool isOptionChecked(const QString& menuOption) const; void setIsOptionChecked(const QString& menuOption, bool isChecked); @@ -125,9 +122,6 @@ public slots: void toggleDeveloperMenus(); void toggleAdvancedMenus(); - bool isInfoViewVisible(const QString& path); - void closeInfoView(const QString& path); - void triggerOption(const QString& menuOption); static bool isSomeSubmenuShown() { return _isSomeSubmenuShown; } diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index ea34f3de76..749a60a578 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -305,7 +305,6 @@ void OffscreenQmlSurface::onItemCreated(QQmlContext* qmlContext, QQuickItem* new qmlContext->setContextProperty("eventBridgeWrapper", new EventBridgeWrapper(eventBridge, qmlContext)); } - connect(newItem, SIGNAL(sendToScript(QVariant)), this, SIGNAL(fromQml(QVariant))); } void OffscreenQmlSurface::onRootCreated() { diff --git a/scripts/developer/debugging/queryAACubeInspector.js b/scripts/developer/debugging/queryAACubeInspector.js index 2d585ffce4..d8a87c3cf5 100644 --- a/scripts/developer/debugging/queryAACubeInspector.js +++ b/scripts/developer/debugging/queryAACubeInspector.js @@ -40,7 +40,6 @@ function updateOverlay(entityID, queryAACube) { blue: 255 }, alpha: 1, - // borderSize: ..., solid: false }); } diff --git a/scripts/system/audio.js b/scripts/system/audio.js index a93177ca38..ee82c0c6ea 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -42,8 +42,9 @@ function onClicked() { // for toolbar-mode: go back to home screen, this will close the window. tablet.gotoHomeScreen(); } else { - var entity = HMD.tabletID; - Entities.editEntity(entity, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); + if (HMD.tabletID) { + Entities.editEntity(HMD.tabletID, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); + } tablet.loadQMLSource(AUDIO_QML_SOURCE); } } diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 8cf5b72b9a..26ffb08796 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -655,6 +655,9 @@ case 'goToPurchases': tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); break; + case 'goToMarketplaceMainPage': + tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); + break; case 'goToMarketplaceItemPage': tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); break; @@ -688,6 +691,13 @@ updateSendMoneyParticleEffect(); sendMoneyParticleEffectUpdateTimer = Script.setInterval(updateSendMoneyParticleEffect, SEND_MONEY_PARTICLE_TIMER_UPDATE); break; + case 'transactionHistory_goToBank': + if (Account.metaverseServerURL.indexOf("staging") >= 0) { + Window.location = "hifi://hifiqa-master-metaverse-staging"; // So that we can test in staging. + } else { + Window.location = "hifi://BankOfHighFidelity"; + } + break; default: print('Unrecognized message from QML:', JSON.stringify(message)); } diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 16f1d086b7..345ab33c0d 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -146,7 +146,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }; this.setIgnorePointerItems = function() { - if (HMD.tabletID !== this.tabletID) { + if (HMD.tabletID && HMD.tabletID !== this.tabletID) { this.tabletID = HMD.tabletID; Pointers.setIgnoreItems(_this.leftPointer, _this.blacklist); Pointers.setIgnoreItems(_this.rightPointer, _this.blacklist); @@ -479,7 +479,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }; } function mouseReleaseOnOverlay(overlayID, event) { - if (overlayID === HMD.homeButtonID && event.button === "Primary") { + if (HMD.homeButtonID && overlayID === HMD.homeButtonID && event.button === "Primary") { Messages.sendLocalMessage("home", overlayID); } } diff --git a/scripts/system/controllers/controllerModules/inEditMode.js b/scripts/system/controllers/controllerModules/inEditMode.js index 763258573d..202290f2df 100644 --- a/scripts/system/controllers/controllerModules/inEditMode.js +++ b/scripts/system/controllers/controllerModules/inEditMode.js @@ -32,7 +32,7 @@ Script.include("/~/system/libraries/utils.js"); this.nearTablet = function(overlays) { for (var i = 0; i < overlays.length; i++) { - if (overlays[i] === HMD.tabletID) { + if (HMD.tabletID && overlays[i] === HMD.tabletID) { return true; } } @@ -44,7 +44,8 @@ Script.include("/~/system/libraries/utils.js"); }; this.pointingAtTablet = function(objectID) { - return objectID === HMD.tabletScreenID || objectID === HMD.homeButtonID; + return (HMD.tabletScreenID && objectID === HMD.tabletScreenID) + || (HMD.homeButtonID && objectID === HMD.homeButtonID); }; this.sendPickData = function(controllerData) { @@ -106,7 +107,7 @@ Script.include("/~/system/libraries/utils.js"); if (nearOverlay) { var nearOverlayReady = nearOverlay.isReady(controllerData); - if (nearOverlayReady.active && nearOverlay.grabbedThingID === HMD.tabletID) { + if (nearOverlayReady.active && HMD.tabletID && nearOverlay.grabbedThingID === HMD.tabletID) { return this.exitModule(); } } diff --git a/scripts/system/controllers/controllerModules/inVREditMode.js b/scripts/system/controllers/controllerModules/inVREditMode.js index 38eca65dd3..7b78d5e1c4 100644 --- a/scripts/system/controllers/controllerModules/inVREditMode.js +++ b/scripts/system/controllers/controllerModules/inVREditMode.js @@ -31,7 +31,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); ); this.pointingAtTablet = function (objectID) { - return objectID === HMD.tabletScreenID || objectID === HMD.homeButtonID; + return (HMD.tabletScreenID && objectID === HMD.tabletScreenID) + || (HMD.homeButtonID && objectID === HMD.homeButtonID); }; this.isReady = function (controllerData) { @@ -76,7 +77,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); : "LeftNearParentingGrabOverlay"); if (nearOverlay) { var nearOverlayReady = nearOverlay.isReady(controllerData); - if (nearOverlayReady.active && nearOverlay.grabbedThingID === HMD.tabletID) { + if (nearOverlayReady.active && HMD.tabletID && nearOverlay.grabbedThingID === HMD.tabletID) { return makeRunningValues(false, [], []); } } diff --git a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js index fa0fe31de2..0f876816b3 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js @@ -117,7 +117,7 @@ Script.include("/~/system/libraries/utils.js"); Overlays.editOverlay(this.grabbedThingID, reparentProps); // resizeTablet to counter adjust offsets to account for change of scale from sensorToWorldMatrix - if (this.grabbedThingID === HMD.tabletID) { + if (HMD.tabletID && this.grabbedThingID === HMD.tabletID) { resizeTablet(getTabletWidthFromSettings(), reparentProps.parentJointIndex); } @@ -143,7 +143,7 @@ Script.include("/~/system/libraries/utils.js"); }); // resizeTablet to counter adjust offsets to account for change of scale from sensorToWorldMatrix - if (this.grabbedThingID === HMD.tabletID) { + if (HMD.tabletID && this.grabbedThingID === HMD.tabletID) { resizeTablet(getTabletWidthFromSettings(), this.previousParentJointIndex[this.grabbedThingID]); } } diff --git a/scripts/system/controllers/controllerModules/stylusInput.js b/scripts/system/controllers/controllerModules/stylusInput.js index aa65135289..a512fd89db 100644 --- a/scripts/system/controllers/controllerModules/stylusInput.js +++ b/scripts/system/controllers/controllerModules/stylusInput.js @@ -20,7 +20,7 @@ Script.include("/~/system/libraries/controllers.js"); var stylusTargetIDs = []; for (var index = 0; index < stylusTargets.length; index++) { var stylusTarget = stylusTargets[index]; - if (stylusTarget.distance <= maxNormalDistance && stylusTarget.id !== HMD.tabletID) { + if (stylusTarget.distance <= maxNormalDistance && !(HMD.tabletID && stylusTarget.id === HMD.tabletID)) { stylusTargetIDs.push(stylusTarget.id); } } @@ -96,7 +96,7 @@ Script.include("/~/system/libraries/controllers.js"); var i, stylusTarget; for (i = 0; i < candidateOverlays.length; i++) { - if (candidateOverlays[i] !== HMD.tabletID && + if (!(HMD.tabletID && candidateOverlays[i] === HMD.tabletID) && Overlays.getProperty(candidateOverlays[i], "visible")) { stylusTarget = getOverlayDistance(controllerPosition, candidateOverlays[i]); if (stylusTarget) { diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index a51cea67f8..b62cb3dd90 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -263,7 +263,10 @@ function Grabber() { filter: Picks.PICK_OVERLAYS, enabled: true }); - RayPick.setIncludeItems(this.mouseRayOverlays, [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]); + var tabletItems = getMainTabletIDs(); + if (tabletItems.length > 0) { + RayPick.setIncludeItems(this.mouseRayOverlays, tabletItems); + } var renderStates = [{name: "grabbed", end: beacon}]; this.mouseRayEntities = Pointers.createPointer(PickType.Ray, { joint: "Mouse", diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 0167b55810..0e1718ac45 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -31,6 +31,7 @@ Script.include([ "libraries/entityCameraTool.js", "libraries/gridTool.js", "libraries/entityList.js", + "libraries/utils.js", "particle_explorer/particleExplorerTool.js", "libraries/entityIconOverlayManager.js" ]); @@ -775,8 +776,7 @@ function findClickedEntity(event) { } var pickRay = Camera.computePickRay(event.x, event.y); - - var overlayResult = Overlays.findRayIntersection(pickRay, true, [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]); + var overlayResult = Overlays.findRayIntersection(pickRay, true, getMainTabletIDs()); if (overlayResult.intersects) { return null; } @@ -814,13 +814,16 @@ function findClickedEntity(event) { // Handles selections on overlays while in edit mode by querying entities from // entityIconOverlayManager. function handleOverlaySelectionToolUpdates(channel, message, sender) { + var wantDebug = false; if (sender !== MyAvatar.sessionUUID || channel !== 'entityToolUpdates') return; var data = JSON.parse(message); if (data.method === "selectOverlay") { - print("setting selection to overlay " + data.overlayID); + if (wantDebug) { + print("setting selection to overlay " + data.overlayID); + } var entity = entityIconOverlayManager.findEntity(data.overlayID); if (entity !== null) { @@ -964,7 +967,7 @@ function mouseReleaseEvent(event) { function wasTabletClicked(event) { var rayPick = Camera.computePickRay(event.x, event.y); - var result = Overlays.findRayIntersection(rayPick, true, [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]); + var result = Overlays.findRayIntersection(rayPick, true, getMainTabletIDs()); return result.intersects; } @@ -987,7 +990,7 @@ function mouseClickEvent(event) { toolBar.setActive(true); var pickRay = result.pickRay; var foundEntity = result.entityID; - if (foundEntity === HMD.tabletID) { + if (HMD.tabletID && foundEntity === HMD.tabletID) { return; } properties = Entities.getEntityProperties(foundEntity); diff --git a/scripts/system/help.js b/scripts/system/help.js index 9ab7fa3fb3..e29fc59e59 100644 --- a/scripts/system/help.js +++ b/scripts/system/help.js @@ -15,6 +15,7 @@ (function() { // BEGIN LOCAL_SCOPE var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; + var HELP_URL = Script.resourcesPath() + "html/tabletHelp.html"; var buttonName = "HELP"; var onHelpScreen = false; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); @@ -30,9 +31,8 @@ if (onHelpScreen) { tablet.gotoHomeScreen(); } else { - var tabletEntity = HMD.tabletID; - if (tabletEntity) { - Entities.editEntity(tabletEntity, {textures: JSON.stringify({"tex.close" : HOME_BUTTON_TEXTURE})}); + if (HMD.tabletID) { + Entities.editEntity(HMD.tabletID, {textures: JSON.stringify({"tex.close" : HOME_BUTTON_TEXTURE})}); } Menu.triggerOption('Help...'); onHelpScreen = true; @@ -40,29 +40,19 @@ } function onScreenChanged(type, url) { - onHelpScreen = type === "Web" && url.startsWith("../../../html/tabletHelp.html"); + onHelpScreen = type === "Web" && url.startsWith(HELP_URL); button.editProperties({ isActive: onHelpScreen }); } button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); - var POLL_RATE = 500; - var interval = Script.setInterval(function () { - var visible = Menu.isInfoViewVisible('InfoView_html/help.html'); - if (visible !== enabled) { - enabled = visible; - button.editProperties({isActive: enabled}); - } - }, POLL_RATE); - Script.scriptEnding.connect(function () { if (onHelpScreen) { tablet.gotoHomeScreen(); } button.clicked.disconnect(onClicked); tablet.screenChanged.disconnect(onScreenChanged); - Script.clearInterval(interval); if (tablet) { tablet.removeButton(button); } diff --git a/scripts/system/html/css/colpick.css b/scripts/system/html/css/colpick.css index 98417a5e9a..fc50c4b3fb 100644 --- a/scripts/system/html/css/colpick.css +++ b/scripts/system/html/css/colpick.css @@ -279,7 +279,7 @@ colpick Color Picker / colpick.com } /*full layout with no submit button*/ -.colpick_full_ns .colpick_submit, .colpick_full_ns .colpick_current_color{ +.colpick_full_ns .colpick_submit { display:none; } .colpick_full_ns .colpick_new_color { @@ -320,11 +320,11 @@ colpick Color Picker / colpick.com } /*rgbhex layout, no submit button*/ -.colpick_rgbhex_ns .colpick_submit, .colpick_rgbhex_ns .colpick_current_color{ +.colpick_rgbhex_ns .colpick_submit { display:none; } .colpick_rgbhex_ns .colpick_new_color{ - width:68px; + width:34px; border: 1px solid #8f8f8f; } .colpick_rgbhex_ns .colpick_rgb_r { @@ -379,7 +379,7 @@ colpick Color Picker / colpick.com } /*hex layout, no submit button*/ -.colpick_hex_ns .colpick_submit, .colpick_hex_ns .colpick_current_color { +.colpick_hex_ns .colpick_submit { display:none; } .colpick_hex_ns .colpick_hex_field { diff --git a/scripts/system/html/js/colpick.js b/scripts/system/html/js/colpick.js index f808262e9e..199c624bc5 100644 --- a/scripts/system/html/js/colpick.js +++ b/scripts/system/html/js/colpick.js @@ -1,14 +1,41 @@ /* colpick Color Picker -Copyright 2013 Jose Vargas. Licensed under GPL license. Based on Stefan Petre's Color Picker www.eyecon.ro, dual licensed under the MIT and GPL licenses +Copyright 2013 Jose Vargas. Licensed under GPL license. Based on Stefan Petre's Color Picker www.eyecon.ro, dual licensed +under the MIT and GPL licenses For usage and examples: colpick.com/plugin */ +/* global console, document, Element, EventBridge, jQuery, navigator, window, _ $ */ + (function ($) { var colpick = function () { var - tpl = '
    #
    R
    G
    B
    H
    S
    B
    ', + tpl = '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    #
    ' + + '
    ' + + '
    R
    ' + + '
    ' + + '
    ' + + '
    G
    ' + + '
    ' + + '
    B
    ' + + '
    ' + + '
    ' + + '
    H
    ' + + '
    ' + + '
    ' + + '
    S
    ' + + '
    ' + + '
    ' + + '
    B
    ' + + '
    ' + + '
    ' + + '
    ', defaults = { showEvent: 'click', onShow: function () {}, @@ -25,15 +52,15 @@ For usage and examples: colpick.com/plugin submitText: 'OK', height: 156 }, - //Fill the inputs of the plugin - fillRGBFields = function (hsb, cal) { + // Fill the inputs of the plugin + fillRGBFields = function (hsb, cal) { var rgb = hsbToRgb(hsb); $(cal).data('colpick').fields .eq(1).val(rgb.r).end() .eq(2).val(rgb.g).end() .eq(3).val(rgb.b).end(); }, - fillHSBFields = function (hsb, cal) { + fillHSBFields = function (hsb, cal) { $(cal).data('colpick').fields .eq(4).val(Math.round(hsb.h)).end() .eq(5).val(Math.round(hsb.s)).end() @@ -42,7 +69,7 @@ For usage and examples: colpick.com/plugin fillHexFields = function (hsb, cal) { $(cal).data('colpick').fields.eq(0).val(hsbToHex(hsb)); }, - //Set the round selector position + // Set the round selector position setSelector = function (hsb, cal) { $(cal).data('colpick').selector.css('backgroundColor', '#' + hsbToHex({h: hsb.h, s: 100, b: 100})); $(cal).data('colpick').selectorIndic.css({ @@ -50,18 +77,19 @@ For usage and examples: colpick.com/plugin top: parseInt($(cal).data('colpick').height * (100-hsb.b)/100, 10) }); }, - //Set the hue selector position + // Set the hue selector position setHue = function (hsb, cal) { - $(cal).data('colpick').hue.css('top', parseInt($(cal).data('colpick').height - $(cal).data('colpick').height * hsb.h/360, 10)); + $(cal).data('colpick').hue.css('top', + parseInt($(cal).data('colpick').height - $(cal).data('colpick').height * hsb.h / 360, 10)); }, - //Set current and new colors + // Set current and new colors setCurrentColor = function (hsb, cal) { $(cal).data('colpick').currentColor.css('backgroundColor', '#' + hsbToHex(hsb)); }, setNewColor = function (hsb, cal) { $(cal).data('colpick').newColor.css('backgroundColor', '#' + hsbToHex(hsb)); }, - //Called when the new color is changed + // Called when the new color is changed change = function (ev) { var cal = $(this).parent().parent(), col; if (this.parentNode.className.indexOf('_hex') > 0) { @@ -91,9 +119,10 @@ For usage and examples: colpick.com/plugin setSelector(col, cal.get(0)); setHue(col, cal.get(0)); setNewColor(col, cal.get(0)); - cal.data('colpick').onChange.apply(cal.parent(), [col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el, 0]); + cal.data('colpick').onChange.apply(cal.parent(), + [col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el, 0]); }, - //Change style on blur and on focus of inputs + // Change style on blur and on focus of inputs blur = function (ev) { $(this).parent().removeClass('colpick_focus'); }, @@ -101,13 +130,14 @@ For usage and examples: colpick.com/plugin $(this).parent().parent().data('colpick').fields.parent().removeClass('colpick_focus'); $(this).parent().addClass('colpick_focus'); }, - //Increment/decrement arrows functions + // Increment/decrement arrows functions downIncrement = function (ev) { ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; var field = $(this).parent().find('input').focus(); var current = { el: $(this).parent().addClass('colpick_slider'), - max: this.parentNode.className.indexOf('_hsb_h') > 0 ? 360 : (this.parentNode.className.indexOf('_hsb') > 0 ? 100 : 255), + max: this.parentNode.className.indexOf('_hsb_h') > 0 ? 360 : + (this.parentNode.className.indexOf('_hsb') > 0 ? 100 : 255), y: ev.pageY, field: field, val: parseInt(field.val(), 10), @@ -130,7 +160,7 @@ For usage and examples: colpick.com/plugin $(document).off('mousemove', moveIncrement); return false; }, - //Hue slider functions + // Hue slider functions downHue = function (ev) { ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; var current = { @@ -140,20 +170,23 @@ For usage and examples: colpick.com/plugin $(document).on('mouseup touchend',current,upHue); $(document).on('mousemove touchmove',current,moveHue); - var pageY = ((ev.type == 'touchstart') ? ev.originalEvent.changedTouches[0].pageY : ev.pageY ); + var pageY = ((ev.type === 'touchstart') ? ev.originalEvent.changedTouches[0].pageY : ev.pageY ); change.apply( current.cal.data('colpick') - .fields.eq(4).val(parseInt(360*(current.cal.data('colpick').height - (pageY - current.y))/current.cal.data('colpick').height, 10)) + .fields.eq(4).val(parseInt(360 * (current.cal.data('colpick').height - + (pageY - current.y)) / current.cal.data('colpick').height, 10)) .get(0), [current.cal.data('colpick').livePreview] ); return false; }, moveHue = function (ev) { - var pageY = ((ev.type == 'touchmove') ? ev.originalEvent.changedTouches[0].pageY : ev.pageY ); + var pageY = ((ev.type === 'touchmove') ? ev.originalEvent.changedTouches[0].pageY : ev.pageY ); change.apply( ev.data.cal.data('colpick') - .fields.eq(4).val(parseInt(360*(ev.data.cal.data('colpick').height - Math.max(0,Math.min(ev.data.cal.data('colpick').height,(pageY - ev.data.y))))/ev.data.cal.data('colpick').height, 10)) + .fields.eq(4).val(parseInt(360 * (ev.data.cal.data('colpick').height - + Math.max(0, Math.min(ev.data.cal.data('colpick').height, (pageY - ev.data.y)))) / + ev.data.cal.data('colpick').height, 10)) .get(0), [ev.data.preview] ); @@ -166,7 +199,7 @@ For usage and examples: colpick.com/plugin $(document).off('mousemove touchmove',moveHue); return false; }, - //Color selector functions + // Color selector functions downSelector = function (ev) { ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; var current = { @@ -178,8 +211,8 @@ For usage and examples: colpick.com/plugin $(document).on('mouseup touchend',current,upSelector); $(document).on('mousemove touchmove',current,moveSelector); - var payeX,pageY; - if(ev.type == 'touchstart') { + var pageX,pageY; + if (ev.type === 'touchstart') { pageX = ev.originalEvent.changedTouches[0].pageX, pageY = ev.originalEvent.changedTouches[0].pageY; } else { @@ -189,16 +222,17 @@ For usage and examples: colpick.com/plugin change.apply( current.cal.data('colpick').fields - .eq(6).val(parseInt(100*(current.cal.data('colpick').height - (pageY - current.pos.top))/current.cal.data('colpick').height, 10)).end() - .eq(5).val(parseInt(100*(pageX - current.pos.left)/current.cal.data('colpick').height, 10)) - .get(0), + .eq(6).val(parseInt(100 * (current.cal.data('colpick').height - (pageY - current.pos.top)) / + current.cal.data('colpick').height, 10)).end() + .eq(5).val(parseInt(100*(pageX - current.pos.left)/current.cal.data('colpick').height, 10)) + .get(0), [current.preview] ); return false; }, moveSelector = function (ev) { - var payeX,pageY; - if(ev.type == 'touchmove') { + var pageX,pageY; + if (ev.type === 'touchmove') { pageX = ev.originalEvent.changedTouches[0].pageX, pageY = ev.originalEvent.changedTouches[0].pageY; } else { @@ -208,9 +242,12 @@ For usage and examples: colpick.com/plugin change.apply( ev.data.cal.data('colpick').fields - .eq(6).val(parseInt(100*(ev.data.cal.data('colpick').height - Math.max(0,Math.min(ev.data.cal.data('colpick').height,(pageY - ev.data.pos.top))))/ev.data.cal.data('colpick').height, 10)).end() - .eq(5).val(parseInt(100*(Math.max(0,Math.min(ev.data.cal.data('colpick').height,(pageX - ev.data.pos.left))))/ev.data.cal.data('colpick').height, 10)) - .get(0), + .eq(6).val(parseInt(100 * (ev.data.cal.data('colpick').height - + Math.max(0, Math.min(ev.data.cal.data('colpick').height, (pageY - ev.data.pos.top)))) / + ev.data.cal.data('colpick').height, 10)).end() + .eq(5).val(parseInt(100 * (Math.max(0, Math.min(ev.data.cal.data('colpick').height, + (pageX - ev.data.pos.left)))) / ev.data.cal.data('colpick').height, 10)) + .get(0), [ev.data.preview] ); return false; @@ -222,7 +259,7 @@ For usage and examples: colpick.com/plugin $(document).off('mousemove touchmove',moveSelector); return false; }, - //Submit button + // Submit button clickSubmit = function (ev) { var cal = $(this).parent(); var col = cal.data('colpick').color; @@ -230,7 +267,7 @@ For usage and examples: colpick.com/plugin setCurrentColor(col, cal.get(0)); cal.data('colpick').onSubmit(col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el); }, - //Show/hide the color picker + // Show/hide the color picker show = function (ev) { // Prevent the trigger of any direct parent ev.stopPropagation(); @@ -245,27 +282,29 @@ For usage and examples: colpick.com/plugin left -= calW; } cal.css({left: left + 'px', top: top + 'px'}); - if (cal.data('colpick').onShow.apply(this, [cal.get(0)]) != false) { + if (cal.data('colpick').onShow.apply(this, [cal.get(0)]) !== false) { cal.show(); } - //Hide when user clicks outside + // Hide when user clicks outside $('html').mousedown({cal:cal}, hide); - cal.mousedown(function(ev){ev.stopPropagation();}) + cal.mousedown(function(ev){ + ev.stopPropagation(); + }); }, hide = function (ev) { - if (ev.data.cal.data('colpick').onHide.apply(this, [ev.data.cal.get(0)]) != false) { + if (ev.data.cal.data('colpick').onHide.apply(this, [ev.data.cal.get(0)]) !== false) { ev.data.cal.hide(); } $('html').off('mousedown', hide); }, getViewport = function () { - var m = document.compatMode == 'CSS1Compat'; + var m = document.compatMode === 'CSS1Compat'; return { l : window.pageXOffset || (m ? document.documentElement.scrollLeft : document.body.scrollLeft), w : window.innerWidth || (m ? document.documentElement.clientWidth : document.body.clientWidth) }; }, - //Fix the values if the user enters a negative or high value + // Fix the values if the user enters a negative or high value fixHSB = function (hsb) { return { h: Math.min(360, Math.max(0, hsb.h)), @@ -302,70 +341,85 @@ For usage and examples: colpick.com/plugin setSelector(col, cal.get(0)); setHue(col, cal.get(0)); setNewColor(col, cal.get(0)); + // If the user triggered this behavior, then any prior color change should be negated. + cal.data('colpick').onChange.apply(cal.parent(), [col, hsbToHex(col), + hsbToRgb(col), cal.data('colpick').el, 0]); }; return { init: function (opt) { opt = $.extend({}, defaults, opt||{}); - //Set color - if (typeof opt.color == 'string') { + // Set color + if (typeof opt.color === 'string') { opt.color = hexToHsb(opt.color); - } else if (opt.color.r != undefined && opt.color.g != undefined && opt.color.b != undefined) { + } else if (opt.color.r !== undefined && opt.color.g !== undefined && opt.color.b !== undefined) { opt.color = rgbToHsb(opt.color); - } else if (opt.color.h != undefined && opt.color.s != undefined && opt.color.b != undefined) { + } else if (opt.color.h !== undefined && opt.color.s !== undefined && opt.color.b !== undefined) { opt.color = fixHSB(opt.color); } else { return this; } - //For each selected DOM element + // For each selected DOM element return this.each(function () { - //If the element does not have an ID + // If the element does not have an ID if (!$(this).data('colpickId')) { var options = $.extend({}, opt); options.origColor = opt.color; - //Generate and assign a random ID + // Generate and assign a random ID var id = 'collorpicker_' + parseInt(Math.random() * 1000); $(this).data('colpickId', id); - //Set the tpl's ID and get the HTML + // Set the tpl's ID and get the HTML var cal = $(tpl).attr('id', id); - //Add class according to layout + // Add class according to layout cal.addClass('colpick_'+options.layout+(options.submit?'':' colpick_'+options.layout+'_ns')); - //Add class if the color scheme is not default - if(options.colorScheme != 'light') { + // Add class if the color scheme is not default + if (options.colorScheme !== 'light') { cal.addClass('colpick_'+options.colorScheme); } - //Setup submit button + // Setup submit button cal.find('div.colpick_submit').html(options.submitText).click(clickSubmit); - //Setup input fields + // Setup input fields options.fields = cal.find('input').change(change).blur(blur).focus(focus); - cal.find('div.colpick_field_arrs').mousedown(downIncrement).end().find('div.colpick_current_color').click(restoreOriginal); - //Setup hue selector + cal.find('div.colpick_field_arrs').mousedown(downIncrement); + cal.find('div.colpick_current_color').click(restoreOriginal); + // Setup hue selector options.selector = cal.find('div.colpick_color').on('mousedown touchstart',downSelector); options.selectorIndic = options.selector.find('div.colpick_selector_outer'); - //Store parts of the plugin + // Store parts of the plugin options.el = this; options.hue = cal.find('div.colpick_hue_arrs'); - huebar = options.hue.parent(); - //Paint the hue bar + var huebar = options.hue.parent(); + // Paint the hue bar var UA = navigator.userAgent.toLowerCase(); var isIE = navigator.appName === 'Microsoft Internet Explorer'; - var IEver = isIE ? parseFloat( UA.match( /msie ([0-9]{1,}[\.0-9]{0,})/ )[1] ) : 0; + var IEver = isIE ? parseFloat( UA.match( /msie ([0-9]{1,}[.0-9]{0,})/ )[1] ) : 0; var ngIE = ( isIE && IEver < 10 ); - var stops = ['#ff0000','#ff0080','#ff00ff','#8000ff','#0000ff','#0080ff','#00ffff','#00ff80','#00ff00','#80ff00','#ffff00','#ff8000','#ff0000']; - if(ngIE) { + var stops = ['#ff0000', '#ff0080', '#ff00ff', '#8000ff', '#0000ff', '#0080ff', '#00ffff', '#00ff80', + '#00ff00', '#80ff00', '#ffff00', '#ff8000', '#ff0000']; + if (ngIE) { var i, div; - for(i=0; i<=11; i++) { - div = $('
    ').attr('style','height:8.333333%; filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='+stops[i]+', endColorstr='+stops[i+1]+'); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='+stops[i]+', endColorstr='+stops[i+1]+')";'); + for (i=0; i<=11; i++) { + div = $('
    ').attr('style', + 'height:8.333333%; filter:progid:' + + 'DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=' + stops[i] + + ', endColorstr=' + stops[i + 1] + + '); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=' + + stops[i] + ', endColorstr=' + stops[i + 1] + ')";'); huebar.append(div); } } else { - stopList = stops.join(','); - huebar.attr('style','background:-webkit-linear-gradient(top,'+stopList+'); background: -o-linear-gradient(top,'+stopList+'); background: -ms-linear-gradient(top,'+stopList+'); background:-moz-linear-gradient(top,'+stopList+'); -webkit-linear-gradient(top,'+stopList+'); background:linear-gradient(to bottom,'+stopList+'); '); + var stopList = stops.join(','); + huebar.attr('style', 'background:-webkit-linear-gradient(top,' + stopList + + '); background: -o-linear-gradient(top,' + stopList + + '); background: -ms-linear-gradient(top,' + stopList + + '); background:-moz-linear-gradient(top,' + stopList + + '); -webkit-linear-gradient(top,' + stopList + + '); background:linear-gradient(to bottom,' + stopList + '); '); } cal.find('div.colpick_hue').on('mousedown touchstart',downHue); options.newColor = cal.find('div.colpick_new_color'); options.currentColor = cal.find('div.colpick_current_color'); - //Store options and fill with default color + // Store options and fill with default color cal.data('colpick', options); fillRGBFields(options.color, cal.get(0)); fillHSBFields(options.color, cal.get(0)); @@ -374,7 +428,7 @@ For usage and examples: colpick.com/plugin setSelector(options.color, cal.get(0)); setCurrentColor(options.color, cal.get(0)); setNewColor(options.color, cal.get(0)); - //Append to body if flat=false, else show in place + // Append to body if flat=false, else show in place if (options.flat) { cal.appendTo(this).show(); cal.css({ @@ -391,7 +445,7 @@ For usage and examples: colpick.com/plugin } }); }, - //Shows the picker + // Shows the picker showPicker: function() { return this.each( function () { if ($(this).data('colpickId')) { @@ -399,7 +453,7 @@ For usage and examples: colpick.com/plugin } }); }, - //Hides the picker + // Hides the picker hidePicker: function() { return this.each( function () { if ($(this).data('colpickId')) { @@ -407,14 +461,14 @@ For usage and examples: colpick.com/plugin } }); }, - //Sets a color as new and current (default) + // Sets a color as new and current (default) setColor: function(col, setCurrent) { setCurrent = (typeof setCurrent === "undefined") ? 1 : setCurrent; - if (typeof col == 'string') { + if (typeof col === 'string') { col = hexToHsb(col); - } else if (col.r != undefined && col.g != undefined && col.b != undefined) { + } else if (col.r !== undefined && col.g !== undefined && col.b !== undefined) { col = rgbToHsb(col); - } else if (col.h != undefined && col.s != undefined && col.b != undefined) { + } else if (col.h !== undefined && col.s !== undefined && col.b !== undefined) { col = fixHSB(col); } else { return this; @@ -431,8 +485,9 @@ For usage and examples: colpick.com/plugin setSelector(col, cal.get(0)); setNewColor(col, cal.get(0)); - cal.data('colpick').onChange.apply(cal.parent(), [col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el, 1]); - if(setCurrent) { + cal.data('colpick').onChange.apply(cal.parent(), + [col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el, 1]); + if (setCurrent) { setCurrentColor(col, cal.get(0)); } } @@ -440,13 +495,23 @@ For usage and examples: colpick.com/plugin } }; }(); - //Color space convertions - var hexToRgb = function (hex) { - var hex = parseInt(((hex.indexOf('#') > -1) ? hex.substring(1) : hex), 16); - return {r: hex >> 16, g: (hex & 0x00FF00) >> 8, b: (hex & 0x0000FF)}; + // Color space convertions + var hexToRgb = function (hexString) { + if (typeof hexString !== "string") { + print("Error - ColPick.js::hexToRgb expects string object."); + return; + } + + var hexNumber = parseInt(((hexString.indexOf('#') > -1) ? hexString.substring(1) : hexString), 16); + return { r: hexNumber >> 16, g: (hexNumber & 0x00FF00) >> 8, b: (hexNumber & 0x0000FF)}; }; - var hexToHsb = function (hex) { - return rgbToHsb(hexToRgb(hex)); + var hexToHsb = function (hexString) { + if (typeof hexString !== "string") { + print("Error - ColPick.js::hexToHsb expects string object."); + return; + } + + return rgbToHsb(hexToRgb(hexString)); }; var rgbToHsb = function (rgb) { var hsb = {h: 0, s: 0, b: 0}; @@ -454,14 +519,22 @@ For usage and examples: colpick.com/plugin var max = Math.max(rgb.r, rgb.g, rgb.b); var delta = max - min; hsb.b = max; - hsb.s = max != 0 ? 255 * delta / max : 0; - if (hsb.s != 0) { - if (rgb.r == max) hsb.h = (rgb.g - rgb.b) / delta; - else if (rgb.g == max) hsb.h = 2 + (rgb.b - rgb.r) / delta; - else hsb.h = 4 + (rgb.r - rgb.g) / delta; - } else hsb.h = -1; + hsb.s = max != 0 ? 255 * delta / max : 0; // eslint-disable-line eqeqeq + if (hsb.s != 0) { // eslint-disable-line eqeqeq + if (rgb.r == max) { // eslint-disable-line eqeqeq + hsb.h = (rgb.g - rgb.b) / delta; + } else if (rgb.g == max) { // eslint-disable-line eqeqeq + hsb.h = 2 + (rgb.b - rgb.r) / delta; + } else { + hsb.h = 4 + (rgb.r - rgb.g) / delta; + } + } else { + hsb.h = -1; + } hsb.h *= 60; - if (hsb.h < 0) hsb.h += 360; + if (hsb.h < 0) { + hsb.h += 360; + } hsb.s *= 100/255; hsb.b *= 100/255; return hsb; @@ -471,20 +544,30 @@ For usage and examples: colpick.com/plugin var h = hsb.h; var s = hsb.s*255/100; var v = hsb.b*255/100; - if(s == 0) { + if (s == 0) { // eslint-disable-line eqeqeq rgb.r = rgb.g = rgb.b = v; } else { var t1 = v; var t2 = (255-s)*v/255; var t3 = (t1-t2)*(h%60)/60; - if(h==360) h = 0; - if(h<60) {rgb.r=t1; rgb.b=t2; rgb.g=t2+t3} - else if(h<120) {rgb.g=t1; rgb.b=t2; rgb.r=t1-t3} - else if(h<180) {rgb.g=t1; rgb.r=t2; rgb.b=t2+t3} - else if(h<240) {rgb.b=t1; rgb.r=t2; rgb.g=t1-t3} - else if(h<300) {rgb.b=t1; rgb.g=t2; rgb.r=t2+t3} - else if(h<360) {rgb.r=t1; rgb.g=t2; rgb.b=t1-t3} - else {rgb.r=0; rgb.g=0; rgb.b=0} + if (h==360) { // eslint-disable-line eqeqeq + h = 0; + } + if (h<60) { + rgb.r=t1; rgb.b=t2; rgb.g=t2+t3; + } else if (h<120) { + rgb.g=t1; rgb.b=t2; rgb.r=t1-t3; + } else if (h<180) { + rgb.g=t1; rgb.r=t2; rgb.b=t2+t3; + } else if (h<240) { + rgb.b=t1; rgb.r=t2; rgb.g=t1-t3; + } else if (h<300) { + rgb.b=t1; rgb.g=t2; rgb.r=t2+t3; + } else if (h<360) { + rgb.r=t1; rgb.g=t2; rgb.b=t1-t3; + } else { + rgb.r=0; rgb.g=0; rgb.b=0; + } } return {r:Math.round(rgb.r), g:Math.round(rgb.g), b:Math.round(rgb.b)}; }; @@ -495,7 +578,7 @@ For usage and examples: colpick.com/plugin rgb.b.toString(16) ]; $.each(hex, function (nr, val) { - if (val.length == 1) { + if (val.length == 1) { // eslint-disable-line eqeqeq hex[nr] = '0' + val; } }); @@ -521,3 +604,4 @@ For usage and examples: colpick.com/plugin } }); })(jQuery); + diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 41abc00cff..2b29fbf041 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -31,7 +31,7 @@ var ICON_FOR_TYPE = { var EDITOR_TIMEOUT_DURATION = 1500; var KEY_P = 80; // Key code for letter p used for Parenting hotkey. -var colorPickers = []; +var colorPickers = {}; var lastEntityID = null; var MATERIAL_PREFIX_STRING = "mat::"; @@ -73,8 +73,8 @@ function enableProperties() { function disableProperties() { disableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); disableChildren(document, ".colpick"); - for (var i = 0; i < colorPickers.length; i++) { - colorPickers[i].colpickHide(); + for (var pickKey in colorPickers) { + colorPickers[pickKey].colpickHide(); } var elLocked = document.getElementById("property-locked"); @@ -86,7 +86,6 @@ function disableProperties() { function showElements(els, show) { for (var i = 0; i < els.length; i++) { els[i].style.display = (show) ? 'table' : 'none'; - } } @@ -509,15 +508,6 @@ function unbindAllInputs() { } } -function clearSelection() { - if (document.selection && document.selection.empty) { - document.selection.empty(); - } else if (window.getSelection) { - var sel = window.getSelection(); - sel.removeAllRanges(); - } -} - function showParentMaterialNameBox(number, elNumber, elString) { if (number) { $('#property-parent-material-id-number-container').show(); @@ -827,8 +817,10 @@ function loaded() { if (lastEntityID !== '"' + properties.id + '"' && lastEntityID !== null && editor !== null) { saveJSONUserData(true); } - // the event bridge and json parsing handle our avatar id string differently. + var doSelectElement = lastEntityID === '"' + properties.id + '"'; + + // the event bridge and json parsing handle our avatar id string differently. lastEntityID = '"' + properties.id + '"'; elID.value = properties.id; @@ -1193,12 +1185,10 @@ function loaded() { } var activeElement = document.activeElement; - - if (typeof activeElement.select !== "undefined") { + if (doSelectElement && typeof activeElement.select !== "undefined") { activeElement.select(); } } - clearSelection(); } }); } @@ -1381,13 +1371,19 @@ function loaded() { elColorRed.addEventListener('change', colorChangeFunction); elColorGreen.addEventListener('change', colorChangeFunction); elColorBlue.addEventListener('change', colorChangeFunction); - colorPickers.push($('#property-color-control2').colpick({ + colorPickers['#property-color-control2'] = $('#property-color-control2').colpick({ colorScheme: 'dark', layout: 'hex', color: '000000', submit: false, // We don't want to have a submission button onShow: function(colpick) { $('#property-color-control2').attr('active', 'true'); + // The original color preview within the picker needs to be updated on show because + // prior to the picker being shown we don't have access to the selections' starting color. + colorPickers['#property-color-control2'].colpickSetColor({ + "r": elColorRed.value, + "g": elColorGreen.value, + "b": elColorBlue.value}); }, onHide: function(colpick) { $('#property-color-control2').attr('active', 'false'); @@ -1396,7 +1392,7 @@ function loaded() { $(el).css('background-color', '#' + hex); emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); } - })); + }); elLightSpotLight.addEventListener('change', createEmitCheckedPropertyUpdateFunction('isSpotlight')); @@ -1405,13 +1401,20 @@ function loaded() { elLightColorRed.addEventListener('change', lightColorChangeFunction); elLightColorGreen.addEventListener('change', lightColorChangeFunction); elLightColorBlue.addEventListener('change', lightColorChangeFunction); - colorPickers.push($('#property-light-color').colpick({ + colorPickers['#property-light-color'] = $('#property-light-color').colpick({ colorScheme: 'dark', layout: 'hex', color: '000000', submit: false, // We don't want to have a submission button onShow: function(colpick) { $('#property-light-color').attr('active', 'true'); + // The original color preview within the picker needs to be updated on show because + // prior to the picker being shown we don't have access to the selections' starting color. + colorPickers['#property-light-color'].colpickSetColor({ + "r": elLightColorRed.value, + "g": elLightColorGreen.value, + "b": elLightColorBlue.value + }); }, onHide: function(colpick) { $('#property-light-color').attr('active', 'false'); @@ -1420,7 +1423,7 @@ function loaded() { $(el).css('background-color', '#' + hex); emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); } - })); + }); elLightIntensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('intensity', 1)); elLightFalloffRadius.addEventListener('change', createEmitNumberPropertyUpdateFunction('falloffRadius', 1)); @@ -1486,13 +1489,20 @@ function loaded() { elTextTextColorRed.addEventListener('change', textTextColorChangeFunction); elTextTextColorGreen.addEventListener('change', textTextColorChangeFunction); elTextTextColorBlue.addEventListener('change', textTextColorChangeFunction); - colorPickers.push($('#property-text-text-color').colpick({ + colorPickers['#property-text-text-color'] = $('#property-text-text-color').colpick({ colorScheme: 'dark', layout: 'hex', color: '000000', submit: false, // We don't want to have a submission button onShow: function(colpick) { $('#property-text-text-color').attr('active', 'true'); + // The original color preview within the picker needs to be updated on show because + // prior to the picker being shown we don't have access to the selections' starting color. + colorPickers['#property-text-text-color'].colpickSetColor({ + "r": elTextTextColorRed.value, + "g": elTextTextColorGreen.value, + "b": elTextTextColorBlue.value + }); }, onHide: function(colpick) { $('#property-text-text-color').attr('active', 'false'); @@ -1502,7 +1512,7 @@ function loaded() { $(el).attr('active', 'false'); emitColorPropertyUpdate('textColor', rgb.r, rgb.g, rgb.b); } - })); + }); var textBackgroundColorChangeFunction = createEmitColorPropertyUpdateFunction( 'backgroundColor', elTextBackgroundColorRed, elTextBackgroundColorGreen, elTextBackgroundColorBlue); @@ -1510,13 +1520,20 @@ function loaded() { elTextBackgroundColorRed.addEventListener('change', textBackgroundColorChangeFunction); elTextBackgroundColorGreen.addEventListener('change', textBackgroundColorChangeFunction); elTextBackgroundColorBlue.addEventListener('change', textBackgroundColorChangeFunction); - colorPickers.push($('#property-text-background-color').colpick({ + colorPickers['#property-text-background-color'] = $('#property-text-background-color').colpick({ colorScheme: 'dark', layout: 'hex', color: '000000', submit: false, // We don't want to have a submission button onShow: function(colpick) { $('#property-text-background-color').attr('active', 'true'); + // The original color preview within the picker needs to be updated on show because + // prior to the picker being shown we don't have access to the selections' starting color. + colorPickers['#property-text-background-color'].colpickSetColor({ + "r": elTextBackgroundColorRed.value, + "g": elTextBackgroundColorGreen.value, + "b": elTextBackgroundColorBlue.value + }); }, onHide: function(colpick) { $('#property-text-background-color').attr('active', 'false'); @@ -1525,7 +1542,7 @@ function loaded() { $(el).css('background-color', '#' + hex); emitColorPropertyUpdate('backgroundColor', rgb.r, rgb.g, rgb.b); } - })); + }); // Key light var keyLightModeChanged = createZoneComponentModeChangedFunction('keyLightMode', @@ -1535,13 +1552,20 @@ function loaded() { elZoneKeyLightModeDisabled.addEventListener('change', keyLightModeChanged); elZoneKeyLightModeEnabled.addEventListener('change', keyLightModeChanged); - colorPickers.push($('#property-zone-key-light-color').colpick({ + colorPickers['#property-zone-key-light-color'] = $('#property-zone-key-light-color').colpick({ colorScheme: 'dark', layout: 'hex', color: '000000', submit: false, // We don't want to have a submission button onShow: function(colpick) { $('#property-zone-key-light-color').attr('active', 'true'); + // The original color preview within the picker needs to be updated on show because + // prior to the picker being shown we don't have access to the selections' starting color. + colorPickers['#property-zone-key-light-color'].colpickSetColor({ + "r": elZoneKeyLightColorRed.value, + "g": elZoneKeyLightColorGreen.value, + "b": elZoneKeyLightColorBlue.value + }); }, onHide: function(colpick) { $('#property-zone-key-light-color').attr('active', 'false'); @@ -1550,7 +1574,7 @@ function loaded() { $(el).css('background-color', '#' + hex); emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'keyLight'); } - })); + }); var zoneKeyLightColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('keyLight', 'color', elZoneKeyLightColorRed, elZoneKeyLightColorGreen, elZoneKeyLightColorBlue); @@ -1604,13 +1628,20 @@ function loaded() { elZoneHazeRange.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeRange')); - colorPickers.push($('#property-zone-haze-color').colpick({ + colorPickers['#property-zone-haze-color'] = $('#property-zone-haze-color').colpick({ colorScheme: 'dark', layout: 'hex', color: '000000', submit: false, // We don't want to have a submission button onShow: function(colpick) { $('#property-zone-haze-color').attr('active', 'true'); + // The original color preview within the picker needs to be updated on show because + // prior to the picker being shown we don't have access to the selections' starting color. + colorPickers['#property-zone-haze-color'].colpickSetColor({ + "r": elZoneHazeColorRed.value, + "g": elZoneHazeColorGreen.value, + "b": elZoneHazeColorBlue.value + }); }, onHide: function(colpick) { $('#property-zone-haze-color').attr('active', 'false'); @@ -1619,7 +1650,7 @@ function loaded() { $(el).css('background-color', '#' + hex); emitColorPropertyUpdate('hazeColor', rgb.r, rgb.g, rgb.b, 'haze'); } - })); + }); var zoneHazeColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('haze', 'hazeColor', elZoneHazeColorRed, elZoneHazeColorGreen, @@ -1629,13 +1660,20 @@ function loaded() { elZoneHazeColorGreen.addEventListener('change', zoneHazeColorChangeFunction); elZoneHazeColorBlue.addEventListener('change', zoneHazeColorChangeFunction); - colorPickers.push($('#property-zone-haze-glare-color').colpick({ + colorPickers['#property-zone-haze-glare-color'] = $('#property-zone-haze-glare-color').colpick({ colorScheme: 'dark', layout: 'hex', color: '000000', submit: false, // We don't want to have a submission button onShow: function(colpick) { $('#property-zone-haze-glare-color').attr('active', 'true'); + // The original color preview within the picker needs to be updated on show because + // prior to the picker being shown we don't have access to the selections' starting color. + colorPickers['#property-zone-haze-glare-color'].colpickSetColor({ + "r": elZoneHazeGlareColorRed.value, + "g": elZoneHazeGlareColorGreen.value, + "b": elZoneHazeGlareColorBlue.value + }); }, onHide: function(colpick) { $('#property-zone-haze-glare-color').attr('active', 'false'); @@ -1644,7 +1682,7 @@ function loaded() { $(el).css('background-color', '#' + hex); emitColorPropertyUpdate('hazeGlareColor', rgb.r, rgb.g, rgb.b, 'haze'); } - })); + }); var zoneHazeGlareColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('haze', 'hazeGlareColor', elZoneHazeGlareColorRed, elZoneHazeGlareColorGreen, @@ -1671,13 +1709,20 @@ function loaded() { elZoneSkyboxColorRed.addEventListener('change', zoneSkyboxColorChangeFunction); elZoneSkyboxColorGreen.addEventListener('change', zoneSkyboxColorChangeFunction); elZoneSkyboxColorBlue.addEventListener('change', zoneSkyboxColorChangeFunction); - colorPickers.push($('#property-zone-skybox-color').colpick({ + colorPickers['#property-zone-skybox-color'] = $('#property-zone-skybox-color').colpick({ colorScheme: 'dark', layout: 'hex', color: '000000', submit: false, // We don't want to have a submission button onShow: function(colpick) { $('#property-zone-skybox-color').attr('active', 'true'); + // The original color preview within the picker needs to be updated on show because + // prior to the picker being shown we don't have access to the selections' starting color. + colorPickers['#property-zone-skybox-color'].colpickSetColor({ + "r": elZoneSkyboxColorRed.value, + "g": elZoneSkyboxColorGreen.value, + "b": elZoneSkyboxColorBlue.value + }); }, onHide: function(colpick) { $('#property-zone-skybox-color').attr('active', 'false'); @@ -1686,7 +1731,7 @@ function loaded() { $(el).css('background-color', '#' + hex); emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'skybox'); } - })); + }); elZoneSkyboxURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('skybox', 'url')); @@ -1761,34 +1806,13 @@ function loaded() { }; // For input and textarea elements, select all of the text on focus - // WebKit-based browsers, such as is used with QWebView, have a quirk - // where the mouseup event comes after the focus event, causing the - // text to be deselected immediately after selecting all of the text. - // To make this work we block the first mouseup event after the elements - // received focus. If we block all mouseup events the user will not - // be able to click within the selected text. - // We also check to see if the value has changed to make sure we aren't - // blocking a mouse-up event when clicking on an input spinner. var els = document.querySelectorAll("input, textarea"); for (var i = 0; i < els.length; i++) { - var clicked = false; - var originalText; - // TODO FIXME: (JSHint) Functions declared within loops referencing - // an outer scoped variable may lead to confusing semantics. - els[i].onfocus = function(e) { - originalText = this.value; - this.select(); - clicked = false; - }; - // TODO FIXME: (JSHint) Functions declared within loops referencing - // an outer scoped variable may lead to confusing semantics. - els[i].onmouseup = function(e) { - if (!clicked && originalText === this.value) { - e.preventDefault(); - } - clicked = true; + els[i].onfocus = function (e) { + e.target.select(); }; } + bindAllNonJSONEditorElements(); }); diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index a28de5abc2..a34191b951 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -306,10 +306,6 @@ WebTablet.prototype.setScriptURL = function (scriptURL) { Overlays.editOverlay(this.webOverlayID, { scriptURL: scriptURL }); }; -WebTablet.prototype.getOverlayObject = function () { - return Overlays.getOverlayObject(this.webOverlayID); -}; - WebTablet.prototype.setWidth = function (width) { // imported from libraries/utils.js resizeTablet(width); @@ -335,7 +331,7 @@ WebTablet.prototype.destroy = function () { }; WebTablet.prototype.geometryChanged = function (geometry) { - if (!HMD.active) { + if (!HMD.active && HMD.tabletID) { var tabletProperties = {}; // compute position, rotation & parentJointIndex of the tablet this.calculateTabletAttachmentProperties(NO_HANDS, false, tabletProperties); @@ -463,6 +459,9 @@ WebTablet.prototype.calculateTabletAttachmentProperties = function (hand, useMou }; WebTablet.prototype.onHmdChanged = function () { + if (!HMD.tabletID) { + return; + } var tabletProperties = {}; // compute position, rotation & parentJointIndex of the tablet this.calculateTabletAttachmentProperties(NO_HANDS, false, tabletProperties); diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 44325d4a12..a419e9d49c 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -21,7 +21,10 @@ SPACE_LOCAL = "local"; SPACE_WORLD = "world"; HIGHLIGHT_LIST_NAME = "editHandleHighlightList"; -Script.include("./controllers.js"); +Script.include([ + "./controllers.js", + "./utils.js" +]); SelectionManager = (function() { var that = {}; @@ -668,7 +671,7 @@ SelectionDisplay = (function() { var pickRay = generalComputePickRay(event.x, event.y); // TODO_Case6491: Move this out to setup just to make it once - var interactiveOverlays = [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]; + var interactiveOverlays = getMainTabletIDs(); for (var key in handleTools) { if (handleTools.hasOwnProperty(key)) { interactiveOverlays.push(key); @@ -681,8 +684,8 @@ SelectionDisplay = (function() { var results = testRayIntersect(pickRay, interactiveOverlays); if (results.intersects) { var hitOverlayID = results.overlayID; - if ((hitOverlayID === HMD.tabletID) || (hitOverlayID === HMD.tabletScreenID) || - (hitOverlayID === HMD.homeButtonID)) { + if ((HMD.tabletID && hitOverlayID === HMD.tabletID) || (HMD.tabletScreenID && hitOverlayID === HMD.tabletScreenID) + || (HMD.homeButtonID && hitOverlayID === HMD.homeButtonID)) { // EARLY EXIT-(mouse clicks on the tablet should override the edit affordances) return false; } @@ -1319,8 +1322,9 @@ SelectionDisplay = (function() { isActiveTool(handleScaleRTFCube) || isActiveTool(handleStretchXSphere) || isActiveTool(handleStretchYSphere) || isActiveTool(handleStretchZSphere)); - var showOutlineForZone = (SelectionManager.selections.length === 1 && - SelectionManager.savedProperties[SelectionManager.selections[0]].type === "Zone"); + var showOutlineForZone = (SelectionManager.selections.length === 1 && + typeof SelectionManager.savedProperties[SelectionManager.selections[0]] !== "undefined" && + SelectionManager.savedProperties[SelectionManager.selections[0]].type === "Zone"); that.setHandleScaleEdgeVisible(showOutlineForZone || (!isActiveTool(handleRotatePitchRing) && !isActiveTool(handleRotateYawRing) && !isActiveTool(handleRotateRollRing))); diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index bc83cc582c..442a9f6d24 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -428,3 +428,17 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride) dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim } }); }; + +getMainTabletIDs = function () { + var tabletIDs = []; + if (HMD.tabletID) { + tabletIDs.push(HMD.tabletID); + } + if (HMD.tabletScreenID) { + tabletIDs.push(HMD.tabletScreenID); + } + if (HMD.homeButtonID) { + tabletIDs.push(HMD.homeButtonID); + } + return tabletIDs; +}; \ No newline at end of file diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index fd1275a251..8f51d88f2d 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -75,11 +75,11 @@ var selectionDisplay = null; // for gridTool.js to ignore tablet.pushOntoStack(MARKETPLACE_CHECKOUT_QML_PATH); tablet.sendToQml({ method: 'updateCheckoutQML', params: { - itemId: '0d90d21c-ce7a-4990-ad18-e9d2cf991027', - itemName: 'Test Flaregun', - itemPrice: (debugError ? 10 : 17), - itemHref: 'http://mpassets.highfidelity.com/0d90d21c-ce7a-4990-ad18-e9d2cf991027-v1/flaregun.json', - categories: ["Wearables", "Miscellaneous"] + itemId: '424611a2-73d0-4c03-9087-26a6a279257b', + itemName: '2018-02-15 Finnegon', + itemPrice: (debugError ? 10 : 3), + itemHref: 'http://devmpassets.highfidelity.com/424611a2-73d0-4c03-9087-26a6a279257b-v1/finnigon.fst', + categories: ["Miscellaneous"] } }); } @@ -104,8 +104,9 @@ var selectionDisplay = null; // for gridTool.js to ignore tablet.gotoHomeScreen(); } else { Wallet.refreshWalletStatus(); - var entity = HMD.tabletID; - Entities.editEntity(entity, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); + if (HMD.tabletID) { + Entities.editEntity(HMD.tabletID, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); + } showMarketplace(); } } @@ -232,13 +233,19 @@ var selectionDisplay = null; // for gridTool.js to ignore return position; } - function rezEntity(itemHref, isWearable) { + function rezEntity(itemHref, itemType) { + var isWearable = itemType === "wearable"; var success = Clipboard.importEntities(itemHref); var wearableLocalPosition = null; var wearableLocalRotation = null; var wearableLocalDimensions = null; var wearableDimensions = null; + if (itemType === "contentSet") { + console.log("Item is a content set; codepath shouldn't go here.") + return; + } + if (isWearable) { var wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.transforms"); if (!wearableTransforms) { @@ -544,7 +551,7 @@ var selectionDisplay = null; // for gridTool.js to ignore break; case 'checkout_rezClicked': case 'purchases_rezClicked': - rezEntity(message.itemHref, message.isWearable); + rezEntity(message.itemHref, message.itemType); break; case 'header_marketplaceImageClicked': case 'purchases_backClicked': diff --git a/scripts/system/menu.js b/scripts/system/menu.js index c7a44d3e48..c27dae6780 100644 --- a/scripts/system/menu.js +++ b/scripts/system/menu.js @@ -28,8 +28,9 @@ var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet- // for toolbar-mode: go back to home screen, this will close the window. tablet.gotoHomeScreen(); } else { - var entity = HMD.tabletID; - Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); + if (HMD.tabletID) { + Entities.editEntity(HMD.tabletID, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); + } tablet.gotoMenuScreen(); } } diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index 36a1cbcdd9..100d0e82ee 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -41,14 +41,14 @@ if (!UIWebTablet) { return false; } - if (Overlays.getProperty(HMD.tabletID, "type") != "model") { + if (Overlays.getProperty(HMD.tabletID, "type") !== "model") { if (debugTablet) { print("TABLET is invalid due to frame: " + JSON.stringify(Overlays.getProperty(HMD.tabletID, "type"))); } return false; } - if (Overlays.getProperty(HMD.homeButtonID, "type") != "circle3d" || - Overlays.getProperty(HMD.tabletScreenID, "type") != "web3d") { + if (Overlays.getProperty(HMD.homeButtonID, "type") !== "circle3d" || + Overlays.getProperty(HMD.tabletScreenID, "type") !== "web3d") { if (debugTablet) { print("TABLET is invalid due to other"); } @@ -112,7 +112,7 @@ } function showTabletUI() { - checkTablet() + checkTablet(); if (!tabletRezzed || !tabletIsValid()) { closeTabletUI(); @@ -157,7 +157,7 @@ } function closeTabletUI() { - checkTablet() + checkTablet(); gTablet.tabletShown = false; if (UIWebTablet) { if (UIWebTablet.onClose) { @@ -178,14 +178,14 @@ print("TABLET closeTabletUI, UIWebTablet is null"); } tabletRezzed = false; - gTablet = null + gTablet = null; } function updateShowTablet() { var now = Date.now(); - checkTablet() + checkTablet(); // close the WebTablet if it we go into toolbar mode. var tabletShown = gTablet.tabletShown; @@ -270,7 +270,7 @@ } if (channel === "home") { if (UIWebTablet) { - checkTablet() + checkTablet(); gTablet.landscape = false; } } diff --git a/scripts/system/tablet-users.js b/scripts/system/tablet-users.js index 6181173818..92aefd1e80 100644 --- a/scripts/system/tablet-users.js +++ b/scripts/system/tablet-users.js @@ -48,9 +48,8 @@ // for toolbar-mode: go back to home screen, this will close the window. tablet.gotoHomeScreen(); } else { - var tabletEntity = HMD.tabletID; - if (tabletEntity) { - Entities.editEntity(tabletEntity, {textures: JSON.stringify({"tex.close" : HOME_BUTTON_TEXTURE})}); + if (HMD.tabletID) { + Entities.editEntity(HMD.tabletID, {textures: JSON.stringify({"tex.close" : HOME_BUTTON_TEXTURE})}); } shouldActivateButton = true; tablet.gotoWebScreen(USERS_URL); diff --git a/scripts/tutorials/entity_edit_filters/keep-in-zone-example.js b/scripts/tutorials/entity_edit_filters/keep-in-zone-example.js new file mode 100644 index 0000000000..6d83ce1410 --- /dev/null +++ b/scripts/tutorials/entity_edit_filters/keep-in-zone-example.js @@ -0,0 +1,41 @@ +// +// keep-in-zone-example.js +// +// +// Created by Brad Hefta-Gaub to use Entities on Dec. 15, 2017 +// Copyright 2017 High Fidelity, Inc. +// +// This sample entity edit filter script will keep any entity inside the bounding box of the zone this filter is applied to. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function filter(properties, type, originalProperties, zoneProperties) { + + var nearZero = 0.0001 * Math.random() + 0.001; + + /* Clamp position changes to bounding box of zone.*/ + function clamp(val, min, max) { + /* Random near-zero value used as "zero" to prevent two sequential updates from being + exactly the same (which would cause them to be ignored) */ + if (val > max) { + val = max - nearZero; + } else if (val < min) { + val = min + nearZero; + } + return val; + } + + if (properties.position) { + properties.position.x = clamp(properties.position.x, zoneProperties.boundingBox.brn.x, zoneProperties.boundingBox.tfl.x); + properties.position.y = clamp(properties.position.y, zoneProperties.boundingBox.brn.y, zoneProperties.boundingBox.tfl.y); + properties.position.z = clamp(properties.position.z, zoneProperties.boundingBox.brn.z, zoneProperties.boundingBox.tfl.z); + } + + return properties; +} + +filter.wantsOriginalProperties = true; +filter.wantsZoneProperties = true; +filter; \ No newline at end of file diff --git a/scripts/tutorials/entity_edit_filters/position-example.js b/scripts/tutorials/entity_edit_filters/position-example.js new file mode 100644 index 0000000000..314ba0fd9f --- /dev/null +++ b/scripts/tutorials/entity_edit_filters/position-example.js @@ -0,0 +1,45 @@ +// +// position-example.js +// +// +// Created by Brad Hefta-Gaub to use Entities on Dec. 15, 2017 +// Copyright 2017 High Fidelity, Inc. +// +// This sample entity edit filter script will only allow position to be changed by no more than 5 meters on any axis. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function filter(properties, type, originalProperties) { + + /* Clamp position changes.*/ + var maxChange = 5; + + function clamp(val, min, max) { + if (val > max) { + val = max; + } else if (val < min) { + val = min; + } + return val; + } + + + if (properties.position) { + /* Random near-zero value used as "zero" to prevent two sequential updates from being + exactly the same (which would cause them to be ignored) */ + var nearZero = 0.0001 * Math.random() + 0.001; + var maxFudgeChange = (maxChange + nearZero); + properties.position.x = clamp(properties.position.x, originalProperties.position.x + - maxFudgeChange, originalProperties.position.x + maxFudgeChange); + properties.position.y = clamp(properties.position.y, originalProperties.position.y + - maxFudgeChange, originalProperties.position.y + maxFudgeChange); + properties.position.z = clamp(properties.position.z, originalProperties.position.z + - maxFudgeChange, originalProperties.position.z + maxFudgeChange); + } + + return properties; +} +filter.wantsOriginalProperties = "position"; +filter; \ No newline at end of file diff --git a/scripts/tutorials/entity_edit_filters/prevent-add-delete-or-edit-of-entities-with-name-of-zone.js b/scripts/tutorials/entity_edit_filters/prevent-add-delete-or-edit-of-entities-with-name-of-zone.js new file mode 100644 index 0000000000..0da03b822d --- /dev/null +++ b/scripts/tutorials/entity_edit_filters/prevent-add-delete-or-edit-of-entities-with-name-of-zone.js @@ -0,0 +1,40 @@ +// +// prevent-add-delete-or-edit-of-entities-with-name-of-zone.js +// +// +// Created by Brad Hefta-Gaub to use Entities on Feb. 14, 2018 +// Copyright 2018 High Fidelity, Inc. +// +// This sample entity edit filter script will get all edits, adds, physcis, and deletes, but will only block +// deletes, and will pass through all others. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function filter(properties, type, originalProperties, zoneProperties) { + + // for adds, check the new properties, if the name is the same as the zone, prevent the add + // note: this case tests the example where originalProperties will be unknown, since as an + // add message, there is no existing entity to get proprties from. But we need to make sure + // zoneProperties is still set in the correct 4th parameter. + if (type == Entities.ADD_FILTER_TYPE) { + if (properties.name == zoneProperties.name) { + return false; + } + } else { + // for edits or deletes, check the "original" property for the entity against the zone's name + if (originalProperties.name == zoneProperties.name) { + return false; + } + } + return properties; +} + +filter.wantsToFilterAdd = true; // do run on add +filter.wantsToFilterEdit = true; // do not run on edit +filter.wantsToFilterPhysics = false; // do not run on physics +filter.wantsToFilterDelete = true; // do not run on delete +filter.wantsOriginalProperties = "name"; +filter.wantsZoneProperties = "name"; +filter; \ No newline at end of file diff --git a/scripts/tutorials/entity_edit_filters/prevent-add-of-entities-named-bob-example.js b/scripts/tutorials/entity_edit_filters/prevent-add-of-entities-named-bob-example.js new file mode 100644 index 0000000000..03fbe97430 --- /dev/null +++ b/scripts/tutorials/entity_edit_filters/prevent-add-of-entities-named-bob-example.js @@ -0,0 +1,26 @@ +// +// prevent-add-of-entities-named-bob-example.js +// +// +// Created by Brad Hefta-Gaub to use Entities on Jan. 25, 2018 +// Copyright 2018 High Fidelity, Inc. +// +// This sample entity edit filter script will get all edits, adds, physcis, and deletes, but will only block +// deletes, and will pass through all others. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function filter(properties, type) { + if (properties.name == "bob") { + return false; + } + return properties; +} + +filter.wantsToFilterAdd = true; // do run on add +filter.wantsToFilterEdit = false; // do not run on edit +filter.wantsToFilterPhysics = false; // do not run on physics +filter.wantsToFilterDelete = false; // do not run on delete +filter; \ No newline at end of file diff --git a/scripts/tutorials/entity_edit_filters/prevent-all-deletes.js b/scripts/tutorials/entity_edit_filters/prevent-all-deletes.js new file mode 100644 index 0000000000..0e2c54a04a --- /dev/null +++ b/scripts/tutorials/entity_edit_filters/prevent-all-deletes.js @@ -0,0 +1,22 @@ +// +// prevent-all-deletes.js +// +// +// Created by Brad Hefta-Gaub to use Entities on Jan. 25, 2018 +// Copyright 2018 High Fidelity, Inc. +// +// This sample entity edit filter script will prevent deletes of any entities. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function filter() { + return false; // all deletes are blocked +} + +filter.wantsToFilterAdd = false; // don't run on adds +filter.wantsToFilterEdit = false; // don't run on edits +filter.wantsToFilterPhysics = false; // don't run on physics +filter.wantsToFilterDelete = true; // do run on deletes +filter; \ No newline at end of file diff --git a/scripts/tutorials/entity_edit_filters/prevent-delete-in-zone-example.js b/scripts/tutorials/entity_edit_filters/prevent-delete-in-zone-example.js new file mode 100644 index 0000000000..521bb94d00 --- /dev/null +++ b/scripts/tutorials/entity_edit_filters/prevent-delete-in-zone-example.js @@ -0,0 +1,24 @@ +// +// prevent-delete-in-zone-example.js +// +// +// Created by Brad Hefta-Gaub to use Entities on Jan. 25, 2018 +// Copyright 2018 High Fidelity, Inc. +// +// This sample entity edit filter script will get all edits, adds, physcis, and deletes, but will only block +// deletes, and will pass through all others. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function filter(properties, type) { + + if (type == Entities.DELETE_FILTER_TYPE) { + return false; + } + return properties; +} + +filter.wantsToFilterDelete = true; // do run on deletes +filter; \ No newline at end of file diff --git a/tests/entities/src/main.cpp b/tests/entities/src/main.cpp index 792ef7d9c6..bf79f9d3e9 100644 --- a/tests/entities/src/main.cpp +++ b/tests/entities/src/main.cpp @@ -20,6 +20,7 @@ #include #include #include +#include const QString& getTestResourceDir() { static QString dir; @@ -136,6 +137,8 @@ void testPropertyFlags() { } int main(int argc, char** argv) { + setupHifiApplication("Entities Test"); + QCoreApplication app(argc, argv); { auto start = usecTimestampNow(); diff --git a/tests/gpu-test/src/main.cpp b/tests/gpu-test/src/main.cpp index 6a509afe4e..77ce015e3f 100644 --- a/tests/gpu-test/src/main.cpp +++ b/tests/gpu-test/src/main.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include @@ -192,7 +193,9 @@ void testSparseRectify() { } } -int main(int argc, char** argv) { +int main(int argc, char** argv) { + setupHifiApplication("GPU Test"); + testSparseRectify(); // FIXME this test appears to be broken diff --git a/tests/ktx/src/KtxTests.cpp b/tests/ktx/src/KtxTests.cpp index 94e5d7e8e7..65d9cbec3d 100644 --- a/tests/ktx/src/KtxTests.cpp +++ b/tests/ktx/src/KtxTests.cpp @@ -149,7 +149,8 @@ static const QString TEST_FOLDER { "H:/ktx_cacheold" }; static const QString EXTENSIONS { "*.ktx" }; int mainTemp(int, char**) { - qInstallMessageHandler(messageHandler); + setupHifiApplication("KTX Tests"); + auto fileInfoList = QDir { TEST_FOLDER }.entryInfoList(QStringList { EXTENSIONS }); for (auto fileInfo : fileInfoList) { qDebug() << fileInfo.filePath(); diff --git a/tests/qml/src/main.cpp b/tests/qml/src/main.cpp index 219efa5996..022f7290f4 100644 --- a/tests/qml/src/main.cpp +++ b/tests/qml/src/main.cpp @@ -179,18 +179,10 @@ void TestWindow::resizeEvent(QResizeEvent* ev) { resizeWindow(ev->size()); } -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - if (!message.isEmpty()) { -#ifdef Q_OS_WIN - OutputDebugStringA(message.toLocal8Bit().constData()); - OutputDebugStringA("\n"); -#endif - } -} +int main(int argc, char** argv) { + setupHifiApplication("QML Test"); -int main(int argc, char** argv) { QGuiApplication app(argc, argv); - qInstallMessageHandler(messageHandler); TestWindow window; app.exec(); return 0; diff --git a/tests/qt59/src/main.cpp b/tests/qt59/src/main.cpp index 7b95cabd6c..19b922de9f 100644 --- a/tests/qt59/src/main.cpp +++ b/tests/qt59/src/main.cpp @@ -63,14 +63,11 @@ void Qt59TestApp::finish(int exitCode) { int main(int argc, char * argv[]) { - QCoreApplication::setApplicationName("Qt59Test"); - QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); - QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); - QCoreApplication::setApplicationVersion(BuildInfo::VERSION); + setupHifiApplication("Qt59Test"); Qt59TestApp app(argc, argv); return app.exec(); } -#include "main.moc" \ No newline at end of file +#include "main.moc" diff --git a/tests/recording/src/main.cpp b/tests/recording/src/main.cpp index f4049b04b7..1b4e5adf6d 100644 --- a/tests/recording/src/main.cpp +++ b/tests/recording/src/main.cpp @@ -10,6 +10,8 @@ #include #include +#include + #include "Constants.h" using namespace recording; @@ -97,18 +99,10 @@ void testClipOrdering() { Q_UNUSED(lastFrameTimeOffset); // FIXME - Unix build not yet upgraded to Qt 5.5.1 we can remove this once it is } -#ifdef Q_OS_WIN32 -void myMessageHandler(QtMsgType type, const QMessageLogContext & context, const QString & msg) { - OutputDebugStringA(msg.toLocal8Bit().toStdString().c_str()); - OutputDebugStringA("\n"); -} -#endif - int main(int, const char**) { -#ifdef Q_OS_WIN32 - qInstallMessageHandler(myMessageHandler); -#endif + setupHifiApplication("Recording Test"); + testFrameTypeRegistration(); testFilePersist(); testClipOrdering(); -} \ No newline at end of file +} diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index 80b9b0eb85..ef7f35cf10 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -16,7 +16,7 @@ link_hifi_libraries( shared task workload networking animation ktx image octree gl gpu gpu-gl render render-utils - graphics fbx model-networking + graphics fbx model-networking graphics-scripting entities entities-renderer audio avatars script-engine physics procedural midi qml ui ${PLATFORM_GL_BACKEND} diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 2bf0c74067..ec56555f68 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -1138,31 +1138,17 @@ private: bool QTestWindow::_cullingEnabled = true; -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - QString logMessage = LogHandler::getInstance().printMessage((LogMsgType)type, context, message); - - if (!logMessage.isEmpty()) { -#ifdef Q_OS_WIN - OutputDebugStringA(logMessage.toLocal8Bit().constData()); - OutputDebugStringA("\n"); -#endif - logger->addMessage(qPrintable(logMessage + "\n")); - } -} - const char * LOG_FILTER_RULES = R"V0G0N( hifi.gpu=true )V0G0N"; int main(int argc, char** argv) { + setupHifiApplication("RenderPerf"); + QApplication app(argc, argv); - QCoreApplication::setApplicationName("RenderPerf"); - QCoreApplication::setOrganizationName("High Fidelity"); - QCoreApplication::setOrganizationDomain("highfidelity.com"); logger.reset(new FileLogger()); - qInstallMessageHandler(messageHandler); QLoggingCategory::setFilterRules(LOG_FILTER_RULES); QTestWindow::setup(); QTestWindow window; diff --git a/tests/render-texture-load/src/main.cpp b/tests/render-texture-load/src/main.cpp index 066b39fc40..62c970cab5 100644 --- a/tests/render-texture-load/src/main.cpp +++ b/tests/render-texture-load/src/main.cpp @@ -610,16 +610,6 @@ private: bool _ready { false }; }; -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - if (!message.isEmpty()) { -#ifdef Q_OS_WIN - OutputDebugStringA(message.toLocal8Bit().constData()); - OutputDebugStringA("\n"); -#endif - std::cout << message.toLocal8Bit().constData() << std::endl; - } -} - const char * LOG_FILTER_RULES = R"V0G0N( hifi.gpu=true )V0G0N"; @@ -645,11 +635,9 @@ void unzipTestData(const QByteArray& zipData) { } int main(int argc, char** argv) { + setupHifiApplication("RenderPerf"); + QApplication app(argc, argv); - QCoreApplication::setApplicationName("RenderPerf"); - QCoreApplication::setOrganizationName("High Fidelity"); - QCoreApplication::setOrganizationDomain("highfidelity.com"); - qInstallMessageHandler(messageHandler); QLoggingCategory::setFilterRules(LOG_FILTER_RULES); if (!DATA_DIR.exists()) { diff --git a/tests/render-utils/src/main.cpp b/tests/render-utils/src/main.cpp index 741fdbdddd..e30a80f3d9 100644 --- a/tests/render-utils/src/main.cpp +++ b/tests/render-utils/src/main.cpp @@ -179,25 +179,14 @@ void QTestWindow::draw() { } } -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - if (!message.isEmpty()) { -#ifdef Q_OS_WIN - OutputDebugStringA(message.toLocal8Bit().constData()); - OutputDebugStringA("\n"); -#else - std::cout << message.toLocal8Bit().constData() << std::endl; -#endif - } -} - - const char * LOG_FILTER_RULES = R"V0G0N( hifi.gpu=true )V0G0N"; -int main(int argc, char** argv) { +int main(int argc, char** argv) { + setupHifiApplication("Render Utils Test"); + QGuiApplication app(argc, argv); - qInstallMessageHandler(messageHandler); QLoggingCategory::setFilterRules(LOG_FILTER_RULES); QTestWindow window; QTimer timer; diff --git a/tests/shaders/src/main.cpp b/tests/shaders/src/main.cpp index 585bc432a9..a3b4196031 100644 --- a/tests/shaders/src/main.cpp +++ b/tests/shaders/src/main.cpp @@ -214,24 +214,14 @@ void QTestWindow::draw() { _context.swapBuffers(this); } -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - if (!message.isEmpty()) { -#ifdef Q_OS_WIN - OutputDebugStringA(message.toLocal8Bit().constData()); - OutputDebugStringA("\n"); -#else - std::cout << message.toLocal8Bit().constData() << std::endl; -#endif - } -} - const char * LOG_FILTER_RULES = R"V0G0N( hifi.gpu=true )V0G0N"; -int main(int argc, char** argv) { +int main(int argc, char** argv) { + setupHifiApplication("Shaders Test"); + QGuiApplication app(argc, argv); - qInstallMessageHandler(messageHandler); QLoggingCategory::setFilterRules(LOG_FILTER_RULES); QTestWindow window; QTimer timer; diff --git a/tools/ac-client/src/main.cpp b/tools/ac-client/src/main.cpp index c9affde3b5..024a891d3c 100644 --- a/tools/ac-client/src/main.cpp +++ b/tools/ac-client/src/main.cpp @@ -19,15 +19,11 @@ using namespace std; -int main(int argc, char * argv[]) { - QCoreApplication::setApplicationName(BuildInfo::AC_CLIENT_SERVER_NAME); - QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); - QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); - QCoreApplication::setApplicationVersion(BuildInfo::VERSION); - +int main(int argc, char* argv[]) { + setupHifiApplication(BuildInfo::AC_CLIENT_SERVER_NAME); + Setting::init(); ACClientApp app(argc, argv); - return app.exec(); } diff --git a/tools/atp-client/src/ATPClientApp.cpp b/tools/atp-client/src/ATPClientApp.cpp index 1623cf01cd..dbc2ad53f6 100644 --- a/tools/atp-client/src/ATPClientApp.cpp +++ b/tools/atp-client/src/ATPClientApp.cpp @@ -197,7 +197,7 @@ ATPClientApp::ATPClientApp(int argc, char* argv[]) : } auto assetClient = DependencyManager::set(); - assetClient->init(); + assetClient->initCaching(); if (_verbose) { qDebug() << "domain-server address is" << _domainServerAddress; diff --git a/tools/atp-client/src/main.cpp b/tools/atp-client/src/main.cpp index 830c049bc7..2a267c088c 100644 --- a/tools/atp-client/src/main.cpp +++ b/tools/atp-client/src/main.cpp @@ -20,14 +20,10 @@ using namespace std; int main(int argc, char * argv[]) { - QCoreApplication::setApplicationName(BuildInfo::AC_CLIENT_SERVER_NAME); - QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); - QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); - QCoreApplication::setApplicationVersion(BuildInfo::VERSION); - + setupHifiApplication("ATP Client"); + Setting::init(); ATPClientApp app(argc, argv); - return app.exec(); } diff --git a/tools/auto-tester/src/main.cpp b/tools/auto-tester/src/main.cpp index 45a3743482..6e5e06b732 100644 --- a/tools/auto-tester/src/main.cpp +++ b/tools/auto-tester/src/main.cpp @@ -17,4 +17,4 @@ int main(int argc, char *argv[]) { autoTester.show(); return application.exec(); -} \ No newline at end of file +} diff --git a/tools/ice-client/src/main.cpp b/tools/ice-client/src/main.cpp index c70a7eb7d7..60a5d4e0e4 100644 --- a/tools/ice-client/src/main.cpp +++ b/tools/ice-client/src/main.cpp @@ -13,11 +13,15 @@ #include #include +#include + #include "ICEClientApp.h" using namespace std; int main(int argc, char * argv[]) { + setupHifiApplication("ICE Client"); + ICEClientApp app(argc, argv); return app.exec(); } diff --git a/tools/oven/src/main.cpp b/tools/oven/src/main.cpp index 788470b75e..3f4afe1f15 100644 --- a/tools/oven/src/main.cpp +++ b/tools/oven/src/main.cpp @@ -10,11 +10,12 @@ #include "Oven.h" +#include #include +#include int main (int argc, char** argv) { - QCoreApplication::setOrganizationName("High Fidelity"); - QCoreApplication::setApplicationName("Oven"); + setupHifiApplication("Oven"); // init the settings interface so we can save and load settings Setting::init(); diff --git a/tools/skeleton-dump/src/main.cpp b/tools/skeleton-dump/src/main.cpp index 6cf4d41f31..d5919c4a88 100644 --- a/tools/skeleton-dump/src/main.cpp +++ b/tools/skeleton-dump/src/main.cpp @@ -14,9 +14,13 @@ #include #include +#include + #include "SkeletonDumpApp.h" int main(int argc, char * argv[]) { + setupHifiApplication("Skeleton Dump App"); + SkeletonDumpApp app(argc, argv); return app.getReturnCode(); } diff --git a/tools/udt-test/src/UDTTest.cpp b/tools/udt-test/src/UDTTest.cpp index 6161dbfdbc..ce89f04ce5 100644 --- a/tools/udt-test/src/UDTTest.cpp +++ b/tools/udt-test/src/UDTTest.cpp @@ -71,8 +71,6 @@ const QStringList SERVER_STATS_TABLE_HEADERS { UDTTest::UDTTest(int& argc, char** argv) : QCoreApplication(argc, argv) { - qInstallMessageHandler(LogHandler::verboseMessageHandler); - parseArguments(); // randomize the seed for packet size randomization diff --git a/tools/udt-test/src/main.cpp b/tools/udt-test/src/main.cpp index ccb7d0af0f..d88218c0d0 100644 --- a/tools/udt-test/src/main.cpp +++ b/tools/udt-test/src/main.cpp @@ -10,9 +10,13 @@ #include +#include + #include "UDTTest.h" int main(int argc, char* argv[]) { + setupHifiApplication("UDT Test); + UDTTest app(argc, argv); return app.exec(); } diff --git a/tools/vhacd-util/src/main.cpp b/tools/vhacd-util/src/main.cpp index 42c9db9513..817e77bf8e 100644 --- a/tools/vhacd-util/src/main.cpp +++ b/tools/vhacd-util/src/main.cpp @@ -15,6 +15,8 @@ #include #include +#include + #include "VHACDUtilApp.h" using namespace std; @@ -22,6 +24,8 @@ using namespace VHACD; int main(int argc, char * argv[]) { + setupHifiApplication("VHACD Util"); + VHACDUtilApp app(argc, argv); return app.getReturnCode(); } diff --git a/unpublishedScripts/marketplace/record/html/js/record.js b/unpublishedScripts/marketplace/record/html/js/record.js index aae4d1c89a..39278e9d7e 100644 --- a/unpublishedScripts/marketplace/record/html/js/record.js +++ b/unpublishedScripts/marketplace/record/html/js/record.js @@ -252,16 +252,17 @@ function onFinishOnOpenClicked() { } function signalBodyLoaded() { - EventBridge.emitWebEvent(JSON.stringify({ - type: EVENT_BRIDGE_TYPE, - action: BODY_LOADED_ACTION - })); + var EVENTBRIDGE_OPEN_DELAY = 500; // Delay required to ensure EventBridge is ready for use. + setTimeout(function () { + EventBridge.scriptEventReceived.connect(onScriptEventReceived); + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: BODY_LOADED_ACTION + })); + }, EVENTBRIDGE_OPEN_DELAY); } function onBodyLoaded() { - - EventBridge.scriptEventReceived.connect(onScriptEventReceived); - elRecordings = document.getElementById("recordings"); elRecordingsTable = document.getElementById("recordings-table"); diff --git a/unpublishedScripts/marketplace/record/record.js b/unpublishedScripts/marketplace/record/record.js index 68c7ea3f5a..8e49589e3c 100644 --- a/unpublishedScripts/marketplace/record/record.js +++ b/unpublishedScripts/marketplace/record/record.js @@ -139,7 +139,8 @@ } function setMappingCallback(status) { - if (status !== "") { + // FIXME: "" is for RC <= 63, null is for RC > 63. Remove the former when RC63 is no longer used. + if (status !== null && status !== "") { error("Error mapping recording to " + mappingPath + " on Asset Server!", status); return; } diff --git a/unpublishedScripts/marketplace/shapes/modules/laser.js b/unpublishedScripts/marketplace/shapes/modules/laser.js index d5feda0e1f..067b605047 100644 --- a/unpublishedScripts/marketplace/shapes/modules/laser.js +++ b/unpublishedScripts/marketplace/shapes/modules/laser.js @@ -173,8 +173,18 @@ Laser = function (side) { // Normal laser operation with trigger. intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, VISIBLE_ONLY); + var tabletIDs = []; + if (HMD.tabletID) { + tabletIDs.push(HMD.tabletID); + } + if (HMD.tabletScreenID) { + tabletIDs.push(HMD.tabletScreenID); + } + if (HMD.homeButtonID) { + tabletIDs.push(HMD.homeButtonID); + } if (Reticle.pointingAtSystemOverlay || (intersection.overlayID - && [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID].indexOf(intersection.overlayID) !== -1)) { + && tabletIDs.indexOf(intersection.overlayID) !== -1)) { // No laser if pointing at HUD overlay or tablet; system provides lasers for these cases. if (isLaserOn) { isLaserOn = false;