From 06afaa7470f90a6b152c474958315d4c7e80ddd5 Mon Sep 17 00:00:00 2001 From: humbletim Date: Thu, 21 Dec 2017 15:13:57 -0500 Subject: [PATCH 001/260] BufferView <-> QVariant/QScriptValue conversion update MeshProxy/SimpleMeshProxy and ScriptableModel ModelScriptingInterface / scriptable::ModelProvider integration update to RC-63 initial graphics-scripting refactoring graphics-scripting baseline commit wip commit Geometry -> MeshPart remove SimpleMeshProxy collapse graphics-utils -> graphics-scripting scriptable::Model => scriptable::ScriptableModel --- interface/CMakeLists.txt | 2 +- interface/src/Application.cpp | 68 +- interface/src/Application.h | 1 - interface/src/ui/overlays/Base3DOverlay.h | 6 +- interface/src/ui/overlays/ModelOverlay.cpp | 7 + interface/src/ui/overlays/ModelOverlay.h | 1 + interface/src/ui/overlays/Shape3DOverlay.cpp | 15 + interface/src/ui/overlays/Shape3DOverlay.h | 1 + libraries/avatars-renderer/CMakeLists.txt | 1 + .../src/avatars-renderer/Avatar.cpp | 28 + .../src/avatars-renderer/Avatar.h | 5 +- libraries/entities-renderer/CMakeLists.txt | 1 + .../src/RenderableEntityItem.h | 6 +- .../src/RenderableModelEntityItem.cpp | 15 +- .../src/RenderableModelEntityItem.h | 2 +- .../src/RenderablePolyLineEntityItem.cpp | 6 +- .../src/RenderablePolyLineEntityItem.h | 1 + .../src/RenderablePolyVoxEntityItem.cpp | 54 +- .../src/RenderablePolyVoxEntityItem.h | 7 +- .../src/RenderableShapeEntityItem.cpp | 21 + .../src/RenderableShapeEntityItem.h | 2 + libraries/entities/src/EntityItem.h | 4 - .../entities/src/EntityScriptingInterface.cpp | 24 - .../entities/src/EntityScriptingInterface.h | 4 - libraries/fbx/src/FBXReader.cpp | 16 +- libraries/fbx/src/OBJWriter.cpp | 78 +- libraries/graphics-scripting/CMakeLists.txt | 5 + .../graphics-scripting/BufferViewHelpers.cpp | 195 ++++ .../graphics-scripting/BufferViewHelpers.h | 18 + .../BufferViewScripting.cpp | 83 ++ .../graphics-scripting/BufferViewScripting.h | 11 + .../src/graphics-scripting/DebugNames.h | 72 ++ .../ModelScriptingInterface.cpp | 836 ++++++++++++++++++ .../ModelScriptingInterface.h | 68 ++ .../src/graphics-scripting/ScriptableMesh.cpp | 359 ++++++++ .../src/graphics-scripting/ScriptableMesh.h | 127 +++ .../src/graphics-scripting/ScriptableModel.h | 80 ++ libraries/graphics/src/graphics/Geometry.cpp | 6 + libraries/graphics/src/graphics/Geometry.h | 1 + .../src/model-networking/SimpleMeshProxy.cpp | 27 - .../src/model-networking/SimpleMeshProxy.h | 36 - libraries/render-utils/CMakeLists.txt | 1 + libraries/render-utils/src/GeometryCache.cpp | 45 + libraries/render-utils/src/GeometryCache.h | 1 + libraries/render-utils/src/Model.cpp | 87 +- libraries/render-utils/src/Model.h | 5 +- .../src/ModelScriptingInterface.cpp | 251 ------ .../src/ModelScriptingInterface.h | 39 - libraries/script-engine/src/ScriptEngine.cpp | 6 - libraries/shared/src/RegisteredMetaTypes.cpp | 65 -- libraries/shared/src/RegisteredMetaTypes.h | 42 - 51 files changed, 2242 insertions(+), 600 deletions(-) create mode 100644 libraries/graphics-scripting/CMakeLists.txt create mode 100644 libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.cpp create mode 100644 libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.h create mode 100644 libraries/graphics-scripting/src/graphics-scripting/BufferViewScripting.cpp create mode 100644 libraries/graphics-scripting/src/graphics-scripting/BufferViewScripting.h create mode 100644 libraries/graphics-scripting/src/graphics-scripting/DebugNames.h create mode 100644 libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.cpp create mode 100644 libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.h create mode 100644 libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp create mode 100644 libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h create mode 100644 libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h delete mode 100644 libraries/model-networking/src/model-networking/SimpleMeshProxy.cpp delete mode 100644 libraries/model-networking/src/model-networking/SimpleMeshProxy.h delete mode 100644 libraries/script-engine/src/ModelScriptingInterface.cpp delete mode 100644 libraries/script-engine/src/ModelScriptingInterface.h diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index ee2997e216..081eeae02e 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -191,7 +191,7 @@ endif() # link required hifi libraries link_hifi_libraries( - shared octree ktx gpu gl procedural graphics render + shared 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/src/Application.cpp b/interface/src/Application.cpp index 99bd4d5758..f24969ce60 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -166,6 +166,7 @@ #include "scripting/AccountServicesScriptingInterface.h" #include "scripting/HMDScriptingInterface.h" #include "scripting/MenuScriptingInterface.h" +#include "graphics-scripting/ModelScriptingInterface.h" #include "scripting/SettingsScriptingInterface.h" #include "scripting/WindowScriptingInterface.h" #include "scripting/ControllerScriptingInterface.h" @@ -198,7 +199,6 @@ #include #include #include -#include #include #include @@ -593,6 +593,66 @@ void messageHandler(QtMsgType type, const QMessageLogContext& context, const QSt } } + +class ApplicationMeshProvider : public scriptable::ModelProviderFactory { +public: + virtual scriptable::ModelProviderPointer lookupModelProvider(QUuid uuid) { + QString error; + + scriptable::ModelProviderPointer provider; + if (auto entityInterface = getEntityModelProvider(static_cast(uuid))) { + provider = entityInterface; + } else if (auto overlayInterface = getOverlayModelProvider(static_cast(uuid))) { + provider = overlayInterface; + } else if (auto avatarInterface = getAvatarModelProvider(uuid)) { + provider = avatarInterface; + } + + return provider; + } + + 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->metadata["providerType"] = "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->metadata["providerType"] = "overlay"; + } else { + qCWarning(interfaceapp) << "no renderer for overlay ID" << overlayID.toString(); + } + } + return provider; + } + + scriptable::ModelProviderPointer getAvatarModelProvider(QUuid sessionUUID) { + scriptable::ModelProviderPointer provider; + auto avatarManager = DependencyManager::get(); + if (auto avatar = avatarManager->getAvatarBySessionID(sessionUUID)) { + if (avatar->getSessionUUID() == sessionUUID) { + provider = std::dynamic_pointer_cast(avatar); + provider->metadata["providerType"] = "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"; @@ -737,6 +797,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(); @@ -5915,6 +5978,9 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("Scene", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Render", _renderEngine->getConfiguration().get()); + ModelScriptingInterface::registerMetaTypes(scriptEngine.data()); + scriptEngine->registerGlobalObject("Model", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); diff --git a/interface/src/Application.h b/interface/src/Application.h index ddb8ce11e5..ae07ebd9dd 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -72,7 +72,6 @@ #include #include -#include #include "FrameTimingsScriptingInterface.h" #include "Sound.h" diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index df0f3c4728..0c8bc5aacb 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -13,10 +13,11 @@ #include #include - +#include #include "Overlay.h" -class Base3DOverlay : public Overlay, public SpatiallyNestable { +namespace model { class Mesh; } +class Base3DOverlay : public Overlay, public SpatiallyNestable, public scriptable::ModelProvider { Q_OBJECT using Parent = Overlay; @@ -36,6 +37,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::ScriptableModel getScriptableModel(bool* ok = nullptr) override { return scriptable::ModelProvider::modelUnavailableError(ok); } // TODO: consider implementing registration points in this class glm::vec3 getCenter() const { return getWorldPosition(); } diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 310dbf78d8..5a80ca1abf 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -629,3 +629,10 @@ uint32_t ModelOverlay::fetchMetaSubItems(render::ItemIDs& subItems) const { } return 0; } + +scriptable::ScriptableModel ModelOverlay::getScriptableModel(bool* ok) { + if (!_model || !_model->isLoaded()) { + return Base3DOverlay::getScriptableModel(ok); + } + return _model->getScriptableModel(ok); +} diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index 60ba90e568..32d9a08c70 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -59,6 +59,7 @@ public: void setDrawInFront(bool drawInFront) override; void setDrawHUDLayer(bool drawHUDLayer) override; + virtual scriptable::ScriptableModel getScriptableModel(bool* ok = nullptr) override; protected: Transform evalRenderTransform() override; diff --git a/interface/src/ui/overlays/Shape3DOverlay.cpp b/interface/src/ui/overlays/Shape3DOverlay.cpp index 97342a80ab..8bb3d16888 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.cpp +++ b/interface/src/ui/overlays/Shape3DOverlay.cpp @@ -179,3 +179,18 @@ Transform Shape3DOverlay::evalRenderTransform() { transform.setRotation(rotation); return transform; } + +scriptable::ScriptableModel Shape3DOverlay::getScriptableModel(bool* ok) { + auto geometryCache = DependencyManager::get(); + auto vertexColor = ColorUtils::toVec3(_color); + scriptable::ScriptableModel result; + result.metadata = { + { "origin", "Shape3DOverlay::"+shapeStrings[_shape] }, + { "overlayID", getID() }, + }; + result.meshes << geometryCache->meshFromShape(_shape, vertexColor); + if (ok) { + *ok = true; + } + return result; +} diff --git a/interface/src/ui/overlays/Shape3DOverlay.h b/interface/src/ui/overlays/Shape3DOverlay.h index 7fc95ec981..34f82af278 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.h +++ b/interface/src/ui/overlays/Shape3DOverlay.h @@ -37,6 +37,7 @@ public: void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; + virtual scriptable::ScriptableModel getScriptableModel(bool* ok = nullptr) override; protected: Transform evalRenderTransform() override; diff --git a/libraries/avatars-renderer/CMakeLists.txt b/libraries/avatars-renderer/CMakeLists.txt index 53edc692f2..f7e951500b 100644 --- a/libraries/avatars-renderer/CMakeLists.txt +++ b/libraries/avatars-renderer/CMakeLists.txt @@ -13,5 +13,6 @@ include_hifi_library_headers(entities-renderer) include_hifi_library_headers(audio) include_hifi_library_headers(entities) include_hifi_library_headers(octree) +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 500a24763d..8e22f355e4 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; @@ -1760,3 +1762,29 @@ float Avatar::getUnscaledEyeHeightFromSkeleton() const { return DEFAULT_AVATAR_EYE_HEIGHT; } } + +scriptable::ScriptableModel Avatar::getScriptableModel(bool* ok) { + qDebug() << "Avatar::getScriptableModel" ; + if (!_skeletonModel || !_skeletonModel->isLoaded()) { + return scriptable::ModelProvider::modelUnavailableError(ok); + } + scriptable::ScriptableModel result; + result.metadata = { + { "avatarID", getSessionUUID().toString() }, + { "url", _skeletonModelURL.toString() }, + { "origin", "Avatar/avatar::" + _displayName }, + { "textures", _skeletonModel->getTextures() }, + }; + result.mixin(_skeletonModel->getScriptableModel(ok)); + + // FIXME: for now access to attachment models are merged with the main avatar model + for (auto& attachmentModel : _attachmentModels) { + if (attachmentModel->isLoaded()) { + result.mixin(attachmentModel->getScriptableModel(ok)); + } + } + if (ok) { + *ok = true; + } + return result; +} diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index c2b404a925..5cfc399b65 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 @@ -272,6 +273,8 @@ public: virtual void setAvatarEntityDataChanged(bool value) override; + + virtual scriptable::ScriptableModel getScriptableModel(bool* ok = nullptr) override; public slots: // FIXME - these should be migrated to use Pose data instead diff --git a/libraries/entities-renderer/CMakeLists.txt b/libraries/entities-renderer/CMakeLists.txt index ea75367e1e..27ea04f642 100644 --- a/libraries/entities-renderer/CMakeLists.txt +++ b/libraries/entities-renderer/CMakeLists.txt @@ -13,6 +13,7 @@ include_hifi_library_headers(fbx) include_hifi_library_headers(entities) include_hifi_library_headers(avatars) include_hifi_library_headers(controllers) +include_hifi_library_headers(graphics-scripting) # for ScriptableModel.h target_bullet() target_polyvox() diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 8eb82e2c6e..f07b67fbd0 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; } @@ -54,6 +55,7 @@ public: const uint64_t& getUpdateTime() const { return _updateTime; } + virtual scriptable::ScriptableModel getScriptableModel(bool* ok = nullptr) override { return scriptable::ModelProvider::modelUnavailableError(ok); } 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 9fcb7640ef..7b022fefac 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -950,13 +950,18 @@ QStringList RenderableModelEntityItem::getJointNames() const { return result; } -bool RenderableModelEntityItem::getMeshes(MeshProxyList& result) { - auto model = getModel(); + +scriptable::ScriptableModel render::entities::ModelEntityRenderer::getScriptableModel(bool* ok) { + ModelPointer model; + withReadLock([&] { + model = _model; + }); + if (!model || !model->isLoaded()) { - return false; + return scriptable::ModelProvider::modelUnavailableError(ok); } - BLOCKING_INVOKE_METHOD(model.get(), "getMeshes", Q_RETURN_ARG(MeshProxyList, result)); - return !result.isEmpty(); + + return _model->getScriptableModel(ok); } void RenderableModelEntityItem::simulateRelayedJoints() { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 33fc9910a0..3e952cb9a7 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -111,7 +111,6 @@ public: virtual int getJointIndex(const QString& name) const override; virtual QStringList getJointNames() const override; - bool getMeshes(MeshProxyList& result) override; const void* getCollisionMeshKey() const { return _collisionMeshKey; } signals: @@ -141,6 +140,7 @@ class ModelEntityRenderer : public TypedEntityRenderer PolyLineEntityRenderer::updateVertic return vertices; } +scriptable::ScriptableModel PolyLineEntityRenderer::getScriptableModel(bool *ok) { + // TODO: adapt polyline into a triangles mesh... + return EntityRenderer::getScriptableModel(ok); +} 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..3bb8901178 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::ScriptableModel getScriptableModel(bool* ok = nullptr) 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 ade3790df6..fd923c40b0 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -20,8 +20,6 @@ #include #include -#include -#include #include #include #include @@ -1416,36 +1414,36 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() { } } -bool RenderablePolyVoxEntityItem::getMeshes(MeshProxyList& result) { - if (!updateDependents()) { - return false; +scriptable::ScriptableModel RenderablePolyVoxEntityItem::getScriptableModel(bool * ok) { + if (!updateDependents() || !_mesh) { + return scriptable::ModelProvider::modelUnavailableError(ok); } bool success = false; - if (_mesh) { - MeshProxy* meshProxy = nullptr; - glm::mat4 transform = voxelToLocalMatrix(); - 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 - meshProxy = new SimpleMeshProxy( - _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; })); - result << meshProxy; - } - }); + glm::mat4 transform = voxelToLocalMatrix(); + scriptable::ScriptableModel result; + 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.meshes << + _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; }); + } + }); + if (ok) { + *ok = success; } - return success; + return result; } using namespace render; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index db0f0b729a..55b9be23d8 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,7 @@ public: void setVolDataDirty() { withWriteLock([&] { _volDataDirty = true; _meshReady = false; }); } - bool getMeshes(MeshProxyList& result) override; + virtual scriptable::ScriptableModel getScriptableModel(bool* ok = nullptr) override; private: bool updateOnCount(const ivec3& v, uint8_t toValue); @@ -163,6 +163,9 @@ class PolyVoxEntityRenderer : public TypedEntityRenderer()->getScriptableModel(ok); + } protected: virtual ItemKey getKey() override { return ItemKey::Builder::opaqueShape(); } diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index cdee2c5ec9..746102681c 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -156,3 +156,24 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { const auto triCount = geometryCache->getShapeTriangleCount(geometryShape); args->_details._trianglesRendered += (int)triCount; } + +scriptable::ScriptableModel ShapeEntityRenderer::getScriptableModel(bool* ok) { + scriptable::ScriptableModel result; + result.metadata = { + { "entityID", getEntity()->getID().toString() }, + { "shape", entity::stringFromShape(_shape) }, + { "userData", getEntity()->getUserData() }, + }; + auto geometryCache = DependencyManager::get(); + auto geometryShape = geometryCache->getShapeForEntityShape(_shape); + auto vertexColor = glm::vec3(_color); + auto success = false; + if (auto mesh = geometryCache->meshFromShape(geometryShape, vertexColor)) { + result.meshes << mesh; + success = true; + } + if (ok) { + *ok = success; + } + return result; +} diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h index 433cb41ad2..6ada7e7317 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::ScriptableModel getScriptableModel(bool* ok = nullptr) override; + private: virtual bool needsRenderUpdate() const override; virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 4c398b8a29..5c9324fc8a 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -57,8 +57,6 @@ using EntityTreeElementExtraEncodeDataPointer = std::shared_ptr(_entityTree->findEntityByEntityItemID(entityID)); - if (!entity) { - qCDebug(entities) << "EntityScriptingInterface::getMeshes no entity with ID" << entityID; - QScriptValueList args { callback.engine()->undefinedValue(), false }; - callback.call(QScriptValue(), args); - return; - } - - MeshProxyList result; - bool success = entity->getMeshes(result); - - if (success) { - QScriptValue resultAsScriptValue = meshesToScriptValue(callback.engine(), result); - QScriptValueList args { resultAsScriptValue, true }; - callback.call(QScriptValue(), args); - } else { - QScriptValueList args { callback.engine()->undefinedValue(), false }; - callback.call(QScriptValue(), args); - } -} - glm::mat4 EntityScriptingInterface::getEntityTransform(const QUuid& entityID) { glm::mat4 result; if (_entityTree) { diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 4c4e2ffbfd..da201f93eb 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -37,7 +37,6 @@ #include "BaseScriptEngine.h" class EntityTree; -class MeshProxy; // helper factory to compose standardized, async metadata queries for "magic" Entity properties // like .script and .serverScripts. This is used for automated testing of core scripting features @@ -401,9 +400,6 @@ public slots: Q_INVOKABLE bool AABoxIntersectsCapsule(const glm::vec3& low, const glm::vec3& dimensions, const glm::vec3& start, const glm::vec3& end, float radius); - // FIXME move to a renderable entity interface - Q_INVOKABLE void getMeshes(QUuid entityID, QScriptValue callback); - /**jsdoc * Returns object to world transform, excluding scale * diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 14462e0558..50abe7928f 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1967,19 +1967,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } } } - { - int i = 0; - for (const auto& mesh : geometry.meshes) { - auto name = geometry.getModelNameOfMesh(i++); - if (!name.isEmpty()) { - if (mesh._mesh) { - mesh._mesh->displayName += "#" + name; - } else { - qDebug() << "modelName but no mesh._mesh" << name; - } - } - } - } + return geometryPtr; } @@ -1995,7 +1983,7 @@ FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QStri reader._loadLightmaps = loadLightmaps; reader._lightmapLevel = lightmapLevel; - qCDebug(modelformat) << "Reading FBX: " << url; + qDebug() << "Reading FBX: " << url; return reader.extractFBXGeometry(mapping, url); } diff --git a/libraries/fbx/src/OBJWriter.cpp b/libraries/fbx/src/OBJWriter.cpp index 4441ae6649..37bced8458 100644 --- a/libraries/fbx/src/OBJWriter.cpp +++ b/libraries/fbx/src/OBJWriter.cpp @@ -46,9 +46,12 @@ 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(); @@ -81,7 +84,9 @@ bool writeOBJToTextStream(QTextStream& out, QList meshes) { // 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(); @@ -98,7 +103,9 @@ bool writeOBJToTextStream(QTextStream& out, QList meshes) { // 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,35 +113,25 @@ 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(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"; + out << QString("g %1-%2-%3\n").arg(subMeshIndex, 3, 10, QChar('0')).arg(name).arg(partIndex); - // 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; + const bool shorts = indexBuffer._element == gpu::Element::INDEX_UINT16; + auto face = [&](uint32_t i0, uint32_t i1, uint32_t i2) { + uint32_t index0, index1, index2; + if (shorts) { + index0 = indexBuffer.get(i0); + index1 = indexBuffer.get(i1); + index2 = indexBuffer.get(i2); + } else { + index0 = indexBuffer.get(i0); + index1 = indexBuffer.get(i1); + index2 = indexBuffer.get(i2); } - 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 << "f "; if (haveNormals) { @@ -146,6 +143,39 @@ bool writeOBJToTextStream(QTextStream& out, QList meshes) { out << currentVertexStartOffset + index1 + 1 << " "; out << currentVertexStartOffset + index2 + 1 << "\n"; } + }; + + // graphics::Mesh::TRIANGLES / graphics::Mesh::QUADS + // TODO -- handle other formats + uint32_t len = part._startIndex + part._numIndices; + auto stringFromTopology = [&](graphics::Mesh::Topology topo) -> QString { + return topo == graphics::Mesh::Topology::QUADS ? "QUADS" : + topo == graphics::Mesh::Topology::QUAD_STRIP ? "QUAD_STRIP" : + topo == graphics::Mesh::Topology::TRIANGLES ? "TRIANGLES" : + topo == graphics::Mesh::Topology::TRIANGLE_STRIP ? "TRIANGLE_STRIP" : + topo == graphics::Mesh::Topology::QUAD_STRIP ? "QUAD_STRIP" : + QString("topo:%1").arg((int)topo); + }; + + qCDebug(modelformat) << "OBJWriter -- part" << partIndex << "topo" << stringFromTopology(part._topology) << "index elements" << (shorts ? "uint16_t" : "uint32_t"); + 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/graphics-scripting/CMakeLists.txt b/libraries/graphics-scripting/CMakeLists.txt new file mode 100644 index 0000000000..e7fa3de155 --- /dev/null +++ b/libraries/graphics-scripting/CMakeLists.txt @@ -0,0 +1,5 @@ +set(TARGET_NAME graphics-scripting) +setup_hifi_library() +link_hifi_libraries(shared networking graphics fbx model-networking script-engine) +include_hifi_library_headers(gpu) +include_hifi_library_headers(graphics-scripting) diff --git a/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.cpp b/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.cpp new file mode 100644 index 0000000000..e865ed0e5a --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.cpp @@ -0,0 +1,195 @@ +#include "./graphics-scripting/BufferViewHelpers.h" + +#include +#include + +#include +#include +#include + +#include + +#ifdef DEBUG_BUFFERVIEW_SCRIPTING + #include "DebugNames.h" +#endif + +namespace { + const std::array XYZW = {{ "x", "y", "z", "w" }}; + const std::array ZERO123 = {{ "0", "1", "2", "3" }}; +} + +template +QVariant getBufferViewElement(const gpu::BufferView& view, quint32 index, bool asArray = false) { + return glmVecToVariant(view.get(index), asArray); +} + +template +void setBufferViewElement(const gpu::BufferView& view, quint32 index, const QVariant& v) { + view.edit(index) = glmVecFromVariant(v); +} + +//FIXME copied from Model.cpp +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; +} + +bool bufferViewElementFromVariant(const gpu::BufferView& view, quint32 index, const QVariant& v) { + const auto& element = view._element; + const auto vecN = element.getScalarCount(); + const auto dataType = element.getType(); + const auto byteLength = element.getSize(); + const auto BYTES_PER_ELEMENT = byteLength / vecN; + if (BYTES_PER_ELEMENT == 1) { + switch(vecN) { + case 2: setBufferViewElement(view, index, v); return true; + case 3: setBufferViewElement(view, index, v); return true; + case 4: { + if (element == gpu::Element::COLOR_RGBA_32) { + glm::uint32 rawColor;// = glm::packUnorm4x8(glm::vec4(glmVecFromVariant(v), 0.0f)); + glm::uint32 unused; + packNormalAndTangent(glmVecFromVariant(v), glm::vec3(), rawColor, unused); + view.edit(index) = rawColor; + } else if (element == gpu::Element::VEC4F_NORMALIZED_XYZ10W2) { + glm::uint32 packedNormal;// = glm::packSnorm3x10_1x2(glm::vec4(glmVecFromVariant(v), 0.0f)); + glm::uint32 unused; + packNormalAndTangent(glm::vec3(), glmVecFromVariant(v), unused, packedNormal); + view.edit(index) = packedNormal; + } + setBufferViewElement(view, index, v); return true; + } + } + } else if (BYTES_PER_ELEMENT == 2) { + switch(vecN) { + case 2: setBufferViewElement(view, index, v); return true; + case 3: setBufferViewElement(view, index, v); return true; + case 4: setBufferViewElement(view, index, v); return true; + } + } else if (BYTES_PER_ELEMENT == 4) { + if (dataType == gpu::FLOAT) { + switch(vecN) { + case 2: setBufferViewElement(view, index, v); return true; + case 3: setBufferViewElement(view, index, v); return true; + case 4: setBufferViewElement(view, index, v); return true; + } + } else { + switch(vecN) { + case 2: setBufferViewElement(view, index, v); return true; + case 3: setBufferViewElement(view, index, v); return true; + case 4: setBufferViewElement(view, index, v); return true; + } + } + } + return false; +} + +QVariant bufferViewElementToVariant(const gpu::BufferView& view, quint32 index, bool asArray, const char* hint) { + const auto& element = view._element; + const auto vecN = element.getScalarCount(); + const auto dataType = element.getType(); + const auto byteLength = element.getSize(); + const auto BYTES_PER_ELEMENT = byteLength / vecN; + Q_ASSERT(index < view.getNumElements()); + Q_ASSERT(index * vecN * BYTES_PER_ELEMENT < (view._size - vecN * BYTES_PER_ELEMENT)); + if (BYTES_PER_ELEMENT == 1) { + switch(vecN) { + case 2: return getBufferViewElement(view, index, asArray); + case 3: return getBufferViewElement(view, index, asArray); + case 4: { + if (element == gpu::Element::COLOR_RGBA_32) { + auto rawColor = view.get(index); + return glmVecToVariant(glm::vec3(glm::unpackUnorm4x8(rawColor))); + } else if (element == gpu::Element::VEC4F_NORMALIZED_XYZ10W2) { + auto packedNormal = view.get(index); + return glmVecToVariant(glm::vec3(glm::unpackSnorm3x10_1x2(packedNormal))); + } + + return getBufferViewElement(view, index, asArray); + } + } + } else if (BYTES_PER_ELEMENT == 2) { + switch(vecN) { + case 2: return getBufferViewElement(view, index, asArray); + case 3: return getBufferViewElement(view, index, asArray); + case 4: return getBufferViewElement(view, index, asArray); + } + } else if (BYTES_PER_ELEMENT == 4) { + if (dataType == gpu::FLOAT) { + switch(vecN) { + case 2: return getBufferViewElement(view, index, asArray); + case 3: return getBufferViewElement(view, index, asArray); + case 4: return getBufferViewElement(view, index, asArray); + } + } else { + switch(vecN) { + case 2: return getBufferViewElement(view, index, asArray); + case 3: return getBufferViewElement(view, index, asArray); + case 4: return getBufferViewElement(view, index, asArray); + } + } + } + return QVariant(); +} + +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(); + } + if (value != value) { // NAN + qWarning().nospace()<< "vec" << len << "." << components[i] << " NAN received from script.... " << v.toString(); + } + result[i] = value; + } + return result; +} + diff --git a/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.h b/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.h new file mode 100644 index 0000000000..0fe2602f6c --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.h @@ -0,0 +1,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 +// +#pragma once + +#include + +namespace gpu { class BufferView; } + +template QVariant glmVecToVariant(const T& v, bool asArray = false); +template const T glmVecFromVariant(const QVariant& v); +QVariant bufferViewElementToVariant(const gpu::BufferView& view, quint32 index, bool asArray = false, const char* hint = ""); +bool bufferViewElementFromVariant(const gpu::BufferView& view, quint32 index, const QVariant& v); + + diff --git a/libraries/graphics-scripting/src/graphics-scripting/BufferViewScripting.cpp b/libraries/graphics-scripting/src/graphics-scripting/BufferViewScripting.cpp new file mode 100644 index 0000000000..367c0589e9 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/BufferViewScripting.cpp @@ -0,0 +1,83 @@ +#include "BufferViewScripting.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#ifdef DEBUG_BUFFERVIEW_SCRIPTING + #include +#endif + +namespace { + const std::array XYZW = {{ "x", "y", "z", "w" }}; + const std::array ZERO123 = {{ "0", "1", "2", "3" }}; +} + +template +QScriptValue getBufferViewElement(QScriptEngine* js, const gpu::BufferView& view, quint32 index, bool asArray = false) { + return glmVecToScriptValue(js, view.get(index), asArray); +} + +QScriptValue bufferViewElementToScriptValue(QScriptEngine* engine, const gpu::BufferView& view, quint32 index, bool asArray, const char* hint) { + QVariant result = bufferViewElementToVariant(view, index, asArray, hint); + if (!result.isValid()) { + return QScriptValue::NullValue; + } + return engine->toScriptValue(result); +} + +template +void setBufferViewElement(const gpu::BufferView& view, quint32 index, const QScriptValue& v) { + view.edit(index) = glmVecFromScriptValue(v); +} + +bool bufferViewElementFromScriptValue(const QScriptValue& v, const gpu::BufferView& view, quint32 index) { + return bufferViewElementFromVariant(view, index, v.toVariant()); +} + +// + +template +QScriptValue glmVecToScriptValue(QScriptEngine *js, const T& v, bool asArray) { + static const auto len = T().length(); + const auto& components = asArray ? ZERO123 : XYZW; + auto obj = asArray ? js->newArray() : js->newObject(); + for (int i = 0; i < len ; i++) { + const auto key = components[i]; + const auto value = v[i]; + if (value != value) { // NAN +#ifdef DEV_BUILD + qWarning().nospace()<< "vec" << len << "." << key << " converting NAN to javascript NaN.... " << value; +#endif + obj.setProperty(key, js->globalObject().property("NaN")); + } else { + obj.setProperty(key, value); + } + } + return obj; +} + +template +const T glmVecFromScriptValue(const QScriptValue& v) { + static const auto len = T().length(); + const auto& components = v.property("x").isValid() ? XYZW : ZERO123; + T result; + for (int i = 0; i < len ; i++) { + const auto key = components[i]; + const auto value = v.property(key).toNumber(); +#ifdef DEV_BUILD + if (value != value) { // NAN + qWarning().nospace()<< "vec" << len << "." << key << " NAN received from script.... " << v.toVariant().toString(); + } +#endif + result[i] = value; + } + return result; +} diff --git a/libraries/graphics-scripting/src/graphics-scripting/BufferViewScripting.h b/libraries/graphics-scripting/src/graphics-scripting/BufferViewScripting.h new file mode 100644 index 0000000000..f2e3fe734e --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/BufferViewScripting.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace gpu { class BufferView; } +class QScriptValue; +class QScriptEngine; +template QScriptValue glmVecToScriptValue(QScriptEngine *js, const T& v, bool asArray = false); +template const T glmVecFromScriptValue(const QScriptValue& v); +QScriptValue bufferViewElementToScriptValue(QScriptEngine* engine, const gpu::BufferView& view, quint32 index, bool asArray = false, const char* hint = ""); +bool bufferViewElementFromScriptValue(const QScriptValue& v, const gpu::BufferView& view, quint32 index); diff --git a/libraries/graphics-scripting/src/graphics-scripting/DebugNames.h b/libraries/graphics-scripting/src/graphics-scripting/DebugNames.h new file mode 100644 index 0000000000..e5edf1c9d8 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/DebugNames.h @@ -0,0 +1,72 @@ +#pragma once +#include +#include +#include +#include +#include +//#include + +Q_DECLARE_METATYPE(gpu::Type); +#ifdef QT_MOC_RUN +class DebugNames { + Q_OBJECT +public: +#else + namespace DebugNames { + Q_NAMESPACE + #endif + +enum Type : uint8_t { + + FLOAT = 0, + INT32, + UINT32, + HALF, + INT16, + UINT16, + INT8, + UINT8, + + NINT32, + NUINT32, + NINT16, + NUINT16, + NINT8, + NUINT8, + + COMPRESSED, + + NUM_TYPES, + + BOOL = UINT8, + NORMALIZED_START = NINT32, +}; + + Q_ENUM_NS(Type) + enum InputSlot { + POSITION = 0, + NORMAL = 1, + COLOR = 2, + TEXCOORD0 = 3, + TEXCOORD = TEXCOORD0, + TANGENT = 4, + SKIN_CLUSTER_INDEX = 5, + SKIN_CLUSTER_WEIGHT = 6, + TEXCOORD1 = 7, + TEXCOORD2 = 8, + TEXCOORD3 = 9, + TEXCOORD4 = 10, + + NUM_INPUT_SLOTS, + + DRAW_CALL_INFO = 15, // Reserve last input slot for draw call infos + }; + + Q_ENUM_NS(InputSlot) + inline QString stringFrom(Type t) { return QVariant::fromValue(t).toString(); } + inline QString stringFrom(InputSlot t) { return QVariant::fromValue(t).toString(); } + inline QString stringFrom(gpu::Type t) { return stringFrom((Type)t); } + inline QString stringFrom(gpu::Stream::Slot t) { return stringFrom((InputSlot)t); } + + extern const QMetaObject staticMetaObject; + }; diff --git a/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.cpp new file mode 100644 index 0000000000..68a00bc02c --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.cpp @@ -0,0 +1,836 @@ +// +// ModelScriptingInterface.cpp +// libraries/script-engine/src +// +// Created by Seth Alves on 2017-1-27. +// 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 "ModelScriptingInterface.h" +#include +#include +#include +#include +#include "BaseScriptEngine.h" +#include "ScriptEngineLogging.h" +#include "OBJWriter.h" +#include "OBJReader.h" +//#include "ui/overlays/Base3DOverlay.h" +//#include "EntityTreeRenderer.h" +//#include "avatar/AvatarManager.h" +//#include "RenderableEntityItem.h" + +#include +#include + +#include + + +#include + +#include +#include "BufferViewScripting.h" + +#include "ScriptableMesh.h" + +using ScriptableMesh = scriptable::ScriptableMesh; + +#include "ModelScriptingInterface.moc" + +namespace { + QLoggingCategory model_scripting { "hifi.model.scripting" }; +} + +ModelScriptingInterface::ModelScriptingInterface(QObject* parent) : QObject(parent) { + if (auto scriptEngine = qobject_cast(parent)) { + this->registerMetaTypes(scriptEngine); + } +} + +QString ModelScriptingInterface::meshToOBJ(const scriptable::ScriptableModel& _in) { + const auto& in = _in.getMeshes(); + qCDebug(model_scripting) << "meshToOBJ" << in.size(); + if (in.size()) { + QList meshes; + foreach (const auto meshProxy, in) { + qCDebug(model_scripting) << "meshToOBJ" << meshProxy.get(); + if (meshProxy) { + meshes.append(getMeshPointer(meshProxy)); + } + } + if (meshes.size()) { + return writeOBJToString(meshes); + } + } + context()->throwError(QString("null mesh")); + return QString(); +} + +QScriptValue ModelScriptingInterface::appendMeshes(scriptable::ScriptableModel _in) { + const auto& in = _in.getMeshes(); + + // figure out the size of the resulting mesh + size_t totalVertexCount { 0 }; + size_t totalColorCount { 0 }; + size_t totalNormalCount { 0 }; + size_t totalIndexCount { 0 }; + foreach (const scriptable::ScriptableMeshPointer meshProxy, in) { + scriptable::MeshPointer mesh = getMeshPointer(meshProxy); + totalVertexCount += mesh->getNumVertices(); + + int attributeTypeColor = gpu::Stream::InputSlot::COLOR; // libraries/gpu/src/gpu/Stream.h + const gpu::BufferView& colorsBufferView = mesh->getAttributeBuffer(attributeTypeColor); + gpu::BufferView::Index numColors = (gpu::BufferView::Index)colorsBufferView.getNumElements(); + totalColorCount += numColors; + + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + const gpu::BufferView& normalsBufferView = mesh->getAttributeBuffer(attributeTypeNormal); + gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); + totalNormalCount += numNormals; + + totalIndexCount += mesh->getNumIndices(); + } + + // alloc the resulting mesh + gpu::Resource::Size combinedVertexSize = totalVertexCount * sizeof(glm::vec3); + unsigned char* combinedVertexData = new unsigned char[combinedVertexSize]; + unsigned char* combinedVertexDataCursor = combinedVertexData; + + gpu::Resource::Size combinedColorSize = totalColorCount * sizeof(glm::vec3); + unsigned char* combinedColorData = new unsigned char[combinedColorSize]; + unsigned char* combinedColorDataCursor = combinedColorData; + + gpu::Resource::Size combinedNormalSize = totalNormalCount * sizeof(glm::vec3); + unsigned char* combinedNormalData = new unsigned char[combinedNormalSize]; + unsigned char* combinedNormalDataCursor = combinedNormalData; + + gpu::Resource::Size combinedIndexSize = totalIndexCount * sizeof(uint32_t); + unsigned char* combinedIndexData = new unsigned char[combinedIndexSize]; + unsigned char* combinedIndexDataCursor = combinedIndexData; + + uint32_t indexStartOffset { 0 }; + + foreach (const scriptable::ScriptableMeshPointer meshProxy, in) { + scriptable::MeshPointer mesh = getMeshPointer(meshProxy); + mesh->forEach( + [&](glm::vec3 position){ + memcpy(combinedVertexDataCursor, &position, sizeof(position)); + combinedVertexDataCursor += sizeof(position); + }, + [&](glm::vec3 color){ + memcpy(combinedColorDataCursor, &color, sizeof(color)); + combinedColorDataCursor += sizeof(color); + }, + [&](glm::vec3 normal){ + memcpy(combinedNormalDataCursor, &normal, sizeof(normal)); + combinedNormalDataCursor += sizeof(normal); + }, + [&](uint32_t index){ + index += indexStartOffset; + memcpy(combinedIndexDataCursor, &index, sizeof(index)); + combinedIndexDataCursor += sizeof(index); + }); + + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)mesh->getNumVertices(); + indexStartOffset += numVertices; + } + + graphics::MeshPointer result(new graphics::Mesh()); + + gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* combinedVertexBuffer = new gpu::Buffer(combinedVertexSize, combinedVertexData); + gpu::BufferPointer combinedVertexBufferPointer(combinedVertexBuffer); + gpu::BufferView combinedVertexBufferView(combinedVertexBufferPointer, vertexElement); + result->setVertexBuffer(combinedVertexBufferView); + + int attributeTypeColor = gpu::Stream::InputSlot::COLOR; // libraries/gpu/src/gpu/Stream.h + gpu::Element colorElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* combinedColorsBuffer = new gpu::Buffer(combinedColorSize, combinedColorData); + gpu::BufferPointer combinedColorsBufferPointer(combinedColorsBuffer); + gpu::BufferView combinedColorsBufferView(combinedColorsBufferPointer, colorElement); + result->addAttribute(attributeTypeColor, combinedColorsBufferView); + + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* combinedNormalsBuffer = new gpu::Buffer(combinedNormalSize, combinedNormalData); + gpu::BufferPointer combinedNormalsBufferPointer(combinedNormalsBuffer); + gpu::BufferView combinedNormalsBufferView(combinedNormalsBufferPointer, normalElement); + result->addAttribute(attributeTypeNormal, combinedNormalsBufferView); + + gpu::Element indexElement = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW); + gpu::Buffer* combinedIndexesBuffer = new gpu::Buffer(combinedIndexSize, combinedIndexData); + gpu::BufferPointer combinedIndexesBufferPointer(combinedIndexesBuffer); + gpu::BufferView combinedIndexesBufferView(combinedIndexesBufferPointer, indexElement); + result->setIndexBuffer(combinedIndexesBufferView); + + std::vector parts; + parts.emplace_back(graphics::Mesh::Part((graphics::Index)0, // startIndex + (graphics::Index)result->getNumIndices(), // numIndices + (graphics::Index)0, // baseVertex + graphics::Mesh::TRIANGLES)); // topology + result->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(graphics::Mesh::Part), + (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); + + + scriptable::ScriptableMeshPointer resultProxy = scriptable::ScriptableMeshPointer(new ScriptableMesh(nullptr, result)); + return engine()->toScriptValue(result); +} + +QScriptValue ModelScriptingInterface::transformMesh(scriptable::ScriptableMeshPointer meshProxy, glm::mat4 transform) { + auto mesh = getMeshPointer(meshProxy); + if (!mesh) { + return false; + } + + graphics::MeshPointer result = mesh->map([&](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, + [&](glm::vec3 color){ return color; }, + [&](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, + [&](uint32_t index){ return index; }); + scriptable::ScriptableMeshPointer resultProxy = scriptable::ScriptableMeshPointer(new ScriptableMesh(nullptr, result)); + return engine()->toScriptValue(resultProxy); +} + +QScriptValue ModelScriptingInterface::getVertexCount(scriptable::ScriptableMeshPointer meshProxy) { + auto mesh = getMeshPointer(meshProxy); + if (!mesh) { + return -1; + } + return (uint32_t)mesh->getNumVertices(); +} + +QScriptValue ModelScriptingInterface::getVertex(scriptable::ScriptableMeshPointer meshProxy, mesh::uint32 vertexIndex) { + auto mesh = getMeshPointer(meshProxy); + + const gpu::BufferView& vertexBufferView = mesh->getVertexBuffer(); + auto numVertices = mesh->getNumVertices(); + + if (vertexIndex >= numVertices) { + context()->throwError(QString("invalid index: %1 [0,%2)").arg(vertexIndex).arg(numVertices)); + return QScriptValue::NullValue; + } + + glm::vec3 pos = vertexBufferView.get(vertexIndex); + return engine()->toScriptValue(pos); +} + +QScriptValue ModelScriptingInterface::newMesh(const QVector& vertices, + const QVector& normals, + const QVector& faces) { + graphics::MeshPointer mesh(new graphics::Mesh()); + + // vertices + auto vertexBuffer = std::make_shared(vertices.size() * sizeof(glm::vec3), (gpu::Byte*)vertices.data()); + auto vertexBufferPtr = gpu::BufferPointer(vertexBuffer); + gpu::BufferView vertexBufferView(vertexBufferPtr, 0, vertexBufferPtr->getSize(), + sizeof(glm::vec3), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + mesh->setVertexBuffer(vertexBufferView); + + if (vertices.size() == normals.size()) { + // normals + auto normalBuffer = std::make_shared(normals.size() * sizeof(glm::vec3), (gpu::Byte*)normals.data()); + auto normalBufferPtr = gpu::BufferPointer(normalBuffer); + gpu::BufferView normalBufferView(normalBufferPtr, 0, normalBufferPtr->getSize(), + sizeof(glm::vec3), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + mesh->addAttribute(gpu::Stream::NORMAL, normalBufferView); + } else { + qCWarning(model_scripting, "ModelScriptingInterface::newMesh normals must be same length as vertices"); + } + + // indices (faces) + int VERTICES_PER_TRIANGLE = 3; + int indexBufferSize = faces.size() * sizeof(uint32_t) * VERTICES_PER_TRIANGLE; + unsigned char* indexData = new unsigned char[indexBufferSize]; + unsigned char* indexDataCursor = indexData; + foreach(const mesh::MeshFace& meshFace, faces) { + for (int i = 0; i < VERTICES_PER_TRIANGLE; i++) { + memcpy(indexDataCursor, &meshFace.vertexIndices[i], sizeof(uint32_t)); + indexDataCursor += sizeof(uint32_t); + } + } + auto indexBuffer = std::make_shared(indexBufferSize, (gpu::Byte*)indexData); + auto indexBufferPtr = gpu::BufferPointer(indexBuffer); + gpu::BufferView indexBufferView(indexBufferPtr, gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW)); + mesh->setIndexBuffer(indexBufferView); + + // parts + std::vector parts; + parts.emplace_back(graphics::Mesh::Part((graphics::Index)0, // startIndex + (graphics::Index)faces.size() * 3, // numIndices + (graphics::Index)0, // baseVertex + graphics::Mesh::TRIANGLES)); // topology + mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(graphics::Mesh::Part), + (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); + + + + scriptable::ScriptableMeshPointer meshProxy = scriptable::ScriptableMeshPointer(new ScriptableMesh(nullptr, mesh)); + return engine()->toScriptValue(meshProxy); +} + +QScriptValue ModelScriptingInterface::mapAttributeValues( + QScriptValue _in, + QScriptValue scopeOrCallback, + QScriptValue methodOrName + ) { + qCInfo(model_scripting) << "mapAttributeValues" << _in.toVariant().typeName() << _in.toVariant().toString() << _in.toQObject(); + auto in = qscriptvalue_cast(_in).getMeshes(); + if (in.size()) { + foreach (scriptable::ScriptableMeshPointer meshProxy, in) { + mapMeshAttributeValues(meshProxy, scopeOrCallback, methodOrName); + } + return thisObject(); + } else if (auto meshProxy = qobject_cast(_in.toQObject())) { + return mapMeshAttributeValues(meshProxy->shared_from_this(), scopeOrCallback, methodOrName); + } else { + context()->throwError("invalid ModelProxy || MeshProxyPointer"); + } + return false; +} + + +QScriptValue ModelScriptingInterface::unrollVertices(scriptable::ScriptableMeshPointer meshProxy, bool recalcNormals) { + auto mesh = getMeshPointer(meshProxy); + qCInfo(model_scripting) << "ModelScriptingInterface::unrollVertices" << !!mesh<< !!meshProxy; + if (!mesh) { + return QScriptValue(); + } + + auto positions = mesh->getVertexBuffer(); + auto indices = mesh->getIndexBuffer(); + quint32 numPoints = (quint32)indices.getNumElements(); + auto buffer = new gpu::Buffer(); + buffer->resize(numPoints * sizeof(uint32_t)); + auto newindices = gpu::BufferView(buffer, { gpu::SCALAR, gpu::UINT32, gpu::INDEX }); + qCInfo(model_scripting) << "ModelScriptingInterface::unrollVertices numPoints" << numPoints; + auto attributeViews = ScriptableMesh::gatherBufferViews(mesh); + for (const auto& a : attributeViews) { + auto& view = a.second; + auto sz = view._element.getSize(); + auto buffer = new gpu::Buffer(); + buffer->resize(numPoints * sz); + auto points = gpu::BufferView(buffer, view._element); + auto src = (uint8_t*)view._buffer->getData(); + auto dest = (uint8_t*)points._buffer->getData(); + auto slot = ScriptableMesh::ATTRIBUTES[a.first]; + qCInfo(model_scripting) << "ModelScriptingInterface::unrollVertices buffer" << a.first; + qCInfo(model_scripting) << "ModelScriptingInterface::unrollVertices source" << view.getNumElements(); + qCInfo(model_scripting) << "ModelScriptingInterface::unrollVertices dest" << points.getNumElements(); + qCInfo(model_scripting) << "ModelScriptingInterface::unrollVertices sz" << sz << src << dest << slot; + auto esize = indices._element.getSize(); + const char* hint= a.first.toStdString().c_str(); + for(quint32 i = 0; i < numPoints; i++) { + quint32 index = esize == 4 ? indices.get(i) : indices.get(i); + newindices.edit(i) = i; + bufferViewElementFromVariant( + points, i, + bufferViewElementToVariant(view, index, false, hint) + ); + } + if (slot == gpu::Stream::POSITION) { + mesh->setVertexBuffer(points); + } else { + mesh->addAttribute(slot, points); + } + } + mesh->setIndexBuffer(newindices); + if (recalcNormals) { + recalculateNormals(meshProxy); + } + return true; +} + +namespace { + template + gpu::BufferView bufferViewFromVector(QVector elements, gpu::Element elementType) { + auto vertexBuffer = std::make_shared( + elements.size() * sizeof(T), + (gpu::Byte*)elements.data() + ); + return { vertexBuffer, 0, vertexBuffer->getSize(),sizeof(T), elementType }; + } + + gpu::BufferView cloneBufferView(const gpu::BufferView& input) { + //qCInfo(model_scripting) << "input" << input.getNumElements() << input._buffer->getSize(); + auto output = gpu::BufferView( + std::make_shared(input._buffer->getSize(), input._buffer->getData()), + input._offset, + input._size, + input._stride, + input._element + ); + //qCInfo(model_scripting) << "after" << output.getNumElements() << output._buffer->getSize(); + return output; + } + + gpu::BufferView resizedBufferView(const gpu::BufferView& input, quint32 numElements) { + auto effectiveSize = input._buffer->getSize() / input.getNumElements(); + qCInfo(model_scripting) << "resize input" << input.getNumElements() << input._buffer->getSize() << "effectiveSize" << effectiveSize; + auto vsize = input._element.getSize() * numElements; + gpu::Byte *data = new gpu::Byte[vsize]; + memset(data, 0, vsize); + auto buffer = new gpu::Buffer(vsize, (gpu::Byte*)data); + delete[] data; + auto output = gpu::BufferView(buffer, input._element); + qCInfo(model_scripting) << "resized output" << output.getNumElements() << output._buffer->getSize(); + return output; + } +} + +bool ModelScriptingInterface::replaceMeshData(scriptable::ScriptableMeshPointer dest, scriptable::ScriptableMeshPointer src, const QVector& attributeNames) { + auto target = getMeshPointer(dest); + auto source = getMeshPointer(src); + if (!target || !source) { + context()->throwError("ModelScriptingInterface::replaceMeshData -- expected dest and src to be valid mesh proxy pointers"); + return false; + } + + QVector attributes = attributeNames.isEmpty() ? src->getAttributeNames() : attributeNames; + + //qCInfo(model_scripting) << "ModelScriptingInterface::replaceMeshData -- source:" << source->displayName << "target:" << 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 = ScriptableMesh::gatherBufferViews(target); + for (const auto& a : attributeViews) { + auto slot = ScriptableMesh::ATTRIBUTES[a.first]; + if (!attributes.contains(a.first)) { + //qCInfo(model_scripting) << "ModelScriptingInterface::replaceMeshData -- pruning target attribute" << a.first << slot; + target->removeAttribute(slot); + } + } + } + + target->setVertexBuffer(cloneBufferView(source->getVertexBuffer())); + target->setIndexBuffer(cloneBufferView(source->getIndexBuffer())); + target->setPartBuffer(cloneBufferView(source->getPartBuffer())); + + for (const auto& a : attributes) { + auto slot = ScriptableMesh::ATTRIBUTES[a]; + if (slot == gpu::Stream::POSITION) { + continue; + } + // auto& before = target->getAttributeBuffer(slot); + auto& input = source->getAttributeBuffer(slot); + if (input.getNumElements() == 0) { + //qCInfo(model_scripting) << "ModelScriptingInterface::replaceMeshData buffer is empty -- pruning" << a << slot; + target->removeAttribute(slot); + } else { + // if (before.getNumElements() == 0) { + // qCInfo(model_scripting) << "ModelScriptingInterface::replaceMeshData target buffer is empty -- adding" << a << slot; + // } else { + // qCInfo(model_scripting) << "ModelScriptingInterface::replaceMeshData target buffer exists -- updating" << a << slot; + // } + target->addAttribute(slot, cloneBufferView(input)); + } + // auto& after = target->getAttributeBuffer(slot); + // qCInfo(model_scripting) << "ModelScriptingInterface::replaceMeshData" << a << slot << before.getNumElements() << " -> " << after.getNumElements(); + } + + + return true; +} + +bool ModelScriptingInterface::dedupeVertices(scriptable::ScriptableMeshPointer meshProxy, float epsilon) { + auto mesh = getMeshPointer(meshProxy); + 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 (quint32 i = 0; i < numPositions; i++) { + const quint32 numUnique = uniqueVerts.size(); + const auto& position = positions.get(i); + bool unique = true; + for (quint32 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(model_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 (quint32 i = 0; i < numIndices; i++) { + quint32 index = esize == 4 ? indices.get(i) : indices.get(i); + if (remapIndices.contains(index)) { + //qCInfo(model_scripting) << i << index << "->" << remapIndices[index]; + newIndices << remapIndices[index]; + } else { + qCInfo(model_scripting) << i << index << "!remapIndices[index]"; + } + } + + mesh->setIndexBuffer(bufferViewFromVector(newIndices, { gpu::SCALAR, gpu::UINT32, gpu::INDEX })); + mesh->setVertexBuffer(bufferViewFromVector(uniqueVerts, { gpu::VEC3, gpu::FLOAT, gpu::XYZ })); + + auto attributeViews = ScriptableMesh::gatherBufferViews(mesh); + quint32 numUniqueVerts = uniqueVerts.size(); + for (const auto& a : attributeViews) { + auto& view = a.second; + auto slot = ScriptableMesh::ATTRIBUTES[a.first]; + if (slot == gpu::Stream::POSITION) { + continue; + } + qCInfo(model_scripting) << "ModelScriptingInterface::dedupeVertices" << a.first << slot << view.getNumElements(); + auto newView = resizedBufferView(view, numUniqueVerts); + qCInfo(model_scripting) << a.first << "before: #" << view.getNumElements() << "after: #" << newView.getNumElements(); + quint32 numElements = (quint32)view.getNumElements(); + for (quint32 i = 0; i < numElements; i++) { + quint32 fromVertexIndex = i; + quint32 toVertexIndex = remapIndices.contains(fromVertexIndex) ? remapIndices[fromVertexIndex] : fromVertexIndex; + bufferViewElementFromVariant( + newView, toVertexIndex, + bufferViewElementToVariant(view, fromVertexIndex, false, "dedupe") + ); + } + mesh->addAttribute(slot, newView); + } + return true; +} + +QScriptValue ModelScriptingInterface::cloneMesh(scriptable::ScriptableMeshPointer meshProxy, bool recalcNormals) { + auto mesh = getMeshPointer(meshProxy); + if (!mesh) { + return QScriptValue::NullValue; + } + graphics::MeshPointer clone(new graphics::Mesh()); + clone->displayName = mesh->displayName + "-clone"; + qCInfo(model_scripting) << "ModelScriptingInterface::cloneMesh" << !!mesh<< !!meshProxy; + if (!mesh) { + return QScriptValue::NullValue; + } + + clone->setIndexBuffer(cloneBufferView(mesh->getIndexBuffer())); + clone->setPartBuffer(cloneBufferView(mesh->getPartBuffer())); + auto attributeViews = ScriptableMesh::gatherBufferViews(mesh); + for (const auto& a : attributeViews) { + auto& view = a.second; + auto slot = ScriptableMesh::ATTRIBUTES[a.first]; + qCInfo(model_scripting) << "ModelScriptingInterface::cloneVertices buffer" << a.first << slot; + auto points = cloneBufferView(view); + qCInfo(model_scripting) << "ModelScriptingInterface::cloneVertices source" << view.getNumElements(); + qCInfo(model_scripting) << "ModelScriptingInterface::cloneVertices dest" << points.getNumElements(); + if (slot == gpu::Stream::POSITION) { + clone->setVertexBuffer(points); + } else { + clone->addAttribute(slot, points); + } + } + + auto result = scriptable::ScriptableMeshPointer(new ScriptableMesh(nullptr, clone)); + if (recalcNormals) { + recalculateNormals(result); + } + return engine()->toScriptValue(result); +} + +bool ModelScriptingInterface::recalculateNormals(scriptable::ScriptableMeshPointer meshProxy) { + qCInfo(model_scripting) << "Recalculating normals" << !!meshProxy; + auto mesh = getMeshPointer(meshProxy); + if (!mesh) { + return false; + } + ScriptableMesh::gatherBufferViews(mesh, { "normal", "color" }); // ensures #normals >= #positions + auto normals = mesh->getAttributeBuffer(gpu::Stream::NORMAL); + auto verts = mesh->getVertexBuffer(); + auto indices = mesh->getIndexBuffer(); + auto esize = indices._element.getSize(); + auto numPoints = indices.getNumElements(); + const auto TRIANGLE = 3; + quint32 numFaces = (quint32)numPoints / TRIANGLE; + //QVector faces; + QVector faceNormals; + QMap> vertexToFaces; + //faces.resize(numFaces); + faceNormals.resize(numFaces); + auto numNormals = normals.getNumElements(); + qCInfo(model_scripting) << QString("numFaces: %1, numNormals: %2, numPoints: %3").arg(numFaces).arg(numNormals).arg(numPoints); + if (normals.getNumElements() != verts.getNumElements()) { + return false; + } + for (quint32 i = 0; i < numFaces; i++) { + quint32 I = TRIANGLE * i; + quint32 i0 = esize == 4 ? indices.get(I+0) : indices.get(I+0); + quint32 i1 = esize == 4 ? indices.get(I+1) : indices.get(I+1); + quint32 i2 = esize == 4 ? indices.get(I+2) : indices.get(I+2); + + Triangle face = { + verts.get(i1), + verts.get(i2), + verts.get(i0) + }; + faceNormals[i] = face.getNormal(); + if (glm::isnan(faceNormals[i].x)) { + qCInfo(model_scripting) << i << i0 << i1 << i2 << vec3toVariant(face.v0) << vec3toVariant(face.v1) << vec3toVariant(face.v2); + break; + } + vertexToFaces[glm::to_string(face.v0).c_str()] << i; + vertexToFaces[glm::to_string(face.v1).c_str()] << i; + vertexToFaces[glm::to_string(face.v2).c_str()] << i; + } + for (quint32 j = 0; j < numNormals; j++) { + //auto v = verts.get(j); + glm::vec3 normal { 0.0f, 0.0f, 0.0f }; + QString key { glm::to_string(verts.get(j)).c_str() }; + const auto& faces = vertexToFaces.value(key); + if (faces.size()) { + for (const auto i : faces) { + normal += faceNormals[i]; + } + normal *= 1.0f / (float)faces.size(); + } else { + static int logged = 0; + if (logged++ < 10) { + qCInfo(model_scripting) << "no faces for key!?" << key; + } + normal = verts.get(j); + } + if (glm::isnan(normal.x)) { + static int logged = 0; + if (logged++ < 10) { + qCInfo(model_scripting) << "isnan(normal.x)" << j << vec3toVariant(normal); + } + break; + } + normals.edit(j) = glm::normalize(normal); + } + return true; +} + +QScriptValue ModelScriptingInterface::mapMeshAttributeValues( + scriptable::ScriptableMeshPointer meshProxy, QScriptValue scopeOrCallback, QScriptValue methodOrName +) { + auto mesh = getMeshPointer(meshProxy); + if (!mesh) { + return false; + } + auto scopedHandler = makeScopedHandlerObject(scopeOrCallback, methodOrName); + + // input buffers + gpu::BufferView positions = mesh->getVertexBuffer(); + + const auto nPositions = positions.getNumElements(); + + // 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(); // cache value to avoid resolving each iteration + auto meshPart = js->toScriptValue(meshProxy); + + auto obj = js->newObject(); + auto attributeViews = ScriptableMesh::gatherBufferViews(mesh, { "normal", "color" }); + for (uint32_t i=0; i < nPositions; i++) { + for (const auto& a : attributeViews) { + bool asArray = a.second._element.getType() != gpu::FLOAT; + obj.setProperty(a.first, bufferViewElementToScriptValue(js, a.second, i, asArray, a.first.toStdString().c_str())); + } + auto result = callback.call(scope, { obj, i, meshPart }); + if (js->hasUncaughtException()) { + context()->throwValue(js->uncaughtException()); + return false; + } + + if (result.isBool() && !result.toBool()) { + // bail without modifying data if user explicitly returns false + continue; + } + 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); + auto& view = a.second; + if (attribute.isValid()) { + bufferViewElementFromScriptValue(attribute, view, i); + } + } + } + return thisObject(); +} + +void ModelScriptingInterface::getMeshes(QUuid uuid, QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); + Q_ASSERT(handler.engine() == this->engine()); + QPointer engine = dynamic_cast(handler.engine()); + + scriptable::ScriptableModel meshes; + bool success = false; + QString error; + + auto appProvider = DependencyManager::get(); + qDebug() << "appProvider" << appProvider.data(); + scriptable::ModelProviderPointer provider = appProvider ? appProvider->lookupModelProvider(uuid) : nullptr; + QString providerType = provider ? provider->metadata.value("providerType").toString() : QString(); + if (providerType.isEmpty()) { + providerType = "unknown"; + } + if (provider) { + qCDebug(model_scripting) << "fetching meshes from " << providerType << "..."; + auto scriptableMeshes = provider->getScriptableModel(&success); + qCDebug(model_scripting) << "//fetched meshes from " << providerType << "success:" <makeError(error), QScriptValue::NullValue); + } else { + callScopedHandlerObject(handler, QScriptValue::NullValue, engine->toScriptValue(meshes)); + } +} + +namespace { + QScriptValue meshToScriptValue(QScriptEngine* engine, scriptable::ScriptableMeshPointer const &in) { + return engine->newQObject(in.get(), QScriptEngine::QtOwnership, + QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects + ); + } + + void meshFromScriptValue(const QScriptValue& value, scriptable::ScriptableMeshPointer &out) { + auto obj = value.toQObject(); + //qDebug() << "meshFromScriptValue" << obj; + if (auto tmp = qobject_cast(obj)) { + out = tmp->shared_from_this(); + } + // FIXME: Why does above cast not work on Win32!? + if (!out) { + auto smp = static_cast(obj); + //qDebug() << "meshFromScriptValue2" << smp; + out = smp->shared_from_this(); + } + } + + QScriptValue meshesToScriptValue(QScriptEngine* engine, const scriptable::ScriptableModelPointer &in) { + // QScriptValueList result; + QScriptValue result = engine->newArray(); + int i = 0; + foreach(scriptable::ScriptableMeshPointer const meshProxy, in->getMeshes()) { + result.setProperty(i++, meshToScriptValue(engine, meshProxy)); + } + return result; + } + + void meshesFromScriptValue(const QScriptValue& value, scriptable::ScriptableModelPointer &out) { + const auto length = value.property("length").toInt32(); + qCDebug(model_scripting) << "in meshesFromScriptValue, length =" << length; + for (int i = 0; i < length; i++) { + if (const auto meshProxy = qobject_cast(value.property(i).toQObject())) { + out->meshes.append(meshProxy->getMeshPointer()); + } else { + qCDebug(model_scripting) << "null meshProxy" << i; + } + } + } + + void modelProxyFromScriptValue(const QScriptValue& object, scriptable::ScriptableModel &meshes) { + auto meshesProperty = object.property("meshes"); + if (meshesProperty.property("length").toInt32() > 0) { + //meshes._meshes = qobject_cast(meshesProperty.toQObject()); + // qDebug() << "modelProxyFromScriptValue" << meshesProperty.property("length").toInt32() << meshesProperty.toVariant().typeName(); + qScriptValueToSequence(meshesProperty, meshes.meshes); + } else if (auto mesh = qobject_cast(object.toQObject())) { + meshes.meshes << mesh->getMeshPointer(); + } else { + qDebug() << "modelProxyFromScriptValue -- unrecognized input" << object.toVariant().toString(); + } + + meshes.metadata = object.property("metadata").toVariant().toMap(); + } + + QScriptValue modelProxyToScriptValue(QScriptEngine* engine, const scriptable::ScriptableModel &in) { + QScriptValue obj = engine->newObject(); + obj.setProperty("meshes", qScriptValueFromSequence(engine, in.meshes)); + obj.setProperty("metadata", engine->toScriptValue(in.metadata)); + return obj; + } + + QScriptValue meshFaceToScriptValue(QScriptEngine* engine, const mesh::MeshFace &meshFace) { + QScriptValue obj = engine->newObject(); + obj.setProperty("vertices", qVectorIntToScriptValue(engine, meshFace.vertexIndices)); + return obj; + } + + void meshFaceFromScriptValue(const QScriptValue &object, mesh::MeshFace& meshFaceResult) { + qScriptValueToSequence(object.property("vertices"), meshFaceResult.vertexIndices); + } + + QScriptValue qVectorMeshFaceToScriptValue(QScriptEngine* engine, const QVector& vector) { + return qScriptValueFromSequence(engine, vector); + } + + void qVectorMeshFaceFromScriptValue(const QScriptValue& array, QVector& result) { + qScriptValueToSequence(array, result); + } + + QScriptValue qVectorUInt32ToScriptValue(QScriptEngine* engine, const QVector& vector) { + return qScriptValueFromSequence(engine, vector); + } + + void qVectorUInt32FromScriptValue(const QScriptValue& array, QVector& result) { + qScriptValueToSequence(array, result); + } +} + +int meshUint32 = qRegisterMetaType(); +namespace mesh { + int meshUint32 = qRegisterMetaType(); +} +int qVectorMeshUint32 = qRegisterMetaType>(); + +void ModelScriptingInterface::registerMetaTypes(QScriptEngine* engine) { + qScriptRegisterSequenceMetaType>(engine); + qScriptRegisterSequenceMetaType(engine); + qScriptRegisterSequenceMetaType>(engine); + qScriptRegisterMetaType(engine, modelProxyToScriptValue, modelProxyFromScriptValue); + + qScriptRegisterMetaType(engine, qVectorUInt32ToScriptValue, qVectorUInt32FromScriptValue); + qScriptRegisterMetaType(engine, meshToScriptValue, meshFromScriptValue); + qScriptRegisterMetaType(engine, meshesToScriptValue, meshesFromScriptValue); + qScriptRegisterMetaType(engine, meshFaceToScriptValue, meshFaceFromScriptValue); + qScriptRegisterMetaType(engine, qVectorMeshFaceToScriptValue, qVectorMeshFaceFromScriptValue); +} + +MeshPointer ModelScriptingInterface::getMeshPointer(scriptable::ScriptableMeshPointer meshProxy) { + MeshPointer result; + if (!meshProxy) { + if (context()){ + context()->throwError("expected meshProxy as first parameter"); + } + return result; + } + auto mesh = meshProxy->getMeshPointer(); + if (!mesh) { + if (context()) { + context()->throwError("expected valid meshProxy as first parameter"); + } + return result; + } + return mesh; +} diff --git a/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.h b/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.h new file mode 100644 index 0000000000..d10fd28170 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.h @@ -0,0 +1,68 @@ +// +// ModelScriptingInterface.h +// libraries/script-engine/src +// +// Created by Seth Alves on 2017-1-27. +// 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_ModelScriptingInterface_h +#define hifi_ModelScriptingInterface_h + +#include +#include + +#include + +#include +#include + +#include "ScriptableMesh.h" +#include +class ModelScriptingInterface : public QObject, public QScriptable, public Dependency { + Q_OBJECT + +public: + ModelScriptingInterface(QObject* parent = nullptr); + static void registerMetaTypes(QScriptEngine* engine); + +public slots: + /**jsdoc + * Returns the meshes associated with a UUID (entityID, overlayID, or avatarID) + * + * @function ModelScriptingInterface.getMeshes + * @param {EntityID} entityID The ID of the entity whose meshes are to be retrieve + */ + void getMeshes(QUuid uuid, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue()); + + bool dedupeVertices(scriptable::ScriptableMeshPointer meshProxy, float epsilon = 1e-6); + bool recalculateNormals(scriptable::ScriptableMeshPointer meshProxy); + QScriptValue cloneMesh(scriptable::ScriptableMeshPointer meshProxy, bool recalcNormals = true); + QScriptValue unrollVertices(scriptable::ScriptableMeshPointer meshProxy, bool recalcNormals = true); + QScriptValue mapAttributeValues(QScriptValue in, + QScriptValue scopeOrCallback, + QScriptValue methodOrName = QScriptValue()); + QScriptValue mapMeshAttributeValues(scriptable::ScriptableMeshPointer meshProxy, + QScriptValue scopeOrCallback, + QScriptValue methodOrName = QScriptValue()); + + QString meshToOBJ(const scriptable::ScriptableModel& in); + + bool replaceMeshData(scriptable::ScriptableMeshPointer dest, scriptable::ScriptableMeshPointer source, const QVector& attributeNames = QVector()); + QScriptValue appendMeshes(scriptable::ScriptableModel in); + QScriptValue transformMesh(scriptable::ScriptableMeshPointer meshProxy, glm::mat4 transform); + QScriptValue newMesh(const QVector& vertices, + const QVector& normals, + const QVector& faces); + QScriptValue getVertexCount(scriptable::ScriptableMeshPointer meshProxy); + QScriptValue getVertex(scriptable::ScriptableMeshPointer meshProxy, mesh::uint32 vertexIndex); + +private: + scriptable::MeshPointer getMeshPointer(scriptable::ScriptableMeshPointer meshProxy); + +}; + +#endif // hifi_ModelScriptingInterface_h 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..47d91e9e59 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp @@ -0,0 +1,359 @@ +// +// SimpleMeshProxy.cpp +// libraries/model-networking/src/model-networking/ +// +// Created by Seth Alves on 2017-3-22. +// 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 "ScriptableMesh.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "ScriptableMesh.moc" + +#include + +QLoggingCategory mesh_logging { "hifi.scripting.mesh" }; + +// FIXME: unroll/resolve before PR +using namespace scriptable; +QMap ScriptableMesh::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 }, +}; + +QVector scriptable::ScriptableModel::getMeshes() const { + QVector out; + for(auto& mesh : meshes) { + out << scriptable::ScriptableMeshPointer(new ScriptableMesh(std::const_pointer_cast(this->shared_from_this()), mesh)); + } + return out; +} + +quint32 ScriptableMesh::getNumVertices() const { + if (auto mesh = getMeshPointer()) { + return (quint32)mesh->getNumVertices(); + } + return 0; +} + +// glm::vec3 ScriptableMesh::getPos3(quint32 index) const { +// if (auto mesh = getMeshPointer()) { +// if (index < getNumVertices()) { +// return mesh->getPos3(index); +// } +// } +// return glm::vec3(NAN); +// } + +namespace { + gpu::BufferView getBufferView(scriptable::MeshPointer mesh, gpu::Stream::Slot slot) { + return slot == gpu::Stream::POSITION ? mesh->getVertexBuffer() : mesh->getAttributeBuffer(slot); + } +} + +QVector ScriptableMesh::findNearbyIndices(const glm::vec3& origin, float epsilon) const { + QVector result; + if (auto mesh = getMeshPointer()) { + const auto& pos = getBufferView(mesh, gpu::Stream::POSITION); + const uint32_t num = (uint32_t)pos.getNumElements(); + for (uint32_t i = 0; i < num; i++) { + const auto& position = pos.get(i); + if (glm::distance(position, origin) <= epsilon) { + result << i; + } + } + } + return result; +} + +QVector ScriptableMesh::getIndices() const { + QVector result; + if (auto mesh = getMeshPointer()) { + qCDebug(mesh_logging, "getTriangleIndices mesh %p", mesh.get()); + gpu::BufferView indexBufferView = mesh->getIndexBuffer(); + if (quint32 count = (quint32)indexBufferView.getNumElements()) { + result.resize(count); + auto buffer = indexBufferView._buffer; + if (indexBufferView._element.getSize() == 4) { + // memcpy(result.data(), buffer->getData(), result.size()*sizeof(quint32)); + for (quint32 i = 0; i < count; i++) { + result[i] = indexBufferView.get(i); + } + } else { + for (quint32 i = 0; i < count; i++) { + result[i] = indexBufferView.get(i); + } + } + } + } + return result; +} + +quint32 ScriptableMesh::getNumAttributes() const { + if (auto mesh = getMeshPointer()) { + return (quint32)mesh->getNumAttributes(); + } + return 0; +} +QVector ScriptableMesh::getAttributeNames() const { + QVector result; + if (auto mesh = getMeshPointer()) { + for (const auto& a : ATTRIBUTES.toStdMap()) { + auto bufferView = getBufferView(mesh, a.second); + if (bufferView.getNumElements() > 0) { + result << a.first; + } + } + } + return result; +} + +// override +QVariantMap ScriptableMesh::getVertexAttributes(quint32 vertexIndex) const { + return getVertexAttributes(vertexIndex, getAttributeNames()); +} + +bool ScriptableMesh::setVertexAttributes(quint32 vertexIndex, QVariantMap attributes) { + qDebug() << "setVertexAttributes" << vertexIndex << attributes; + for (auto& a : gatherBufferViews(getMeshPointer())) { + const auto& name = a.first; + const auto& value = attributes.value(name); + if (value.isValid()) { + auto& view = a.second; + bufferViewElementFromVariant(view, vertexIndex, value); + } else { + qCDebug(mesh_logging) << "setVertexAttributes" << vertexIndex << name; + } + } + return true; +} + +int ScriptableMesh::_getSlotNumber(const QString& attributeName) const { + if (auto mesh = getMeshPointer()) { + return ATTRIBUTES.value(attributeName, -1); + } + return -1; +} + + +QVariantMap ScriptableMesh::getMeshExtents() const { + auto mesh = getMeshPointer(); + auto box = mesh ? mesh->evalPartsBound(0, (int)mesh->getNumParts()) : AABox(); + return { + { "brn", glmVecToVariant(box.getCorner()) }, + { "tfl", glmVecToVariant(box.calcTopFarLeft()) }, + { "center", glmVecToVariant(box.calcCenter()) }, + { "min", glmVecToVariant(box.getMinimumPoint()) }, + { "max", glmVecToVariant(box.getMaximumPoint()) }, + { "dimensions", glmVecToVariant(box.getDimensions()) }, + }; +} + +quint32 ScriptableMesh::getNumParts() const { + if (auto mesh = getMeshPointer()) { + return (quint32)mesh->getNumParts(); + } + return 0; +} + +QVariantMap ScriptableMesh::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 ScriptableMesh::translate(const glm::vec3& translation) { + return transform(glm::translate(translation)); +} +QVariantMap ScriptableMesh::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 ScriptableMesh::rotateDegrees(const glm::vec3& eulerAngles, const glm::vec3& origin) { + return rotate(glm::quat(glm::radians(eulerAngles)), origin); +} +QVariantMap ScriptableMesh::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 ScriptableMesh::transform(const glm::mat4& transform) { + if (auto mesh = getMeshPointer()) { + const auto& pos = getBufferView(mesh, gpu::Stream::POSITION); + const uint32_t num = (uint32_t)pos.getNumElements(); + for (uint32_t i = 0; i < num; i++) { + auto& position = pos.edit(i); + position = transform * glm::vec4(position, 1.0f); + } + } + return getMeshExtents(); +} + +QVariantList ScriptableMesh::getAttributeValues(const QString& attributeName) const { + QVariantList result; + auto slotNum = _getSlotNumber(attributeName); + if (slotNum >= 0) { + auto slot = (gpu::Stream::Slot)slotNum; + const auto& bufferView = getBufferView(getMeshPointer(), slot); + if (auto len = bufferView.getNumElements()) { + bool asArray = bufferView._element.getType() != gpu::FLOAT; + for (quint32 i = 0; i < len; i++) { + result << bufferViewElementToVariant(bufferView, i, asArray, attributeName.toStdString().c_str()); + } + } + } + return result; +} +QVariantMap ScriptableMesh::getVertexAttributes(quint32 vertexIndex, QVector names) const { + QVariantMap result; + auto mesh = getMeshPointer(); + if (!mesh || vertexIndex >= getNumVertices()) { + return result; + } + for (const auto& a : ATTRIBUTES.toStdMap()) { + auto name = a.first; + if (!names.contains(name)) { + continue; + } + auto slot = a.second; + const gpu::BufferView& bufferView = getBufferView(mesh, slot); + if (vertexIndex < bufferView.getNumElements()) { + bool asArray = bufferView._element.getType() != gpu::FLOAT; + result[name] = bufferViewElementToVariant(bufferView, vertexIndex, asArray, name.toStdString().c_str()); + } + } + return result; +} + +/// --- buffer view <-> variant helpers + +namespace { + // expand the corresponding attribute buffer (creating it if needed) so that it matches POSITIONS size and specified element type + gpu::BufferView _expandedAttributeBuffer(const scriptable::MeshPointer mesh, gpu::Stream::Slot slot, const gpu::Element& elementType) { + gpu::Size elementSize = elementType.getSize(); + gpu::BufferView bufferView = getBufferView(mesh, slot); + auto nPositions = mesh->getNumVertices(); + auto vsize = nPositions * elementSize; + auto diffTypes = (elementType.getType() != bufferView._element.getType() || + elementType.getSize() > bufferView._element.getSize() || + elementType.getScalarCount() > bufferView._element.getScalarCount() || + vsize > bufferView._size + ); + auto hint = DebugNames::stringFrom(slot); + +#ifdef DEV_BUILD + auto beforeCount = bufferView.getNumElements(); + auto beforeTotal = bufferView._size; +#endif + if (bufferView.getNumElements() < nPositions || diffTypes) { + if (!bufferView._buffer || bufferView.getNumElements() == 0) { + qCInfo(mesh_logging).nospace() << "ScriptableMesh -- adding missing mesh attribute '" << hint << "' for BufferView"; + gpu::Byte *data = new gpu::Byte[vsize]; + memset(data, 0, vsize); + auto buffer = new gpu::Buffer(vsize, (gpu::Byte*)data); + delete[] data; + bufferView = gpu::BufferView(buffer, elementType); + mesh->addAttribute(slot, bufferView); + } else { + qCInfo(mesh_logging) << "ScriptableMesh -- resizing Buffer current:" << hint << bufferView._buffer->getSize() << "wanted:" << vsize; + bufferView._element = elementType; + bufferView._buffer->resize(vsize); + bufferView._size = bufferView._buffer->getSize(); + } + } +#ifdef DEV_BUILD + auto afterCount = bufferView.getNumElements(); + auto afterTotal = bufferView._size; + if (beforeTotal != afterTotal || beforeCount != afterCount) { + auto typeName = DebugNames::stringFrom(bufferView._element.getType()); + qCDebug(mesh_logging, "NOTE:: _expandedAttributeBuffer.%s vec%d %s (before count=%lu bytes=%lu // after count=%lu bytes=%lu)", + hint.toStdString().c_str(), bufferView._element.getScalarCount(), + typeName.toStdString().c_str(), beforeCount, beforeTotal, afterCount, afterTotal); + } +#endif + return bufferView; + } + const gpu::Element UNUSED{ gpu::SCALAR, gpu::UINT8, gpu::RAW }; + + gpu::Element getVecNElement(gpu::Type T, int N) { + switch(N) { + case 2: return { gpu::VEC2, T, gpu::XY }; + case 3: return { gpu::VEC3, T, gpu::XYZ }; + case 4: return { gpu::VEC4, T, gpu::XYZW }; + } + Q_ASSERT(false); + return UNUSED; + } + + gpu::BufferView expandAttributeToMatchPositions(scriptable::MeshPointer mesh, gpu::Stream::Slot slot) { + if (slot == gpu::Stream::POSITION) { + return getBufferView(mesh, slot); + } + return _expandedAttributeBuffer(mesh, slot, getVecNElement(gpu::FLOAT, 3)); + } +} + +std::map ScriptableMesh::gatherBufferViews(scriptable::MeshPointer mesh, const QStringList& expandToMatchPositions) { + std::map attributeViews; + if (!mesh) { + return attributeViews; + } + for (const auto& a : ScriptableMesh::ATTRIBUTES.toStdMap()) { + auto name = a.first; + auto slot = a.second; + if (expandToMatchPositions.contains(name)) { + expandAttributeToMatchPositions(mesh, slot); + } + auto view = getBufferView(mesh, slot); + auto beforeCount = view.getNumElements(); + if (beforeCount > 0) { + auto element = view._element; + auto vecN = element.getScalarCount(); + auto type = element.getType(); + QString typeName = DebugNames::stringFrom(element.getType()); + auto beforeTotal = view._size; + + attributeViews[name] = _expandedAttributeBuffer(mesh, slot, getVecNElement(type, vecN)); + +#if DEV_BUILD + auto afterTotal = attributeViews[name]._size; + auto afterCount = attributeViews[name].getNumElements(); + if (beforeTotal != afterTotal || beforeCount != afterCount) { + qCDebug(mesh_logging, "NOTE:: gatherBufferViews.%s vec%d %s (before count=%lu bytes=%lu // after count=%lu bytes=%lu)", + name.toStdString().c_str(), vecN, typeName.toStdString().c_str(), beforeCount, beforeTotal, afterCount, afterTotal); + } +#endif + } + } + return attributeViews; +} 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..da11002906 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace graphics { + class Mesh; +} +namespace gpu { + class BufferView; +} +namespace scriptable { + class ScriptableMesh : public QObject, public std::enable_shared_from_this { + Q_OBJECT + public: + ScriptableModelPointer _model; + scriptable::MeshPointer _mesh; + QVariantMap _metadata; + ScriptableMesh() : QObject() {} + ScriptableMesh(ScriptableModelPointer parent, scriptable::MeshPointer mesh) : QObject(), _model(parent), _mesh(mesh) {} + ScriptableMesh(const ScriptableMesh& other) : QObject(), _model(other._model), _mesh(other._mesh), _metadata(other._metadata) {} + ~ScriptableMesh() { qDebug() << "~ScriptableMesh" << this; } + Q_PROPERTY(quint32 numParts READ getNumParts) + Q_PROPERTY(quint32 numAttributes READ getNumAttributes) + Q_PROPERTY(quint32 numVertices READ getNumVertices) + Q_PROPERTY(quint32 numIndices READ getNumIndices) + Q_PROPERTY(QVector attributeNames READ getAttributeNames) + + virtual scriptable::MeshPointer getMeshPointer() const { return _mesh; } + Q_INVOKABLE virtual quint32 getNumParts() const; + Q_INVOKABLE virtual quint32 getNumVertices() const; + Q_INVOKABLE virtual quint32 getNumAttributes() const; + Q_INVOKABLE virtual quint32 getNumIndices() const { return 0; } + Q_INVOKABLE virtual QVector getAttributeNames() const; + Q_INVOKABLE virtual QVariantMap getVertexAttributes(quint32 vertexIndex) const; + Q_INVOKABLE virtual QVariantMap getVertexAttributes(quint32 vertexIndex, QVector attributes) const; + + Q_INVOKABLE virtual QVector getIndices() const; + Q_INVOKABLE virtual QVector findNearbyIndices(const glm::vec3& origin, float epsilon = 1e-6) const; + Q_INVOKABLE virtual QVariantMap getMeshExtents() const; + Q_INVOKABLE virtual bool setVertexAttributes(quint32 vertexIndex, QVariantMap attributes); + Q_INVOKABLE virtual QVariantMap scaleToFit(float unitScale); + + static QMap ATTRIBUTES; + static std::map gatherBufferViews(MeshPointer mesh, const QStringList& expandToMatchPositions = QStringList()); + + Q_INVOKABLE QVariantList getAttributeValues(const QString& attributeName) const; + + Q_INVOKABLE int _getSlotNumber(const QString& attributeName) const; + + 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)); + Q_INVOKABLE QVariantMap transform(const glm::mat4& transform); + }; + + // TODO: for now this is a part-specific wrapper around ScriptableMesh + class ScriptableMeshPart : public ScriptableMesh { + Q_OBJECT + public: + ScriptableMeshPart& operator=(const ScriptableMeshPart& view) { _model=view._model; _mesh=view._mesh; return *this; }; + ScriptableMeshPart(const ScriptableMeshPart& other) : ScriptableMesh(other._model, other._mesh) {} + ScriptableMeshPart() : ScriptableMesh(nullptr, nullptr) {} + ~ScriptableMeshPart() { qDebug() << "~ScriptableMeshPart" << this; } + ScriptableMeshPart(ScriptableMeshPointer mesh) : ScriptableMesh(mesh->_model, mesh->_mesh) {} + Q_PROPERTY(QString topology READ getTopology) + Q_PROPERTY(quint32 numFaces READ getNumFaces) + + scriptable::MeshPointer parentMesh; + int partIndex; + QString getTopology() const { return "triangles"; } + Q_INVOKABLE virtual quint32 getNumFaces() const { return getIndices().size() / 3; } + Q_INVOKABLE virtual QVector getFace(quint32 faceIndex) const { + auto inds = getIndices(); + return faceIndex+2 < (quint32)inds.size() ? inds.mid(faceIndex*3, 3) : QVector(); + } + }; + + class GraphicsScriptingInterface : public QObject { + Q_OBJECT + public: + GraphicsScriptingInterface(QObject* parent = nullptr) : QObject(parent) {} + GraphicsScriptingInterface(const GraphicsScriptingInterface& other) {} + public slots: + ScriptableMeshPart exportMeshPart(ScriptableMesh mesh, int part) { return {}; } + + }; +} + +Q_DECLARE_METATYPE(scriptable::ScriptableMesh) +Q_DECLARE_METATYPE(scriptable::ScriptableMeshPointer) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(scriptable::ScriptableMeshPart) +Q_DECLARE_METATYPE(scriptable::GraphicsScriptingInterface) + +// FIXME: faces were supported in the original Model.* API -- are they still needed/used/useful for anything yet? +#include + +namespace mesh { + using uint32 = quint32; + class MeshFace; + using MeshFaces = QVector; + class MeshFace { + public: + MeshFace() {} + MeshFace(QVector vertexIndices) : vertexIndices(vertexIndices) {} + ~MeshFace() {} + + QVector vertexIndices; + // TODO -- material... + }; +}; + +Q_DECLARE_METATYPE(mesh::MeshFace) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(mesh::uint32) +Q_DECLARE_METATYPE(QVector) 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..e8cf6f1656 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace graphics { + class Mesh; +} +namespace gpu { + class BufferView; +} +namespace scriptable { + using Mesh = graphics::Mesh; + using MeshPointer = std::shared_ptr; + + class ScriptableModel; + class ScriptableMesh; + class ScriptableMeshPart; + using ScriptableModelPointer = std::shared_ptr; + using ScriptableMeshPointer = std::shared_ptr; + using ScriptableMeshPartPointer = std::shared_ptr; + class ScriptableModel : public QObject, public std::enable_shared_from_this { + Q_OBJECT + public: + Q_PROPERTY(QVector meshes READ getMeshes) + + Q_INVOKABLE QString toString() { return "[ScriptableModel " + objectName()+"]"; } + ScriptableModel(QObject* parent = nullptr) : QObject(parent) {} + ScriptableModel(const ScriptableModel& other) : objectID(other.objectID), metadata(other.metadata), meshes(other.meshes) {} + ScriptableModel& operator=(const ScriptableModel& view) { + objectID = view.objectID; + metadata = view.metadata; + meshes = view.meshes; + return *this; + } + ~ScriptableModel() { qDebug() << "~ScriptableModel" << this; } + void mixin(const ScriptableModel& other) { + for (const auto& key : other.metadata.keys()) { + metadata[key] = other.metadata[key]; + } + for(const auto&mesh : other.meshes) { + meshes << mesh; + } + } + QUuid objectID; + QVariantMap metadata; + QVector meshes; + // TODO: in future accessors for these could go here + QVariantMap shapes; + QVariantMap materials; + QVariantMap armature; + + QVector getMeshes() const; + }; + + class ModelProvider { + public: + QVariantMap metadata; + static scriptable::ScriptableModel modelUnavailableError(bool* ok) { if (ok) { *ok = false; } return {}; } + virtual scriptable::ScriptableModel getScriptableModel(bool* ok = nullptr) = 0; + }; + using ModelProviderPointer = std::shared_ptr; + class ModelProviderFactory : public Dependency { + public: + virtual scriptable::ModelProviderPointer lookupModelProvider(QUuid uuid) = 0; + }; + +} + +Q_DECLARE_METATYPE(scriptable::MeshPointer) +Q_DECLARE_METATYPE(scriptable::ScriptableModel) +Q_DECLARE_METATYPE(scriptable::ScriptableModelPointer) + 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..23ebec2965 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 diff --git a/libraries/model-networking/src/model-networking/SimpleMeshProxy.cpp b/libraries/model-networking/src/model-networking/SimpleMeshProxy.cpp deleted file mode 100644 index 741478789e..0000000000 --- a/libraries/model-networking/src/model-networking/SimpleMeshProxy.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// -// SimpleMeshProxy.cpp -// libraries/model-networking/src/model-networking/ -// -// Created by Seth Alves on 2017-3-22. -// 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 "SimpleMeshProxy.h" - -#include - -MeshPointer SimpleMeshProxy::getMeshPointer() const { - return _mesh; -} - -int SimpleMeshProxy::getNumVertices() const { - return (int)_mesh->getNumVertices(); -} - -glm::vec3 SimpleMeshProxy::getPos3(int index) const { - return _mesh->getPos3(index); -} - diff --git a/libraries/model-networking/src/model-networking/SimpleMeshProxy.h b/libraries/model-networking/src/model-networking/SimpleMeshProxy.h deleted file mode 100644 index 24c3fca27e..0000000000 --- a/libraries/model-networking/src/model-networking/SimpleMeshProxy.h +++ /dev/null @@ -1,36 +0,0 @@ -// -// SimpleMeshProxy.h -// libraries/model-networking/src/model-networking/ -// -// Created by Seth Alves on 2017-1-27. -// 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_SimpleMeshProxy_h -#define hifi_SimpleMeshProxy_h - -#include -#include -#include - -#include - -class SimpleMeshProxy : public MeshProxy { -public: - SimpleMeshProxy(const MeshPointer& mesh) : _mesh(mesh) { } - - MeshPointer getMeshPointer() const override; - - int getNumVertices() const override; - - glm::vec3 getPos3(int index) const override; - - -protected: - const MeshPointer _mesh; -}; - -#endif // hifi_SimpleMeshProxy_h diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 6be3057c93..55762e38fd 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -7,6 +7,7 @@ link_hifi_libraries(shared ktx gpu graphics model-networking render animation fb include_hifi_library_headers(networking) include_hifi_library_headers(octree) include_hifi_library_headers(audio) +include_hifi_library_headers(graphics-scripting) # for ScriptableModel.h if (NOT ANDROID) target_nsight() diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 23473e74f2..6aa42cf6df 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -2407,3 +2407,48 @@ 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 cloneBufferView = [](const gpu::BufferView& in) -> gpu::BufferView { + auto buffer = std::make_shared(*in._buffer); // copy + // FIXME: gpu::BufferView seems to have a bug where constructing a new instance from an existing one + // results in over-multiplied buffer/view sizes -- hence constructing manually here from each input prop + auto out = gpu::BufferView(buffer, in._offset, in._size, in._stride, in._element); + Q_ASSERT(out.getNumElements() == in.getNumElements()); + Q_ASSERT(out._size == in._size); + Q_ASSERT(out._buffer->getSize() == in._buffer->getSize()); + return out; + }; + + auto positionsBufferView = cloneBufferView(shapeData->_positionView); + auto normalsBufferView = cloneBufferView(shapeData->_normalView); + auto indexBufferView = cloneBufferView(shapeData->_indicesView); + + gpu::BufferView::Size numVertices = positionsBufferView.getNumElements(); + Q_ASSERT(numVertices == normalsBufferView.getNumElements()); + + // apply input color across all vertices + auto colorsBufferView = cloneBufferView(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->displayName = QString("GeometryCache/shape::%1").arg(GeometryCache::stringFromShape(geometryShape)); + + 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/Model.cpp b/libraries/render-utils/src/Model.cpp index b0763c0fb3..d595136c56 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -26,7 +26,7 @@ #include #include -#include +#include #include #include @@ -573,15 +573,21 @@ bool Model::convexHullContains(glm::vec3 point) { return false; } -MeshProxyList Model::getMeshes() const { - MeshProxyList result; +scriptable::ScriptableModel Model::getScriptableModel(bool* ok) { + scriptable::ScriptableModel result; const Geometry::Pointer& renderGeometry = getGeometry(); - const Geometry::GeometryMeshes& meshes = renderGeometry->getMeshes(); if (!isLoaded()) { + qDebug() << "Model::getScriptableModel -- !isLoaded"; + if (ok) { + *ok = false; + } return result; } +// TODO: remove -- this was an earlier approach using renderGeometry instead of FBXGeometry +#if 0 // renderGeometry approach + const Geometry::GeometryMeshes& meshes = renderGeometry->getMeshes(); Transform offset; offset.setScale(_scale); offset.postTranslate(_offset); @@ -591,20 +597,67 @@ MeshProxyList Model::getMeshes() const { if (!mesh) { continue; } - - MeshProxy* meshProxy = new SimpleMeshProxy( - mesh->map( - [=](glm::vec3 position) { - return glm::vec3(offsetMat * glm::vec4(position, 1.0f)); - }, - [=](glm::vec3 color) { return color; }, - [=](glm::vec3 normal) { - return glm::normalize(glm::vec3(offsetMat * glm::vec4(normal, 0.0f))); - }, - [&](uint32_t index) { return index; })); - result << meshProxy; + qDebug() << "Model::getScriptableModel #" << i++ << mesh->displayName; + auto newmesh = mesh->map( + [=](glm::vec3 position) { + return glm::vec3(offsetMat * glm::vec4(position, 1.0f)); + }, + [=](glm::vec3 color) { return color; }, + [=](glm::vec3 normal) { + return glm::normalize(glm::vec3(offsetMat * glm::vec4(normal, 0.0f))); + }, + [&](uint32_t index) { return index; }); + newmesh->displayName = mesh->displayName; + result << newmesh; } - +#endif + const FBXGeometry& geometry = getFBXGeometry(); + auto mat4toVariant = [](const glm::mat4& mat4) -> QVariant { + QVector floats; + floats.resize(16); + memcpy(floats.data(), &mat4, sizeof(glm::mat4)); + QVariant v; + v.setValue>(floats); + return v; + }; + result.metadata = { + { "url", _url.toString() }, + { "textures", renderGeometry->getTextures() }, + { "offset", vec3toVariant(_offset) }, + { "scale", vec3toVariant(_scale) }, + { "rotation", quatToVariant(_rotation) }, + { "translation", vec3toVariant(_translation) }, + { "meshToModel", mat4toVariant(glm::scale(_scale) * glm::translate(_offset)) }, + { "meshToWorld", mat4toVariant(createMatFromQuatAndPos(_rotation, _translation) * (glm::scale(_scale) * glm::translate(_offset))) }, + { "geometryOffset", mat4toVariant(geometry.offset) }, + }; + QVariantList submeshes; + int numberOfMeshes = geometry.meshes.size(); + for (int i = 0; i < numberOfMeshes; i++) { + const FBXMesh& fbxMesh = geometry.meshes.at(i); + auto mesh = fbxMesh._mesh; + if (!mesh) { + continue; + } + result.meshes << std::const_pointer_cast(mesh); + auto extraInfo = geometry.getModelNameOfMesh(i); + qDebug() << "Model::getScriptableModel #" << i << QString(mesh->displayName) << extraInfo; + submeshes << QVariantMap{ + { "index", i }, + { "meshIndex", fbxMesh.meshIndex }, + { "modelName", extraInfo }, + { "transform", mat4toVariant(fbxMesh.modelTransform) }, + { "extents", QVariantMap({ + { "minimum", vec3toVariant(fbxMesh.meshExtents.minimum) }, + { "maximum", vec3toVariant(fbxMesh.meshExtents.maximum) }, + })}, + }; + } + if (ok) { + *ok = true; + } + qDebug() << "//Model::getScriptableModel -- #" << result.meshes.size(); + result.metadata["submeshes"] = submeshes; return result; } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 027d52ecfd..4fd00c9f9a 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: @@ -313,7 +314,7 @@ public: int getResourceDownloadAttempts() { return _renderWatcher.getResourceDownloadAttempts(); } int getResourceDownloadAttemptsRemaining() { return _renderWatcher.getResourceDownloadAttemptsRemaining(); } - Q_INVOKABLE MeshProxyList getMeshes() const; + Q_INVOKABLE virtual scriptable::ScriptableModel getScriptableModel(bool* ok = nullptr) override; void scaleToFit(); diff --git a/libraries/script-engine/src/ModelScriptingInterface.cpp b/libraries/script-engine/src/ModelScriptingInterface.cpp deleted file mode 100644 index c693083ebf..0000000000 --- a/libraries/script-engine/src/ModelScriptingInterface.cpp +++ /dev/null @@ -1,251 +0,0 @@ -// -// ModelScriptingInterface.cpp -// libraries/script-engine/src -// -// Created by Seth Alves on 2017-1-27. -// 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 "ModelScriptingInterface.h" -#include -#include -#include -#include -#include "ScriptEngine.h" -#include "ScriptEngineLogging.h" -#include "OBJWriter.h" - -ModelScriptingInterface::ModelScriptingInterface(QObject* parent) : QObject(parent) { - _modelScriptEngine = qobject_cast(parent); - - qScriptRegisterSequenceMetaType>(_modelScriptEngine); - qScriptRegisterMetaType(_modelScriptEngine, meshFaceToScriptValue, meshFaceFromScriptValue); - qScriptRegisterMetaType(_modelScriptEngine, qVectorMeshFaceToScriptValue, qVectorMeshFaceFromScriptValue); -} - -QString ModelScriptingInterface::meshToOBJ(MeshProxyList in) { - QList meshes; - foreach (const MeshProxy* meshProxy, in) { - meshes.append(meshProxy->getMeshPointer()); - } - - return writeOBJToString(meshes); -} - -QScriptValue ModelScriptingInterface::appendMeshes(MeshProxyList in) { - // figure out the size of the resulting mesh - size_t totalVertexCount { 0 }; - size_t totalColorCount { 0 }; - size_t totalNormalCount { 0 }; - size_t totalIndexCount { 0 }; - foreach (const MeshProxy* meshProxy, in) { - MeshPointer mesh = meshProxy->getMeshPointer(); - totalVertexCount += mesh->getNumVertices(); - - int attributeTypeColor = gpu::Stream::InputSlot::COLOR; // libraries/gpu/src/gpu/Stream.h - const gpu::BufferView& colorsBufferView = mesh->getAttributeBuffer(attributeTypeColor); - gpu::BufferView::Index numColors = (gpu::BufferView::Index)colorsBufferView.getNumElements(); - totalColorCount += numColors; - - int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h - const gpu::BufferView& normalsBufferView = mesh->getAttributeBuffer(attributeTypeNormal); - gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); - totalNormalCount += numNormals; - - totalIndexCount += mesh->getNumIndices(); - } - - // alloc the resulting mesh - gpu::Resource::Size combinedVertexSize = totalVertexCount * sizeof(glm::vec3); - std::unique_ptr combinedVertexData{ new unsigned char[combinedVertexSize] }; - unsigned char* combinedVertexDataCursor = combinedVertexData.get(); - - gpu::Resource::Size combinedColorSize = totalColorCount * sizeof(glm::vec3); - std::unique_ptr combinedColorData{ new unsigned char[combinedColorSize] }; - unsigned char* combinedColorDataCursor = combinedColorData.get(); - - gpu::Resource::Size combinedNormalSize = totalNormalCount * sizeof(glm::vec3); - std::unique_ptr combinedNormalData{ new unsigned char[combinedNormalSize] }; - unsigned char* combinedNormalDataCursor = combinedNormalData.get(); - - gpu::Resource::Size combinedIndexSize = totalIndexCount * sizeof(uint32_t); - std::unique_ptr combinedIndexData{ new unsigned char[combinedIndexSize] }; - unsigned char* combinedIndexDataCursor = combinedIndexData.get(); - - uint32_t indexStartOffset { 0 }; - - foreach (const MeshProxy* meshProxy, in) { - MeshPointer mesh = meshProxy->getMeshPointer(); - mesh->forEach( - [&](glm::vec3 position){ - memcpy(combinedVertexDataCursor, &position, sizeof(position)); - combinedVertexDataCursor += sizeof(position); - }, - [&](glm::vec3 color){ - memcpy(combinedColorDataCursor, &color, sizeof(color)); - combinedColorDataCursor += sizeof(color); - }, - [&](glm::vec3 normal){ - memcpy(combinedNormalDataCursor, &normal, sizeof(normal)); - combinedNormalDataCursor += sizeof(normal); - }, - [&](uint32_t index){ - index += indexStartOffset; - memcpy(combinedIndexDataCursor, &index, sizeof(index)); - combinedIndexDataCursor += sizeof(index); - }); - - gpu::BufferView::Index numVertices = (gpu::BufferView::Index)mesh->getNumVertices(); - indexStartOffset += numVertices; - } - - graphics::MeshPointer result(new graphics::Mesh()); - - gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); - gpu::Buffer* combinedVertexBuffer = new gpu::Buffer(combinedVertexSize, combinedVertexData.get()); - gpu::BufferPointer combinedVertexBufferPointer(combinedVertexBuffer); - gpu::BufferView combinedVertexBufferView(combinedVertexBufferPointer, vertexElement); - result->setVertexBuffer(combinedVertexBufferView); - - int attributeTypeColor = gpu::Stream::InputSlot::COLOR; // libraries/gpu/src/gpu/Stream.h - gpu::Element colorElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); - gpu::Buffer* combinedColorsBuffer = new gpu::Buffer(combinedColorSize, combinedColorData.get()); - gpu::BufferPointer combinedColorsBufferPointer(combinedColorsBuffer); - gpu::BufferView combinedColorsBufferView(combinedColorsBufferPointer, colorElement); - result->addAttribute(attributeTypeColor, combinedColorsBufferView); - - int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h - gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); - gpu::Buffer* combinedNormalsBuffer = new gpu::Buffer(combinedNormalSize, combinedNormalData.get()); - gpu::BufferPointer combinedNormalsBufferPointer(combinedNormalsBuffer); - gpu::BufferView combinedNormalsBufferView(combinedNormalsBufferPointer, normalElement); - result->addAttribute(attributeTypeNormal, combinedNormalsBufferView); - - gpu::Element indexElement = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW); - gpu::Buffer* combinedIndexesBuffer = new gpu::Buffer(combinedIndexSize, combinedIndexData.get()); - gpu::BufferPointer combinedIndexesBufferPointer(combinedIndexesBuffer); - gpu::BufferView combinedIndexesBufferView(combinedIndexesBufferPointer, indexElement); - result->setIndexBuffer(combinedIndexesBufferView); - - std::vector parts; - parts.emplace_back(graphics::Mesh::Part((graphics::Index)0, // startIndex - (graphics::Index)result->getNumIndices(), // numIndices - (graphics::Index)0, // baseVertex - graphics::Mesh::TRIANGLES)); // topology - result->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(graphics::Mesh::Part), - (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); - - - MeshProxy* resultProxy = new SimpleMeshProxy(result); - return meshToScriptValue(_modelScriptEngine, resultProxy); -} - -QScriptValue ModelScriptingInterface::transformMesh(glm::mat4 transform, MeshProxy* meshProxy) { - if (!meshProxy) { - return QScriptValue(false); - } - MeshPointer mesh = meshProxy->getMeshPointer(); - if (!mesh) { - return QScriptValue(false); - } - - const auto inverseTransposeTransform = glm::inverse(glm::transpose(transform)); - graphics::MeshPointer result = mesh->map([&](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, - [&](glm::vec3 color){ return color; }, - [&](glm::vec3 normal){ return glm::vec3(inverseTransposeTransform * glm::vec4(normal, 0.0f)); }, - [&](uint32_t index){ return index; }); - MeshProxy* resultProxy = new SimpleMeshProxy(result); - return meshToScriptValue(_modelScriptEngine, resultProxy); -} - -QScriptValue ModelScriptingInterface::getVertexCount(MeshProxy* meshProxy) { - if (!meshProxy) { - return QScriptValue(false); - } - MeshPointer mesh = meshProxy->getMeshPointer(); - if (!mesh) { - return QScriptValue(false); - } - - gpu::BufferView::Index numVertices = (gpu::BufferView::Index)mesh->getNumVertices(); - - return numVertices; -} - -QScriptValue ModelScriptingInterface::getVertex(MeshProxy* meshProxy, int vertexIndex) { - if (!meshProxy) { - return QScriptValue(false); - } - MeshPointer mesh = meshProxy->getMeshPointer(); - if (!mesh) { - return QScriptValue(false); - } - - const gpu::BufferView& vertexBufferView = mesh->getVertexBuffer(); - gpu::BufferView::Index numVertices = (gpu::BufferView::Index)mesh->getNumVertices(); - - if (vertexIndex < 0 || vertexIndex >= numVertices) { - return QScriptValue(false); - } - - glm::vec3 pos = vertexBufferView.get(vertexIndex); - return vec3toScriptValue(_modelScriptEngine, pos); -} - - -QScriptValue ModelScriptingInterface::newMesh(const QVector& vertices, - const QVector& normals, - const QVector& faces) { - graphics::MeshPointer mesh(new graphics::Mesh()); - - // vertices - auto vertexBuffer = std::make_shared(vertices.size() * sizeof(glm::vec3), (gpu::Byte*)vertices.data()); - auto vertexBufferPtr = gpu::BufferPointer(vertexBuffer); - gpu::BufferView vertexBufferView(vertexBufferPtr, 0, vertexBufferPtr->getSize(), - sizeof(glm::vec3), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - mesh->setVertexBuffer(vertexBufferView); - - if (vertices.size() == normals.size()) { - // normals - auto normalBuffer = std::make_shared(normals.size() * sizeof(glm::vec3), (gpu::Byte*)normals.data()); - auto normalBufferPtr = gpu::BufferPointer(normalBuffer); - gpu::BufferView normalBufferView(normalBufferPtr, 0, normalBufferPtr->getSize(), - sizeof(glm::vec3), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - mesh->addAttribute(gpu::Stream::NORMAL, normalBufferView); - } else { - qCDebug(scriptengine) << "ModelScriptingInterface::newMesh normals must be same length as vertices"; - } - - // indices (faces) - int VERTICES_PER_TRIANGLE = 3; - int indexBufferSize = faces.size() * sizeof(uint32_t) * VERTICES_PER_TRIANGLE; - unsigned char* indexData = new unsigned char[indexBufferSize]; - unsigned char* indexDataCursor = indexData; - foreach(const MeshFace& meshFace, faces) { - for (int i = 0; i < VERTICES_PER_TRIANGLE; i++) { - memcpy(indexDataCursor, &meshFace.vertexIndices[i], sizeof(uint32_t)); - indexDataCursor += sizeof(uint32_t); - } - } - auto indexBuffer = std::make_shared(indexBufferSize, (gpu::Byte*)indexData); - auto indexBufferPtr = gpu::BufferPointer(indexBuffer); - gpu::BufferView indexBufferView(indexBufferPtr, gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW)); - mesh->setIndexBuffer(indexBufferView); - - // parts - std::vector parts; - parts.emplace_back(graphics::Mesh::Part((graphics::Index)0, // startIndex - (graphics::Index)faces.size() * 3, // numIndices - (graphics::Index)0, // baseVertex - graphics::Mesh::TRIANGLES)); // topology - mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(graphics::Mesh::Part), - (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); - - - - MeshProxy* meshProxy = new SimpleMeshProxy(mesh); - return meshToScriptValue(_modelScriptEngine, meshProxy); -} diff --git a/libraries/script-engine/src/ModelScriptingInterface.h b/libraries/script-engine/src/ModelScriptingInterface.h deleted file mode 100644 index 3c239f006f..0000000000 --- a/libraries/script-engine/src/ModelScriptingInterface.h +++ /dev/null @@ -1,39 +0,0 @@ -// -// ModelScriptingInterface.h -// libraries/script-engine/src -// -// Created by Seth Alves on 2017-1-27. -// 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_ModelScriptingInterface_h -#define hifi_ModelScriptingInterface_h - -#include - -#include -class QScriptEngine; - -class ModelScriptingInterface : public QObject { - Q_OBJECT - -public: - ModelScriptingInterface(QObject* parent); - - Q_INVOKABLE QString meshToOBJ(MeshProxyList in); - Q_INVOKABLE QScriptValue appendMeshes(MeshProxyList in); - Q_INVOKABLE QScriptValue transformMesh(glm::mat4 transform, MeshProxy* meshProxy); - Q_INVOKABLE QScriptValue newMesh(const QVector& vertices, - const QVector& normals, - const QVector& faces); - Q_INVOKABLE QScriptValue getVertexCount(MeshProxy* meshProxy); - Q_INVOKABLE QScriptValue getVertex(MeshProxy* meshProxy, int vertexIndex); - -private: - QScriptEngine* _modelScriptEngine { nullptr }; -}; - -#endif // hifi_ModelScriptingInterface_h diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index c79ffffec7..ffb1b7bc74 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -73,8 +73,6 @@ #include "WebSocketClass.h" #include "RecordingScriptingInterface.h" #include "ScriptEngines.h" -#include "ModelScriptingInterface.h" - #include @@ -711,10 +709,6 @@ void ScriptEngine::init() { registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); - registerGlobalObject("Model", new ModelScriptingInterface(this)); - qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue); - qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue); - registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); } diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 7b455beae5..9ad5c27072 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -855,68 +855,3 @@ QScriptValue animationDetailsToScriptValue(QScriptEngine* engine, const Animatio void animationDetailsFromScriptValue(const QScriptValue& object, AnimationDetails& details) { // nothing for now... } - -QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in) { - return engine->newQObject(in, QScriptEngine::QtOwnership, - QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); -} - -void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out) { - out = qobject_cast(value.toQObject()); -} - -QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in) { - // QScriptValueList result; - QScriptValue result = engine->newArray(); - int i = 0; - foreach(MeshProxy* const meshProxy, in) { - result.setProperty(i++, meshToScriptValue(engine, meshProxy)); - } - return result; -} - -void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out) { - QScriptValueIterator itr(value); - - qDebug() << "in meshesFromScriptValue, value.length =" << value.property("length").toInt32(); - - while (itr.hasNext()) { - itr.next(); - MeshProxy* meshProxy = qscriptvalue_cast(itr.value()); - if (meshProxy) { - out.append(meshProxy); - } else { - qDebug() << "null meshProxy"; - } - } -} - - -QScriptValue meshFaceToScriptValue(QScriptEngine* engine, const MeshFace &meshFace) { - QScriptValue obj = engine->newObject(); - obj.setProperty("vertices", qVectorIntToScriptValue(engine, meshFace.vertexIndices)); - return obj; -} - -void meshFaceFromScriptValue(const QScriptValue &object, MeshFace& meshFaceResult) { - qVectorIntFromScriptValue(object.property("vertices"), meshFaceResult.vertexIndices); -} - -QScriptValue qVectorMeshFaceToScriptValue(QScriptEngine* engine, const QVector& vector) { - QScriptValue array = engine->newArray(); - for (int i = 0; i < vector.size(); i++) { - array.setProperty(i, meshFaceToScriptValue(engine, vector.at(i))); - } - return array; -} - -void qVectorMeshFaceFromScriptValue(const QScriptValue& array, QVector& result) { - int length = array.property("length").toInteger(); - result.clear(); - - for (int i = 0; i < length; i++) { - MeshFace meshFace = MeshFace(); - meshFaceFromScriptValue(array.property(i), meshFace); - result << meshFace; - } -} diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 25b2cec331..e8390c3a86 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -315,51 +315,9 @@ Q_DECLARE_METATYPE(AnimationDetails); QScriptValue animationDetailsToScriptValue(QScriptEngine* engine, const AnimationDetails& event); void animationDetailsFromScriptValue(const QScriptValue& object, AnimationDetails& event); -namespace graphics { - class Mesh; -} - -using MeshPointer = std::shared_ptr; -class MeshProxy : public QObject { - Q_OBJECT -public: - virtual MeshPointer getMeshPointer() const = 0; - Q_INVOKABLE virtual int getNumVertices() const = 0; - Q_INVOKABLE virtual glm::vec3 getPos3(int index) const = 0; -}; - -Q_DECLARE_METATYPE(MeshProxy*); - -class MeshProxyList : public QList {}; // typedef and using fight with the Qt macros/templates, do this instead -Q_DECLARE_METATYPE(MeshProxyList); - - -QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in); -void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out); - -QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in); -void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out); - -class MeshFace { - -public: - MeshFace() {} - ~MeshFace() {} - - QVector vertexIndices; - // TODO -- material... -}; - -Q_DECLARE_METATYPE(MeshFace) -Q_DECLARE_METATYPE(QVector) - -QScriptValue meshFaceToScriptValue(QScriptEngine* engine, const MeshFace &meshFace); -void meshFaceFromScriptValue(const QScriptValue &object, MeshFace& meshFaceResult); -QScriptValue qVectorMeshFaceToScriptValue(QScriptEngine* engine, const QVector& vector); -void qVectorMeshFaceFromScriptValue(const QScriptValue& array, QVector& result); #endif // hifi_RegisteredMetaTypes_h From a63393bc55cf1d68deff6dfceda6d744a9cba214 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 7 Feb 2018 13:32:30 -0800 Subject: [PATCH 002/260] undo part of #12312 --- interface/src/ui/overlays/Web3DOverlay.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 5fb4d58340..4f96e70aa9 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -80,7 +80,6 @@ Web3DOverlay::Web3DOverlay() { _webSurface->getSurfaceContext()->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED _webSurface->getSurfaceContext()->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); _webSurface->getSurfaceContext()->setContextProperty("AddressManager", DependencyManager::get().data()); - } Web3DOverlay::Web3DOverlay(const Web3DOverlay* Web3DOverlay) : @@ -201,6 +200,11 @@ void Web3DOverlay::setupQmlSurface() { _webSurface->getSurfaceContext()->setContextProperty("offscreenFlags", flags); _webSurface->getSurfaceContext()->setContextProperty("AddressManager", DependencyManager::get().data()); + + _webSurface->getSurfaceContext()->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + _webSurface->getSurfaceContext()->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + _webSurface->getSurfaceContext()->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); + // in Qt 5.10.0 there is already an "Audio" object in the QML context // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" _webSurface->getSurfaceContext()->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); From 3a7e626463de5401babf9f5286f36c8463a014c6 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 7 Feb 2018 10:19:30 -0800 Subject: [PATCH 003/260] Merge pull request #12316 from zfox23/commerce_inspectionCertRedesign Commerce: Inspection Certificate Redesign --- .../InspectionCertificate.qml | 548 ++++++++++++------ .../images/cert-bg-gold-split.png | Bin 0 -> 189708 bytes .../inspectionCertificate/images/cert-bg.jpg | Bin 64011 -> 0 bytes .../images/nocert-bg-split.png | Bin 0 -> 17201 bytes .../commerce/wallet/sendMoney/SendMoney.qml | 2 +- .../wallet/sendMoney/images/loader.gif | Bin 59412 -> 0 bytes 6 files changed, 362 insertions(+), 188 deletions(-) create mode 100644 interface/resources/qml/hifi/commerce/inspectionCertificate/images/cert-bg-gold-split.png delete mode 100644 interface/resources/qml/hifi/commerce/inspectionCertificate/images/cert-bg.jpg create mode 100644 interface/resources/qml/hifi/commerce/inspectionCertificate/images/nocert-bg-split.png delete mode 100644 interface/resources/qml/hifi/commerce/wallet/sendMoney/images/loader.gif diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml index 421fa4b074..2c7319be09 100644 --- a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -19,21 +19,30 @@ import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls import "../wallet" as HifiWallet -// references XXX from root context - Rectangle { HifiConstants { id: hifi; } id: root; - property string marketplaceUrl; - property string certificateId; + property string marketplaceUrl: ""; + property string certificateId: ""; property string itemName: "--"; property string itemOwner: "--"; property string itemEdition: "--"; property string dateOfPurchase: "--"; + property string itemCost: "--"; + property string certTitleTextColor: hifi.colors.darkGray; + property string certTextColor: hifi.colors.white; + property string infoTextColor: hifi.colors.blueAccent; + // 0 means replace none + // 4 means replace all but "Item Edition" + // 5 means replace all 5 replaceable fields + property int certInfoReplaceMode: 5; property bool isLightbox: false; property bool isMyCert: false; - property bool isCertificateInvalid: false; + property bool useGoldCert: true; + property bool certificateInfoPending: true; + property int certificateStatus: 0; + property bool certificateStatusPending: true; // Style color: hifi.colors.faintGray; Connections { @@ -45,71 +54,130 @@ Rectangle { } else { root.marketplaceUrl = result.data.marketplace_item_url; root.isMyCert = result.isMyCert ? result.isMyCert : false; - root.itemOwner = root.isCertificateInvalid ? "--" : (root.isMyCert ? Account.username : - "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"); - root.itemEdition = root.isCertificateInvalid ? "Uncertified Copy" : - (result.data.edition_number + "/" + (result.data.limited_run === -1 ? "\u221e" : result.data.limited_run)); - root.dateOfPurchase = root.isCertificateInvalid ? "" : getFormattedDate(result.data.transfer_created_at * 1000); - root.itemName = result.data.marketplace_item_name; + + if (root.certInfoReplaceMode > 3) { + root.itemName = result.data.marketplace_item_name; + // "\u2022" is the Unicode character 'BULLET' - it's what's used in password fields on the web, etc + root.itemOwner = root.isMyCert ? Account.username : + "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"; + root.dateOfPurchase = root.isMyCert ? getFormattedDate(result.data.transfer_created_at * 1000) : "Undisclosed"; + root.itemCost = (root.isMyCert && result.data.cost !== undefined) ? result.data.cost : "Undisclosed"; + } + if (root.certInfoReplaceMode > 4) { + root.itemEdition = result.data.edition_number + "/" + (result.data.limited_run === -1 ? "\u221e" : result.data.limited_run); + } + + if (root.certificateStatus === 4) { // CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED + if (root.isMyCert) { + errorText.text = "This item is an uncertified copy of an item you purchased."; + } else { + errorText.text = "The person who placed this item doesn't own it."; + } + } if (result.data.invalid_reason || result.data.transfer_status[0] === "failed") { - titleBarText.text = "Invalid Certificate"; - titleBarText.color = hifi.colors.redHighlight; + root.useGoldCert = false; + root.certTitleTextColor = hifi.colors.redHighlight; + root.certTextColor = hifi.colors.redHighlight; + root.infoTextColor = hifi.colors.redHighlight; + titleBarText.text = "Certificate\nNo Longer Valid"; popText.text = ""; + showInMarketplaceButton.visible = false; + // "Edition" text previously set above in this function + // "Owner" text previously set above in this function + // "Purchase Date" text previously set above in this function + // "Purchase Price" text previously set above in this function if (result.data.invalid_reason) { errorText.text = result.data.invalid_reason; } } else if (result.data.transfer_status[0] === "pending") { + root.useGoldCert = false; + root.certTitleTextColor = hifi.colors.redHighlight; + root.certTextColor = hifi.colors.redHighlight; + root.infoTextColor = hifi.colors.redHighlight; titleBarText.text = "Certificate Pending"; + popText.text = ""; + showInMarketplaceButton.visible = true; + // "Edition" text previously set above in this function + // "Owner" text previously set above in this function + // "Purchase Date" text previously set above in this function + // "Purchase Price" text previously set above in this function errorText.text = "The status of this item is still pending confirmation. If the purchase is not confirmed, " + "this entity will be cleaned up by the domain."; - errorText.color = hifi.colors.baseGray; } } + root.certificateInfoPending = false; } onUpdateCertificateStatus: { - if (certStatus === 1) { // CERTIFICATE_STATUS_VERIFICATION_SUCCESS - // NOP - } else if (certStatus === 2) { // CERTIFICATE_STATUS_VERIFICATION_TIMEOUT - root.isCertificateInvalid = true; - errorText.text = "Verification of this certificate timed out."; - errorText.color = hifi.colors.redHighlight; - } else if (certStatus === 3) { // CERTIFICATE_STATUS_STATIC_VERIFICATION_FAILED - root.isCertificateInvalid = true; - titleBarText.text = "Invalid Certificate"; - titleBarText.color = hifi.colors.redHighlight; - + root.certificateStatus = certStatus; + if (root.certificateStatus === 1) { // CERTIFICATE_STATUS_VERIFICATION_SUCCESS + root.useGoldCert = true; + root.certTitleTextColor = hifi.colors.darkGray; + root.certTextColor = hifi.colors.white; + root.infoTextColor = hifi.colors.blueAccent; + titleBarText.text = "Certificate"; + popText.text = "PROOF OF PROVENANCE"; + showInMarketplaceButton.visible = true; + root.certInfoReplaceMode = 5; + // "Item Name" text will be set in "onCertificateInfoResult()" + // "Edition" text will be set in "onCertificateInfoResult()" + // "Owner" text will be set in "onCertificateInfoResult()" + // "Purchase Date" text will be set in "onCertificateInfoResult()" + // "Purchase Price" text will be set in "onCertificateInfoResult()" + errorText.text = ""; + } else if (root.certificateStatus === 2) { // CERTIFICATE_STATUS_VERIFICATION_TIMEOUT + root.useGoldCert = false; + root.certTitleTextColor = hifi.colors.redHighlight; + root.certTextColor = hifi.colors.redHighlight; + root.infoTextColor = hifi.colors.redHighlight; + titleBarText.text = "Request Timed Out"; popText.text = ""; + showInMarketplaceButton.visible = false; + root.certInfoReplaceMode = 0; + root.itemName = ""; + root.itemEdition = ""; root.itemOwner = ""; - dateOfPurchaseHeader.text = ""; root.dateOfPurchase = ""; - root.itemEdition = "Uncertified Copy"; - + root.itemCost = ""; + errorText.text = "Your request to inspect this item timed out. Please try again later."; + } else if (root.certificateStatus === 3) { // CERTIFICATE_STATUS_STATIC_VERIFICATION_FAILED + root.useGoldCert = false; + root.certTitleTextColor = hifi.colors.redHighlight; + root.certTextColor = hifi.colors.redHighlight; + root.infoTextColor = hifi.colors.redHighlight; + titleBarText.text = "Certificate\nNo Longer Valid"; + popText.text = ""; + showInMarketplaceButton.visible = true; + root.certInfoReplaceMode = 5; + // "Item Name" text will be set in "onCertificateInfoResult()" + // "Edition" text will be set in "onCertificateInfoResult()" + // "Owner" text will be set in "onCertificateInfoResult()" + // "Purchase Date" text will be set in "onCertificateInfoResult()" + // "Purchase Price" text will be set in "onCertificateInfoResult()" errorText.text = "The information associated with this item has been modified and it no longer matches the original certified item."; - errorText.color = hifi.colors.baseGray; - } else if (certStatus === 4) { // CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED - root.isCertificateInvalid = true; + } else if (root.certificateStatus === 4) { // CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED + root.useGoldCert = false; + root.certTitleTextColor = hifi.colors.redHighlight; + root.certTextColor = hifi.colors.redHighlight; + root.infoTextColor = hifi.colors.redHighlight; titleBarText.text = "Invalid Certificate"; - titleBarText.color = hifi.colors.redHighlight; - popText.text = ""; - root.itemOwner = ""; - dateOfPurchaseHeader.text = ""; - root.dateOfPurchase = ""; - root.itemEdition = "Uncertified Copy"; - - errorText.text = "The avatar who rezzed this item doesn't own it."; - errorText.color = hifi.colors.baseGray; + showInMarketplaceButton.visible = true; + root.certInfoReplaceMode = 4; + // "Item Name" text will be set in "onCertificateInfoResult()" + root.itemEdition = "Uncertified Copy" + // "Owner" text will be set in "onCertificateInfoResult()" + // "Purchase Date" text will be set in "onCertificateInfoResult()" + // "Purchase Price" text will be set in "onCertificateInfoResult()" + // "Error Text" text will be set in "onCertificateInfoResult()" } else { console.log("Unknown certificate status received from ledger signal!"); } - } - } - - onCertificateIdChanged: { - if (certificateId !== "") { - Commerce.certificateInfo(certificateId); + + root.certificateStatusPending = false; + // We've gotten cert status - we are GO on getting the cert info + Commerce.certificateInfo(root.certificateId); } } @@ -122,9 +190,35 @@ Rectangle { hoverEnabled: true; } - Image { + Rectangle { + id: loadingOverlay; + z: 998; + + visible: root.certificateInfoPending || root.certificateStatusPending; anchors.fill: parent; - source: "images/cert-bg.jpg"; + color: Qt.rgba(0.0, 0.0, 0.0, 0.7); + + // This object is always used in a popup or full-screen Wallet section. + // 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; + propagateComposedEvents: false; + } + + AnimatedImage { + source: "../common/images/loader.gif" + width: 96; + height: width; + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + } + } + + Image { + id: backgroundImage; + anchors.fill: parent; + source: root.useGoldCert ? "images/cert-bg-gold-split.png" : "images/nocert-bg-split.png"; } // Title text @@ -137,16 +231,17 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: 40; anchors.left: parent.left; - anchors.leftMargin: 45; + anchors.leftMargin: 36; anchors.right: parent.right; + anchors.rightMargin: 8; height: paintedHeight; // Style - color: hifi.colors.darkGray; + color: root.certTitleTextColor; + wrapMode: Text.WordWrap; } // Title text RalewayRegular { id: popText; - text: "Proof of Provenance"; // Text size size: 16; // Anchors @@ -154,9 +249,38 @@ Rectangle { anchors.topMargin: 4; anchors.left: titleBarText.left; anchors.right: titleBarText.right; - height: paintedHeight; + height: text === "" ? 0 : paintedHeight; // Style - color: hifi.colors.darkGray; + color: root.certTitleTextColor; + } + + // "Close" button + HiFiGlyphs { + id: closeGlyphButton; + text: hifi.glyphs.close; + color: hifi.colors.white; + size: 26; + anchors.top: parent.top; + anchors.topMargin: 10; + anchors.right: parent.right; + anchors.rightMargin: 10; + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + parent.text = hifi.glyphs.closeInverted; + } + onExited: { + parent.text = hifi.glyphs.close; + } + onClicked: { + if (root.isLightbox) { + root.visible = false; + } else { + sendToScript({method: 'inspectionCertificate_closeClicked', closeGoesToPurchases: root.closeGoesToPurchases}); + } + } + } } // @@ -164,11 +288,13 @@ Rectangle { // Item { id: certificateContainer; - anchors.top: popText.bottom; - anchors.topMargin: 30; - anchors.bottom: buttonsContainer.top; + anchors.top: titleBarText.top; + anchors.topMargin: 110; + anchors.bottom: infoContainer.top; anchors.left: parent.left; + anchors.leftMargin: titleBarText.anchors.leftMargin; anchors.right: parent.right; + anchors.rightMargin: 24; RalewayRegular { id: itemNameHeader; @@ -178,9 +304,7 @@ Rectangle { // Anchors anchors.top: parent.top; anchors.left: parent.left; - anchors.leftMargin: 45; anchors.right: parent.right; - anchors.rightMargin: 16; height: paintedHeight; // Style color: hifi.colors.darkGray; @@ -197,79 +321,30 @@ Rectangle { anchors.right: itemNameHeader.right; height: paintedHeight; // Style - color: hifi.colors.white; + color: root.certTextColor; elide: Text.ElideRight; MouseArea { + enabled: showInMarketplaceButton.visible; anchors.fill: parent; hoverEnabled: enabled; onClicked: { sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', marketplaceUrl: root.marketplaceUrl}); } onEntered: itemName.color = hifi.colors.blueHighlight; - onExited: itemName.color = hifi.colors.white; + onExited: itemName.color = root.certTextColor; } } - RalewayRegular { - id: ownedByHeader; - text: "OWNER"; - // Text size - size: 16; - // Anchors - anchors.top: itemName.bottom; - anchors.topMargin: 28; - anchors.left: parent.left; - anchors.leftMargin: 45; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: paintedHeight; - // Style - color: hifi.colors.darkGray; - } - RalewayRegular { - id: ownedBy; - text: root.itemOwner; - // Text size - size: 22; - // Anchors - anchors.top: ownedByHeader.bottom; - anchors.topMargin: 8; - anchors.left: ownedByHeader.left; - height: paintedHeight; - // Style - color: hifi.colors.white; - elide: Text.ElideRight; - } - AnonymousProRegular { - id: isMyCertText; - visible: root.isMyCert && !root.isCertificateInvalid; - text: "(Private)"; - size: 18; - // Anchors - anchors.top: ownedBy.top; - anchors.topMargin: 4; - anchors.bottom: ownedBy.bottom; - anchors.left: ownedBy.right; - anchors.leftMargin: 6; - anchors.right: ownedByHeader.right; - // Style - color: hifi.colors.white; - elide: Text.ElideRight; - verticalAlignment: Text.AlignVCenter; - } - RalewayRegular { id: editionHeader; text: "EDITION"; // Text size size: 16; // Anchors - anchors.top: ownedBy.bottom; + anchors.top: itemName.bottom; anchors.topMargin: 28; anchors.left: parent.left; - anchors.leftMargin: 45; anchors.right: parent.right; - anchors.rightMargin: 16; height: paintedHeight; // Style color: hifi.colors.darkGray; @@ -286,21 +361,117 @@ Rectangle { anchors.right: editionHeader.right; height: paintedHeight; // Style - color: hifi.colors.white; + color: root.certTextColor; + } + + // "Show In Marketplace" button + HifiControlsUit.Button { + id: showInMarketplaceButton; + enabled: root.marketplaceUrl; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.light; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 48; + anchors.right: parent.right; + width: 200; + height: 40; + text: "View In Market" + onClicked: { + sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', marketplaceUrl: root.marketplaceUrl}); + } + } + } + // + // "CERTIFICATE" END + // + + // + // "INFO CONTAINER" START + // + Item { + id: infoContainer; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: titleBarText.anchors.leftMargin; + anchors.right: parent.right; + anchors.rightMargin: 24; + height: root.useGoldCert ? 220 : 372; + + RalewayRegular { + id: errorText; + visible: !root.useGoldCert; + // Text size + size: 20; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 36; + anchors.left: parent.left; + anchors.right: parent.right; + height: 116; + // Style + wrapMode: Text.WordWrap; + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignTop; + } + + RalewayRegular { + id: ownedByHeader; + text: "OWNER"; + // Text size + size: 16; + // Anchors + anchors.top: errorText.visible ? errorText.bottom : parent.top; + anchors.topMargin: 28; + anchors.left: parent.left; + anchors.right: parent.right; + height: paintedHeight; + // Style + color: hifi.colors.darkGray; + } + + RalewayRegular { + id: ownedBy; + text: root.itemOwner; + // Text size + size: 22; + // Anchors + anchors.top: ownedByHeader.bottom; + anchors.topMargin: 8; + anchors.left: ownedByHeader.left; + height: paintedHeight; + // Style + color: root.infoTextColor; + elide: Text.ElideRight; + } + AnonymousProRegular { + id: isMyCertText; + visible: root.isMyCert && ownedBy.text !== "--" && ownedBy.text !== ""; + text: "(Private)"; + size: 18; + // Anchors + anchors.top: ownedBy.top; + anchors.topMargin: 4; + anchors.bottom: ownedBy.bottom; + anchors.left: ownedBy.right; + anchors.leftMargin: 6; + anchors.right: ownedByHeader.right; + // Style + color: root.infoTextColor; + elide: Text.ElideRight; + verticalAlignment: Text.AlignVCenter; } RalewayRegular { id: dateOfPurchaseHeader; - text: "DATE OF PURCHASE"; + text: "PURCHASE DATE"; // Text size size: 16; // Anchors - anchors.top: edition.bottom; + anchors.top: ownedBy.bottom; anchors.topMargin: 28; anchors.left: parent.left; - anchors.leftMargin: 45; - anchors.right: parent.right; - anchors.rightMargin: 16; + anchors.right: parent.horizontalCenter; + anchors.rightMargin: 8; height: paintedHeight; // Style color: hifi.colors.darkGray; @@ -317,73 +488,58 @@ Rectangle { anchors.right: dateOfPurchaseHeader.right; height: paintedHeight; // Style - color: hifi.colors.white; + color: root.infoTextColor; } RalewayRegular { - id: errorText; + id: priceHeader; + text: "PURCHASE PRICE"; // Text size - size: 20; + size: 16; // Anchors - anchors.top: dateOfPurchase.bottom; - anchors.topMargin: 36; - anchors.left: dateOfPurchase.left; - anchors.right: dateOfPurchase.right; - anchors.bottom: parent.bottom; - // Style - wrapMode: Text.WordWrap; - color: hifi.colors.redHighlight; - verticalAlignment: Text.AlignTop; - } - } - // - // "CERTIFICATE" END - // - - Item { - id: buttonsContainer; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 30; - anchors.left: parent.left; - anchors.right: parent.right; - height: 50; - - // "Cancel" button - HifiControlsUit.Button { - color: hifi.buttons.noneBorderlessWhite; - colorScheme: hifi.colorSchemes.light; - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 30; - width: parent.width/2 - 50; - height: 50; - text: "close"; - onClicked: { - if (root.isLightbox) { - root.visible = false; - } else { - sendToScript({method: 'inspectionCertificate_closeClicked', closeGoesToPurchases: root.closeGoesToPurchases}); - } - } - } - - // "Show In Marketplace" button - HifiControlsUit.Button { - id: showInMarketplaceButton; - enabled: root.marketplaceUrl; - color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.light; - anchors.top: parent.top; + anchors.top: ownedBy.bottom; + anchors.topMargin: 28; + anchors.left: parent.horizontalCenter; anchors.right: parent.right; - anchors.rightMargin: 30; - width: parent.width/2 - 50; - height: 50; - text: "View In Market" - onClicked: { - sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', marketplaceUrl: root.marketplaceUrl}); - } + height: paintedHeight; + // Style + color: hifi.colors.darkGray; + } + HiFiGlyphs { + id: hfcGlyph; + visible: priceText.text !== "Undisclosed" && priceText.text !== ""; + text: hifi.glyphs.hfc; + // Size + size: 24; + // Anchors + anchors.top: priceHeader.bottom; + anchors.topMargin: 8; + anchors.left: priceHeader.left; + width: visible ? paintedWidth + 6 : 0; + height: 40; + // Style + color: root.infoTextColor; + verticalAlignment: Text.AlignTop; + horizontalAlignment: Text.AlignLeft; + } + AnonymousProRegular { + id: priceText; + text: root.itemCost; + // Text size + size: 18; + // Anchors + anchors.top: priceHeader.bottom; + anchors.topMargin: 8; + anchors.left: hfcGlyph.right; + anchors.right: priceHeader.right; + height: paintedHeight; + // Style + color: root.infoTextColor; } } + // + // "INFO CONTAINER" END + // // // FUNCTION DEFINITIONS START @@ -404,19 +560,11 @@ Rectangle { function fromScript(message) { switch (message.method) { case 'inspectionCertificate_setCertificateId': + resetCert(false); root.certificateId = message.certificateId; break; case 'inspectionCertificate_resetCert': - titleBarText.text = "Certificate"; - popText.text = "PROOF OF PURCHASE"; - root.certificateId = ""; - root.itemName = "--"; - root.itemOwner = "--"; - root.itemEdition = "--"; - root.dateOfPurchase = "--"; - root.marketplaceUrl = ""; - root.isMyCert = false; - errorText.text = ""; + resetCert(true); break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); @@ -424,7 +572,33 @@ Rectangle { } signal sendToScript(var message); + function resetCert(alsoResetCertID) { + if (alsoResetCertID) { + root.certificateId = ""; + } + root.certInfoReplaceMode = 5; + root.certificateInfoPending = true; + root.certificateStatusPending = true; + root.useGoldCert = true; + root.certTitleTextColor = hifi.colors.darkGray; + root.certTextColor = hifi.colors.white; + root.infoTextColor = hifi.colors.blueAccent; + titleBarText.text = "Certificate"; + popText.text = ""; + root.itemName = "--"; + root.itemOwner = "--"; + root.itemEdition = "--"; + root.dateOfPurchase = "--"; + root.marketplaceUrl = ""; + root.itemCost = "--"; + root.isMyCert = false; + errorText.text = ""; + } + function getFormattedDate(timestamp) { + if (timestamp === "--") { + return "--"; + } function addLeadingZero(n) { return n < 10 ? '0' + n : '' + n; } @@ -449,7 +623,7 @@ Rectangle { var min = addLeadingZero(a.getMinutes()); var sec = addLeadingZero(a.getSeconds()); - return year + '-' + month + '-' + day + '
' + drawnHour + ':' + min + amOrPm; + return year + '-' + month + '-' + day + ' ' + drawnHour + ':' + min + amOrPm; } // // FUNCTION DEFINITIONS END diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/images/cert-bg-gold-split.png b/interface/resources/qml/hifi/commerce/inspectionCertificate/images/cert-bg-gold-split.png new file mode 100644 index 0000000000000000000000000000000000000000..14a17df0b16f35320b67561440d6d9cf11cb2f29 GIT binary patch literal 189708 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 k+*$f8F` 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

0~BjTbon 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 literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/images/cert-bg.jpg b/interface/resources/qml/hifi/commerce/inspectionCertificate/images/cert-bg.jpg deleted file mode 100644 index b39a55e4e83f3290d7befdfeb1055cf9b73f4bfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64011 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 diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/images/nocert-bg-split.png b/interface/resources/qml/hifi/commerce/inspectionCertificate/images/nocert-bg-split.png new file mode 100644 index 0000000000000000000000000000000000000000..b2f5a492653b84bf6b739ae270180b1bb7b0d48d GIT binary patch literal 17201 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 F{|C0gl@I^` literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/commerce/wallet/sendMoney/SendMoney.qml b/interface/resources/qml/hifi/commerce/wallet/sendMoney/SendMoney.qml index ee1246c0c4..7dd72b904e 100644 --- a/interface/resources/qml/hifi/commerce/wallet/sendMoney/SendMoney.qml +++ b/interface/resources/qml/hifi/commerce/wallet/sendMoney/SendMoney.qml @@ -1115,7 +1115,7 @@ Item { AnimatedImage { id: sendingMoneyImage; - source: "./images/loader.gif" + source: "../../common/images/loader.gif" width: 96; height: width; anchors.verticalCenter: parent.verticalCenter; diff --git a/interface/resources/qml/hifi/commerce/wallet/sendMoney/images/loader.gif b/interface/resources/qml/hifi/commerce/wallet/sendMoney/images/loader.gif deleted file mode 100644 index 0536bd1884f1cb4a814b729803b1996c8347d182..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59412 zcmce9hd@o-+;#{>D4J%a zasM92%l+zopF{WkdwlzJ{R{8M`+8i@>v=t|m%65!jI1r@56mAQ|G%>#+pb-^I5;>sIXStwxOjMYczJpE?AgP|$H&jlFR%i^ zzab%>wr}4)2?+@)DJf}bX&D(ASy@@R73}{T^78Tu3JM1f95{IJ zprWFpl9H0Lva*VbimIxry1Kf?-_ZPT(E1zN+S)ogI(m9~1_lObG}_3>$i&11i^ZCm znORs^SX*1$+S=OL+1cCMJ2*HvuHeWDoSd9+IGnSyv#YD?(W6H_Jw1;dJLcu(<>TYy z=jV6g#EDaVPkiHSLX{(M|qTtY&^g$oxF zSCEvHl#-H?nwpxCk&&61nVp@To12@Lmse0wP*_-4R8&-4TwGdOT2@w8US3{NQE}zU zm8z<$t5>g96QSmB)YjJ4)zw|QcCEg?{`&Rn4Gj&Ajg3uBO}B2{x_$e0b93{Z6|}Up zw6?akx3}NBcdw(PfByMrS65f}3VM2adV71HJb5xWI5<2!{OsAY@$vDAiHWJH zsp%EW%*?!b^XBc_x9{G)d;k9ZhxIYLg1Nc5#l^*sA3uKj^l5o{`RmuO-`B@q>x1*J z?Ru&hw6Y>fb)SgnrVW4m@dxMU&7@oY*!qVM{^!8|CV!B8|AT^rRiiSe`5XzIfW^Ja z+?FtEZmo+NRe5buJH(xb?^WgBJ1IR!xZ#Q$@0t(NOQSqAt4HxK@rjxU(F6?bxCi#hWHm z|2o$(vrhHy(y8$?HYcfD%}XPux)nZom&)!sNn@AP@KE*nIg#f(54eh!VQxoncPC{T ze9l)owP4Kd_tEO&qs;+2edKcSy{0*&lo!(XEZ*+Ze$pU?l3H@zgg*KrnQWhL$B>r8 z@ryUS(zFJe6=+T5-pII|_b-srm&NkW#8!VOqZdw+P@_h3HWf?kS)6Z|Nj0^y8hUh6 zwb4+$%0ka5A6I(E$nkV^{CCro2+h7ncJ$XJ8O{nB6({Z^QJPS;*22;D>_dBVk1Csc z`JDF@JNpMK`W@%yI{}8ebX?j8;!MU^_Ko1esC&Bc1 z^TrTIhP1wY!tDh)@s67JE^PlICfg@O@vf$gPhcnRG2iE^$sW-b>g(jG5t~b&ra2Xr zPNlonO;2TbwDZ5b=+%#Vndvu`W^jo3gNFTD)`Os?reGfFyYC+&RdDNYV=n3kweqkBp3rj*gCvjU^_#q@<+e*IXQsu^7HeNlvh$x0w@pg9pJjZ`E8Zp>g($Xs13<(kmCSaTU+nmy$flsa|K;1 zK=NH*Umu{np`oGS;o;HI(Pz(|J%9duY;0_NeEh|W7n75dFJHc#o}PaF`t_SPZvgiJ z`kS4_bKLy={KCS*^78Wf`uX@FS<@9N3toz z^%-`RQ!JP*^VZ$IrR4Hd%I)bNJ)<{M=3+9V>+Y6Be6VK@>|X3tV6UQNC^T$98M7Zb zXmUUFqZ*CQo*UWZ*XWnkUN79K5|XbOnvUvAHncW9f!iJPYFbRc)N)^Yo3`zfZt@9= zEwO$y5!Bh9B9kxKRn>%KdILlmu3;n_6c@HQDe-b&AlY(rsgqnwjNx&h2s4e{$%q>! zaU{Iy;m@@NoRhS<9x!t#G5T2~Y!6d573bCw$CC8NEAzhK(tX28%IGQ=Fm~}m8AU`7 zd0sh5zf}Zt)iy!3?jCJ3=8Z?D)+RJ}dg|ZDXA~3^^z`&QcI;qgX5RTX@XYp40F6fg zwiU3mv+vrqi<6U+o12@Lmlvt`1O!$kpOBCc5PRa{;>0o!0x}zt)eapxq@<(-l^%h_ z)6&Azn69p_fq{XMk&&sXDNt@ewmCUDxwyEvySsaMcpN`|+}qn5$g`6tPo6q;Dljnc z%$YMmLH{5p03=2I9ARO2wH6f>1=uS-J{~F60DA$XtROWtH9b8Yh&MoHfY0*s@RSBv z4M}XJrKOiHU8<<4xP1BYl`B^&D=UGV1Kb9b+x6?$8yg#M+_(V*+nqbBQmw76t-Zbd z?%lih?%lh8|Nesq4<0{$+}VkzvF>iXVC(7W>Few3@9!TN7#JEFdiwO~$jHd(3IL4( zCR-EZD|qqZ#l*zK%a<=_W@eDmZfydo_!oKBlt|OyNfQHfbx2kel0>7KgeNUlyhoct5Itn&rc{lfv(wRXYBqC%+(h zSKQ7bFR%l9cq;eGV~jfP6rWqn!}rx^ji(3sufJt9ivFH8*U(eR-0}?FWVqBb%&_ZV z-=yKK9^5O{US(f{CZ1hl8{`V^xan0rF%(1T9v)P-jNYB4v&pJZJ!o4gHK`Td&50-I zo=U}MiV7-9&ruV>Bt79M=8zC)tWEAV2W+h$9o^H^$$WIfMW+j=4mu~((>2hO8u5$F z^qyl^SgzM)VcjjsBt%VF8zWL!&pVuA6Usl8YM+ezC!bCK23bZ$MMX_b4G0WLSOi1` zz{d82tH6W`gc{Ume@~}BAhZHuVd1@d_lk;&!Z{TTsc=q}llupC9XRlVwA5Arhg8U3 zP=SE~6e+w+Oiawo%q%S}ZEbBG92|(JOakEr0OXgiFJ6HG`32|=2rsC;)&!E_!o$M> zpG8MUpFe*d@EI67)6&u|Uc3k=P9$9cAP3O$a=a9~di82;ZEZtC!_AvFf!G3ARa<{! zRdcnpw6(Q?;qu|bhmRjW{_{_~@Pc&J+qP2Ek>XtCcGr~l6y4=v zQx!l>_gpf`G?3&Vy<}4C7}x!Lv9{be%dXBpNpjnEinAJ(52ty(DD`=qvqSguC~?Nf z26%Vhd!6{+IE3q-9EJ3v#~o$;lUsbE<3dO8CsGBT7?P=0!Kw*U2`xwqpFW?yUk#9Dkc z`JB&A^SbIgtZmiQzSpl$*S(`tEg7NDEOhw5jN9%Oa?}2EQ6|6p(dGM-C+8O>B?jzX zRAJ1}H|R+W>~h;wQqWH)o=`Ielk%GCNCY2GvZaXN$i{6b;t37acRL;)w8bBri;C_G z-W*{vJFdon<}_&2w+iWD*OidR_Me|$2pZR6-)Nf5V>fc70F_OiMn{`)>SMmRZfs~P z=hb@~4<{ejs9%r9?i>Eb=@Nj7ii(DYhJk?r8g0zX%t(4ef|%+c&q0zS&}77OC;((T zQPEYQMr_6@C@3f@Dgrg8skusHV1xu5hQVNrj8=z7(-oMTn_F61T3cJ&*x1~G}IPlSYeSN_odFs?DI7psb0iMnNo2Le*OQ_GFID$oCE|0Of>uq`8Nm%i~xYi78VwOo3^~X{1Y-;o3)7WYiLYN ziwpB`iqt{vl3}KxJe^u8JqEAF=&ozXiINDHV5^)j(FYetX_>Y-y5)<9i&i;hCS*j4 zN7hxAys@#3Qq{@RN3~6OMJ<}L)RCM!#U)K;q{nnLJK@4i=a$14O&HX3hdOC7wwC;R z$v1jeIEMOV+kL*l?9A1@U3vRrw4AM7Zm~Ic^hSotH$f3uJ8uSvK0)ulaWab1o!Z%P zd{Zvh-jY+F58ct*vtRo8)9F{!WiQ)&vYt-6zpTl=Hl+6b4gcVDa^^mkUGg`2-ZH)H zC>OzGctv5S#Co=0m`A(a)eTPd-EcKA3w@{ZWt?eI^TZv~zbv=WSnE%6p{Y9+7I!Ge zHr1sE38}{CqKEtpZ4^an#>SjK-siEyX7SK&HQZY7p~@C-tEv|ze*KWOnUif1+V@bX z18KO#7Tf#bs(Huq^kS1z`o)t!lrS^;@FYYf&Xb(e;xg9TvxXYO+hxv-zj3exBu2a^ zfLd(Vu3cbBuzLjrs{-Wc2v!Aq_wE%FTeZl*42c(Df3FH4;0y@>HwR#h1UmyYH8o;e z?C+%j9$+z`tE;Q8uMY-Gb8~YW8=E6XjsOP?(A1hjj2KXwK>(L*%_;O>nGM)wU^j5# z!m2dO$iSOpxw*Od`T0dfMI|LAU~as8`7)dsp(jRQ_ct~+-n@AejE+#F{mouKNDSIy zNFD<_0Ah^>IStxkaFT?!*woZN6d9Zz0gxmHwHP3=p9`_?-~TI#0sI;&^X%BEdv%=j z?QZSle13U4#?oHhlAdCH$wwM;l^UoLY*ojdgkFRGE2eDILR{qKrX_cZhJ?B$T=>hJY)o|TYrzE7m>#1J7tX1gzJ7lV&+^JIZZv;>LWR4Bis3U(S1bgxsUf5z{e@ zouvy_=GX&a`q8TqY~+DQCX=v^rIX1PKGT!P!Ls4^m}0cFvXEV$$3;^N|7 z0c0>Jv5>Alpe%x^@xL<_@bJNM6Aq40bO8YK3(yo?VVat*iY}nI!15AoE{OpI81W(# z2(XhUSLevHXYuOmC+xJQaR%0zaHfonjZH{MNK8xwmVSDAIvgkoq!-v?Lg%ZpvXWqh z3H>j?X29DAGK_%A;Qo>rkkjB&^TC5vPV4IG>gnl$>&($nLI#_dz)P~1FJB^68MOD; zTyTQPa(;gP=etY@BH2ZVU&CEp5j+<4H~TJqq)6GVY~i;%(NODEXE7=)@WO)f^dbKG zJm--}!B9g@^A>kW9p(71h#> zZ;Btj9pL;bmqgj@?0tEb+KcoN2Rjr-dKq`ymvqaiKeRZS@T?`(X34?VAj zx0N3Fi#_Z4r(CZNDhVs9>nAoD<^-~2WiFlE^dL;BaaYUtlTDpXOxdRvH1Ap|bYhN- z_#5)q$I(AKqke%Ty~{X9@w}uT9nL^exjkg7eBBN^ccT&`{!kf%6gs_!23pvJ5tTGE ziH)1awVAv5l5z5Fw{v0_UB``F59-+EVJ8dr@pxM9xJoL@ptXryG*rXy+1lJyH2uGx zAt@;-X=!Phn3&dFOmcAG2@4s=fR@*qqaaLYASD-6TI&g{g9i^1Xe{7*A(@L9NU?=N zp)eSXnVFfDl@;*s9UUE=oSdAUory<8Z|@(gM*={Z1>m;=fB&`h)|!S`NC@5&gVsLS zO~%K^L*@c{D<>xh*!D=n9&92jDk^~Rs;>S)RUl*m-1?@bCW46(42saR2eV>F$3Ltv zptNA-0=W1{TWoL;KSmA@4+A5gV3>rS{p92i`GpK+tVvrRKCCJ*;OIko`Uwb$YAyT{ z@*12~W>USJ)y1N9CFPQ&g5T~p@ue(X#i(qDhCHvF?Ix&d=SLGtIGzfdTD9f4dF3mW zPPt1n&edV)ihTNrT<7O4NwSrW>8<&$@GBRCI5bY%jCfu1MMacU}xPT@wrz2>jYJM~cfoSf6S2 zadG3*Jz@GE#Jda@NcWwdIo0HHVqkI)%0tDw$J+l3jd%K)t<)O5CtnvXVWjgawlN>s z7HKjRd@J{3^J(SgskrU&m;t8n1+Qwg(5>}x_fePM6fk4eC7X@uZ29&*MQ@mzxD$#C z@lMs-HNeD!oqszKbWWXeOH_ocWv-@#VnV)VSli+mrnvg%yn75n`}o$GA>aK59Y#$} zO+!ONOG`^nPfzTEA=MbzJpz8?;UV<%k@(^1uK+A03G^8SDT8*HiHV81xw(~<6-ZGWIdTM8`tI)To}R0Q*H73C0@4~JG~n}tP3Gy- zr`KC%A^~U(&X6E)k(jvZh$ScEx0GpVX>dQ8mzP&mR8&@01}4d?SMiI-+S=M{*Z#p{ zFe&yEdmqk?4<9~6X2(Eq3@jm^K7C3+Spbl@pz4~Qo(6FWq~szV9f<+$urFV}{Di>> zuoi*+z%QY)>8|j6gUXsg7P9S(EZp*RLmpnPB|XJ@wWe3&y>em z?}8o;bd^)~Ql4`U$v}DSJB*_9ZMCHRwRfEJ_xHFNAM+jH;u#CK=x+*mCfB4go}1rt z%6chCOXO;cz=NEnNPp6@z0EY5pLTw^O4jQTBRC|=%23mxj}q3G*yxSXW+@Q&@mVgN ze3v^RmB69?IR15Fh81MYO`H8)D9j!b8M-nMH$Z^*OfOeJ^6QqlsxF)U;#48?`*a@V&sGnRnAKF?-1#OXr~P>eOe^l;b|;Y~W#^(g-uII%})T@9dCB@5;=%g{6|bc0^C= zc1pjk(5Tn*^LlUZU7)F?VK$1B8T8PMuoFH!9PS&Jz~oUIHy$3ohhT<;=`p*ln}0{b zf{u=ko}L~V7egk-Kxhn@_5UR^3kFL_TAq&_HSo|O=3j!Blahac=UszaJ zQc`m165buFs=_NY=!gMB|Hh3QF#SsGh{2%|y7@>K46s>WU*EvM!0_-ew7rl(ve|!k z!JrJApP&Ey`SZWJU;sZz>fi8dNK7n}1=Bo1I%&K;DOW(AuH#-S-U!o+zaAg&Wy63$ zRXeMyCKYp5*i;`AFR0e&taQrWDtN3~qo+c>k+F1cD-+h;71!`+=3yL3$Ubsex(w+l zs}g&*$N?4Xlxgu}vV%gjl+Kl@)_vbI`Ce3YwhBAkVA$y{u1a$*o6F5~dzpQtQBEuw zh4+Q_hkA`=_T+;S?HziG#jL{mGHSzf^%c?IFurOuN||mOt7&Bg&f8qP)H<`EYZRVk za{2U8lD?0AA*et1>s-mQ{k*gty`6#TujeJj?XT%h&C{m@2CLrqn5}p_Kq^P^25HE{ zvs><;*>P_3$j4rnOLI3&CJc>+%o5vd19Z(-F$i3cRVdDb0Mmkfkr#_k%`QW1$D4+DU}c# zVSvOUQ2+a_B#64gm_HIwgAs$B9nWUm+}yi&|1iZMh-Dbk+?SA$fEogG7HZ}%|%huKwtQ#F2@w&@t1t7T(hDujgSFn2Y@bFkujDbJ}7#`so z5=O=VkoxN901SrAyq-UQJ|Q6?DJco$!iblVYoc%kAnXsk{_^s2pwOzTtLy4k6Jcx4 zm;_@c%<{vy1v2Ui1Fy(*|I?@V+{M_~7>rebcq?@9iA5IyfDr~_ufLGTei4I3uo$<# zA}v&p?$6`r_T#FlnjY*dMpe0K<#}~Cx8hdsZfzFSUDYfZrTLyd0aL{$9Wl~g1L0Dk{#TzRweKV;o4bCa|JZR&x!L25 z@WNn+5Gur)Rm`0C_=$4)DYl*JlRmi}`|GU!%$nP;AfT|PEv@nDiM}w@Gv(;L&vNZ@ zU%eKW$hN+vG^i{*{!GbJ-r>O+vcaW9FA_?xT?!ke{HQ&YY+G#3l_)Ue25&LoqjzI< zlt~O>Wn4%yj?KGQ}@&9G{F@J0r&b@*>Zc|FISW-WUS|1K35vs4G&7 z0Tl-4NM2t2QW9zDLsx(AUcB-GL47GHd_V#UE+imJ?ce_o{RNJfR8&^=7u-U^2@(#G zAd?SsVHga?*w`4HCz+a>nytXx+}y�)#8TsS*rVAU%J0tpt=B2=IG*djp3b$!Y|O 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 L8@}NiK8F7Q4CAa& From 679d53e2cf3cb4393c2507df731dcadfa1c3627d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 7 Feb 2018 15:13:22 -0800 Subject: [PATCH 004/260] This works, but is it correct? --- .gitignore | 5 ++++- .../inspectionCertificate/InspectionCertificate.qml | 2 ++ .../src/ui/overlays/ContextOverlayInterface.cpp | 12 ++++++++---- interface/src/ui/overlays/ContextOverlayInterface.h | 2 +- scripts/system/marketplaces/marketplaces.js | 3 +++ 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index f45572c388..1ffb93fe80 100644 --- a/.gitignore +++ b/.gitignore @@ -85,4 +85,7 @@ npm-debug.log android/app/src/main/assets # Resource binary file -interface/compiledResources \ No newline at end of file +interface/compiledResources + +# GPUCache +interface/resources/GPUCache/* \ No newline at end of file diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml index 2c7319be09..bef03bd4c1 100644 --- a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -256,6 +256,7 @@ Rectangle { // "Close" button HiFiGlyphs { + z: 999; id: closeGlyphButton; text: hifi.glyphs.close; color: hifi.colors.white; @@ -562,6 +563,7 @@ Rectangle { case 'inspectionCertificate_setCertificateId': resetCert(false); root.certificateId = message.certificateId; + sendToScript({method: 'inspectionCertificate_requestOwnershipVerification', certificateId: root.certificateId}); break; case 'inspectionCertificate_resetCert': resetCert(true); diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index ed7b811fb0..77284408cd 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -221,13 +221,13 @@ bool ContextOverlayInterface::destroyContextOverlay(const EntityItemID& entityIt void ContextOverlayInterface::contextOverlays_mousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { if (overlayID == _contextOverlayID && event.getButton() == PointerEvent::PrimaryButton) { qCDebug(context_overlay) << "Clicked Context Overlay. Entity ID:" << _currentEntityWithContextOverlay << "Overlay ID:" << overlayID; + emit contextOverlayClicked(_currentEntityWithContextOverlay); Setting::Handle _settingSwitch{ "commerce", true }; if (_settingSwitch.get()) { openInspectionCertificate(); } else { openMarketplace(); } - emit contextOverlayClicked(_currentEntityWithContextOverlay); _contextOverlayJustClicked = true; } } @@ -350,6 +350,12 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID emit DependencyManager::get()->ownershipVerificationFailed(_lastInspectedEntity); qCDebug(context_overlay) << "Entity" << _lastInspectedEntity << "failed static certificate verification!"; } + } else { + // We don't currently verify ownership of entities that aren't Avatar Entities, + // so they always pass Ownership Verification. It's necessary to emit this signal + // so that the Inspection Certificate can continue its information-grabbing process. + auto ledger = DependencyManager::get(); + emit ledger->updateCertificateStatus(entityProperties.getCertificateID(), (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); } } @@ -357,12 +363,10 @@ static const QString INSPECTION_CERTIFICATE_QML_PATH = "hifi/commerce/inspection void ContextOverlayInterface::openInspectionCertificate() { // lets open the tablet to the inspection certificate QML if (!_currentEntityWithContextOverlay.isNull() && _entityMarketplaceID.length() > 0) { + setLastInspectedEntity(_currentEntityWithContextOverlay); auto tablet = dynamic_cast(_tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); tablet->loadQMLSource(INSPECTION_CERTIFICATE_QML_PATH); _hmdScriptingInterface->openTablet(); - - setLastInspectedEntity(_currentEntityWithContextOverlay); - requestOwnershipVerification(_lastInspectedEntity); } } diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h index 6aad2a773b..fcdf2d5820 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.h +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -57,7 +57,7 @@ public: bool getEnabled() { return _enabled; } bool getIsInMarketplaceInspectionMode() { return _isInMarketplaceInspectionMode; } void setIsInMarketplaceInspectionMode(bool mode) { _isInMarketplaceInspectionMode = mode; } - void requestOwnershipVerification(const QUuid& entityID); + Q_INVOKABLE void requestOwnershipVerification(const QUuid& entityID); EntityPropertyFlags getEntityPropertyFlags() { return _entityPropertyFlags; } signals: diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index edcd488a01..cec139faae 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -583,6 +583,9 @@ var selectionDisplay = null; // for gridTool.js to ignore case 'inspectionCertificate_closeClicked': tablet.gotoHomeScreen(); break; + case 'inspectionCertificate_requestOwnershipVerification': + ContextOverlay.requestOwnershipVerification(message.certificateId); + break; case 'inspectionCertificate_showInMarketplaceClicked': tablet.gotoWebScreen(message.marketplaceUrl, MARKETPLACES_INJECT_SCRIPT_URL); break; From 704d4255a56ac04027cf47ccc78669f21aee2c6d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 7 Feb 2018 15:44:21 -0800 Subject: [PATCH 005/260] Comment change --- interface/src/ui/overlays/ContextOverlayInterface.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index 77284408cd..4dacab8936 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -221,13 +221,13 @@ bool ContextOverlayInterface::destroyContextOverlay(const EntityItemID& entityIt void ContextOverlayInterface::contextOverlays_mousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { if (overlayID == _contextOverlayID && event.getButton() == PointerEvent::PrimaryButton) { qCDebug(context_overlay) << "Clicked Context Overlay. Entity ID:" << _currentEntityWithContextOverlay << "Overlay ID:" << overlayID; - emit contextOverlayClicked(_currentEntityWithContextOverlay); Setting::Handle _settingSwitch{ "commerce", true }; if (_settingSwitch.get()) { openInspectionCertificate(); } else { openMarketplace(); } + emit contextOverlayClicked(_currentEntityWithContextOverlay); _contextOverlayJustClicked = true; } } @@ -352,8 +352,8 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID } } else { // We don't currently verify ownership of entities that aren't Avatar Entities, - // so they always pass Ownership Verification. It's necessary to emit this signal - // so that the Inspection Certificate can continue its information-grabbing process. + // so they always pass Ownership Verification. It's necessary to emit this signal + // so that the Inspection Certificate can continue its information-grabbing process. auto ledger = DependencyManager::get(); emit ledger->updateCertificateStatus(entityProperties.getCertificateID(), (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); } From ecc0a2f43bb98974fea6623e1fa1a31d73afa545 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 8 Feb 2018 10:35:34 -0800 Subject: [PATCH 006/260] Bugfix. --- .../InspectionCertificate.qml | 149 ++++++++++-------- .../ui/overlays/ContextOverlayInterface.cpp | 142 ++++++++--------- scripts/system/marketplaces/marketplaces.js | 3 +- 3 files changed, 153 insertions(+), 141 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml index bef03bd4c1..f493747c5e 100644 --- a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -24,6 +24,7 @@ Rectangle { id: root; property string marketplaceUrl: ""; + property string entityId: ""; property string certificateId: ""; property string itemName: "--"; property string itemOwner: "--"; @@ -110,77 +111,81 @@ Rectangle { } onUpdateCertificateStatus: { - root.certificateStatus = certStatus; - if (root.certificateStatus === 1) { // CERTIFICATE_STATUS_VERIFICATION_SUCCESS - root.useGoldCert = true; - root.certTitleTextColor = hifi.colors.darkGray; - root.certTextColor = hifi.colors.white; - root.infoTextColor = hifi.colors.blueAccent; - titleBarText.text = "Certificate"; - popText.text = "PROOF OF PROVENANCE"; - showInMarketplaceButton.visible = true; - root.certInfoReplaceMode = 5; - // "Item Name" text will be set in "onCertificateInfoResult()" - // "Edition" text will be set in "onCertificateInfoResult()" - // "Owner" text will be set in "onCertificateInfoResult()" - // "Purchase Date" text will be set in "onCertificateInfoResult()" - // "Purchase Price" text will be set in "onCertificateInfoResult()" - errorText.text = ""; - } else if (root.certificateStatus === 2) { // CERTIFICATE_STATUS_VERIFICATION_TIMEOUT - root.useGoldCert = false; - root.certTitleTextColor = hifi.colors.redHighlight; - root.certTextColor = hifi.colors.redHighlight; - root.infoTextColor = hifi.colors.redHighlight; - titleBarText.text = "Request Timed Out"; - popText.text = ""; - showInMarketplaceButton.visible = false; - root.certInfoReplaceMode = 0; - root.itemName = ""; - root.itemEdition = ""; - root.itemOwner = ""; - root.dateOfPurchase = ""; - root.itemCost = ""; - errorText.text = "Your request to inspect this item timed out. Please try again later."; - } else if (root.certificateStatus === 3) { // CERTIFICATE_STATUS_STATIC_VERIFICATION_FAILED - root.useGoldCert = false; - root.certTitleTextColor = hifi.colors.redHighlight; - root.certTextColor = hifi.colors.redHighlight; - root.infoTextColor = hifi.colors.redHighlight; - titleBarText.text = "Certificate\nNo Longer Valid"; - popText.text = ""; - showInMarketplaceButton.visible = true; - root.certInfoReplaceMode = 5; - // "Item Name" text will be set in "onCertificateInfoResult()" - // "Edition" text will be set in "onCertificateInfoResult()" - // "Owner" text will be set in "onCertificateInfoResult()" - // "Purchase Date" text will be set in "onCertificateInfoResult()" - // "Purchase Price" text will be set in "onCertificateInfoResult()" - errorText.text = "The information associated with this item has been modified and it no longer matches the original certified item."; - } else if (root.certificateStatus === 4) { // CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED - root.useGoldCert = false; - root.certTitleTextColor = hifi.colors.redHighlight; - root.certTextColor = hifi.colors.redHighlight; - root.infoTextColor = hifi.colors.redHighlight; - titleBarText.text = "Invalid Certificate"; - popText.text = ""; - showInMarketplaceButton.visible = true; - root.certInfoReplaceMode = 4; - // "Item Name" text will be set in "onCertificateInfoResult()" - root.itemEdition = "Uncertified Copy" - // "Owner" text will be set in "onCertificateInfoResult()" - // "Purchase Date" text will be set in "onCertificateInfoResult()" - // "Purchase Price" text will be set in "onCertificateInfoResult()" - // "Error Text" text will be set in "onCertificateInfoResult()" - } else { - console.log("Unknown certificate status received from ledger signal!"); - } - - root.certificateStatusPending = false; - // We've gotten cert status - we are GO on getting the cert info - Commerce.certificateInfo(root.certificateId); + updateCertificateStatus(certStatus); } } + function updateCertificateStatus(status) { + root.certificateStatus = status; + if (root.certificateStatus === 1) { // CERTIFICATE_STATUS_VERIFICATION_SUCCESS + root.useGoldCert = true; + root.certTitleTextColor = hifi.colors.darkGray; + root.certTextColor = hifi.colors.white; + root.infoTextColor = hifi.colors.blueAccent; + titleBarText.text = "Certificate"; + popText.text = "PROOF OF PROVENANCE"; + showInMarketplaceButton.visible = true; + root.certInfoReplaceMode = 5; + // "Item Name" text will be set in "onCertificateInfoResult()" + // "Edition" text will be set in "onCertificateInfoResult()" + // "Owner" text will be set in "onCertificateInfoResult()" + // "Purchase Date" text will be set in "onCertificateInfoResult()" + // "Purchase Price" text will be set in "onCertificateInfoResult()" + errorText.text = ""; + } else if (root.certificateStatus === 2) { // CERTIFICATE_STATUS_VERIFICATION_TIMEOUT + root.useGoldCert = false; + root.certTitleTextColor = hifi.colors.redHighlight; + root.certTextColor = hifi.colors.redHighlight; + root.infoTextColor = hifi.colors.redHighlight; + titleBarText.text = "Request Timed Out"; + popText.text = ""; + showInMarketplaceButton.visible = false; + root.certInfoReplaceMode = 0; + root.itemName = ""; + root.itemEdition = ""; + root.itemOwner = ""; + root.dateOfPurchase = ""; + root.itemCost = ""; + errorText.text = "Your request to inspect this item timed out. Please try again later."; + } else if (root.certificateStatus === 3) { // CERTIFICATE_STATUS_STATIC_VERIFICATION_FAILED + root.useGoldCert = false; + root.certTitleTextColor = hifi.colors.redHighlight; + root.certTextColor = hifi.colors.redHighlight; + root.infoTextColor = hifi.colors.redHighlight; + titleBarText.text = "Certificate\nNo Longer Valid"; + popText.text = ""; + showInMarketplaceButton.visible = true; + root.certInfoReplaceMode = 5; + // "Item Name" text will be set in "onCertificateInfoResult()" + // "Edition" text will be set in "onCertificateInfoResult()" + // "Owner" text will be set in "onCertificateInfoResult()" + // "Purchase Date" text will be set in "onCertificateInfoResult()" + // "Purchase Price" text will be set in "onCertificateInfoResult()" + errorText.text = "The information associated with this item has been modified and it no longer matches the original certified item."; + } else if (root.certificateStatus === 4) { // CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED + root.useGoldCert = false; + root.certTitleTextColor = hifi.colors.redHighlight; + root.certTextColor = hifi.colors.redHighlight; + root.infoTextColor = hifi.colors.redHighlight; + titleBarText.text = "Invalid Certificate"; + popText.text = ""; + showInMarketplaceButton.visible = true; + root.certInfoReplaceMode = 4; + // "Item Name" text will be set in "onCertificateInfoResult()" + root.itemEdition = "Uncertified Copy" + // "Owner" text will be set in "onCertificateInfoResult()" + // "Purchase Date" text will be set in "onCertificateInfoResult()" + // "Purchase Price" text will be set in "onCertificateInfoResult()" + // "Error Text" text will be set in "onCertificateInfoResult()" + } else { + console.log("Unknown certificate status received from ledger signal!"); + } + + root.certificateStatusPending = false; + // We've gotten cert status - we are GO on getting the cert info + Commerce.certificateInfo(root.certificateId); + } + // 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. @@ -563,7 +568,12 @@ Rectangle { case 'inspectionCertificate_setCertificateId': resetCert(false); root.certificateId = message.certificateId; - sendToScript({method: 'inspectionCertificate_requestOwnershipVerification', certificateId: root.certificateId}); + if (message.entityId === "") { + updateCertificateStatus(1); // CERTIFICATE_STATUS_VERIFICATION_SUCCESS + } else { + root.entityId = message.entityId; + sendToScript({method: 'inspectionCertificate_requestOwnershipVerification', entity: root.entityId}); + } break; case 'inspectionCertificate_resetCert': resetCert(true); @@ -576,6 +586,7 @@ Rectangle { function resetCert(alsoResetCertID) { if (alsoResetCertID) { + root.entityId = ""; root.certificateId = ""; } root.certInfoReplaceMode = 5; diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index 4dacab8936..dd05e5c6a8 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -274,88 +274,88 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID auto nodeList = DependencyManager::get(); - if (entityProperties.getClientOnly()) { - if (entityProperties.verifyStaticCertificateProperties()) { - SharedNodePointer entityServer = nodeList->soloNodeOfType(NodeType::EntityServer); + if (entityProperties.verifyStaticCertificateProperties()) { + if (entityProperties.getClientOnly()) { + SharedNodePointer entityServer = nodeList->soloNodeOfType(NodeType::EntityServer); - if (entityServer) { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest; - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); - requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/transfer"); - QJsonObject request; - request["certificate_id"] = entityProperties.getCertificateID(); - networkRequest.setUrl(requestURL); + if (entityServer) { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest; + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); + requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/transfer"); + QJsonObject request; + request["certificate_id"] = entityProperties.getCertificateID(); + networkRequest.setUrl(requestURL); - QNetworkReply* networkReply = NULL; - networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); + QNetworkReply* networkReply = NULL; + networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); - connect(networkReply, &QNetworkReply::finished, [=]() { - QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); - jsonObject = jsonObject["data"].toObject(); + connect(networkReply, &QNetworkReply::finished, [=]() { + QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); + jsonObject = jsonObject["data"].toObject(); - if (networkReply->error() == QNetworkReply::NoError) { - if (!jsonObject["invalid_reason"].toString().isEmpty()) { - qCDebug(entities) << "invalid_reason not empty"; - } else if (jsonObject["transfer_status"].toArray().first().toString() == "failed") { - qCDebug(entities) << "'transfer_status' is 'failed'"; - } else if (jsonObject["transfer_status"].toArray().first().toString() == "pending") { - qCDebug(entities) << "'transfer_status' is 'pending'"; - } else { - QString ownerKey = jsonObject["transfer_recipient_key"].toString(); - - QByteArray certID = entityProperties.getCertificateID().toUtf8(); - QByteArray text = DependencyManager::get()->getTree()->computeNonce(certID, ownerKey); - QByteArray nodeToChallengeByteArray = entityProperties.getOwningAvatarID().toRfc4122(); - - int certIDByteArraySize = certID.length(); - int textByteArraySize = text.length(); - int nodeToChallengeByteArraySize = nodeToChallengeByteArray.length(); - - auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnershipRequest, - certIDByteArraySize + textByteArraySize + nodeToChallengeByteArraySize + 3 * sizeof(int), - true); - challengeOwnershipPacket->writePrimitive(certIDByteArraySize); - challengeOwnershipPacket->writePrimitive(textByteArraySize); - challengeOwnershipPacket->writePrimitive(nodeToChallengeByteArraySize); - challengeOwnershipPacket->write(certID); - challengeOwnershipPacket->write(text); - challengeOwnershipPacket->write(nodeToChallengeByteArray); - nodeList->sendPacket(std::move(challengeOwnershipPacket), *entityServer); - - // Kickoff a 10-second timeout timer that marks the cert if we don't get an ownership response in time - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "startChallengeOwnershipTimer"); - return; + if (networkReply->error() == QNetworkReply::NoError) { + if (!jsonObject["invalid_reason"].toString().isEmpty()) { + qCDebug(entities) << "invalid_reason not empty"; + } else if (jsonObject["transfer_status"].toArray().first().toString() == "failed") { + qCDebug(entities) << "'transfer_status' is 'failed'"; + } else if (jsonObject["transfer_status"].toArray().first().toString() == "pending") { + qCDebug(entities) << "'transfer_status' is 'pending'"; } else { - startChallengeOwnershipTimer(); - } - } - } else { - qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << - "More info:" << networkReply->readAll(); - } + QString ownerKey = jsonObject["transfer_recipient_key"].toString(); - networkReply->deleteLater(); - }); - } else { - qCWarning(context_overlay) << "Couldn't get Entity Server!"; - } + QByteArray certID = entityProperties.getCertificateID().toUtf8(); + QByteArray text = DependencyManager::get()->getTree()->computeNonce(certID, ownerKey); + QByteArray nodeToChallengeByteArray = entityProperties.getOwningAvatarID().toRfc4122(); + + int certIDByteArraySize = certID.length(); + int textByteArraySize = text.length(); + int nodeToChallengeByteArraySize = nodeToChallengeByteArray.length(); + + auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnershipRequest, + certIDByteArraySize + textByteArraySize + nodeToChallengeByteArraySize + 3 * sizeof(int), + true); + challengeOwnershipPacket->writePrimitive(certIDByteArraySize); + challengeOwnershipPacket->writePrimitive(textByteArraySize); + challengeOwnershipPacket->writePrimitive(nodeToChallengeByteArraySize); + challengeOwnershipPacket->write(certID); + challengeOwnershipPacket->write(text); + challengeOwnershipPacket->write(nodeToChallengeByteArray); + nodeList->sendPacket(std::move(challengeOwnershipPacket), *entityServer); + + // Kickoff a 10-second timeout timer that marks the cert if we don't get an ownership response in time + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "startChallengeOwnershipTimer"); + return; + } else { + startChallengeOwnershipTimer(); + } + } + } else { + qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << + "More info:" << networkReply->readAll(); + } + + networkReply->deleteLater(); + }); + } else { + qCWarning(context_overlay) << "Couldn't get Entity Server!"; + } } else { + // We don't currently verify ownership of entities that aren't Avatar Entities, + // so they always pass Ownership Verification. It's necessary to emit this signal + // so that the Inspection Certificate can continue its information-grabbing process. auto ledger = DependencyManager::get(); - _challengeOwnershipTimeoutTimer.stop(); - emit ledger->updateCertificateStatus(entityProperties.getCertificateID(), (uint)(ledger->CERTIFICATE_STATUS_STATIC_VERIFICATION_FAILED)); - emit DependencyManager::get()->ownershipVerificationFailed(_lastInspectedEntity); - qCDebug(context_overlay) << "Entity" << _lastInspectedEntity << "failed static certificate verification!"; + emit ledger->updateCertificateStatus(entityProperties.getCertificateID(), (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); } } else { - // We don't currently verify ownership of entities that aren't Avatar Entities, - // so they always pass Ownership Verification. It's necessary to emit this signal - // so that the Inspection Certificate can continue its information-grabbing process. auto ledger = DependencyManager::get(); - emit ledger->updateCertificateStatus(entityProperties.getCertificateID(), (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); + _challengeOwnershipTimeoutTimer.stop(); + emit ledger->updateCertificateStatus(entityProperties.getCertificateID(), (uint)(ledger->CERTIFICATE_STATUS_STATIC_VERIFICATION_FAILED)); + emit DependencyManager::get()->ownershipVerificationFailed(_lastInspectedEntity); + qCDebug(context_overlay) << "Entity" << _lastInspectedEntity << "failed static certificate verification!"; } } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index cec139faae..fd1275a251 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -163,6 +163,7 @@ var selectionDisplay = null; // for gridTool.js to ignore var certificateId = itemCertificateId || (Entities.getEntityProperties(currentEntityWithContextOverlay, ['certificateID']).certificateID); tablet.sendToQml({ method: 'inspectionCertificate_setCertificateId', + entityId: currentEntityWithContextOverlay, certificateId: certificateId }); } @@ -584,7 +585,7 @@ var selectionDisplay = null; // for gridTool.js to ignore tablet.gotoHomeScreen(); break; case 'inspectionCertificate_requestOwnershipVerification': - ContextOverlay.requestOwnershipVerification(message.certificateId); + ContextOverlay.requestOwnershipVerification(message.entity); break; case 'inspectionCertificate_showInMarketplaceClicked': tablet.gotoWebScreen(message.marketplaceUrl, MARKETPLACES_INJECT_SCRIPT_URL); From 725eb1416370efac3b40883455be10cce9662fc9 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Wed, 7 Feb 2018 16:43:15 -0800 Subject: [PATCH 007/260] Fix for deadlock triggering while loading QML engine --- interface/src/Application.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3e7dd3e223..c22a370b1f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2404,7 +2404,9 @@ void Application::initializeUi() { tabletScriptingInterface->getTablet(SYSTEM_TABLET); } auto offscreenUi = DependencyManager::get(); + DeadlockWatchdogThread::pause(); offscreenUi->create(); + DeadlockWatchdogThread::resume(); auto surfaceContext = offscreenUi->getSurfaceContext(); From d2f5645f96e5b1b36b7258b6952bac4521a9a298 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Wed, 7 Feb 2018 15:27:34 -0800 Subject: [PATCH 008/260] Don't trigger a backtrace exception on quitting while in HMD --- interface/src/Application_render.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application_render.cpp b/interface/src/Application_render.cpp index e1f198eed2..5cc072df37 100644 --- a/interface/src/Application_render.cpp +++ b/interface/src/Application_render.cpp @@ -55,7 +55,7 @@ void Application::paintGL() { // If a display plugin loses it's underlying support, it // needs to be able to signal us to not use it if (!displayPlugin->beginFrameRender(_renderFrameCount)) { - updateDisplayMode(); + QMetaObject::invokeMethod(this, "updateDisplayMode"); return; } } From 7eecc4767274a3e1aeba90b5670f963a3f0d7453 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Thu, 8 Feb 2018 10:23:25 -0800 Subject: [PATCH 009/260] Prevent deadlock crashes when building shaders at startup --- interface/src/Application.cpp | 45 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c22a370b1f..be2a54b8e9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -318,7 +318,7 @@ static QTimer pingTimer; static bool DISABLE_WATCHDOG = true; #else static const QString DISABLE_WATCHDOG_FLAG{ "HIFI_DISABLE_WATCHDOG" }; -static bool DISABLE_WATCHDOG = QProcessEnvironment::systemEnvironment().contains(DISABLE_WATCHDOG_FLAG); +static bool DISABLE_WATCHDOG = nsightActive() || QProcessEnvironment::systemEnvironment().contains(DISABLE_WATCHDOG_FLAG); #endif #if defined(USE_GLES) @@ -415,20 +415,26 @@ public: *crashTrigger = 0xDEAD10CC; } + static void withPause(const std::function& lambda) { + pause(); + lambda(); + resume(); + } static void pause() { _paused = true; } static void resume() { - _paused = false; + // Update the heartbeat BEFORE resuming the checks updateHeartbeat(); + _paused = false; } void run() override { while (!_quit) { QThread::sleep(HEARTBEAT_UPDATE_INTERVAL_SECS); // Don't do heartbeat detection under nsight - if (nsightActive() || _paused) { + if (_paused) { continue; } uint64_t lastHeartbeat = _heartbeat; // sample atomic _heartbeat, because we could context switch away and have it updated on us @@ -2283,29 +2289,22 @@ void Application::initializeGL() { initDisplay(); qCDebug(interfaceapp, "Initialized Display."); -#ifdef Q_OS_OSX - // FIXME: on mac os the shaders take up to 1 minute to compile, so we pause the deadlock watchdog thread. - DeadlockWatchdogThread::pause(); -#endif - - // Set up the render engine - render::CullFunctor cullFunctor = LODManager::shouldRender; - static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD"; - _renderEngine->addJob("UpdateScene"); + // FIXME: on low end systems os the shaders take up to 1 minute to compile, so we pause the deadlock watchdog thread. + DeadlockWatchdogThread::withPause([&] { + // Set up the render engine + render::CullFunctor cullFunctor = LODManager::shouldRender; + static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD"; + _renderEngine->addJob("UpdateScene"); #ifndef Q_OS_ANDROID - _renderEngine->addJob("SecondaryCameraJob", cullFunctor, !DISABLE_DEFERRED); + _renderEngine->addJob("SecondaryCameraJob", cullFunctor, !DISABLE_DEFERRED); #endif - _renderEngine->addJob("RenderMainView", cullFunctor, !DISABLE_DEFERRED, render::ItemKey::TAG_BITS_0, render::ItemKey::TAG_BITS_0); + _renderEngine->addJob("RenderMainView", cullFunctor, !DISABLE_DEFERRED, render::ItemKey::TAG_BITS_0, render::ItemKey::TAG_BITS_0); + _renderEngine->load(); + _renderEngine->registerScene(_main3DScene); - _renderEngine->load(); - _renderEngine->registerScene(_main3DScene); - - // Now that OpenGL is initialized, we are sure we have a valid context and can create the various pipeline shaders with success. - DependencyManager::get()->initializeShapePipelines(); - -#ifdef Q_OS_OSX - DeadlockWatchdogThread::resume(); -#endif + // Now that OpenGL is initialized, we are sure we have a valid context and can create the various pipeline shaders with success. + DependencyManager::get()->initializeShapePipelines(); + }); _offscreenContext = new OffscreenGLCanvas(); _offscreenContext->setObjectName("MainThreadContext"); From 950a62f3f8a88d7a3f0b9e1d05b62c49207990d8 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 8 Feb 2018 15:52:20 -0800 Subject: [PATCH 010/260] Global graphics flag default to on. Added cast shadows flag to zone/keylight. Exit the RenderShadowMap job if current keylight doesn't cast shadows. --- .../src/RenderableZoneEntityItem.cpp | 1 + .../entities/src/EntityItemProperties.cpp | 2 ++ libraries/entities/src/EntityPropertyFlags.h | 9 ++--- .../entities/src/KeyLightPropertyGroup.cpp | 35 +++++++++++++++---- .../entities/src/KeyLightPropertyGroup.h | 2 ++ libraries/graphics/src/graphics/Light.cpp | 9 ++++- libraries/graphics/src/graphics/Light.h | 5 +++ .../networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 3 +- .../src/DeferredLightingEffect.cpp | 16 ++++++--- .../render-utils/src/DeferredLightingEffect.h | 2 +- .../render-utils/src/RenderShadowTask.cpp | 15 ++++++++ libraries/render-utils/src/RenderShadowTask.h | 2 +- scripts/system/html/entityProperties.html | 4 +++ scripts/system/html/js/entityProperties.js | 7 ++++ 15 files changed, 94 insertions(+), 20 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 04f07c5bd3..c46409c4ee 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -330,6 +330,7 @@ void ZoneEntityRenderer::updateKeySunFromEntity(const TypedEntityPointer& entity sunLight->setColor(ColorUtils::toVec3(_keyLightProperties.getColor())); sunLight->setIntensity(_keyLightProperties.getIntensity()); sunLight->setDirection(_keyLightProperties.getDirection()); + sunLight->setCastShadows(_keyLightProperties.getCastShadows()); } void ZoneEntityRenderer::updateAmbientLightFromEntity(const TypedEntityPointer& entity) { diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index e2a5ddf8b5..cca4e858fa 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -1128,6 +1128,8 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_COLOR, KeyLightColor, keyLightColor, xColor); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_INTENSITY, KeyLightIntensity, keyLightIntensity, float); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_DIRECTION, KeyLightDirection, keyLightDirection, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_CAST_SHADOWS, KeyLightCastShadows, keyLightCastShadows, bool); + ADD_PROPERTY_TO_MAP(PROP_VOXEL_VOLUME_SIZE, VoxelVolumeSize, voxelVolumeSize, glm::vec3); ADD_PROPERTY_TO_MAP(PROP_VOXEL_DATA, VoxelData, voxelData, QByteArray); ADD_PROPERTY_TO_MAP(PROP_VOXEL_SURFACE_STYLE, VoxelSurfaceStyle, voxelSurfaceStyle, uint16_t); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 90438ab01c..ffcd4f64cb 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -205,6 +205,11 @@ enum EntityPropertyList { PROP_HAZE_MODE, + PROP_KEYLIGHT_COLOR, + PROP_KEYLIGHT_INTENSITY, + PROP_KEYLIGHT_DIRECTION, + PROP_KEYLIGHT_CAST_SHADOWS, + PROP_HAZE_RANGE, PROP_HAZE_COLOR, PROP_HAZE_GLARE_COLOR, @@ -246,10 +251,6 @@ enum EntityPropertyList { // Aliases/Piggyback properties for Zones. These properties intentionally reuse the enum values for // other properties which will never overlap with each other. We do this so that we don't have to expand // the size of the properties bitflags mask - PROP_KEYLIGHT_COLOR = PROP_COLOR, - PROP_KEYLIGHT_INTENSITY = PROP_INTENSITY, - PROP_KEYLIGHT_DIRECTION = PROP_EXPONENT, - PROP_SKYBOX_COLOR = PROP_ANIMATION_URL, PROP_SKYBOX_URL = PROP_ANIMATION_FPS, diff --git a/libraries/entities/src/KeyLightPropertyGroup.cpp b/libraries/entities/src/KeyLightPropertyGroup.cpp index c476b4c23c..70b9a5395a 100644 --- a/libraries/entities/src/KeyLightPropertyGroup.cpp +++ b/libraries/entities/src/KeyLightPropertyGroup.cpp @@ -21,6 +21,7 @@ const xColor KeyLightPropertyGroup::DEFAULT_KEYLIGHT_COLOR = { 255, 255, 255 }; const float KeyLightPropertyGroup::DEFAULT_KEYLIGHT_INTENSITY = 1.0f; const float KeyLightPropertyGroup::DEFAULT_KEYLIGHT_AMBIENT_INTENSITY = 0.5f; const glm::vec3 KeyLightPropertyGroup::DEFAULT_KEYLIGHT_DIRECTION = { 0.0f, -1.0f, 0.0f }; +const bool KeyLightPropertyGroup::DEFAULT_KEYLIGHT_CAST_SHADOWS { false }; void KeyLightPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const { @@ -28,23 +29,27 @@ void KeyLightPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desired COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_COLOR, KeyLight, keyLight, Color, color); COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_INTENSITY, KeyLight, keyLight, Intensity, intensity); COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_DIRECTION, KeyLight, keyLight, Direction, direction); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_CAST_SHADOWS, KeyLight, keyLight, CastShadows, castShadows); } void KeyLightPropertyGroup::copyFromScriptValue(const QScriptValue& object, bool& _defaultSettings) { COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(keyLight, color, xColor, setColor); COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(keyLight, intensity, float, setIntensity); COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(keyLight, direction, glmVec3, setDirection); - + COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(keyLight, castShadows, bool, setCastShadows); + // legacy property support COPY_PROPERTY_FROM_QSCRIPTVALUE_GETTER(keyLightColor, xColor, setColor, getColor); COPY_PROPERTY_FROM_QSCRIPTVALUE_GETTER(keyLightIntensity, float, setIntensity, getIntensity); COPY_PROPERTY_FROM_QSCRIPTVALUE_GETTER(keyLightDirection, glmVec3, setDirection, getDirection); + COPY_PROPERTY_FROM_QSCRIPTVALUE_GETTER(keyLightCastShadows, bool, setCastShadows, getCastShadows); } void KeyLightPropertyGroup::merge(const KeyLightPropertyGroup& other) { COPY_PROPERTY_IF_CHANGED(color); COPY_PROPERTY_IF_CHANGED(intensity); COPY_PROPERTY_IF_CHANGED(direction); + COPY_PROPERTY_IF_CHANGED(castShadows); } void KeyLightPropertyGroup::debugDump() const { @@ -52,6 +57,7 @@ void KeyLightPropertyGroup::debugDump() const { qCDebug(entities) << " color:" << getColor(); // << "," << getColor()[1] << "," << getColor()[2]; qCDebug(entities) << " intensity:" << getIntensity(); qCDebug(entities) << " direction:" << getDirection(); + qCDebug(entities) << " castShadows:" << getCastShadows(); } void KeyLightPropertyGroup::listChangedProperties(QList& out) { @@ -64,6 +70,9 @@ void KeyLightPropertyGroup::listChangedProperties(QList& out) { if (directionChanged()) { out << "keyLight-direction"; } + if (castShadowsChanged()) { + out << "keyLight-castShadows"; + } } bool KeyLightPropertyGroup::appendToEditPacket(OctreePacketData* packetData, @@ -71,19 +80,22 @@ bool KeyLightPropertyGroup::appendToEditPacket(OctreePacketData* packetData, EntityPropertyFlags& propertyFlags, EntityPropertyFlags& propertiesDidntFit, int& propertyCount, - OctreeElement::AppendState& appendState) const { + OctreeElement::AppendState& appendState) const +{ bool successPropertyFits = true; APPEND_ENTITY_PROPERTY(PROP_KEYLIGHT_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_KEYLIGHT_INTENSITY, getIntensity()); APPEND_ENTITY_PROPERTY(PROP_KEYLIGHT_DIRECTION, getDirection()); - + APPEND_ENTITY_PROPERTY(PROP_KEYLIGHT_CAST_SHADOWS, getCastShadows()); + return true; } bool KeyLightPropertyGroup::decodeFromEditPacket(EntityPropertyFlags& propertyFlags, const unsigned char*& dataAt, - int& processedBytes) { + int& processedBytes) +{ int bytesRead = 0; bool overwriteLocalData = true; @@ -92,11 +104,13 @@ bool KeyLightPropertyGroup::decodeFromEditPacket(EntityPropertyFlags& propertyFl READ_ENTITY_PROPERTY(PROP_KEYLIGHT_COLOR, xColor, setColor); READ_ENTITY_PROPERTY(PROP_KEYLIGHT_INTENSITY, float, setIntensity); READ_ENTITY_PROPERTY(PROP_KEYLIGHT_DIRECTION, glm::vec3, setDirection); - + READ_ENTITY_PROPERTY(PROP_KEYLIGHT_CAST_SHADOWS, bool, setCastShadows); + DECODE_GROUP_PROPERTY_HAS_CHANGED(PROP_KEYLIGHT_COLOR, Color); DECODE_GROUP_PROPERTY_HAS_CHANGED(PROP_KEYLIGHT_INTENSITY, Intensity); DECODE_GROUP_PROPERTY_HAS_CHANGED(PROP_KEYLIGHT_DIRECTION, Direction); - + DECODE_GROUP_PROPERTY_HAS_CHANGED(PROP_KEYLIGHT_CAST_SHADOWS, CastShadows); + processedBytes += bytesRead; Q_UNUSED(somethingChanged); @@ -108,6 +122,7 @@ void KeyLightPropertyGroup::markAllChanged() { _colorChanged = true; _intensityChanged = true; _directionChanged = true; + _castShadowsChanged = true; } EntityPropertyFlags KeyLightPropertyGroup::getChangedProperties() const { @@ -116,7 +131,8 @@ EntityPropertyFlags KeyLightPropertyGroup::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_KEYLIGHT_COLOR, color); CHECK_PROPERTY_CHANGE(PROP_KEYLIGHT_INTENSITY, intensity); CHECK_PROPERTY_CHANGE(PROP_KEYLIGHT_DIRECTION, direction); - + CHECK_PROPERTY_CHANGE(PROP_KEYLIGHT_CAST_SHADOWS, castShadows); + return changedProperties; } @@ -124,6 +140,7 @@ void KeyLightPropertyGroup::getProperties(EntityItemProperties& properties) cons COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(KeyLight, Color, getColor); COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(KeyLight, Intensity, getIntensity); COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(KeyLight, Direction, getDirection); + COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(KeyLight, CastShadows, getCastShadows); } bool KeyLightPropertyGroup::setProperties(const EntityItemProperties& properties) { @@ -132,6 +149,7 @@ bool KeyLightPropertyGroup::setProperties(const EntityItemProperties& properties SET_ENTITY_GROUP_PROPERTY_FROM_PROPERTIES(KeyLight, Color, color, setColor); SET_ENTITY_GROUP_PROPERTY_FROM_PROPERTIES(KeyLight, Intensity, intensity, setIntensity); SET_ENTITY_GROUP_PROPERTY_FROM_PROPERTIES(KeyLight, Direction, direction, setDirection); + SET_ENTITY_GROUP_PROPERTY_FROM_PROPERTIES(KeyLight, CastShadows, castShadows, setCastShadows); return somethingChanged; } @@ -142,6 +160,7 @@ EntityPropertyFlags KeyLightPropertyGroup::getEntityProperties(EncodeBitstreamPa requestedProperties += PROP_KEYLIGHT_COLOR; requestedProperties += PROP_KEYLIGHT_INTENSITY; requestedProperties += PROP_KEYLIGHT_DIRECTION; + requestedProperties += PROP_KEYLIGHT_CAST_SHADOWS; return requestedProperties; } @@ -159,6 +178,7 @@ void KeyLightPropertyGroup::appendSubclassData(OctreePacketData* packetData, Enc APPEND_ENTITY_PROPERTY(PROP_KEYLIGHT_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_KEYLIGHT_INTENSITY, getIntensity()); APPEND_ENTITY_PROPERTY(PROP_KEYLIGHT_DIRECTION, getDirection()); + APPEND_ENTITY_PROPERTY(PROP_KEYLIGHT_CAST_SHADOWS, getCastShadows()); } int KeyLightPropertyGroup::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, @@ -172,6 +192,7 @@ int KeyLightPropertyGroup::readEntitySubclassDataFromBuffer(const unsigned char* READ_ENTITY_PROPERTY(PROP_KEYLIGHT_COLOR, xColor, setColor); READ_ENTITY_PROPERTY(PROP_KEYLIGHT_INTENSITY, float, setIntensity); READ_ENTITY_PROPERTY(PROP_KEYLIGHT_DIRECTION, glm::vec3, setDirection); + READ_ENTITY_PROPERTY(PROP_KEYLIGHT_CAST_SHADOWS, bool, setCastShadows); return bytesRead; } diff --git a/libraries/entities/src/KeyLightPropertyGroup.h b/libraries/entities/src/KeyLightPropertyGroup.h index f33ebb282d..d3c8597f95 100644 --- a/libraries/entities/src/KeyLightPropertyGroup.h +++ b/libraries/entities/src/KeyLightPropertyGroup.h @@ -78,10 +78,12 @@ public: static const float DEFAULT_KEYLIGHT_INTENSITY; static const float DEFAULT_KEYLIGHT_AMBIENT_INTENSITY; static const glm::vec3 DEFAULT_KEYLIGHT_DIRECTION; + static const bool DEFAULT_KEYLIGHT_CAST_SHADOWS; DEFINE_PROPERTY_REF(PROP_KEYLIGHT_COLOR, Color, color, xColor, DEFAULT_KEYLIGHT_COLOR); DEFINE_PROPERTY(PROP_KEYLIGHT_INTENSITY, Intensity, intensity, float, DEFAULT_KEYLIGHT_INTENSITY); DEFINE_PROPERTY_REF(PROP_KEYLIGHT_DIRECTION, Direction, direction, glm::vec3, DEFAULT_KEYLIGHT_DIRECTION); + DEFINE_PROPERTY(PROP_KEYLIGHT_CAST_SHADOWS, CastShadows, castShadows, bool, DEFAULT_KEYLIGHT_CAST_SHADOWS); }; #endif // hifi_KeyLightPropertyGroup_h diff --git a/libraries/graphics/src/graphics/Light.cpp b/libraries/graphics/src/graphics/Light.cpp index cb5209d4cf..94ec3a376a 100755 --- a/libraries/graphics/src/graphics/Light.cpp +++ b/libraries/graphics/src/graphics/Light.cpp @@ -65,6 +65,14 @@ const Vec3& Light::getDirection() const { return _lightSchemaBuffer->volume.direction; } +void Light::setCastShadows(const bool castShadows) { + _castShadows = castShadows; +} + +const bool Light::getCastShadows() const { + return _castShadows; +} + void Light::setColor(const Color& color) { _lightSchemaBuffer.edit().irradiance.color = color; updateLightRadius(); @@ -132,7 +140,6 @@ void Light::setSpotExponent(float exponent) { _lightSchemaBuffer.edit().irradiance.falloffSpot = exponent; } - void Light::setAmbientIntensity(float intensity) { _ambientSchemaBuffer.edit().intensity = intensity; } diff --git a/libraries/graphics/src/graphics/Light.h b/libraries/graphics/src/graphics/Light.h index 360e3f224e..7497691185 100755 --- a/libraries/graphics/src/graphics/Light.h +++ b/libraries/graphics/src/graphics/Light.h @@ -103,6 +103,9 @@ public: void setDirection(const Vec3& direction); const Vec3& getDirection() const; + void setCastShadows(const bool castShadows); + const bool getCastShadows() const; + void setOrientation(const Quat& orientation); const glm::quat& getOrientation() const { return _transform.getRotation(); } @@ -187,6 +190,8 @@ protected: void updateLightRadius(); + bool _castShadows{ false }; + }; typedef std::shared_ptr< Light > LightPointer; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index c48c6bfc0b..5cafb4caa2 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -30,7 +30,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return static_cast(EntityVersion::SoftEntities); + return static_cast(EntityVersion::ShadowControl); case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::RemovedJurisdictions); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index d186ed41c3..d996c2826f 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -206,7 +206,8 @@ enum class EntityVersion : PacketVersion { OwnershipChallengeFix, ZoneLightInheritModes = 82, ZoneStageRemoved, - SoftEntities + SoftEntities, + ShadowControl }; enum class EntityScriptCallMethodVersion : PacketVersion { diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index d0d9708c04..c7e6ff1dcf 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -541,15 +541,23 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, auto keyLight = lightAndShadow.first; - graphics::LightPointer keyAmbientLight; + graphics::LightPointer ambientLight; if (lightStage && lightStage->_currentFrame._ambientLights.size()) { - keyAmbientLight = lightStage->getLight(lightStage->_currentFrame._ambientLights.front()); + ambientLight = lightStage->getLight(lightStage->_currentFrame._ambientLights.front()); } - bool hasAmbientMap = (keyAmbientLight != nullptr); + bool hasAmbientMap = (ambientLight != nullptr); // Setup the global directional pass pipeline { - if (deferredLightingEffect->_shadowMapEnabled) { + // Check if keylight casts shadows + bool keyLightCastShadows { false }; + + if (lightStage && lightStage->_currentFrame._sunLights.size()) { + graphics::LightPointer keyLight = lightStage->getLight(lightStage->_currentFrame._sunLights.front()); + keyLightCastShadows = keyLight->getCastShadows(); + } + + if (deferredLightingEffect->_shadowMapEnabled && keyLightCastShadows) { // If the keylight has an ambient Map then use the Skybox version of the pass // otherwise use the ambient sphere version diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 1b776e6409..ce7ecacbbe 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -61,7 +61,7 @@ public: private: DeferredLightingEffect() = default; - bool _shadowMapEnabled{ false }; + bool _shadowMapEnabled{ true }; // note that this value is overwritten in the ::configure method bool _ambientOcclusionEnabled{ false }; graphics::MeshPointer _pointLightMesh; diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index e8963c2e4e..c5bdcf03dd 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -120,6 +120,12 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, con auto lightStage = renderContext->_scene->getStage(); assert(lightStage); + // Exit if current keylight does not cast shadows + bool castShadows = lightStage->getCurrentKeyLight()->getCastShadows(); + if (!castShadows) { + return; + } + auto shadow = lightStage->getCurrentKeyShadow(); if (!shadow || _cascadeIndex >= shadow->getCascadeCount()) { return; @@ -378,6 +384,15 @@ void RenderShadowSetup::run(const render::RenderContextPointer& renderContext, O void RenderShadowCascadeSetup::run(const render::RenderContextPointer& renderContext, Outputs& output) { auto lightStage = renderContext->_scene->getStage(); assert(lightStage); + + // Exit if current keylight does not cast shadows + bool castShadows = lightStage->getCurrentKeyLight()->getCastShadows(); + if (!castShadows) { + output.edit0() = ItemFilter::Builder::nothing(); + output.edit1() = ViewFrustumPointer(); + return; + } + // Cache old render args RenderArgs* args = renderContext->args; diff --git a/libraries/render-utils/src/RenderShadowTask.h b/libraries/render-utils/src/RenderShadowTask.h index 7f127a558c..98b70c0c9f 100644 --- a/libraries/render-utils/src/RenderShadowTask.h +++ b/libraries/render-utils/src/RenderShadowTask.h @@ -38,7 +38,7 @@ class RenderShadowTaskConfig : public render::Task::Config::Persistent { Q_OBJECT Q_PROPERTY(bool enabled MEMBER enabled NOTIFY dirty) public: - RenderShadowTaskConfig() : render::Task::Config::Persistent(QStringList() << "Render" << "Engine" << "Shadows", false) {} + RenderShadowTaskConfig() : render::Task::Config::Persistent(QStringList() << "Render" << "Engine" << "Shadows", true) {} signals: void dirty(); diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index b93974ee77..856ca3c6e1 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -532,6 +532,10 @@

+
+ + +
Skybox diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 7008d0df66..7e8827a9b5 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -652,6 +652,8 @@ function loaded() { var elZoneKeyLightDirectionX = document.getElementById("property-zone-key-light-direction-x"); var elZoneKeyLightDirectionY = document.getElementById("property-zone-key-light-direction-y"); + var elZoneKeyLightCastShadows = document.getElementById("property-zone-key-light-cast-shadows"); + // Skybox var elZoneSkyboxModeInherit = document.getElementById("property-zone-skybox-mode-inherit"); var elZoneSkyboxModeDisabled = document.getElementById("property-zone-skybox-mode-disabled"); @@ -1026,6 +1028,8 @@ function loaded() { elZoneKeyLightDirectionX.value = properties.keyLight.direction.x.toFixed(2); elZoneKeyLightDirectionY.value = properties.keyLight.direction.y.toFixed(2); + elZoneKeyLightCastShadows.checked = properties.keyLight.castShadows; + // Skybox elZoneSkyboxModeInherit.checked = (properties.skyboxMode === 'inherit'); elZoneSkyboxModeDisabled.checked = (properties.skyboxMode === 'disabled'); @@ -1463,6 +1467,9 @@ function loaded() { elZoneKeyLightDirectionX.addEventListener('change', zoneKeyLightDirectionChangeFunction); elZoneKeyLightDirectionY.addEventListener('change', zoneKeyLightDirectionChangeFunction); + elZoneKeyLightCastShadows.addEventListener('change', + createEmitGroupCheckedPropertyUpdateFunction('keyLight', 'castShadows')); + // Skybox var skyboxModeChanged = createZoneComponentModeChangedFunction('skyboxMode', elZoneSkyboxModeInherit, elZoneSkyboxModeDisabled, elZoneSkyboxModeEnabled); From 13ce6bbabd3c979fae0df3de03185ae91dc9c64f Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Tue, 6 Feb 2018 13:44:59 -0800 Subject: [PATCH 011/260] fix lasers going to origin --- scripts/system/libraries/pointersUtils.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/system/libraries/pointersUtils.js b/scripts/system/libraries/pointersUtils.js index 2af563f8d4..53959b91f8 100644 --- a/scripts/system/libraries/pointersUtils.js +++ b/scripts/system/libraries/pointersUtils.js @@ -30,7 +30,6 @@ Pointer = function(hudLayer, pickType, pointerData) { ignoreRayIntersection: true, // always ignore this drawInFront: !hudLayer, // Even when burried inside of something, show it. drawHUDLayer: hudLayer, - parentID: MyAvatar.SELF_ID }; this.halfEnd = { type: "sphere", @@ -53,7 +52,6 @@ Pointer = function(hudLayer, pickType, pointerData) { ignoreRayIntersection: true, // always ignore this drawInFront: !hudLayer, // Even when burried inside of something, show it. drawHUDLayer: hudLayer, - parentID: MyAvatar.SELF_ID }; this.fullEnd = { type: "sphere", @@ -76,7 +74,6 @@ Pointer = function(hudLayer, pickType, pointerData) { ignoreRayIntersection: true, // always ignore this drawInFront: !hudLayer, // Even when burried inside of something, show it. drawHUDLayer: hudLayer, - parentID: MyAvatar.SELF_ID }; this.renderStates = [ From 7c21db93a3c061b93c2cee6118baf87db2837be3 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 8 Feb 2018 17:08:51 -0800 Subject: [PATCH 012/260] fixing case when grabbed target is destroyed --- .../controllerModules/farActionGrabEntity.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 32bf7316a9..b72a38f986 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -208,7 +208,7 @@ Script.include("/~/system/libraries/Xform.js"); var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); - var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, ["position"]); + var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); var now = Date.now(); var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds this.currentObjectTime = now; @@ -369,6 +369,14 @@ Script.include("/~/system/libraries/Xform.js"); } }; + this.targetIsNull = function() { + var properties = Entities.getEntityProperties(this.grabbedThingID); + if (Object.keys(properties).length === 0 && this.distanceHolding) { + return true; + } + return false; + } + this.isReady = function (controllerData) { if (HMD.active) { if (this.notPointingAtEntity(controllerData)) { @@ -391,7 +399,7 @@ Script.include("/~/system/libraries/Xform.js"); this.run = function (controllerData) { if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || - this.notPointingAtEntity(controllerData)) { + this.notPointingAtEntity(controllerData) || this.targetIsNull()) { this.endNearGrabAction(); return makeRunningValues(false, [], []); } From 6471780e210e52cac921c02cdc1bbafcde4e8575 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 8 Feb 2018 17:17:04 -0800 Subject: [PATCH 013/260] allow overlay children to follow avatar from one domain to another --- interface/src/ui/overlays/Base3DOverlay.cpp | 2 ++ interface/src/ui/overlays/Line3DOverlay.cpp | 2 +- libraries/avatars/src/AvatarData.h | 6 +++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 23e09fe5ca..ff5a202910 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -181,6 +181,8 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { if (properties["parentID"].isValid()) { setParentID(QUuid(properties["parentID"].toString())); + bool success; + getParentPointer(success); // call this to hook-up the parent's back-pointers to its child overlays needRenderItemUpdate = true; } if (properties["parentJointIndex"].isValid()) { diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index 7200abf74e..c2e5ad1fb4 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -63,7 +63,7 @@ glm::vec3 Line3DOverlay::getEnd() const { localEnd = getLocalEnd(); worldEnd = localToWorld(localEnd, getParentID(), getParentJointIndex(), getScalesWithParent(), success); if (!success) { - qDebug() << "Line3DOverlay::getEnd failed"; + qDebug() << "Line3DOverlay::getEnd failed, parentID = " << getParentID(); } return worldEnd; } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index a363fb6d15..f24bd51bde 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -707,7 +707,11 @@ public slots: void setJointMappingsFromNetworkReply(); void setSessionUUID(const QUuid& sessionUUID) { if (sessionUUID != getID()) { - setID(sessionUUID); + if (sessionUUID == QUuid()) { + setID(AVATAR_SELF_ID); + } else { + setID(sessionUUID); + } emit sessionUUIDChanged(); } } From a08770c816e46a2d0c2f13e1c4c92076ac130b91 Mon Sep 17 00:00:00 2001 From: humbletim Date: Fri, 9 Feb 2018 02:14:32 -0500 Subject: [PATCH 014/260] cleanup --- libraries/fbx/src/OBJWriter.cpp | 6 +- .../graphics-scripting/BufferViewHelpers.cpp | 55 ++ .../graphics-scripting/BufferViewHelpers.h | 9 +- .../ModelScriptingInterface.cpp | 641 ++++-------------- .../ModelScriptingInterface.h | 16 +- .../src/graphics-scripting/ScriptableMesh.cpp | 403 ++++++++++- .../src/graphics-scripting/ScriptableMesh.h | 123 ++-- .../src/graphics-scripting/ScriptableModel.h | 67 +- libraries/render-utils/src/Model.cpp | 58 +- 9 files changed, 722 insertions(+), 656 deletions(-) diff --git a/libraries/fbx/src/OBJWriter.cpp b/libraries/fbx/src/OBJWriter.cpp index 37bced8458..5307f49f36 100644 --- a/libraries/fbx/src/OBJWriter.cpp +++ b/libraries/fbx/src/OBJWriter.cpp @@ -15,6 +15,9 @@ #include "OBJWriter.h" #include "ModelFormatLogging.h" +// FIXME: should this live in shared? (it depends on gpu/) +#include <../graphics-scripting/src/graphics-scripting/BufferViewHelpers.h> + static QString formatFloat(double n) { // limit precision to 6, but don't output trailing zeros. QString s = QString::number(n, 'f', 6); @@ -91,7 +94,8 @@ bool writeOBJToTextStream(QTextStream& out, QList meshes) { 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); + glm::vec3 normal = glmVecFromVariant(bufferViewElementToVariant(normalsBufferView, i)); + //glm::vec3 normal = normalsBufferView.get(i); out << "vn "; out << formatFloat(normal[0]) << " "; out << formatFloat(normal[1]) << " "; diff --git a/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.cpp b/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.cpp index e865ed0e5a..d83322f360 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.cpp @@ -8,9 +8,15 @@ #include #include +namespace glm { + using hvec2 = glm::tvec2; + using hvec4 = glm::tvec4; +} +//#define DEBUG_BUFFERVIEW_SCRIPTING #ifdef DEBUG_BUFFERVIEW_SCRIPTING #include "DebugNames.h" + QLoggingCategory bufferview_helpers{"hifi.bufferview"}; #endif namespace { @@ -61,6 +67,9 @@ bool bufferViewElementFromVariant(const gpu::BufferView& view, quint32 index, co const auto dataType = element.getType(); const auto byteLength = element.getSize(); const auto BYTES_PER_ELEMENT = byteLength / vecN; +#ifdef DEBUG_BUFFERVIEW_SCRIPTING + qCDebug(bufferview_helpers) << "bufferViewElementFromVariant" << index << DebugNames::stringFrom(dataType) << BYTES_PER_ELEMENT << vecN; +#endif if (BYTES_PER_ELEMENT == 1) { switch(vecN) { case 2: setBufferViewElement(view, index, v); return true; @@ -71,16 +80,25 @@ bool bufferViewElementFromVariant(const gpu::BufferView& view, quint32 index, co glm::uint32 unused; packNormalAndTangent(glmVecFromVariant(v), glm::vec3(), rawColor, unused); view.edit(index) = rawColor; + return true; } else if (element == gpu::Element::VEC4F_NORMALIZED_XYZ10W2) { glm::uint32 packedNormal;// = glm::packSnorm3x10_1x2(glm::vec4(glmVecFromVariant(v), 0.0f)); glm::uint32 unused; packNormalAndTangent(glm::vec3(), glmVecFromVariant(v), unused, packedNormal); view.edit(index) = packedNormal; + return true; } setBufferViewElement(view, index, v); return true; } } } else if (BYTES_PER_ELEMENT == 2) { + if (dataType == gpu::HALF) { + switch(vecN) { + case 2: view.edit(index) = glm::packSnorm2x8(glmVecFromVariant(v)); return true; + case 4: view.edit(index) = glm::packSnorm4x8(glmVecFromVariant(v)); return true; + default: return false; + } + } switch(vecN) { case 2: setBufferViewElement(view, index, v); return true; case 3: setBufferViewElement(view, index, v); return true; @@ -112,6 +130,9 @@ QVariant bufferViewElementToVariant(const gpu::BufferView& view, quint32 index, const auto BYTES_PER_ELEMENT = byteLength / vecN; Q_ASSERT(index < view.getNumElements()); Q_ASSERT(index * vecN * BYTES_PER_ELEMENT < (view._size - vecN * BYTES_PER_ELEMENT)); +#ifdef DEBUG_BUFFERVIEW_SCRIPTING + qCDebug(bufferview_helpers) << "bufferViewElementToVariant" << index << DebugNames::stringFrom(dataType) << BYTES_PER_ELEMENT << vecN; +#endif if (BYTES_PER_ELEMENT == 1) { switch(vecN) { case 2: return getBufferViewElement(view, index, asArray); @@ -129,6 +150,12 @@ QVariant bufferViewElementToVariant(const gpu::BufferView& view, quint32 index, } } } else if (BYTES_PER_ELEMENT == 2) { + if (dataType == gpu::HALF) { + switch(vecN) { + case 2: return glmVecToVariant(glm::vec2(glm::unpackSnorm2x8(view.get(index)))); + case 4: return glmVecToVariant(glm::vec4(glm::unpackSnorm4x8(view.get(index)))); + } + } switch(vecN) { case 2: return getBufferViewElement(view, index, asArray); case 3: return getBufferViewElement(view, index, asArray); @@ -193,3 +220,31 @@ const T glmVecFromVariant(const QVariant& v) { return result; } +template +gpu::BufferView bufferViewFromVector(QVector elements, 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(QVector elements, gpu::Element elementType) { return bufferViewFromVector(elements, elementType); } +template<> gpu::BufferView bufferViewFromVector(QVector elements, gpu::Element elementType) { return bufferViewFromVector(elements, elementType); } + +gpu::BufferView cloneBufferView(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 resizedBufferView(const gpu::BufferView& input, quint32 numElements) { + auto effectiveSize = input._buffer->getSize() / input.getNumElements(); + qDebug() << "resize input" << input.getNumElements() << input._buffer->getSize() << "effectiveSize" << effectiveSize; + auto vsize = input._element.getSize() * numElements; + gpu::Byte *data = new gpu::Byte[vsize]; + memset(data, 0, vsize); + auto buffer = new gpu::Buffer(vsize, (gpu::Byte*)data); + delete[] data; + auto output = gpu::BufferView(buffer, input._element); + qDebug() << "resized output" << output.getNumElements() << output._buffer->getSize(); + return output; +} diff --git a/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.h b/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.h index 0fe2602f6c..d0d42ca419 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.h +++ b/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.h @@ -8,11 +8,18 @@ #include -namespace gpu { class BufferView; } +namespace gpu { + class BufferView; + class Element; +} template QVariant glmVecToVariant(const T& v, bool asArray = false); template const T glmVecFromVariant(const QVariant& v); QVariant bufferViewElementToVariant(const gpu::BufferView& view, quint32 index, bool asArray = false, const char* hint = ""); bool bufferViewElementFromVariant(const gpu::BufferView& view, quint32 index, const QVariant& v); +template gpu::BufferView bufferViewFromVector(QVector elements, gpu::Element elementType); + +gpu::BufferView cloneBufferView(const gpu::BufferView& input); +gpu::BufferView resizedBufferView(const gpu::BufferView& input, quint32 numElements); diff --git a/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.cpp index 68a00bc02c..ab85fb8265 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.cpp @@ -17,27 +17,16 @@ #include "BaseScriptEngine.h" #include "ScriptEngineLogging.h" #include "OBJWriter.h" -#include "OBJReader.h" -//#include "ui/overlays/Base3DOverlay.h" -//#include "EntityTreeRenderer.h" -//#include "avatar/AvatarManager.h" -//#include "RenderableEntityItem.h" -#include #include - #include - #include - #include + #include "BufferViewScripting.h" - #include "ScriptableMesh.h" -using ScriptableMesh = scriptable::ScriptableMesh; - #include "ModelScriptingInterface.moc" namespace { @@ -50,13 +39,56 @@ ModelScriptingInterface::ModelScriptingInterface(QObject* parent) : QObject(pare } } +void ModelScriptingInterface::getMeshes(QUuid uuid, QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); + Q_ASSERT(handler.engine() == this->engine()); + QPointer engine = dynamic_cast(handler.engine()); + + scriptable::ScriptableModel* meshes{ nullptr }; + bool success = false; + QString error; + + auto appProvider = DependencyManager::get(); + qDebug() << "appProvider" << appProvider.data(); + scriptable::ModelProviderPointer provider = appProvider ? appProvider->lookupModelProvider(uuid) : nullptr; + QString providerType = provider ? provider->metadata.value("providerType").toString() : QString(); + if (providerType.isEmpty()) { + providerType = "unknown"; + } + if (provider) { + qCDebug(model_scripting) << "fetching meshes from " << providerType << "..."; + auto scriptableMeshes = provider->getScriptableModel(&success); + qCDebug(model_scripting) << "//fetched meshes from " << providerType << "success:" <objectName().isEmpty()) { + meshes->setObjectName(providerType+"::meshes"); + } + if (meshes->objectID.isNull()) { + meshes->objectID = uuid.toString(); + } + meshes->metadata["provider"] = provider->metadata; + } + } + if (!success) { + error = QString("failed to get meshes from %1 provider for uuid %2").arg(providerType).arg(uuid.toString()); + } + + if (!error.isEmpty()) { + qCWarning(model_scripting) << "ModelScriptingInterface::getMeshes ERROR" << error; + callScopedHandlerObject(handler, engine->makeError(error), QScriptValue::NullValue); + } else { + callScopedHandlerObject(handler, QScriptValue::NullValue, engine->newQObject(meshes, QScriptEngine::ScriptOwnership)); + } +} + QString ModelScriptingInterface::meshToOBJ(const scriptable::ScriptableModel& _in) { - const auto& in = _in.getMeshes(); + const auto& in = _in.getConstMeshes(); qCDebug(model_scripting) << "meshToOBJ" << in.size(); if (in.size()) { QList meshes; - foreach (const auto meshProxy, in) { - qCDebug(model_scripting) << "meshToOBJ" << meshProxy.get(); + foreach (auto meshProxy, in) { + qCDebug(model_scripting) << "meshToOBJ" << meshProxy; if (meshProxy) { meshes.append(getMeshPointer(meshProxy)); } @@ -77,7 +109,7 @@ QScriptValue ModelScriptingInterface::appendMeshes(scriptable::ScriptableModel _ size_t totalColorCount { 0 }; size_t totalNormalCount { 0 }; size_t totalIndexCount { 0 }; - foreach (const scriptable::ScriptableMeshPointer meshProxy, in) { + foreach (auto& meshProxy, in) { scriptable::MeshPointer mesh = getMeshPointer(meshProxy); totalVertexCount += mesh->getNumVertices(); @@ -113,7 +145,7 @@ QScriptValue ModelScriptingInterface::appendMeshes(scriptable::ScriptableModel _ uint32_t indexStartOffset { 0 }; - foreach (const scriptable::ScriptableMeshPointer meshProxy, in) { + foreach (const auto& meshProxy, in) { scriptable::MeshPointer mesh = getMeshPointer(meshProxy); mesh->forEach( [&](glm::vec3 position){ @@ -175,8 +207,7 @@ QScriptValue ModelScriptingInterface::appendMeshes(scriptable::ScriptableModel _ (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); - scriptable::ScriptableMeshPointer resultProxy = scriptable::ScriptableMeshPointer(new ScriptableMesh(nullptr, result)); - return engine()->toScriptValue(result); + return engine()->toScriptValue(scriptable::ScriptableMeshPointer(new scriptable::ScriptableMesh(nullptr, result))); } QScriptValue ModelScriptingInterface::transformMesh(scriptable::ScriptableMeshPointer meshProxy, glm::mat4 transform) { @@ -189,7 +220,7 @@ QScriptValue ModelScriptingInterface::transformMesh(scriptable::ScriptableMeshPo [&](glm::vec3 color){ return color; }, [&](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, [&](uint32_t index){ return index; }); - scriptable::ScriptableMeshPointer resultProxy = scriptable::ScriptableMeshPointer(new ScriptableMesh(nullptr, result)); + scriptable::ScriptableMeshPointer resultProxy = scriptable::ScriptableMeshPointer(new scriptable::ScriptableMesh(nullptr, result)); return engine()->toScriptValue(resultProxy); } @@ -201,8 +232,11 @@ QScriptValue ModelScriptingInterface::getVertexCount(scriptable::ScriptableMeshP return (uint32_t)mesh->getNumVertices(); } -QScriptValue ModelScriptingInterface::getVertex(scriptable::ScriptableMeshPointer meshProxy, mesh::uint32 vertexIndex) { +QScriptValue ModelScriptingInterface::getVertex(scriptable::ScriptableMeshPointer meshProxy, quint32 vertexIndex) { auto mesh = getMeshPointer(meshProxy); + if (!mesh) { + return QScriptValue(); + } const gpu::BufferView& vertexBufferView = mesh->getVertexBuffer(); auto numVertices = mesh->getNumVertices(); @@ -266,480 +300,46 @@ QScriptValue ModelScriptingInterface::newMesh(const QVector& vertices - scriptable::ScriptableMeshPointer meshProxy = scriptable::ScriptableMeshPointer(new ScriptableMesh(nullptr, mesh)); + scriptable::ScriptableMeshPointer meshProxy = scriptable::ScriptableMeshPointer(new scriptable::ScriptableMesh(nullptr, mesh)); return engine()->toScriptValue(meshProxy); } -QScriptValue ModelScriptingInterface::mapAttributeValues( - QScriptValue _in, - QScriptValue scopeOrCallback, - QScriptValue methodOrName - ) { - qCInfo(model_scripting) << "mapAttributeValues" << _in.toVariant().typeName() << _in.toVariant().toString() << _in.toQObject(); - auto in = qscriptvalue_cast(_in).getMeshes(); - if (in.size()) { - foreach (scriptable::ScriptableMeshPointer meshProxy, in) { - mapMeshAttributeValues(meshProxy, scopeOrCallback, methodOrName); - } - return thisObject(); - } else if (auto meshProxy = qobject_cast(_in.toQObject())) { - return mapMeshAttributeValues(meshProxy->shared_from_this(), scopeOrCallback, methodOrName); - } else { - context()->throwError("invalid ModelProxy || MeshProxyPointer"); - } - return false; -} - - -QScriptValue ModelScriptingInterface::unrollVertices(scriptable::ScriptableMeshPointer meshProxy, bool recalcNormals) { - auto mesh = getMeshPointer(meshProxy); - qCInfo(model_scripting) << "ModelScriptingInterface::unrollVertices" << !!mesh<< !!meshProxy; - if (!mesh) { - return QScriptValue(); - } - - auto positions = mesh->getVertexBuffer(); - auto indices = mesh->getIndexBuffer(); - quint32 numPoints = (quint32)indices.getNumElements(); - auto buffer = new gpu::Buffer(); - buffer->resize(numPoints * sizeof(uint32_t)); - auto newindices = gpu::BufferView(buffer, { gpu::SCALAR, gpu::UINT32, gpu::INDEX }); - qCInfo(model_scripting) << "ModelScriptingInterface::unrollVertices numPoints" << numPoints; - auto attributeViews = ScriptableMesh::gatherBufferViews(mesh); - for (const auto& a : attributeViews) { - auto& view = a.second; - auto sz = view._element.getSize(); - auto buffer = new gpu::Buffer(); - buffer->resize(numPoints * sz); - auto points = gpu::BufferView(buffer, view._element); - auto src = (uint8_t*)view._buffer->getData(); - auto dest = (uint8_t*)points._buffer->getData(); - auto slot = ScriptableMesh::ATTRIBUTES[a.first]; - qCInfo(model_scripting) << "ModelScriptingInterface::unrollVertices buffer" << a.first; - qCInfo(model_scripting) << "ModelScriptingInterface::unrollVertices source" << view.getNumElements(); - qCInfo(model_scripting) << "ModelScriptingInterface::unrollVertices dest" << points.getNumElements(); - qCInfo(model_scripting) << "ModelScriptingInterface::unrollVertices sz" << sz << src << dest << slot; - auto esize = indices._element.getSize(); - const char* hint= a.first.toStdString().c_str(); - for(quint32 i = 0; i < numPoints; i++) { - quint32 index = esize == 4 ? indices.get(i) : indices.get(i); - newindices.edit(i) = i; - bufferViewElementFromVariant( - points, i, - bufferViewElementToVariant(view, index, false, hint) - ); - } - if (slot == gpu::Stream::POSITION) { - mesh->setVertexBuffer(points); - } else { - mesh->addAttribute(slot, points); - } - } - mesh->setIndexBuffer(newindices); - if (recalcNormals) { - recalculateNormals(meshProxy); - } - return true; -} - namespace { - template - gpu::BufferView bufferViewFromVector(QVector elements, gpu::Element elementType) { - auto vertexBuffer = std::make_shared( - elements.size() * sizeof(T), - (gpu::Byte*)elements.data() - ); - return { vertexBuffer, 0, vertexBuffer->getSize(),sizeof(T), elementType }; - } - - gpu::BufferView cloneBufferView(const gpu::BufferView& input) { - //qCInfo(model_scripting) << "input" << input.getNumElements() << input._buffer->getSize(); - auto output = gpu::BufferView( - std::make_shared(input._buffer->getSize(), input._buffer->getData()), - input._offset, - input._size, - input._stride, - input._element - ); - //qCInfo(model_scripting) << "after" << output.getNumElements() << output._buffer->getSize(); - return output; - } - - gpu::BufferView resizedBufferView(const gpu::BufferView& input, quint32 numElements) { - auto effectiveSize = input._buffer->getSize() / input.getNumElements(); - qCInfo(model_scripting) << "resize input" << input.getNumElements() << input._buffer->getSize() << "effectiveSize" << effectiveSize; - auto vsize = input._element.getSize() * numElements; - gpu::Byte *data = new gpu::Byte[vsize]; - memset(data, 0, vsize); - auto buffer = new gpu::Buffer(vsize, (gpu::Byte*)data); - delete[] data; - auto output = gpu::BufferView(buffer, input._element); - qCInfo(model_scripting) << "resized output" << output.getNumElements() << output._buffer->getSize(); - return output; - } -} - -bool ModelScriptingInterface::replaceMeshData(scriptable::ScriptableMeshPointer dest, scriptable::ScriptableMeshPointer src, const QVector& attributeNames) { - auto target = getMeshPointer(dest); - auto source = getMeshPointer(src); - if (!target || !source) { - context()->throwError("ModelScriptingInterface::replaceMeshData -- expected dest and src to be valid mesh proxy pointers"); - return false; - } - - QVector attributes = attributeNames.isEmpty() ? src->getAttributeNames() : attributeNames; - - //qCInfo(model_scripting) << "ModelScriptingInterface::replaceMeshData -- source:" << source->displayName << "target:" << 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 = ScriptableMesh::gatherBufferViews(target); - for (const auto& a : attributeViews) { - auto slot = ScriptableMesh::ATTRIBUTES[a.first]; - if (!attributes.contains(a.first)) { - //qCInfo(model_scripting) << "ModelScriptingInterface::replaceMeshData -- pruning target attribute" << a.first << slot; - target->removeAttribute(slot); - } + QScriptValue meshPointerToScriptValue(QScriptEngine* engine, scriptable::ScriptableMeshPointer const &in) { + if (!in) { + return QScriptValue::NullValue; } + return engine->newQObject(in, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); } - target->setVertexBuffer(cloneBufferView(source->getVertexBuffer())); - target->setIndexBuffer(cloneBufferView(source->getIndexBuffer())); - target->setPartBuffer(cloneBufferView(source->getPartBuffer())); - - for (const auto& a : attributes) { - auto slot = ScriptableMesh::ATTRIBUTES[a]; - if (slot == gpu::Stream::POSITION) { - continue; - } - // auto& before = target->getAttributeBuffer(slot); - auto& input = source->getAttributeBuffer(slot); - if (input.getNumElements() == 0) { - //qCInfo(model_scripting) << "ModelScriptingInterface::replaceMeshData buffer is empty -- pruning" << a << slot; - target->removeAttribute(slot); - } else { - // if (before.getNumElements() == 0) { - // qCInfo(model_scripting) << "ModelScriptingInterface::replaceMeshData target buffer is empty -- adding" << a << slot; - // } else { - // qCInfo(model_scripting) << "ModelScriptingInterface::replaceMeshData target buffer exists -- updating" << a << slot; - // } - target->addAttribute(slot, cloneBufferView(input)); - } - // auto& after = target->getAttributeBuffer(slot); - // qCInfo(model_scripting) << "ModelScriptingInterface::replaceMeshData" << a << slot << before.getNumElements() << " -> " << after.getNumElements(); - } - - - return true; -} - -bool ModelScriptingInterface::dedupeVertices(scriptable::ScriptableMeshPointer meshProxy, float epsilon) { - auto mesh = getMeshPointer(meshProxy); - 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 (quint32 i = 0; i < numPositions; i++) { - const quint32 numUnique = uniqueVerts.size(); - const auto& position = positions.get(i); - bool unique = true; - for (quint32 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(model_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 (quint32 i = 0; i < numIndices; i++) { - quint32 index = esize == 4 ? indices.get(i) : indices.get(i); - if (remapIndices.contains(index)) { - //qCInfo(model_scripting) << i << index << "->" << remapIndices[index]; - newIndices << remapIndices[index]; - } else { - qCInfo(model_scripting) << i << index << "!remapIndices[index]"; - } - } - - mesh->setIndexBuffer(bufferViewFromVector(newIndices, { gpu::SCALAR, gpu::UINT32, gpu::INDEX })); - mesh->setVertexBuffer(bufferViewFromVector(uniqueVerts, { gpu::VEC3, gpu::FLOAT, gpu::XYZ })); - - auto attributeViews = ScriptableMesh::gatherBufferViews(mesh); - quint32 numUniqueVerts = uniqueVerts.size(); - for (const auto& a : attributeViews) { - auto& view = a.second; - auto slot = ScriptableMesh::ATTRIBUTES[a.first]; - if (slot == gpu::Stream::POSITION) { - continue; - } - qCInfo(model_scripting) << "ModelScriptingInterface::dedupeVertices" << a.first << slot << view.getNumElements(); - auto newView = resizedBufferView(view, numUniqueVerts); - qCInfo(model_scripting) << a.first << "before: #" << view.getNumElements() << "after: #" << newView.getNumElements(); - quint32 numElements = (quint32)view.getNumElements(); - for (quint32 i = 0; i < numElements; i++) { - quint32 fromVertexIndex = i; - quint32 toVertexIndex = remapIndices.contains(fromVertexIndex) ? remapIndices[fromVertexIndex] : fromVertexIndex; - bufferViewElementFromVariant( - newView, toVertexIndex, - bufferViewElementToVariant(view, fromVertexIndex, false, "dedupe") - ); - } - mesh->addAttribute(slot, newView); - } - return true; -} - -QScriptValue ModelScriptingInterface::cloneMesh(scriptable::ScriptableMeshPointer meshProxy, bool recalcNormals) { - auto mesh = getMeshPointer(meshProxy); - if (!mesh) { - return QScriptValue::NullValue; - } - graphics::MeshPointer clone(new graphics::Mesh()); - clone->displayName = mesh->displayName + "-clone"; - qCInfo(model_scripting) << "ModelScriptingInterface::cloneMesh" << !!mesh<< !!meshProxy; - if (!mesh) { - return QScriptValue::NullValue; - } - - clone->setIndexBuffer(cloneBufferView(mesh->getIndexBuffer())); - clone->setPartBuffer(cloneBufferView(mesh->getPartBuffer())); - auto attributeViews = ScriptableMesh::gatherBufferViews(mesh); - for (const auto& a : attributeViews) { - auto& view = a.second; - auto slot = ScriptableMesh::ATTRIBUTES[a.first]; - qCInfo(model_scripting) << "ModelScriptingInterface::cloneVertices buffer" << a.first << slot; - auto points = cloneBufferView(view); - qCInfo(model_scripting) << "ModelScriptingInterface::cloneVertices source" << view.getNumElements(); - qCInfo(model_scripting) << "ModelScriptingInterface::cloneVertices dest" << points.getNumElements(); - if (slot == gpu::Stream::POSITION) { - clone->setVertexBuffer(points); - } else { - clone->addAttribute(slot, points); - } - } - - auto result = scriptable::ScriptableMeshPointer(new ScriptableMesh(nullptr, clone)); - if (recalcNormals) { - recalculateNormals(result); - } - return engine()->toScriptValue(result); -} - -bool ModelScriptingInterface::recalculateNormals(scriptable::ScriptableMeshPointer meshProxy) { - qCInfo(model_scripting) << "Recalculating normals" << !!meshProxy; - auto mesh = getMeshPointer(meshProxy); - if (!mesh) { - return false; - } - ScriptableMesh::gatherBufferViews(mesh, { "normal", "color" }); // ensures #normals >= #positions - auto normals = mesh->getAttributeBuffer(gpu::Stream::NORMAL); - auto verts = mesh->getVertexBuffer(); - auto indices = mesh->getIndexBuffer(); - auto esize = indices._element.getSize(); - auto numPoints = indices.getNumElements(); - const auto TRIANGLE = 3; - quint32 numFaces = (quint32)numPoints / TRIANGLE; - //QVector faces; - QVector faceNormals; - QMap> vertexToFaces; - //faces.resize(numFaces); - faceNormals.resize(numFaces); - auto numNormals = normals.getNumElements(); - qCInfo(model_scripting) << QString("numFaces: %1, numNormals: %2, numPoints: %3").arg(numFaces).arg(numNormals).arg(numPoints); - if (normals.getNumElements() != verts.getNumElements()) { - return false; - } - for (quint32 i = 0; i < numFaces; i++) { - quint32 I = TRIANGLE * i; - quint32 i0 = esize == 4 ? indices.get(I+0) : indices.get(I+0); - quint32 i1 = esize == 4 ? indices.get(I+1) : indices.get(I+1); - quint32 i2 = esize == 4 ? indices.get(I+2) : indices.get(I+2); - - Triangle face = { - verts.get(i1), - verts.get(i2), - verts.get(i0) - }; - faceNormals[i] = face.getNormal(); - if (glm::isnan(faceNormals[i].x)) { - qCInfo(model_scripting) << i << i0 << i1 << i2 << vec3toVariant(face.v0) << vec3toVariant(face.v1) << vec3toVariant(face.v2); - break; - } - vertexToFaces[glm::to_string(face.v0).c_str()] << i; - vertexToFaces[glm::to_string(face.v1).c_str()] << i; - vertexToFaces[glm::to_string(face.v2).c_str()] << i; - } - for (quint32 j = 0; j < numNormals; j++) { - //auto v = verts.get(j); - glm::vec3 normal { 0.0f, 0.0f, 0.0f }; - QString key { glm::to_string(verts.get(j)).c_str() }; - const auto& faces = vertexToFaces.value(key); - if (faces.size()) { - for (const auto i : faces) { - normal += faceNormals[i]; - } - normal *= 1.0f / (float)faces.size(); - } else { - static int logged = 0; - if (logged++ < 10) { - qCInfo(model_scripting) << "no faces for key!?" << key; - } - normal = verts.get(j); - } - if (glm::isnan(normal.x)) { - static int logged = 0; - if (logged++ < 10) { - qCInfo(model_scripting) << "isnan(normal.x)" << j << vec3toVariant(normal); - } - break; - } - normals.edit(j) = glm::normalize(normal); - } - return true; -} - -QScriptValue ModelScriptingInterface::mapMeshAttributeValues( - scriptable::ScriptableMeshPointer meshProxy, QScriptValue scopeOrCallback, QScriptValue methodOrName -) { - auto mesh = getMeshPointer(meshProxy); - if (!mesh) { - return false; - } - auto scopedHandler = makeScopedHandlerObject(scopeOrCallback, methodOrName); - - // input buffers - gpu::BufferView positions = mesh->getVertexBuffer(); - - const auto nPositions = positions.getNumElements(); - - // 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(); // cache value to avoid resolving each iteration - auto meshPart = js->toScriptValue(meshProxy); - - auto obj = js->newObject(); - auto attributeViews = ScriptableMesh::gatherBufferViews(mesh, { "normal", "color" }); - for (uint32_t i=0; i < nPositions; i++) { - for (const auto& a : attributeViews) { - bool asArray = a.second._element.getType() != gpu::FLOAT; - obj.setProperty(a.first, bufferViewElementToScriptValue(js, a.second, i, asArray, a.first.toStdString().c_str())); - } - auto result = callback.call(scope, { obj, i, meshPart }); - if (js->hasUncaughtException()) { - context()->throwValue(js->uncaughtException()); - return false; - } - - if (result.isBool() && !result.toBool()) { - // bail without modifying data if user explicitly returns false - continue; - } - 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); - auto& view = a.second; - if (attribute.isValid()) { - bufferViewElementFromScriptValue(attribute, view, i); - } - } - } - return thisObject(); -} - -void ModelScriptingInterface::getMeshes(QUuid uuid, QScriptValue scopeOrCallback, QScriptValue methodOrName) { - auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); - Q_ASSERT(handler.engine() == this->engine()); - QPointer engine = dynamic_cast(handler.engine()); - - scriptable::ScriptableModel meshes; - bool success = false; - QString error; - - auto appProvider = DependencyManager::get(); - qDebug() << "appProvider" << appProvider.data(); - scriptable::ModelProviderPointer provider = appProvider ? appProvider->lookupModelProvider(uuid) : nullptr; - QString providerType = provider ? provider->metadata.value("providerType").toString() : QString(); - if (providerType.isEmpty()) { - providerType = "unknown"; - } - if (provider) { - qCDebug(model_scripting) << "fetching meshes from " << providerType << "..."; - auto scriptableMeshes = provider->getScriptableModel(&success); - qCDebug(model_scripting) << "//fetched meshes from " << providerType << "success:" <makeError(error), QScriptValue::NullValue); - } else { - callScopedHandlerObject(handler, QScriptValue::NullValue, engine->toScriptValue(meshes)); - } -} - -namespace { - QScriptValue meshToScriptValue(QScriptEngine* engine, scriptable::ScriptableMeshPointer const &in) { - return engine->newQObject(in.get(), QScriptEngine::QtOwnership, - QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects - ); - } - - void meshFromScriptValue(const QScriptValue& value, scriptable::ScriptableMeshPointer &out) { + void meshPointerFromScriptValue(const QScriptValue& value, scriptable::ScriptableMeshPointer &out) { auto obj = value.toQObject(); - //qDebug() << "meshFromScriptValue" << obj; + qDebug() << "meshPointerFromScriptValue" << obj; if (auto tmp = qobject_cast(obj)) { - out = tmp->shared_from_this(); + out = tmp; } // FIXME: Why does above cast not work on Win32!? if (!out) { - auto smp = static_cast(obj); - //qDebug() << "meshFromScriptValue2" << smp; - out = smp->shared_from_this(); + if (auto smp = static_cast(obj)) { + qDebug() << "meshPointerFromScriptValue2" << smp; + out = smp; + } } } - QScriptValue meshesToScriptValue(QScriptEngine* engine, const scriptable::ScriptableModelPointer &in) { - // QScriptValueList result; - QScriptValue result = engine->newArray(); - int i = 0; - foreach(scriptable::ScriptableMeshPointer const meshProxy, in->getMeshes()) { - result.setProperty(i++, meshToScriptValue(engine, meshProxy)); - } - return result; + QScriptValue modelPointerToScriptValue(QScriptEngine* engine, const scriptable::ScriptableModelPointer &in) { + return engine->newQObject(in, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); + // QScriptValue result = engine->newArray(); + // int i = 0; + // foreach(auto& mesh, in->getMeshes()) { + // result.setProperty(i++, meshPointerToScriptValue(engine, mesh)); + // } + // return result; } - void meshesFromScriptValue(const QScriptValue& value, scriptable::ScriptableModelPointer &out) { + void modelPointerFromScriptValue(const QScriptValue& value, scriptable::ScriptableModelPointer &out) { const auto length = value.property("length").toInt32(); - qCDebug(model_scripting) << "in meshesFromScriptValue, length =" << length; + qCDebug(model_scripting) << "in modelPointerFromScriptValue, length =" << length; for (int i = 0; i < length; i++) { if (const auto meshProxy = qobject_cast(value.property(i).toQObject())) { out->meshes.append(meshProxy->getMeshPointer()); @@ -749,79 +349,64 @@ namespace { } } - void modelProxyFromScriptValue(const QScriptValue& object, scriptable::ScriptableModel &meshes) { - auto meshesProperty = object.property("meshes"); - if (meshesProperty.property("length").toInt32() > 0) { - //meshes._meshes = qobject_cast(meshesProperty.toQObject()); - // qDebug() << "modelProxyFromScriptValue" << meshesProperty.property("length").toInt32() << meshesProperty.toVariant().typeName(); - qScriptValueToSequence(meshesProperty, meshes.meshes); - } else if (auto mesh = qobject_cast(object.toQObject())) { - meshes.meshes << mesh->getMeshPointer(); - } else { - qDebug() << "modelProxyFromScriptValue -- unrecognized input" << object.toVariant().toString(); - } + // FIXME: MESHFACES: + // QScriptValue meshFaceToScriptValue(QScriptEngine* engine, const mesh::MeshFace &meshFace) { + // QScriptValue obj = engine->newObject(); + // obj.setProperty("vertices", qVectorIntToScriptValue(engine, meshFace.vertexIndices)); + // return obj; + // } + // void meshFaceFromScriptValue(const QScriptValue &object, mesh::MeshFace& meshFaceResult) { + // qScriptValueToSequence(object.property("vertices"), meshFaceResult.vertexIndices); + // } + // QScriptValue qVectorMeshFaceToScriptValue(QScriptEngine* engine, const QVector& vector) { + // return qScriptValueFromSequence(engine, vector); + // } + // void qVectorMeshFaceFromScriptValue(const QScriptValue& array, QVector& result) { + // qScriptValueToSequence(array, result); + // } - meshes.metadata = object.property("metadata").toVariant().toMap(); - } - - QScriptValue modelProxyToScriptValue(QScriptEngine* engine, const scriptable::ScriptableModel &in) { - QScriptValue obj = engine->newObject(); - obj.setProperty("meshes", qScriptValueFromSequence(engine, in.meshes)); - obj.setProperty("metadata", engine->toScriptValue(in.metadata)); - return obj; - } - - QScriptValue meshFaceToScriptValue(QScriptEngine* engine, const mesh::MeshFace &meshFace) { - QScriptValue obj = engine->newObject(); - obj.setProperty("vertices", qVectorIntToScriptValue(engine, meshFace.vertexIndices)); - return obj; - } - - void meshFaceFromScriptValue(const QScriptValue &object, mesh::MeshFace& meshFaceResult) { - qScriptValueToSequence(object.property("vertices"), meshFaceResult.vertexIndices); - } - - QScriptValue qVectorMeshFaceToScriptValue(QScriptEngine* engine, const QVector& vector) { + QScriptValue qVectorUInt32ToScriptValue(QScriptEngine* engine, const QVector& vector) { return qScriptValueFromSequence(engine, vector); } - void qVectorMeshFaceFromScriptValue(const QScriptValue& array, QVector& result) { - qScriptValueToSequence(array, result); - } - - QScriptValue qVectorUInt32ToScriptValue(QScriptEngine* engine, const QVector& vector) { - return qScriptValueFromSequence(engine, vector); - } - - void qVectorUInt32FromScriptValue(const QScriptValue& array, QVector& result) { + void qVectorUInt32FromScriptValue(const QScriptValue& array, QVector& result) { qScriptValueToSequence(array, result); } } -int meshUint32 = qRegisterMetaType(); +int meshUint32 = qRegisterMetaType(); namespace mesh { int meshUint32 = qRegisterMetaType(); } -int qVectorMeshUint32 = qRegisterMetaType>(); +int qVectorMeshUint32 = qRegisterMetaType>(); void ModelScriptingInterface::registerMetaTypes(QScriptEngine* engine) { qScriptRegisterSequenceMetaType>(engine); - qScriptRegisterSequenceMetaType(engine); - qScriptRegisterSequenceMetaType>(engine); - qScriptRegisterMetaType(engine, modelProxyToScriptValue, modelProxyFromScriptValue); + qScriptRegisterSequenceMetaType>(engine); qScriptRegisterMetaType(engine, qVectorUInt32ToScriptValue, qVectorUInt32FromScriptValue); - qScriptRegisterMetaType(engine, meshToScriptValue, meshFromScriptValue); - qScriptRegisterMetaType(engine, meshesToScriptValue, meshesFromScriptValue); - qScriptRegisterMetaType(engine, meshFaceToScriptValue, meshFaceFromScriptValue); - qScriptRegisterMetaType(engine, qVectorMeshFaceToScriptValue, qVectorMeshFaceFromScriptValue); + qScriptRegisterMetaType(engine, meshPointerToScriptValue, meshPointerFromScriptValue); + qScriptRegisterMetaType(engine, modelPointerToScriptValue, modelPointerFromScriptValue); + + // FIXME: MESHFACES: remove if MeshFace is not needed anywhere + // qScriptRegisterSequenceMetaType(engine); + // qScriptRegisterMetaType(engine, meshFaceToScriptValue, meshFaceFromScriptValue); + // qScriptRegisterMetaType(engine, qVectorMeshFaceToScriptValue, qVectorMeshFaceFromScriptValue); } +MeshPointer ModelScriptingInterface::getMeshPointer(const scriptable::ScriptableMesh& meshProxy) { + return meshProxy._mesh;//getMeshPointer(&meshProxy); +} +MeshPointer ModelScriptingInterface::getMeshPointer(scriptable::ScriptableMesh& meshProxy) { + return getMeshPointer(&meshProxy); +} MeshPointer ModelScriptingInterface::getMeshPointer(scriptable::ScriptableMeshPointer meshProxy) { MeshPointer result; if (!meshProxy) { if (context()){ context()->throwError("expected meshProxy as first parameter"); + } else { + qDebug() << "expected meshProxy as first parameter"; } return result; } @@ -829,6 +414,8 @@ MeshPointer ModelScriptingInterface::getMeshPointer(scriptable::ScriptableMeshPo if (!mesh) { if (context()) { context()->throwError("expected valid meshProxy as first parameter"); + } else { + qDebug() << "expected valid meshProxy as first parameter"; } return result; } diff --git a/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.h b/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.h index d10fd28170..eac4df3216 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.h +++ b/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.h @@ -38,30 +38,20 @@ public slots: */ void getMeshes(QUuid uuid, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue()); - bool dedupeVertices(scriptable::ScriptableMeshPointer meshProxy, float epsilon = 1e-6); - bool recalculateNormals(scriptable::ScriptableMeshPointer meshProxy); - QScriptValue cloneMesh(scriptable::ScriptableMeshPointer meshProxy, bool recalcNormals = true); - QScriptValue unrollVertices(scriptable::ScriptableMeshPointer meshProxy, bool recalcNormals = true); - QScriptValue mapAttributeValues(QScriptValue in, - QScriptValue scopeOrCallback, - QScriptValue methodOrName = QScriptValue()); - QScriptValue mapMeshAttributeValues(scriptable::ScriptableMeshPointer meshProxy, - QScriptValue scopeOrCallback, - QScriptValue methodOrName = QScriptValue()); - QString meshToOBJ(const scriptable::ScriptableModel& in); - bool replaceMeshData(scriptable::ScriptableMeshPointer dest, scriptable::ScriptableMeshPointer source, const QVector& attributeNames = QVector()); QScriptValue appendMeshes(scriptable::ScriptableModel in); QScriptValue transformMesh(scriptable::ScriptableMeshPointer meshProxy, glm::mat4 transform); QScriptValue newMesh(const QVector& vertices, const QVector& normals, const QVector& faces); QScriptValue getVertexCount(scriptable::ScriptableMeshPointer meshProxy); - QScriptValue getVertex(scriptable::ScriptableMeshPointer meshProxy, mesh::uint32 vertexIndex); + QScriptValue getVertex(scriptable::ScriptableMeshPointer meshProxy, quint32 vertexIndex); private: scriptable::MeshPointer getMeshPointer(scriptable::ScriptableMeshPointer meshProxy); + scriptable::MeshPointer getMeshPointer(scriptable::ScriptableMesh& meshProxy); + scriptable::MeshPointer getMeshPointer(const scriptable::ScriptableMesh& meshProxy); }; diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp index 47d91e9e59..1b16a6d263 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp @@ -14,14 +14,20 @@ #include #include #include +#include #include #include #include +#include #include #include "ScriptableMesh.moc" #include +#include +#include + +#include "OBJWriter.h" QLoggingCategory mesh_logging { "hifi.scripting.mesh" }; @@ -41,10 +47,26 @@ QMap ScriptableMesh::ATTRIBUTES{ {"texcoord4", gpu::Stream::TEXCOORD4 }, }; -QVector scriptable::ScriptableModel::getMeshes() const { + +QString scriptable::ScriptableModel::toString() const { + return QString("[ScriptableModel%1%2]") + .arg(objectID.isNull() ? "" : " objectID="+objectID.toString()) + .arg(objectName().isEmpty() ? "" : " name=" +objectName()); +} + +const QVector scriptable::ScriptableModel::getConstMeshes() const { + QVector out; + for(const auto& mesh : meshes) { + const scriptable::ScriptableMeshPointer m = scriptable::ScriptableMeshPointer(new scriptable::ScriptableMesh(const_cast(this), mesh)); + out << m; + } + return out; +} +QVector scriptable::ScriptableModel::getMeshes() { QVector out; for(auto& mesh : meshes) { - out << scriptable::ScriptableMeshPointer(new ScriptableMesh(std::const_pointer_cast(this->shared_from_this()), mesh)); + scriptable::ScriptableMeshPointer m{new scriptable::ScriptableMesh(this, mesh)}; + out << m; } return out; } @@ -134,15 +156,16 @@ QVariantMap ScriptableMesh::getVertexAttributes(quint32 vertexIndex) const { } bool ScriptableMesh::setVertexAttributes(quint32 vertexIndex, QVariantMap attributes) { - qDebug() << "setVertexAttributes" << vertexIndex << attributes; + //qDebug() << "setVertexAttributes" << vertexIndex << attributes; for (auto& a : gatherBufferViews(getMeshPointer())) { const auto& name = a.first; const auto& value = attributes.value(name); if (value.isValid()) { auto& view = a.second; + //qCDebug(mesh_logging) << "setVertexAttributes" << vertexIndex << name; bufferViewElementFromVariant(view, vertexIndex, value); } else { - qCDebug(mesh_logging) << "setVertexAttributes" << vertexIndex << name; + //qCDebug(mesh_logging) << "(skipping) setVertexAttributes" << vertexIndex << name; } } return true; @@ -357,3 +380,375 @@ std::map ScriptableMesh::gatherBufferViews(scriptable: } return attributeViews; } + +QScriptValue ScriptableModel::mapAttributeValues(QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto context = scopeOrCallback.engine()->currentContext(); + auto _in = context->thisObject(); + qCInfo(mesh_logging) << "mapAttributeValues" << _in.toVariant().typeName() << _in.toVariant().toString() << _in.toQObject(); + auto model = qscriptvalue_cast(_in); + QVector in = model.getMeshes(); + if (in.size()) { + foreach (scriptable::ScriptableMeshPointer meshProxy, in) { + meshProxy->mapAttributeValues(scopeOrCallback, methodOrName); + } + return _in; + } else if (auto meshProxy = qobject_cast(_in.toQObject())) { + return meshProxy->mapAttributeValues(scopeOrCallback, methodOrName); + } else { + context->throwError("invalid ModelProxy || MeshProxyPointer"); + } + return false; +} + + + +QScriptValue ScriptableMesh::mapAttributeValues(QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto mesh = getMeshPointer(); + if (!mesh) { + return false; + } + auto scopedHandler = makeScopedHandlerObject(scopeOrCallback, methodOrName); + + // input buffers + gpu::BufferView positions = mesh->getVertexBuffer(); + + const auto nPositions = positions.getNumElements(); + + // 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(); // cache value to avoid resolving each iteration + auto meshPart = thisObject();//js->toScriptValue(meshProxy); + + auto obj = js->newObject(); + auto attributeViews = ScriptableMesh::gatherBufferViews(mesh, { "normal", "color" }); + for (uint32_t i=0; i < nPositions; i++) { + for (const auto& a : attributeViews) { + bool asArray = a.second._element.getType() != gpu::FLOAT; + obj.setProperty(a.first, bufferViewElementToScriptValue(js, a.second, i, asArray, a.first.toStdString().c_str())); + } + auto result = callback.call(scope, { obj, i, meshPart }); + if (js->hasUncaughtException()) { + context()->throwValue(js->uncaughtException()); + return false; + } + + if (result.isBool() && !result.toBool()) { + // bail without modifying data if user explicitly returns false + continue; + } + 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); + auto& view = a.second; + if (attribute.isValid()) { + bufferViewElementFromScriptValue(attribute, view, i); + } + } + } + return thisObject(); +} + +QScriptValue ScriptableMesh::unrollVertices(bool recalcNormals) { + auto meshProxy = this; + auto mesh = getMeshPointer(); + qCInfo(mesh_logging) << "ScriptableMesh::unrollVertices" << !!mesh<< !!meshProxy; + if (!mesh) { + return QScriptValue(); + } + + auto positions = mesh->getVertexBuffer(); + auto indices = mesh->getIndexBuffer(); + quint32 numPoints = (quint32)indices.getNumElements(); + auto buffer = new gpu::Buffer(); + buffer->resize(numPoints * sizeof(uint32_t)); + auto newindices = gpu::BufferView(buffer, { gpu::SCALAR, gpu::UINT32, gpu::INDEX }); + qCInfo(mesh_logging) << "ScriptableMesh::unrollVertices numPoints" << numPoints; + auto attributeViews = ScriptableMesh::gatherBufferViews(mesh); + for (const auto& a : attributeViews) { + auto& view = a.second; + auto sz = view._element.getSize(); + auto buffer = new gpu::Buffer(); + buffer->resize(numPoints * sz); + auto points = gpu::BufferView(buffer, view._element); + auto src = (uint8_t*)view._buffer->getData(); + auto dest = (uint8_t*)points._buffer->getData(); + auto slot = ScriptableMesh::ATTRIBUTES[a.first]; + qCInfo(mesh_logging) << "ScriptableMesh::unrollVertices buffer" << a.first; + qCInfo(mesh_logging) << "ScriptableMesh::unrollVertices source" << view.getNumElements(); + qCInfo(mesh_logging) << "ScriptableMesh::unrollVertices dest" << points.getNumElements(); + qCInfo(mesh_logging) << "ScriptableMesh::unrollVertices sz" << sz << src << dest << slot; + auto esize = indices._element.getSize(); + const char* hint= a.first.toStdString().c_str(); + for(quint32 i = 0; i < numPoints; i++) { + quint32 index = esize == 4 ? indices.get(i) : indices.get(i); + newindices.edit(i) = i; + bufferViewElementFromVariant( + points, i, + bufferViewElementToVariant(view, index, false, hint) + ); + } + if (slot == gpu::Stream::POSITION) { + mesh->setVertexBuffer(points); + } else { + mesh->addAttribute(slot, points); + } + } + mesh->setIndexBuffer(newindices); + if (recalcNormals) { + recalculateNormals(); + } + return true; +} + +bool ScriptableMesh::replaceMeshData(scriptable::ScriptableMeshPointer src, const QVector& attributeNames) { + auto target = getMeshPointer(); + auto source = src ? src->getMeshPointer() : nullptr; + if (!target || !source) { + context()->throwError("ScriptableMesh::replaceMeshData -- expected dest and src to be valid mesh proxy pointers"); + return false; + } + + QVector attributes = attributeNames.isEmpty() ? src->getAttributeNames() : attributeNames; + + //qCInfo(mesh_logging) << "ScriptableMesh::replaceMeshData -- source:" << source->displayName << "target:" << 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 = ScriptableMesh::gatherBufferViews(target); + for (const auto& a : attributeViews) { + auto slot = ScriptableMesh::ATTRIBUTES[a.first]; + if (!attributes.contains(a.first)) { + //qCInfo(mesh_logging) << "ScriptableMesh::replaceMeshData -- pruning target attribute" << a.first << slot; + target->removeAttribute(slot); + } + } + } + + target->setVertexBuffer(cloneBufferView(source->getVertexBuffer())); + target->setIndexBuffer(cloneBufferView(source->getIndexBuffer())); + target->setPartBuffer(cloneBufferView(source->getPartBuffer())); + + for (const auto& a : attributes) { + auto slot = ScriptableMesh::ATTRIBUTES[a]; + if (slot == gpu::Stream::POSITION) { + continue; + } + // auto& before = target->getAttributeBuffer(slot); + auto& input = source->getAttributeBuffer(slot); + if (input.getNumElements() == 0) { + //qCInfo(mesh_logging) << "ScriptableMesh::replaceMeshData buffer is empty -- pruning" << a << slot; + target->removeAttribute(slot); + } else { + // if (before.getNumElements() == 0) { + // qCInfo(mesh_logging) << "ScriptableMesh::replaceMeshData target buffer is empty -- adding" << a << slot; + // } else { + // qCInfo(mesh_logging) << "ScriptableMesh::replaceMeshData target buffer exists -- updating" << a << slot; + // } + target->addAttribute(slot, cloneBufferView(input)); + } + // auto& after = target->getAttributeBuffer(slot); + // qCInfo(mesh_logging) << "ScriptableMesh::replaceMeshData" << a << slot << before.getNumElements() << " -> " << after.getNumElements(); + } + + + return true; +} + +bool ScriptableMesh::dedupeVertices(float epsilon) { + scriptable::ScriptableMeshPointer meshProxy = this; + 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 (quint32 i = 0; i < numPositions; i++) { + const quint32 numUnique = uniqueVerts.size(); + const auto& position = positions.get(i); + bool unique = true; + for (quint32 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(mesh_logging) << "//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 (quint32 i = 0; i < numIndices; i++) { + quint32 index = esize == 4 ? indices.get(i) : indices.get(i); + if (remapIndices.contains(index)) { + //qCInfo(mesh_logging) << i << index << "->" << remapIndices[index]; + newIndices << remapIndices[index]; + } else { + qCInfo(mesh_logging) << i << index << "!remapIndices[index]"; + } + } + + mesh->setIndexBuffer(bufferViewFromVector(newIndices, { gpu::SCALAR, gpu::UINT32, gpu::INDEX })); + mesh->setVertexBuffer(bufferViewFromVector(uniqueVerts, { gpu::VEC3, gpu::FLOAT, gpu::XYZ })); + + auto attributeViews = ScriptableMesh::gatherBufferViews(mesh); + quint32 numUniqueVerts = uniqueVerts.size(); + for (const auto& a : attributeViews) { + auto& view = a.second; + auto slot = ScriptableMesh::ATTRIBUTES[a.first]; + if (slot == gpu::Stream::POSITION) { + continue; + } + qCInfo(mesh_logging) << "ScriptableMesh::dedupeVertices" << a.first << slot << view.getNumElements(); + auto newView = resizedBufferView(view, numUniqueVerts); + qCInfo(mesh_logging) << a.first << "before: #" << view.getNumElements() << "after: #" << newView.getNumElements(); + quint32 numElements = (quint32)view.getNumElements(); + for (quint32 i = 0; i < numElements; i++) { + quint32 fromVertexIndex = i; + quint32 toVertexIndex = remapIndices.contains(fromVertexIndex) ? remapIndices[fromVertexIndex] : fromVertexIndex; + bufferViewElementFromVariant( + newView, toVertexIndex, + bufferViewElementToVariant(view, fromVertexIndex, false, "dedupe") + ); + } + mesh->addAttribute(slot, newView); + } + return true; +} + +QScriptValue ScriptableMesh::cloneMesh(bool recalcNormals) { + auto mesh = getMeshPointer(); + if (!mesh) { + return QScriptValue::NullValue; + } + graphics::MeshPointer clone(new graphics::Mesh()); + clone->displayName = mesh->displayName + "-clone"; + qCInfo(mesh_logging) << "ScriptableMesh::cloneMesh" << !!mesh; + if (!mesh) { + return QScriptValue::NullValue; + } + + clone->setIndexBuffer(cloneBufferView(mesh->getIndexBuffer())); + clone->setPartBuffer(cloneBufferView(mesh->getPartBuffer())); + auto attributeViews = ScriptableMesh::gatherBufferViews(mesh); + for (const auto& a : attributeViews) { + auto& view = a.second; + auto slot = ScriptableMesh::ATTRIBUTES[a.first]; + qCInfo(mesh_logging) << "ScriptableMesh::cloneVertices buffer" << a.first << slot; + auto points = cloneBufferView(view); + qCInfo(mesh_logging) << "ScriptableMesh::cloneVertices source" << view.getNumElements(); + qCInfo(mesh_logging) << "ScriptableMesh::cloneVertices dest" << points.getNumElements(); + if (slot == gpu::Stream::POSITION) { + clone->setVertexBuffer(points); + } else { + clone->addAttribute(slot, points); + } + } + + auto result = scriptable::ScriptableMeshPointer(new ScriptableMesh(nullptr, clone)); + if (recalcNormals) { + result->recalculateNormals(); + } + return engine()->toScriptValue(result); +} + +bool ScriptableMesh::recalculateNormals() { + scriptable::ScriptableMeshPointer meshProxy = this; + qCInfo(mesh_logging) << "Recalculating normals" << !!meshProxy; + auto mesh = getMeshPointer(); + if (!mesh) { + return false; + } + ScriptableMesh::gatherBufferViews(mesh, { "normal", "color" }); // ensures #normals >= #positions + auto normals = mesh->getAttributeBuffer(gpu::Stream::NORMAL); + auto verts = mesh->getVertexBuffer(); + auto indices = mesh->getIndexBuffer(); + auto esize = indices._element.getSize(); + auto numPoints = indices.getNumElements(); + const auto TRIANGLE = 3; + quint32 numFaces = (quint32)numPoints / TRIANGLE; + //QVector faces; + QVector faceNormals; + QMap> vertexToFaces; + //faces.resize(numFaces); + faceNormals.resize(numFaces); + auto numNormals = normals.getNumElements(); + qCInfo(mesh_logging) << QString("numFaces: %1, numNormals: %2, numPoints: %3").arg(numFaces).arg(numNormals).arg(numPoints); + if (normals.getNumElements() != verts.getNumElements()) { + return false; + } + for (quint32 i = 0; i < numFaces; i++) { + quint32 I = TRIANGLE * i; + quint32 i0 = esize == 4 ? indices.get(I+0) : indices.get(I+0); + quint32 i1 = esize == 4 ? indices.get(I+1) : indices.get(I+1); + quint32 i2 = esize == 4 ? indices.get(I+2) : indices.get(I+2); + + Triangle face = { + verts.get(i1), + verts.get(i2), + verts.get(i0) + }; + faceNormals[i] = face.getNormal(); + if (glm::isnan(faceNormals[i].x)) { + qCInfo(mesh_logging) << i << i0 << i1 << i2 << vec3toVariant(face.v0) << vec3toVariant(face.v1) << vec3toVariant(face.v2); + break; + } + vertexToFaces[glm::to_string(face.v0).c_str()] << i; + vertexToFaces[glm::to_string(face.v1).c_str()] << i; + vertexToFaces[glm::to_string(face.v2).c_str()] << i; + } + for (quint32 j = 0; j < numNormals; j++) { + //auto v = verts.get(j); + glm::vec3 normal { 0.0f, 0.0f, 0.0f }; + QString key { glm::to_string(verts.get(j)).c_str() }; + const auto& faces = vertexToFaces.value(key); + if (faces.size()) { + for (const auto i : faces) { + normal += faceNormals[i]; + } + normal *= 1.0f / (float)faces.size(); + } else { + static int logged = 0; + if (logged++ < 10) { + qCInfo(mesh_logging) << "no faces for key!?" << key; + } + normal = verts.get(j); + } + if (glm::isnan(normal.x)) { + static int logged = 0; + if (logged++ < 10) { + qCInfo(mesh_logging) << "isnan(normal.x)" << j << vec3toVariant(normal); + } + break; + } + normals.edit(j) = glm::normalize(normal); + } + return true; +} + +QString ScriptableMesh::toOBJ() { + if (!getMeshPointer()) { + context()->throwError(QString("null mesh")); + } + return writeOBJToString({ getMeshPointer() }); +} + diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h index da11002906..257285fa90 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h @@ -6,12 +6,16 @@ #include #include #include +#include #include #include #include +#include +#include + namespace graphics { class Mesh; } @@ -19,91 +23,112 @@ namespace gpu { class BufferView; } namespace scriptable { - class ScriptableMesh : public QObject, public std::enable_shared_from_this { + class ScriptableMeshPart; + using ScriptableMeshPartPointer = QPointer; + class ScriptableMesh : public QObject, QScriptable { Q_OBJECT public: - ScriptableModelPointer _model; - scriptable::MeshPointer _mesh; - QVariantMap _metadata; - ScriptableMesh() : QObject() {} - ScriptableMesh(ScriptableModelPointer parent, scriptable::MeshPointer mesh) : QObject(), _model(parent), _mesh(mesh) {} - ScriptableMesh(const ScriptableMesh& other) : QObject(), _model(other._model), _mesh(other._mesh), _metadata(other._metadata) {} - ~ScriptableMesh() { qDebug() << "~ScriptableMesh" << this; } Q_PROPERTY(quint32 numParts READ getNumParts) Q_PROPERTY(quint32 numAttributes READ getNumAttributes) Q_PROPERTY(quint32 numVertices READ getNumVertices) Q_PROPERTY(quint32 numIndices READ getNumIndices) + Q_PROPERTY(QVariantMap metadata MEMBER _metadata) Q_PROPERTY(QVector attributeNames READ getAttributeNames) - virtual scriptable::MeshPointer getMeshPointer() const { return _mesh; } - Q_INVOKABLE virtual quint32 getNumParts() const; - Q_INVOKABLE virtual quint32 getNumVertices() const; - Q_INVOKABLE virtual quint32 getNumAttributes() const; - Q_INVOKABLE virtual quint32 getNumIndices() const { return 0; } - Q_INVOKABLE virtual QVector getAttributeNames() const; - Q_INVOKABLE virtual QVariantMap getVertexAttributes(quint32 vertexIndex) const; - Q_INVOKABLE virtual QVariantMap getVertexAttributes(quint32 vertexIndex, QVector attributes) const; - - Q_INVOKABLE virtual QVector getIndices() const; - Q_INVOKABLE virtual QVector findNearbyIndices(const glm::vec3& origin, float epsilon = 1e-6) const; - Q_INVOKABLE virtual QVariantMap getMeshExtents() const; - Q_INVOKABLE virtual bool setVertexAttributes(quint32 vertexIndex, QVariantMap attributes); - Q_INVOKABLE virtual QVariantMap scaleToFit(float unitScale); + static QMap ATTRIBUTES; + static std::map gatherBufferViews(MeshPointer mesh, const QStringList& expandToMatchPositions = QStringList()); - static QMap ATTRIBUTES; - static std::map gatherBufferViews(MeshPointer mesh, const QStringList& expandToMatchPositions = QStringList()); + ScriptableMesh& operator=(const ScriptableMesh& other) { _model=other._model; _mesh=other._mesh; _metadata=other._metadata; return *this; }; + ScriptableMesh() : QObject(), _model(nullptr) {} + ScriptableMesh(ScriptableModelPointer parent, scriptable::MeshPointer mesh) : QObject(), _model(parent), _mesh(mesh) {} + ScriptableMesh(const ScriptableMesh& other) : QObject(), _model(other._model), _mesh(other._mesh), _metadata(other._metadata) {} + ~ScriptableMesh() { qDebug() << "~ScriptableMesh" << this; } - Q_INVOKABLE QVariantList getAttributeValues(const QString& attributeName) const; + scriptable::MeshPointer getMeshPointer() const { return _mesh; } + public slots: + quint32 getNumParts() const; + quint32 getNumVertices() const; + quint32 getNumAttributes() const; + quint32 getNumIndices() const { return 0; } + QVector getAttributeNames() const; - Q_INVOKABLE int _getSlotNumber(const QString& attributeName) const; + QVariantMap getVertexAttributes(quint32 vertexIndex) const; + QVariantMap getVertexAttributes(quint32 vertexIndex, QVector attributes) const; - 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)); - Q_INVOKABLE QVariantMap transform(const glm::mat4& transform); + QVector getIndices() const; + QVector findNearbyIndices(const glm::vec3& origin, float epsilon = 1e-6) const; + QVariantMap getMeshExtents() const; + bool setVertexAttributes(quint32 vertexIndex, QVariantMap attributes); + QVariantMap scaleToFit(float unitScale); + + QVariantList getAttributeValues(const QString& attributeName) const; + + int _getSlotNumber(const QString& attributeName) const; + + 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); + + public: + operator bool() const { return _mesh != nullptr; } + ScriptableModelPointer _model; + scriptable::MeshPointer _mesh; + QVariantMap _metadata; + + public slots: + // QScriptEngine-specific wrappers + QScriptValue mapAttributeValues(QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue()); + bool dedupeVertices(float epsilon = 1e-6); + bool recalculateNormals(); + QScriptValue cloneMesh(bool recalcNormals = true); + QScriptValue unrollVertices(bool recalcNormals = true); + bool replaceMeshData(scriptable::ScriptableMeshPointer source, const QVector& attributeNames = QVector()); + QString toOBJ(); }; - // TODO: for now this is a part-specific wrapper around ScriptableMesh - class ScriptableMeshPart : public ScriptableMesh { + // TODO: part-specific wrapper for working with raw geometries + class ScriptableMeshPart : public QObject { Q_OBJECT public: - ScriptableMeshPart& operator=(const ScriptableMeshPart& view) { _model=view._model; _mesh=view._mesh; return *this; }; - ScriptableMeshPart(const ScriptableMeshPart& other) : ScriptableMesh(other._model, other._mesh) {} - ScriptableMeshPart() : ScriptableMesh(nullptr, nullptr) {} - ~ScriptableMeshPart() { qDebug() << "~ScriptableMeshPart" << this; } - ScriptableMeshPart(ScriptableMeshPointer mesh) : ScriptableMesh(mesh->_model, mesh->_mesh) {} Q_PROPERTY(QString topology READ getTopology) Q_PROPERTY(quint32 numFaces READ getNumFaces) - scriptable::MeshPointer parentMesh; - int partIndex; - QString getTopology() const { return "triangles"; } - Q_INVOKABLE virtual quint32 getNumFaces() const { return getIndices().size() / 3; } - Q_INVOKABLE virtual QVector getFace(quint32 faceIndex) const { - auto inds = getIndices(); + ScriptableMeshPart& operator=(const ScriptableMeshPart& view) { parentMesh=view.parentMesh; return *this; }; + ScriptableMeshPart(const ScriptableMeshPart& other) : parentMesh(other.parentMesh) {} + ScriptableMeshPart() {} + ~ScriptableMeshPart() { qDebug() << "~ScriptableMeshPart" << this; } + + public slots: + QString getTopology() const { return "triangles"; } + quint32 getNumFaces() const { return parentMesh.getIndices().size() / 3; } + QVector getFace(quint32 faceIndex) const { + auto inds = parentMesh.getIndices(); return faceIndex+2 < (quint32)inds.size() ? inds.mid(faceIndex*3, 3) : QVector(); } + + public: + scriptable::ScriptableMesh parentMesh; + int partIndex; }; class GraphicsScriptingInterface : public QObject { Q_OBJECT public: GraphicsScriptingInterface(QObject* parent = nullptr) : QObject(parent) {} - GraphicsScriptingInterface(const GraphicsScriptingInterface& other) {} + GraphicsScriptingInterface(const GraphicsScriptingInterface& other) {} public slots: ScriptableMeshPart exportMeshPart(ScriptableMesh mesh, int part) { return {}; } - }; } -Q_DECLARE_METATYPE(scriptable::ScriptableMesh) Q_DECLARE_METATYPE(scriptable::ScriptableMeshPointer) Q_DECLARE_METATYPE(QVector) -Q_DECLARE_METATYPE(scriptable::ScriptableMeshPart) +Q_DECLARE_METATYPE(scriptable::ScriptableMeshPartPointer) Q_DECLARE_METATYPE(scriptable::GraphicsScriptingInterface) -// FIXME: faces were supported in the original Model.* API -- are they still needed/used/useful for anything yet? +// FIXME: MESHFACES: faces were supported in the original Model.* API -- are they still needed/used/useful for anything yet? #include namespace mesh { diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h index e8cf6f1656..4ba5a993b1 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -16,50 +17,53 @@ namespace graphics { namespace gpu { class BufferView; } +class QScriptValue; + namespace scriptable { using Mesh = graphics::Mesh; using MeshPointer = std::shared_ptr; class ScriptableModel; + using ScriptableModelPointer = QPointer; class ScriptableMesh; - class ScriptableMeshPart; - using ScriptableModelPointer = std::shared_ptr; - using ScriptableMeshPointer = std::shared_ptr; - using ScriptableMeshPartPointer = std::shared_ptr; - class ScriptableModel : public QObject, public std::enable_shared_from_this { + using ScriptableMeshPointer = QPointer; + + // abstract container for holding one or more scriptable meshes + class ScriptableModel : public QObject { Q_OBJECT public: - Q_PROPERTY(QVector meshes READ getMeshes) - - Q_INVOKABLE QString toString() { return "[ScriptableModel " + objectName()+"]"; } - ScriptableModel(QObject* parent = nullptr) : QObject(parent) {} - ScriptableModel(const ScriptableModel& other) : objectID(other.objectID), metadata(other.metadata), meshes(other.meshes) {} - ScriptableModel& operator=(const ScriptableModel& view) { - objectID = view.objectID; - metadata = view.metadata; - meshes = view.meshes; - return *this; - } - ~ScriptableModel() { qDebug() << "~ScriptableModel" << this; } - void mixin(const ScriptableModel& other) { - for (const auto& key : other.metadata.keys()) { - metadata[key] = other.metadata[key]; - } - for(const auto&mesh : other.meshes) { - meshes << mesh; - } - } QUuid objectID; QVariantMap metadata; QVector meshes; - // TODO: in future accessors for these could go here - QVariantMap shapes; - QVariantMap materials; - QVariantMap armature; - QVector getMeshes() const; + Q_PROPERTY(QVector meshes READ getMeshes) + Q_PROPERTY(QUuid objectID MEMBER objectID CONSTANT) + Q_PROPERTY(QVariantMap metadata MEMBER metadata CONSTANT) + Q_INVOKABLE QString toString() const; + + ScriptableModel(QObject* parent = nullptr) : QObject(parent) {} + ScriptableModel(const ScriptableModel& other) : objectID(other.objectID), metadata(other.metadata), meshes(other.meshes) {} + ScriptableModel& operator=(const ScriptableModel& view) { objectID = view.objectID; metadata = view.metadata; meshes = view.meshes; return *this; } + ~ScriptableModel() { qDebug() << "~ScriptableModel" << this; } + + void mixin(const ScriptableModel& other) { + for (const auto& key : other.metadata.keys()) { metadata[key] = other.metadata[key]; } + for (const auto& mesh : other.meshes) { meshes << mesh; } + } + + // TODO: in future accessors for these could go here + // QVariantMap shapes; + // QVariantMap materials; + // QVariantMap armature; + + QVector getMeshes(); + const QVector getConstMeshes() const; + + // QScriptEngine-specific wrappers + Q_INVOKABLE QScriptValue mapAttributeValues(QScriptValue scopeOrCallback, QScriptValue methodOrName); }; + // mixin class for Avatar/Entity/Overlay Rendering that expose their in-memory graphics::Meshes class ModelProvider { public: QVariantMap metadata; @@ -67,11 +71,12 @@ namespace scriptable { virtual scriptable::ScriptableModel getScriptableModel(bool* ok = nullptr) = 0; }; using ModelProviderPointer = std::shared_ptr; + + // mixin class for Application to resolve UUIDs into a corresponding ModelProvider class ModelProviderFactory : public Dependency { public: virtual scriptable::ModelProviderPointer lookupModelProvider(QUuid uuid) = 0; }; - } Q_DECLARE_METATYPE(scriptable::MeshPointer) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index d595136c56..ae5ac5d61c 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -579,38 +579,9 @@ scriptable::ScriptableModel Model::getScriptableModel(bool* ok) { if (!isLoaded()) { qDebug() << "Model::getScriptableModel -- !isLoaded"; - if (ok) { - *ok = false; - } - return result; + return scriptable::ModelProvider::modelUnavailableError(ok); } -// TODO: remove -- this was an earlier approach using renderGeometry instead of FBXGeometry -#if 0 // renderGeometry approach - const Geometry::GeometryMeshes& meshes = renderGeometry->getMeshes(); - Transform offset; - offset.setScale(_scale); - offset.postTranslate(_offset); - glm::mat4 offsetMat = offset.getMatrix(); - - for (std::shared_ptr mesh : meshes) { - if (!mesh) { - continue; - } - qDebug() << "Model::getScriptableModel #" << i++ << mesh->displayName; - auto newmesh = mesh->map( - [=](glm::vec3 position) { - return glm::vec3(offsetMat * glm::vec4(position, 1.0f)); - }, - [=](glm::vec3 color) { return color; }, - [=](glm::vec3 normal) { - return glm::normalize(glm::vec3(offsetMat * glm::vec4(normal, 0.0f))); - }, - [&](uint32_t index) { return index; }); - newmesh->displayName = mesh->displayName; - result << newmesh; - } -#endif const FBXGeometry& geometry = getFBXGeometry(); auto mat4toVariant = [](const glm::mat4& mat4) -> QVariant { QVector floats; @@ -659,6 +630,33 @@ scriptable::ScriptableModel Model::getScriptableModel(bool* ok) { qDebug() << "//Model::getScriptableModel -- #" << result.meshes.size(); result.metadata["submeshes"] = submeshes; return result; + +// TODO: remove -- this was an earlier approach using renderGeometry instead of FBXGeometry +#if 0 // renderGeometry approach + const Geometry::GeometryMeshes& meshes = renderGeometry->getMeshes(); + Transform offset; + offset.setScale(_scale); + offset.postTranslate(_offset); + glm::mat4 offsetMat = offset.getMatrix(); + + for (std::shared_ptr mesh : meshes) { + if (!mesh) { + continue; + } + qDebug() << "Model::getScriptableModel #" << i++ << mesh->displayName; + auto newmesh = mesh->map( + [=](glm::vec3 position) { + return glm::vec3(offsetMat * glm::vec4(position, 1.0f)); + }, + [=](glm::vec3 color) { return color; }, + [=](glm::vec3 normal) { + return glm::normalize(glm::vec3(offsetMat * glm::vec4(normal, 0.0f))); + }, + [&](uint32_t index) { return index; }); + newmesh->displayName = mesh->displayName; + result << newmesh; + } +#endif } void Model::calculateTriangleSets() { From 7ee5245aebeb645c03e2849cd57a235aebb1d96c Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 9 Feb 2018 10:45:29 -0800 Subject: [PATCH 015/260] Added shadow caster flag to filter. --- libraries/render-utils/src/RenderShadowTask.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index c5bdcf03dd..e34f550def 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -224,7 +224,7 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende const auto setupOutput = task.addJob("ShadowSetup"); const auto queryResolution = setupOutput.getN(2); // Fetch and cull the items from the scene - static const auto shadowCasterFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask); + static const auto shadowCasterFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask).withShadowCaster(); const auto fetchInput = FetchSpatialTree::Inputs(shadowCasterFilter, queryResolution).asVarying(); const auto shadowSelection = task.addJob("FetchShadowTree", fetchInput); const auto selectionInputs = FetchSpatialSelection::Inputs(shadowSelection, shadowCasterFilter).asVarying(); @@ -398,7 +398,7 @@ void RenderShadowCascadeSetup::run(const render::RenderContextPointer& renderCon const auto globalShadow = lightStage->getCurrentKeyShadow(); if (globalShadow && _cascadeIndexgetCascadeCount()) { - output.edit0() = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(_tagBits, _tagMask); + output.edit0() = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(_tagBits, _tagMask).withShadowCaster(); // Set the keylight render args auto& cascade = globalShadow->getCascade(_cascadeIndex); From 522c577e732635b43dbed78e4d7868ad7dac5e64 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 9 Feb 2018 14:08:55 -0800 Subject: [PATCH 016/260] FIxing the bad ambient lighting on scattering surfaces --- libraries/render-utils/src/LightAmbient.slh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/render-utils/src/LightAmbient.slh b/libraries/render-utils/src/LightAmbient.slh index 0502446db8..c45f036486 100644 --- a/libraries/render-utils/src/LightAmbient.slh +++ b/libraries/render-utils/src/LightAmbient.slh @@ -78,7 +78,7 @@ void evalLightingAmbient(out vec3 diffuse, out vec3 specular, LightAmbient ambie vec3 ambientSpaceSurfaceNormal = (ambient.transform * vec4(surface.normal, 0.0)).xyz; vec3 ambientSpaceSurfaceEyeDir = (ambient.transform * vec4(surface.eyeDir, 0.0)).xyz; <@if supportScattering@> - vec3 ambientSpaceLowNormalCurvature = (ambient.transform * lowNormalCurvature).xyz; + vec3 ambientSpaceLowNormal = (ambient.transform * vec4(lowNormalCurvature.xyz, 0.0)).xyz; <@endif@> vec3 ambientFresnel = fresnelSchlickAmbient(fresnelF0, surface.ndotv, 1.0-surface.roughness); @@ -99,7 +99,7 @@ void evalLightingAmbient(out vec3 diffuse, out vec3 specular, LightAmbient ambie obscurance = min(obscurance, ambientOcclusion); // Diffuse from ambient - diffuse = sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), ambientSpaceLowNormalCurvature).xyz; + diffuse = sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), ambientSpaceLowNormal).xyz; // Scattering ambient specular is the same as non scattering for now // TODO: we should use the same specular answer as for direct lighting From 4acd0a34f50ec1219a40ca82cea17df24acf0435 Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Mon, 12 Feb 2018 12:55:14 -0800 Subject: [PATCH 017/260] Seems OK. Before adding flag to entity. --- libraries/render-utils/src/RenderShadowTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index e34f550def..53c109dc9f 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -224,7 +224,7 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende const auto setupOutput = task.addJob("ShadowSetup"); const auto queryResolution = setupOutput.getN(2); // Fetch and cull the items from the scene - static const auto shadowCasterFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask).withShadowCaster(); + static const auto shadowCasterFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask); const auto fetchInput = FetchSpatialTree::Inputs(shadowCasterFilter, queryResolution).asVarying(); const auto shadowSelection = task.addJob("FetchShadowTree", fetchInput); const auto selectionInputs = FetchSpatialSelection::Inputs(shadowSelection, shadowCasterFilter).asVarying(); From cf5452313a01c8eab3404a4fccc9f2145d56fae7 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 12 Feb 2018 18:44:24 -0800 Subject: [PATCH 018/260] WIP check in of making the use of dq or mat dynamic per model --- .../src/CauterizedMeshPartPayload.cpp | 30 ++++- .../src/CauterizedMeshPartPayload.h | 12 +- .../render-utils/src/CauterizedModel.cpp | 123 +++++++++++------- .../render-utils/src/MeshPartPayload.cpp | 99 ++++++++------ libraries/render-utils/src/MeshPartPayload.h | 20 +-- libraries/render-utils/src/Model.cpp | 65 +++++---- libraries/render-utils/src/Model.h | 11 +- .../render-utils/src/SoftAttachmentModel.cpp | 36 ++--- 8 files changed, 238 insertions(+), 158 deletions(-) diff --git a/libraries/render-utils/src/CauterizedMeshPartPayload.cpp b/libraries/render-utils/src/CauterizedMeshPartPayload.cpp index 3d213840dd..e7ea902adb 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 2337632047..3783bd1bf8 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..f4a745278e 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,38 +223,51 @@ 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); - transaction.updateItem(itemID, [modelTransform, clusterTransforms, clusterTransformsCauterized, invalidatePayloadShapeKey, + transaction.updateItem(itemID, [modelTransform, meshState, 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); diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 9655b60a78..595a4013f1 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -340,20 +340,27 @@ ModelMeshPartPayload::ModelMeshPartPayload(ModelPointer model, int meshIndex, in 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); @@ -383,16 +390,30 @@ void ModelMeshPartPayload::notifyLocationChanged() { } -void ModelMeshPartPayload::updateClusterBuffer(const std::vector& clusterTransforms) { +void ModelMeshPartPayload::updateClusterBuffer(const std::vector& clusterMatrices) { // 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) { + // 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()); } } } @@ -550,29 +571,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 21f9dc2e68..7791390203 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -93,14 +93,14 @@ 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) 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 @@ -115,7 +115,11 @@ public: void bindMesh(gpu::Batch& batch) override; void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, 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; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index b9ccc28c01..92abac3520 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -277,26 +277,35 @@ 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); - transaction.updateItem(itemID, [modelTransform, clusterTransforms, + transaction.updateItem(itemID, [modelTransform, meshState, invalidatePayloadShapeKey, isWireframe, isVisible, viewTagBits, isLayeredInFront, isLayeredInHUD](ModelMeshPartPayload& data) { - data.updateClusterBuffer(clusterTransforms); + if (_useDualQuaternions) { + 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 (_useDualQuaternions) { + 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); @@ -377,7 +386,11 @@ bool Model::updateGeometry() { const FBXGeometry& fbxGeometry = getFBXGeometry(); foreach (const FBXMesh& mesh, fbxGeometry.meshes) { MeshState state; - state.clusterTransforms.resize(mesh.clusters.size()); + if (_useDualQuaternions) { + state.clusterDualQuaternions.resize(mesh.clusters.size()); + } else { + 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 @@ -1262,7 +1275,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 (_useDualQuaternions) { + part->computeAdjustedLocalBound(state.clusterDualQuaternions); + } else { + part->computeAdjustedLocalBound(state.clusterMatrices); + } } } @@ -1281,16 +1298,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]); + } } } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index ca0904f334..84d7dcb7cc 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -254,8 +254,6 @@ public: int getRenderInfoDrawCalls() const { return _renderInfoDrawCalls; } bool getRenderInfoHasTransparent() const { return _renderInfoHasTransparent; } - -#if defined(SKIN_DQ) class TransformDualQuaternion { public: TransformDualQuaternion() {} @@ -293,15 +291,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); } @@ -420,6 +414,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/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]); + } } } From 7f5f48bca9ad7a3a9f85934888da314d5f91720a Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 13 Feb 2018 11:37:14 -0800 Subject: [PATCH 019/260] Models can now switch between dual quats and matrix palette skinning. but not dynamically, because we still only compile one version of the shader. --- libraries/render-utils/src/CauterizedModel.cpp | 9 +++++---- libraries/render-utils/src/MeshPartPayload.cpp | 3 +++ libraries/render-utils/src/MeshPartPayload.h | 1 + libraries/render-utils/src/Model.cpp | 11 ++++++----- libraries/render-utils/src/Model.h | 3 ++- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index f4a745278e..fb1d31d273 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -228,10 +228,11 @@ void CauterizedModel::updateRenderItems() { const auto& cauterizedMeshState = self->getCauterizeMeshState(meshIndex); bool invalidatePayloadShapeKey = self->shouldInvalidatePayloadShapeKey(meshIndex); + bool useDualQuaternionSkinning = self->getUseDualQuaternionSkinning(); - transaction.updateItem(itemID, [modelTransform, meshState, cauterizedMeshState, invalidatePayloadShapeKey, + transaction.updateItem(itemID, [modelTransform, meshState, useDualQuaternionSkinning, cauterizedMeshState, invalidatePayloadShapeKey, isWireframe, isVisible, isLayeredInFront, isLayeredInHUD, enableCauterization](CauterizedMeshPartPayload& data) { - if (_useDualQuaternionSkinning) { + if (useDualQuaternionSkinning) { data.updateClusterBuffer(meshState.clusterDualQuaternions, cauterizedMeshState.clusterDualQuaternions); } else { @@ -240,7 +241,7 @@ void CauterizedModel::updateRenderItems() { } Transform renderTransform = modelTransform; - if (_useDualQuaternionSkinning) { + if (useDualQuaternionSkinning) { if (meshState.clusterDualQuaternions.size() == 1) { const auto& dq = meshState.clusterDualQuaternions[0]; Transform transform(dq.getRotation(), @@ -256,7 +257,7 @@ void CauterizedModel::updateRenderItems() { data.updateTransformForSkinnedMesh(renderTransform, modelTransform); renderTransform = modelTransform; - if (_useDualQuaternionSkinning) { + if (useDualQuaternionSkinning) { if (cauterizedMeshState.clusterDualQuaternions.size() == 1) { const auto& dq = cauterizedMeshState.clusterDualQuaternions[0]; Transform transform(dq.getRotation(), diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 595a4013f1..1585a075f5 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -335,6 +335,9 @@ ModelMeshPartPayload::ModelMeshPartPayload(ModelPointer model, int meshIndex, in _shapeID(shapeIndex) { assert(model && model->isLoaded()); + + _useDualQuaternionSkinning = model->getUseDualQuaternionSkinning(); + _blendedVertexBuffer = model->_blendedVertexBuffers[_meshIndex]; auto& modelMesh = model->getGeometry()->getMeshes().at(_meshIndex); const Model::MeshState& state = model->getMeshState(_meshIndex); diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 7791390203..220a0bc48c 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -136,6 +136,7 @@ private: gpu::BufferPointer _blendedVertexBuffer; render::ShapeKey _shapeKey { render::ShapeKey::Builder::invalid() }; int _layer { render::Item::LAYER_3D }; + bool _useDualQuaternionSkinning { false }; }; namespace render { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 92abac3520..1318299f43 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -281,12 +281,13 @@ void Model::updateRenderItems() { const auto& meshState = self->getMeshState(meshIndex); bool invalidatePayloadShapeKey = self->shouldInvalidatePayloadShapeKey(meshIndex); + bool useDualQuaternionSkinning = self->getUseDualQuaternionSkinning(); - transaction.updateItem(itemID, [modelTransform, meshState, + transaction.updateItem(itemID, [modelTransform, meshState, useDualQuaternionSkinning, invalidatePayloadShapeKey, isWireframe, isVisible, viewTagBits, isLayeredInFront, isLayeredInHUD](ModelMeshPartPayload& data) { - if (_useDualQuaternions) { + if (useDualQuaternionSkinning) { data.updateClusterBuffer(meshState.clusterDualQuaternions); } else { data.updateClusterBuffer(meshState.clusterMatrices); @@ -294,7 +295,7 @@ void Model::updateRenderItems() { Transform renderTransform = modelTransform; - if (_useDualQuaternions) { + if (useDualQuaternionSkinning) { if (meshState.clusterDualQuaternions.size() == 1) { const auto& dq = meshState.clusterDualQuaternions[0]; Transform transform(dq.getRotation(), @@ -386,7 +387,7 @@ bool Model::updateGeometry() { const FBXGeometry& fbxGeometry = getFBXGeometry(); foreach (const FBXMesh& mesh, fbxGeometry.meshes) { MeshState state; - if (_useDualQuaternions) { + if (_useDualQuaternionSkinning) { state.clusterDualQuaternions.resize(mesh.clusters.size()); } else { state.clusterMatrices.resize(mesh.clusters.size()); @@ -1275,7 +1276,7 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { void Model::computeMeshPartLocalBounds() { for (auto& part : _modelMeshRenderItems) { const Model::MeshState& state = _meshStates.at(part->_meshIndex); - if (_useDualQuaternions) { + if (_useDualQuaternionSkinning) { part->computeAdjustedLocalBound(state.clusterDualQuaternions); } else { part->computeAdjustedLocalBound(state.clusterMatrices); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 84d7dcb7cc..46dbc90324 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -311,6 +311,7 @@ public: Q_INVOKABLE MeshProxyList getMeshes() const; void scaleToFit(); + bool getUseDualQuaternionSkinning() const { return _useDualQuaternionSkinning; } public slots: void loadURLFinished(bool success); @@ -414,7 +415,7 @@ protected: virtual void createCollisionRenderItemSet(); bool _isWireframe; - bool _useDualQuaternionSkinning { false }; + bool _useDualQuaternionSkinning { true }; // debug rendering support int _debugMeshBoxesID = GeometryCache::UNKNOWN_ID; From 23a29b8d4b18b07f44a667542f20c210131be1d8 Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Tue, 13 Feb 2018 16:16:04 -0800 Subject: [PATCH 020/260] Added don't castr shadow flag to entity and shape classes. --- libraries/entities/src/EntityItemProperties.h | 1 + .../src/EntityItemPropertiesDefaults.h | 1 + libraries/entities/src/EntityPropertyFlags.h | 1 + libraries/entities/src/ModelEntityItem.cpp | 35 +++++++++++++++++-- libraries/entities/src/ModelEntityItem.h | 5 +++ libraries/entities/src/ShapeEntityItem.cpp | 29 +++++++++++++++ libraries/entities/src/ShapeEntityItem.h | 5 +++ .../render-utils/src/RenderShadowTask.cpp | 4 +-- libraries/render/src/render/Item.h | 4 +++ 9 files changed, 80 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 3e0770f386..731bb9390e 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -128,6 +128,7 @@ public: DEFINE_PROPERTY_REF(PROP_SCRIPT, Script, script, QString, ENTITY_ITEM_DEFAULT_SCRIPT); DEFINE_PROPERTY(PROP_SCRIPT_TIMESTAMP, ScriptTimestamp, scriptTimestamp, quint64, ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP); DEFINE_PROPERTY_REF(PROP_COLLISION_SOUND_URL, CollisionSoundURL, collisionSoundURL, QString, ENTITY_ITEM_DEFAULT_COLLISION_SOUND_URL); + DEFINE_PROPERTY(PROP_DONT_CAST_SHADOW, CanCastShadow, canCastShadow, bool, ENTITY_ITEM_DEFAULT_DONT_CAST_SHADOW); DEFINE_PROPERTY_REF(PROP_COLOR, Color, color, xColor, particle::DEFAULT_COLOR); DEFINE_PROPERTY_REF(PROP_COLOR_SPREAD, ColorSpread, colorSpread, xColor, particle::DEFAULT_COLOR_SPREAD); DEFINE_PROPERTY_REF(PROP_COLOR_START, ColorStart, colorStart, xColor, particle::DEFAULT_COLOR); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index eb09a64628..181ba9bbf2 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -46,6 +46,7 @@ const quint32 ENTITY_ITEM_DEFAULT_STATIC_CERTIFICATE_VERSION = 0; const float ENTITY_ITEM_DEFAULT_ALPHA = 1.0f; const float ENTITY_ITEM_DEFAULT_LOCAL_RENDER_ALPHA = 1.0f; const bool ENTITY_ITEM_DEFAULT_VISIBLE = true; +const bool ENTITY_ITEM_DEFAULT_DONT_CAST_SHADOW { false }; const QString ENTITY_ITEM_DEFAULT_SCRIPT = QString(""); const quint64 ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP = 0; diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index ffcd4f64cb..fbc4199097 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -31,6 +31,7 @@ enum EntityPropertyList { PROP_SCRIPT, // these properties are supported by some derived classes + PROP_DONT_CAST_SHADOW, PROP_COLOR, // these are used by models only diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 5d33e4c047..73cf29bce1 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -53,6 +53,8 @@ void ModelEntityItem::setTextures(const QString& textures) { EntityItemProperties ModelEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class + + COPY_ENTITY_PROPERTY_TO_PROPERTIES(canCastShadow, getCanCastShadow); COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getXColor); COPY_ENTITY_PROPERTY_TO_PROPERTIES(modelURL, getModelURL); COPY_ENTITY_PROPERTY_TO_PROPERTIES(compoundShapeURL, getCompoundShapeURL); @@ -64,6 +66,7 @@ EntityItemProperties ModelEntityItem::getProperties(EntityPropertyFlags desiredP COPY_ENTITY_PROPERTY_TO_PROPERTIES(jointTranslations, getJointTranslations); COPY_ENTITY_PROPERTY_TO_PROPERTIES(relayParentJoints, getRelayParentJoints); _animationProperties.getProperties(properties); + return properties; } @@ -71,6 +74,7 @@ bool ModelEntityItem::setProperties(const EntityItemProperties& properties) { bool somethingChanged = false; somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class + SET_ENTITY_PROPERTY_FROM_PROPERTIES(canCastShadow, setCanCastShadow); SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); SET_ENTITY_PROPERTY_FROM_PROPERTIES(modelURL, setModelURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); @@ -112,6 +116,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, const unsigned char* dataAt = data; bool animationPropertiesChanged = false; + READ_ENTITY_PROPERTY(PROP_DONT_CAST_SHADOW, bool, setCanCastShadow); READ_ENTITY_PROPERTY(PROP_COLOR, rgbColor, setColor); READ_ENTITY_PROPERTY(PROP_MODEL_URL, QString, setModelURL); READ_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); @@ -148,6 +153,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, EntityPropertyFlags ModelEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); + requestedProperties += PROP_DONT_CAST_SHADOW; requestedProperties += PROP_MODEL_URL; requestedProperties += PROP_COMPOUND_SHAPE_URL; requestedProperties += PROP_TEXTURES; @@ -172,6 +178,7 @@ void ModelEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit bool successPropertyFits = true; + APPEND_ENTITY_PROPERTY(PROP_DONT_CAST_SHADOW, getCanCastShadow()); APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_MODEL_URL, getModelURL()); APPEND_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, getCompoundShapeURL()); @@ -191,8 +198,6 @@ void ModelEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS, getJointTranslations()); } - - // added update function back for property fix void ModelEntityItem::update(const quint64& now) { @@ -290,6 +295,7 @@ void ModelEntityItem::updateFrameCount() { } void ModelEntityItem::debugDump() const { + qCDebug(entities) << " can cast shadow" << getCanCastShadow(); qCDebug(entities) << "ModelEntityItem id:" << getEntityItemID(); qCDebug(entities) << " edited ago:" << getEditedAgo(); qCDebug(entities) << " position:" << getWorldPosition(); @@ -571,15 +577,16 @@ QVector ModelEntityItem::getJointTranslationsSet() const { return result; } - xColor ModelEntityItem::getXColor() const { xColor color = { _color[RED_INDEX], _color[GREEN_INDEX], _color[BLUE_INDEX] }; return color; } + bool ModelEntityItem::hasModel() const { return resultWithReadLock([&] { return !_modelURL.isEmpty(); }); } + bool ModelEntityItem::hasCompoundShapeURL() const { return !_compoundShapeURL.get().isEmpty(); } @@ -722,3 +729,25 @@ bool ModelEntityItem::isAnimatingSomething() const { (_animationProperties.getFPS() != 0.0f); }); } + +bool ModelEntityItem::getCanCastShadow() const { + bool result; + withReadLock([&] { + result = _canCastShadow; + }); + return result; +} + +void ModelEntityItem::setCanCastShadow(bool value) { + bool changed = false; + withWriteLock([&] { + if (_canCastShadow != value) { + changed = true; + _canCastShadow = value; + } + }); + + if (changed) { + emit requestRenderUpdate(); + } +} diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index c2109ba51f..ec65876d84 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -131,6 +131,9 @@ public: QVector getJointTranslations() const; QVector getJointTranslationsSet() const; + bool getCanCastShadow() const; + void setCanCastShadow(bool value); + private: void setAnimationSettings(const QString& value); // only called for old bitstream format ShapeType computeTrueShapeType() const; @@ -171,6 +174,8 @@ protected: ShapeType _shapeType = SHAPE_TYPE_NONE; + bool _canCastShadow{ ENTITY_ITEM_DEFAULT_DONT_CAST_SHADOW }; + private: uint64_t _lastAnimated{ 0 }; AnimationPropertyGroup _previousAnimationProperties; diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index cbcfcaaa1d..2fbb6702f2 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -91,6 +91,8 @@ EntityItemProperties ShapeEntityItem::getProperties(EntityPropertyFlags desiredP EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class properties.setColor(getXColor()); properties.setShape(entity::stringFromShape(getShape())); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(canCastShadow, getCanCastShadow); + return properties; } @@ -129,6 +131,7 @@ bool ShapeEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(alpha, setAlpha); SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); SET_ENTITY_PROPERTY_FROM_PROPERTIES(shape, setShape); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(canCastShadow, setCanCastShadow); if (somethingChanged) { bool wantDebug = false; @@ -154,6 +157,7 @@ int ShapeEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, READ_ENTITY_PROPERTY(PROP_SHAPE, QString, setShape); READ_ENTITY_PROPERTY(PROP_COLOR, rgbColor, setColor); READ_ENTITY_PROPERTY(PROP_ALPHA, float, setAlpha); + READ_ENTITY_PROPERTY(PROP_DONT_CAST_SHADOW, bool, setCanCastShadow); return bytesRead; } @@ -165,6 +169,8 @@ EntityPropertyFlags ShapeEntityItem::getEntityProperties(EncodeBitstreamParams& requestedProperties += PROP_SHAPE; requestedProperties += PROP_COLOR; requestedProperties += PROP_ALPHA; + requestedProperties += PROP_DONT_CAST_SHADOW; + return requestedProperties; } @@ -180,6 +186,7 @@ void ShapeEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit APPEND_ENTITY_PROPERTY(PROP_SHAPE, entity::stringFromShape(getShape())); APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_ALPHA, getAlpha()); + APPEND_ENTITY_PROPERTY(PROP_DONT_CAST_SHADOW, getCanCastShadow()); } void ShapeEntityItem::setColor(const rgbColor& value) { @@ -259,6 +266,7 @@ void ShapeEntityItem::debugDump() const { qCDebug(entities) << " dimensions:" << debugTreeVector(getScaledDimensions()); qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); qCDebug(entities) << "SHAPE EntityItem Ptr:" << this; + qCDebug(entities) << " can cast shadow" << getCanCastShadow(); } void ShapeEntityItem::computeShapeInfo(ShapeInfo& info) { @@ -362,3 +370,24 @@ ShapeType ShapeEntityItem::getShapeType() const { return _collisionShapeType; } +bool ShapeEntityItem::getCanCastShadow() const { + bool result; + withReadLock([&] { + result = _canCastShadow; + }); + return result; +} + +void ShapeEntityItem::setCanCastShadow(bool value) { + bool changed = false; + withWriteLock([&] { + if (_canCastShadow != value) { + changed = true; + _canCastShadow = value; + } + }); + + if (changed) { + emit requestRenderUpdate(); + } +} diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h index 84ce1ce57e..63c4eb58c5 100644 --- a/libraries/entities/src/ShapeEntityItem.h +++ b/libraries/entities/src/ShapeEntityItem.h @@ -101,6 +101,9 @@ public: virtual void computeShapeInfo(ShapeInfo& info) override; virtual ShapeType getShapeType() const override; + bool getCanCastShadow() const; + void setCanCastShadow(bool value); + protected: float _alpha { 1 }; @@ -111,6 +114,8 @@ protected: //! prior functionality where new or unsupported shapes are treated as //! ellipsoids. ShapeType _collisionShapeType{ ShapeType::SHAPE_TYPE_ELLIPSOID }; + + bool _canCastShadow { ENTITY_ITEM_DEFAULT_DONT_CAST_SHADOW }; }; #endif // hifi_ShapeEntityItem_h diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index 53c109dc9f..eef641e369 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -224,7 +224,7 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende const auto setupOutput = task.addJob("ShadowSetup"); const auto queryResolution = setupOutput.getN(2); // Fetch and cull the items from the scene - static const auto shadowCasterFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask); + static const auto shadowCasterFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask).withNoShadowCaster(); const auto fetchInput = FetchSpatialTree::Inputs(shadowCasterFilter, queryResolution).asVarying(); const auto shadowSelection = task.addJob("FetchShadowTree", fetchInput); const auto selectionInputs = FetchSpatialSelection::Inputs(shadowSelection, shadowCasterFilter).asVarying(); @@ -398,7 +398,7 @@ void RenderShadowCascadeSetup::run(const render::RenderContextPointer& renderCon const auto globalShadow = lightStage->getCurrentKeyShadow(); if (globalShadow && _cascadeIndexgetCascadeCount()) { - output.edit0() = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(_tagBits, _tagMask).withShadowCaster(); + output.edit0() = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(_tagBits, _tagMask).withNoShadowCaster(); // Set the keylight render args auto& cascade = globalShadow->getCascade(_cascadeIndex); diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h index ff4b3a0458..7713c1ea7e 100644 --- a/libraries/render/src/render/Item.h +++ b/libraries/render/src/render/Item.h @@ -120,7 +120,10 @@ public: Builder& withDynamic() { _flags.set(DYNAMIC); return (*this); } Builder& withDeformed() { _flags.set(DEFORMED); return (*this); } Builder& withInvisible() { _flags.set(INVISIBLE); return (*this); } + + Builder& withNoShadowCaster() { _flags.reset(SHADOW_CASTER); return (*this); } Builder& withShadowCaster() { _flags.set(SHADOW_CASTER); return (*this); } + Builder& withLayered() { _flags.set(LAYERED); return (*this); } Builder& withTag(Tag tag) { _flags.set(FIRST_TAG_BIT + tag); return (*this); } @@ -155,6 +158,7 @@ public: bool isInvisible() const { return _flags[INVISIBLE]; } bool isShadowCaster() const { return _flags[SHADOW_CASTER]; } + bool isNotShadowCaster() const { return !_flags[SHADOW_CASTER]; } bool isLayered() const { return _flags[LAYERED]; } bool isSpatial() const { return !isLayered(); } From d2c199104e715b33b4ef59922c1e52a010e425b7 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 13 Feb 2018 17:47:49 -0800 Subject: [PATCH 021/260] Avatars use Dual Quaternion Skinning and Model Entities do not. --- .../src/avatars-renderer/SkeletonModel.cpp | 2 + .../render-utils/src/MeshPartPayload.cpp | 4 + libraries/render-utils/src/MeshPartPayload.h | 2 +- libraries/render-utils/src/Model.h | 2 +- .../render-utils/src/RenderPipelines.cpp | 79 ++++++++++++++++++- libraries/render-utils/src/Skinning.slh | 14 ++-- libraries/render-utils/src/skin_model.slv | 1 + libraries/render-utils/src/skin_model_dq.slv | 52 ++++++++++++ .../render-utils/src/skin_model_fade.slv | 1 + .../render-utils/src/skin_model_fade_dq.slv | 54 +++++++++++++ .../src/skin_model_normal_map.slv | 1 + .../src/skin_model_normal_map_dq.slv | 61 ++++++++++++++ .../src/skin_model_normal_map_fade.slv | 1 + .../src/skin_model_normal_map_fade_dq.slv | 61 ++++++++++++++ .../render-utils/src/skin_model_shadow.slv | 1 + .../render-utils/src/skin_model_shadow_dq.slv | 30 +++++++ .../src/skin_model_shadow_fade.slv | 1 + .../src/skin_model_shadow_fade_dq.slv | 33 ++++++++ libraries/render/src/render/ShapePipeline.h | 5 ++ 19 files changed, 392 insertions(+), 13 deletions(-) create mode 100644 libraries/render-utils/src/skin_model_dq.slv create mode 100644 libraries/render-utils/src/skin_model_fade_dq.slv create mode 100644 libraries/render-utils/src/skin_model_normal_map_dq.slv create mode 100644 libraries/render-utils/src/skin_model_normal_map_fade_dq.slv create mode 100644 libraries/render-utils/src/skin_model_shadow_dq.slv create mode 100644 libraries/render-utils/src/skin_model_shadow_fade_dq.slv 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/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 1585a075f5..da3a6d80dd 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -516,6 +516,10 @@ void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, bool isWireframe if (isWireframe) { builder.withWireframe(); } + if (_useDualQuaternionSkinning) { + builder.withDualQuatSkinned(); + } + _shapeKey = builder.build(); } diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 220a0bc48c..cd4d390b1e 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -129,6 +129,7 @@ public: bool _isSkinned{ false }; bool _isBlendShaped { false }; bool _hasTangents { false }; + bool _useDualQuaternionSkinning { false }; private: void initCache(const ModelPointer& model); @@ -136,7 +137,6 @@ private: gpu::BufferPointer _blendedVertexBuffer; render::ShapeKey _shapeKey { render::ShapeKey::Builder::invalid() }; int _layer { render::Item::LAYER_3D }; - bool _useDualQuaternionSkinning { false }; }; namespace render { diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 46dbc90324..9aa4aa6b97 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -415,7 +415,7 @@ protected: virtual void createCollisionRenderItemSet(); bool _isWireframe; - bool _useDualQuaternionSkinning { true }; + 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 ad7409b731..68c1918044 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 quatenion 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/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/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); } From 1632ab9782cd4ed566ca3b9e417994ec657e6a7a Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Tue, 13 Feb 2018 18:27:36 -0800 Subject: [PATCH 022/260] Changed flag to "cast shadow" instead of "don't cast shadow". --- libraries/entities/src/EntityItemProperties.h | 2 +- libraries/entities/src/EntityItemPropertiesDefaults.h | 2 +- libraries/entities/src/EntityPropertyFlags.h | 2 +- libraries/entities/src/ModelEntityItem.cpp | 6 +++--- libraries/entities/src/ModelEntityItem.h | 2 +- libraries/entities/src/ShapeEntityItem.cpp | 6 +++--- libraries/entities/src/ShapeEntityItem.h | 2 +- libraries/render-utils/src/RenderShadowTask.cpp | 4 ++-- libraries/render/src/render/Item.h | 4 ---- 9 files changed, 13 insertions(+), 17 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 731bb9390e..8feebd3979 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -128,7 +128,7 @@ public: DEFINE_PROPERTY_REF(PROP_SCRIPT, Script, script, QString, ENTITY_ITEM_DEFAULT_SCRIPT); DEFINE_PROPERTY(PROP_SCRIPT_TIMESTAMP, ScriptTimestamp, scriptTimestamp, quint64, ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP); DEFINE_PROPERTY_REF(PROP_COLLISION_SOUND_URL, CollisionSoundURL, collisionSoundURL, QString, ENTITY_ITEM_DEFAULT_COLLISION_SOUND_URL); - DEFINE_PROPERTY(PROP_DONT_CAST_SHADOW, CanCastShadow, canCastShadow, bool, ENTITY_ITEM_DEFAULT_DONT_CAST_SHADOW); + DEFINE_PROPERTY(PROP_CAST_SHADOW, CanCastShadow, canCastShadow, bool, ENTITY_ITEM_DEFAULT_CAST_SHADOW); DEFINE_PROPERTY_REF(PROP_COLOR, Color, color, xColor, particle::DEFAULT_COLOR); DEFINE_PROPERTY_REF(PROP_COLOR_SPREAD, ColorSpread, colorSpread, xColor, particle::DEFAULT_COLOR_SPREAD); DEFINE_PROPERTY_REF(PROP_COLOR_START, ColorStart, colorStart, xColor, particle::DEFAULT_COLOR); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index 181ba9bbf2..f85d55dc3a 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -46,7 +46,7 @@ const quint32 ENTITY_ITEM_DEFAULT_STATIC_CERTIFICATE_VERSION = 0; const float ENTITY_ITEM_DEFAULT_ALPHA = 1.0f; const float ENTITY_ITEM_DEFAULT_LOCAL_RENDER_ALPHA = 1.0f; const bool ENTITY_ITEM_DEFAULT_VISIBLE = true; -const bool ENTITY_ITEM_DEFAULT_DONT_CAST_SHADOW { false }; +const bool ENTITY_ITEM_DEFAULT_CAST_SHADOW { true }; const QString ENTITY_ITEM_DEFAULT_SCRIPT = QString(""); const quint64 ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP = 0; diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index fbc4199097..d2de67735c 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -31,7 +31,7 @@ enum EntityPropertyList { PROP_SCRIPT, // these properties are supported by some derived classes - PROP_DONT_CAST_SHADOW, + PROP_CAST_SHADOW, PROP_COLOR, // these are used by models only diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 73cf29bce1..6d15fded31 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -116,7 +116,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, const unsigned char* dataAt = data; bool animationPropertiesChanged = false; - READ_ENTITY_PROPERTY(PROP_DONT_CAST_SHADOW, bool, setCanCastShadow); + READ_ENTITY_PROPERTY(PROP_CAST_SHADOW, bool, setCanCastShadow); READ_ENTITY_PROPERTY(PROP_COLOR, rgbColor, setColor); READ_ENTITY_PROPERTY(PROP_MODEL_URL, QString, setModelURL); READ_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); @@ -153,7 +153,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, EntityPropertyFlags ModelEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); - requestedProperties += PROP_DONT_CAST_SHADOW; + requestedProperties += PROP_CAST_SHADOW; requestedProperties += PROP_MODEL_URL; requestedProperties += PROP_COMPOUND_SHAPE_URL; requestedProperties += PROP_TEXTURES; @@ -178,7 +178,7 @@ void ModelEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit bool successPropertyFits = true; - APPEND_ENTITY_PROPERTY(PROP_DONT_CAST_SHADOW, getCanCastShadow()); + APPEND_ENTITY_PROPERTY(PROP_CAST_SHADOW, getCanCastShadow()); APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_MODEL_URL, getModelURL()); APPEND_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, getCompoundShapeURL()); diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index ec65876d84..49f20c48e2 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -174,7 +174,7 @@ protected: ShapeType _shapeType = SHAPE_TYPE_NONE; - bool _canCastShadow{ ENTITY_ITEM_DEFAULT_DONT_CAST_SHADOW }; + bool _canCastShadow{ ENTITY_ITEM_DEFAULT_CAST_SHADOW }; private: uint64_t _lastAnimated{ 0 }; diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index 2fbb6702f2..22c3bab4f3 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -157,7 +157,7 @@ int ShapeEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, READ_ENTITY_PROPERTY(PROP_SHAPE, QString, setShape); READ_ENTITY_PROPERTY(PROP_COLOR, rgbColor, setColor); READ_ENTITY_PROPERTY(PROP_ALPHA, float, setAlpha); - READ_ENTITY_PROPERTY(PROP_DONT_CAST_SHADOW, bool, setCanCastShadow); + READ_ENTITY_PROPERTY(PROP_CAST_SHADOW, bool, setCanCastShadow); return bytesRead; } @@ -169,7 +169,7 @@ EntityPropertyFlags ShapeEntityItem::getEntityProperties(EncodeBitstreamParams& requestedProperties += PROP_SHAPE; requestedProperties += PROP_COLOR; requestedProperties += PROP_ALPHA; - requestedProperties += PROP_DONT_CAST_SHADOW; + requestedProperties += PROP_CAST_SHADOW; return requestedProperties; } @@ -186,7 +186,7 @@ void ShapeEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit APPEND_ENTITY_PROPERTY(PROP_SHAPE, entity::stringFromShape(getShape())); APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_ALPHA, getAlpha()); - APPEND_ENTITY_PROPERTY(PROP_DONT_CAST_SHADOW, getCanCastShadow()); + APPEND_ENTITY_PROPERTY(PROP_CAST_SHADOW, getCanCastShadow()); } void ShapeEntityItem::setColor(const rgbColor& value) { diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h index 63c4eb58c5..4a08936bce 100644 --- a/libraries/entities/src/ShapeEntityItem.h +++ b/libraries/entities/src/ShapeEntityItem.h @@ -115,7 +115,7 @@ protected: //! ellipsoids. ShapeType _collisionShapeType{ ShapeType::SHAPE_TYPE_ELLIPSOID }; - bool _canCastShadow { ENTITY_ITEM_DEFAULT_DONT_CAST_SHADOW }; + bool _canCastShadow { ENTITY_ITEM_DEFAULT_CAST_SHADOW }; }; #endif // hifi_ShapeEntityItem_h diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index eef641e369..e34f550def 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -224,7 +224,7 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende const auto setupOutput = task.addJob("ShadowSetup"); const auto queryResolution = setupOutput.getN(2); // Fetch and cull the items from the scene - static const auto shadowCasterFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask).withNoShadowCaster(); + static const auto shadowCasterFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask).withShadowCaster(); const auto fetchInput = FetchSpatialTree::Inputs(shadowCasterFilter, queryResolution).asVarying(); const auto shadowSelection = task.addJob("FetchShadowTree", fetchInput); const auto selectionInputs = FetchSpatialSelection::Inputs(shadowSelection, shadowCasterFilter).asVarying(); @@ -398,7 +398,7 @@ void RenderShadowCascadeSetup::run(const render::RenderContextPointer& renderCon const auto globalShadow = lightStage->getCurrentKeyShadow(); if (globalShadow && _cascadeIndexgetCascadeCount()) { - output.edit0() = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(_tagBits, _tagMask).withNoShadowCaster(); + output.edit0() = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(_tagBits, _tagMask).withShadowCaster(); // Set the keylight render args auto& cascade = globalShadow->getCascade(_cascadeIndex); diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h index 7713c1ea7e..ff4b3a0458 100644 --- a/libraries/render/src/render/Item.h +++ b/libraries/render/src/render/Item.h @@ -120,10 +120,7 @@ public: Builder& withDynamic() { _flags.set(DYNAMIC); return (*this); } Builder& withDeformed() { _flags.set(DEFORMED); return (*this); } Builder& withInvisible() { _flags.set(INVISIBLE); return (*this); } - - Builder& withNoShadowCaster() { _flags.reset(SHADOW_CASTER); return (*this); } Builder& withShadowCaster() { _flags.set(SHADOW_CASTER); return (*this); } - Builder& withLayered() { _flags.set(LAYERED); return (*this); } Builder& withTag(Tag tag) { _flags.set(FIRST_TAG_BIT + tag); return (*this); } @@ -158,7 +155,6 @@ public: bool isInvisible() const { return _flags[INVISIBLE]; } bool isShadowCaster() const { return _flags[SHADOW_CASTER]; } - bool isNotShadowCaster() const { return !_flags[SHADOW_CASTER]; } bool isLayered() const { return _flags[LAYERED]; } bool isSpatial() const { return !isLayered(); } From a92765a83a3578572c1ce9951c807d417b6744f5 Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Wed, 14 Feb 2018 12:01:30 -0800 Subject: [PATCH 023/260] Adding can cast shadow property. --- .../entities/src/EntityItemProperties.cpp | 9 +++++ libraries/entities/src/EntityItemProperties.h | 2 +- libraries/entities/src/EntityPropertyFlags.h | 2 +- libraries/entities/src/ModelEntityItem.cpp | 8 ++-- libraries/entities/src/ShapeEntityItem.cpp | 6 +-- scripts/system/html/entityProperties.html | 38 +++++++++---------- scripts/system/html/js/entityProperties.js | 29 ++++++++++++-- 7 files changed, 62 insertions(+), 32 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index cca4e858fa..79c36180d6 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -290,6 +290,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_MODEL_URL, modelURL); CHECK_PROPERTY_CHANGE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL); CHECK_PROPERTY_CHANGE(PROP_VISIBLE, visible); + CHECK_PROPERTY_CHANGE(PROP_CAN_CAST_SHADOW, canCastShadow); CHECK_PROPERTY_CHANGE(PROP_REGISTRATION_POINT, registrationPoint); CHECK_PROPERTY_CHANGE(PROP_ANGULAR_VELOCITY, angularVelocity); CHECK_PROPERTY_CHANGE(PROP_ANGULAR_DAMPING, angularDamping); @@ -625,6 +626,11 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IS_UV_MODE_STRETCH, isUVModeStretch); } + // Models and Shapes + if (_type == EntityTypes::Model || _type == EntityTypes::Shape || _type == EntityTypes::Box || _type == EntityTypes::Sphere) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CAN_CAST_SHADOW, canCastShadow); + } + if (!skipDefaults && !strictSemantics) { AABox aaBox = getAABox(); QScriptValue boundingBox = engine->newObject(); @@ -707,6 +713,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(angularVelocity, glmVec3, setAngularVelocity); COPY_PROPERTY_FROM_QSCRIPTVALUE(angularDamping, float, setAngularDamping); COPY_PROPERTY_FROM_QSCRIPTVALUE(visible, bool, setVisible); + COPY_PROPERTY_FROM_QSCRIPTVALUE(canCastShadow, bool, setCanCastShadow); COPY_PROPERTY_FROM_QSCRIPTVALUE(color, xColor, setColor); COPY_PROPERTY_FROM_QSCRIPTVALUE(colorSpread, xColor, setColorSpread); COPY_PROPERTY_FROM_QSCRIPTVALUE(colorStart, xColor, setColorStart); @@ -1851,6 +1858,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int properties.getType() == EntityTypes::Box || properties.getType() == EntityTypes::Sphere) { READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE, QString, setShape); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow); } READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_NAME, QString, setName); @@ -1974,6 +1982,7 @@ void EntityItemProperties::markAllChanged() { _angularDampingChanged = true; _nameChanged = true; _visibleChanged = true; + _canCastShadowChanged = true; _colorChanged = true; _alphaChanged = true; _modelURLChanged = true; diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 8feebd3979..ec10910092 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -128,7 +128,7 @@ public: DEFINE_PROPERTY_REF(PROP_SCRIPT, Script, script, QString, ENTITY_ITEM_DEFAULT_SCRIPT); DEFINE_PROPERTY(PROP_SCRIPT_TIMESTAMP, ScriptTimestamp, scriptTimestamp, quint64, ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP); DEFINE_PROPERTY_REF(PROP_COLLISION_SOUND_URL, CollisionSoundURL, collisionSoundURL, QString, ENTITY_ITEM_DEFAULT_COLLISION_SOUND_URL); - DEFINE_PROPERTY(PROP_CAST_SHADOW, CanCastShadow, canCastShadow, bool, ENTITY_ITEM_DEFAULT_CAST_SHADOW); + DEFINE_PROPERTY(PROP_CAN_CAST_SHADOW, CanCastShadow, canCastShadow, bool, ENTITY_ITEM_DEFAULT_CAST_SHADOW); DEFINE_PROPERTY_REF(PROP_COLOR, Color, color, xColor, particle::DEFAULT_COLOR); DEFINE_PROPERTY_REF(PROP_COLOR_SPREAD, ColorSpread, colorSpread, xColor, particle::DEFAULT_COLOR_SPREAD); DEFINE_PROPERTY_REF(PROP_COLOR_START, ColorStart, colorStart, xColor, particle::DEFAULT_COLOR); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index d2de67735c..ab17f2a873 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -31,7 +31,7 @@ enum EntityPropertyList { PROP_SCRIPT, // these properties are supported by some derived classes - PROP_CAST_SHADOW, + PROP_CAN_CAST_SHADOW, PROP_COLOR, // these are used by models only diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 6d15fded31..7c14d8a4a0 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -116,7 +116,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, const unsigned char* dataAt = data; bool animationPropertiesChanged = false; - READ_ENTITY_PROPERTY(PROP_CAST_SHADOW, bool, setCanCastShadow); + READ_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow); READ_ENTITY_PROPERTY(PROP_COLOR, rgbColor, setColor); READ_ENTITY_PROPERTY(PROP_MODEL_URL, QString, setModelURL); READ_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); @@ -153,7 +153,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, EntityPropertyFlags ModelEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); - requestedProperties += PROP_CAST_SHADOW; + requestedProperties += PROP_CAN_CAST_SHADOW; requestedProperties += PROP_MODEL_URL; requestedProperties += PROP_COMPOUND_SHAPE_URL; requestedProperties += PROP_TEXTURES; @@ -178,7 +178,7 @@ void ModelEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit bool successPropertyFits = true; - APPEND_ENTITY_PROPERTY(PROP_CAST_SHADOW, getCanCastShadow()); + APPEND_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, getCanCastShadow()); APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_MODEL_URL, getModelURL()); APPEND_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, getCompoundShapeURL()); @@ -619,6 +619,7 @@ void ModelEntityItem::setColor(const rgbColor& value) { }); } +#pragma optimize("", off) void ModelEntityItem::setColor(const xColor& value) { withWriteLock([&] { _color[RED_INDEX] = value.red; @@ -666,7 +667,6 @@ bool ModelEntityItem::getAnimationLoop() const { }); } - void ModelEntityItem::setAnimationHold(bool hold) { withWriteLock([&] { _animationProperties.setHold(hold); diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index 22c3bab4f3..9d81e850df 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -157,7 +157,7 @@ int ShapeEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, READ_ENTITY_PROPERTY(PROP_SHAPE, QString, setShape); READ_ENTITY_PROPERTY(PROP_COLOR, rgbColor, setColor); READ_ENTITY_PROPERTY(PROP_ALPHA, float, setAlpha); - READ_ENTITY_PROPERTY(PROP_CAST_SHADOW, bool, setCanCastShadow); + READ_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow); return bytesRead; } @@ -169,7 +169,7 @@ EntityPropertyFlags ShapeEntityItem::getEntityProperties(EncodeBitstreamParams& requestedProperties += PROP_SHAPE; requestedProperties += PROP_COLOR; requestedProperties += PROP_ALPHA; - requestedProperties += PROP_CAST_SHADOW; + requestedProperties += PROP_CAN_CAST_SHADOW; return requestedProperties; } @@ -186,7 +186,7 @@ void ShapeEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit APPEND_ENTITY_PROPERTY(PROP_SHAPE, entity::stringFromShape(getShape())); APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_ALPHA, getAlpha()); - APPEND_ENTITY_PROPERTY(PROP_CAST_SHADOW, getCanCastShadow()); + APPEND_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, getCanCastShadow()); } void ShapeEntityItem::setColor(const rgbColor& value) { diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 856ca3c6e1..1420e85b11 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -42,25 +42,28 @@
-
+
+ +
@@ -293,7 +296,6 @@
-
BehaviorM @@ -365,8 +367,6 @@
- -
LightM @@ -400,7 +400,6 @@
-
ModelM @@ -484,7 +483,6 @@
-
ZoneM diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 7e8827a9b5..846a1da21a 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -595,6 +595,8 @@ function loaded() { var elShape = document.getElementById("property-shape"); + var elCanCastShadow = document.getElementById("property-can-cast-shadow"); + var elLightSpotLight = document.getElementById("property-light-spot-light"); var elLightColor = document.getElementById("property-light-color"); var elLightColorRed = document.getElementById("property-light-color-red"); @@ -799,7 +801,6 @@ function loaded() { elLocked.checked = properties.locked; - elName.value = properties.name; elVisible.checked = properties.visible; @@ -966,6 +967,12 @@ function loaded() { properties.color.green + "," + properties.color.blue + ")"; } + if (properties.type === "Model" || + properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { + + elCanCastShadow = properties.canCastShadow; + } + if (properties.type === "Model") { elModelURL.value = properties.modelURL; elShapeType.value = properties.shapeType; @@ -1012,7 +1019,6 @@ function loaded() { elLightFalloffRadius.value = properties.falloffRadius.toFixed(1); elLightExponent.value = properties.exponent.toFixed(2); elLightCutoff.value = properties.cutoff.toFixed(2); - } else if (properties.type === "Zone") { // Key light elZoneKeyLightModeInherit.checked = (properties.keyLightMode === 'inherit'); @@ -1093,13 +1099,15 @@ function loaded() { // Show/hide sections as required showElements(document.getElementsByClassName('skybox-section'), elZoneSkyboxModeEnabled.checked); + showElements(document.getElementsByClassName('keylight-section'), elZoneKeyLightModeEnabled.checked); + showElements(document.getElementsByClassName('ambient-section'), elZoneAmbientLightModeEnabled.checked); + showElements(document.getElementsByClassName('haze-section'), elZoneHazeModeEnabled.checked); - } else if (properties.type === "PolyVox") { elVoxelVolumeSizeX.value = properties.voxelVolumeSize.x.toFixed(2); elVoxelVolumeSizeY.value = properties.voxelVolumeSize.y.toFixed(2); @@ -1111,6 +1119,15 @@ function loaded() { elZTextureURL.value = properties.zTextureURL; } + // Only these types can cast a shadow + if (properties.type === "Model" || + properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { + + showElements(document.getElementsByClassName('can-cast-shadow-section'), true); + } else { + showElements(document.getElementsByClassName('can-cast-shadow-section'), false); + } + if (properties.locked) { disableProperties(); elLocked.removeAttribute('disabled'); @@ -1356,6 +1373,12 @@ function loaded() { elShape.addEventListener('change', createEmitTextPropertyUpdateFunction('shape')); + if (properties.type === "Model" || + properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { + + elCanCastShadow.addEventListener('change', createEmitTextPropertyUpdateFunction('canCastShadow')); + } + elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); elWebDPI.addEventListener('change', createEmitNumberPropertyUpdateFunction('dpi', 0)); From 611c67bf2f6914a7df7edc49be24477ded182572 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Thu, 15 Feb 2018 02:00:51 +0300 Subject: [PATCH 024/260] FB12297 - HMD: Disabled preview mode only occurs during step 2 of the wallet setup wizard --- interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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'}); } } From a679b6f82841b701c06c313b57d18de1c3d1c672 Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Wed, 14 Feb 2018 18:55:04 -0800 Subject: [PATCH 025/260] Fixed possible crash. --- libraries/render-utils/src/DeferredLightingEffect.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 2e77d702c6..665e767c7c 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -550,7 +550,9 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, if (lightStage && lightStage->_currentFrame._sunLights.size()) { graphics::LightPointer keyLight = lightStage->getLight(lightStage->_currentFrame._sunLights.front()); - keyLightCastShadows = keyLight->getCastShadows(); + if (keyLight) { + keyLightCastShadows = keyLight->getCastShadows(); + } } if (deferredLightingEffect->_shadowMapEnabled && keyLightCastShadows) { From ef771b6db49ff48655fe772d01b9f6683e1e3234 Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Wed, 14 Feb 2018 19:11:13 -0800 Subject: [PATCH 026/260] Minor indentation correction. --- scripts/system/html/entityProperties.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 1cfa5c3899..7dbeae067c 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -678,7 +678,7 @@ min="-1000" max="50000" step="10"> -
+
From 12f4b8dbb19321c74c7b967b569627b87b8c23aa Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Wed, 14 Feb 2018 19:12:24 -0800 Subject: [PATCH 027/260] Corrected event listener for canCastShadow - still bad. --- scripts/system/html/js/entityProperties.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 9502e9f4d4..8868159848 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1392,12 +1392,8 @@ function loaded() { elShape.addEventListener('change', createEmitTextPropertyUpdateFunction('shape')); - if (properties.type === "Model" || - properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { - - elCanCastShadow.addEventListener('change', createEmitTextPropertyUpdateFunction('canCastShadow')); - } - + elCanCastShadow.addEventListener('change', createEmitCheckedPropertyUpdateFunction('canCastShadow')); + elImageURL.addEventListener('change', createImageURLUpdateFunction('textures')); elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); From 26e7a85a955db374b526f8a117585ff6c47ccbd2 Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Wed, 14 Feb 2018 21:15:29 -0800 Subject: [PATCH 028/260] Fixed possible crash. --- libraries/render-utils/src/RenderShadowTask.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index e34f550def..24a14a697c 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -386,6 +386,10 @@ void RenderShadowCascadeSetup::run(const render::RenderContextPointer& renderCon assert(lightStage); // Exit if current keylight does not cast shadows + if (!lightStage->getCurrentKeyLight()) { + return; + } + bool castShadows = lightStage->getCurrentKeyLight()->getCastShadows(); if (!castShadows) { output.edit0() = ItemFilter::Builder::nothing(); From adb02d69f9c50721d9e186870ae8ca6cf201a413 Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Wed, 14 Feb 2018 21:45:56 -0800 Subject: [PATCH 029/260] WIP -adding canCastShadow flag. --- libraries/entities/src/EntityItemProperties.cpp | 5 +++++ libraries/entities/src/EntityItemProperties.h | 3 ++- .../entities/src/EntityItemPropertiesDefaults.h | 3 ++- libraries/entities/src/ModelEntityItem.h | 2 +- libraries/entities/src/ShapeEntityItem.h | 2 +- scripts/system/html/js/entityProperties.js | 16 ++++++++-------- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 79c36180d6..3c3c0742da 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -878,6 +878,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(angularVelocity); COPY_PROPERTY_IF_CHANGED(angularDamping); COPY_PROPERTY_IF_CHANGED(visible); + COPY_PROPERTY_IF_CHANGED(canCastShadow); COPY_PROPERTY_IF_CHANGED(color); COPY_PROPERTY_IF_CHANGED(colorSpread); COPY_PROPERTY_IF_CHANGED(colorStart); @@ -1050,6 +1051,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue std::call_once(initMap, [](){ ADD_PROPERTY_TO_MAP(PROP_VISIBLE, Visible, visible, bool); + ADD_PROPERTY_TO_MAP(PROP_CAN_CAST_SHADOW, CanCastShadow, canCastShadow, bool); ADD_PROPERTY_TO_MAP(PROP_POSITION, Position, position, glm::vec3); ADD_PROPERTY_TO_MAP(PROP_DIMENSIONS, Dimensions, dimensions, glm::vec3); ADD_PROPERTY_TO_MAP(PROP_ROTATION, Rotation, rotation, glm::quat); @@ -2172,6 +2174,9 @@ QList EntityItemProperties::listChangedProperties() { if (visibleChanged()) { out += "visible"; } + if (canCastShadowChanged()) { + out += "canCastShadow"; + } if (rotationChanged()) { out += "rotation"; } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index ec10910092..dcec1a1f81 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -128,7 +128,7 @@ public: DEFINE_PROPERTY_REF(PROP_SCRIPT, Script, script, QString, ENTITY_ITEM_DEFAULT_SCRIPT); DEFINE_PROPERTY(PROP_SCRIPT_TIMESTAMP, ScriptTimestamp, scriptTimestamp, quint64, ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP); DEFINE_PROPERTY_REF(PROP_COLLISION_SOUND_URL, CollisionSoundURL, collisionSoundURL, QString, ENTITY_ITEM_DEFAULT_COLLISION_SOUND_URL); - DEFINE_PROPERTY(PROP_CAN_CAST_SHADOW, CanCastShadow, canCastShadow, bool, ENTITY_ITEM_DEFAULT_CAST_SHADOW); + DEFINE_PROPERTY(PROP_CAN_CAST_SHADOW, CanCastShadow, canCastShadow, bool, ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW); DEFINE_PROPERTY_REF(PROP_COLOR, Color, color, xColor, particle::DEFAULT_COLOR); DEFINE_PROPERTY_REF(PROP_COLOR_SPREAD, ColorSpread, colorSpread, xColor, particle::DEFAULT_COLOR_SPREAD); DEFINE_PROPERTY_REF(PROP_COLOR_START, ColorStart, colorStart, xColor, particle::DEFAULT_COLOR); @@ -415,6 +415,7 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, Velocity, velocity, "in meters"); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Name, name, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Visible, visible, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, CanCastShadow, canCastShadow, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Rotation, rotation, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Density, density, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Gravity, gravity, ""); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index f85d55dc3a..efbf45ce8d 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -46,7 +46,8 @@ const quint32 ENTITY_ITEM_DEFAULT_STATIC_CERTIFICATE_VERSION = 0; const float ENTITY_ITEM_DEFAULT_ALPHA = 1.0f; const float ENTITY_ITEM_DEFAULT_LOCAL_RENDER_ALPHA = 1.0f; const bool ENTITY_ITEM_DEFAULT_VISIBLE = true; -const bool ENTITY_ITEM_DEFAULT_CAST_SHADOW { true }; +const bool ENTITY_ITEM_DEFAULT_CAST_SHADOWS { true }; +const bool ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW { false }; const QString ENTITY_ITEM_DEFAULT_SCRIPT = QString(""); const quint64 ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP = 0; diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 49f20c48e2..791eebb7d9 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -174,7 +174,7 @@ protected: ShapeType _shapeType = SHAPE_TYPE_NONE; - bool _canCastShadow{ ENTITY_ITEM_DEFAULT_CAST_SHADOW }; + bool _canCastShadow{ ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW }; private: uint64_t _lastAnimated{ 0 }; diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h index 4a08936bce..4bc008f761 100644 --- a/libraries/entities/src/ShapeEntityItem.h +++ b/libraries/entities/src/ShapeEntityItem.h @@ -115,7 +115,7 @@ protected: //! ellipsoids. ShapeType _collisionShapeType{ ShapeType::SHAPE_TYPE_ELLIPSOID }; - bool _canCastShadow { ENTITY_ITEM_DEFAULT_CAST_SHADOW }; + bool _canCastShadow { ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW }; }; #endif // hifi_ShapeEntityItem_h diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 8868159848..fca43c4665 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -802,11 +802,11 @@ function loaded() { // HTML workaround since image is not yet a separate entity type var IMAGE_MODEL_NAME = 'default-image-model.fbx'; - var urlParts = properties.modelURL.split('/') - var propsFilename = urlParts[urlParts.length - 1]; - if (properties.type === "Model" && propsFilename === IMAGE_MODEL_NAME) { - properties.type = "Image"; - } +//// var urlParts = properties.modelURL.split('/') +//// var propsFilename = urlParts[urlParts.length - 1]; +//// if (properties.type === "Model" && propsFilename === IMAGE_MODEL_NAME) { +//// properties.type = "Image"; +//// } // Create class name for css ruleset filtering elPropertiesList.className = properties.type + 'Menu'; @@ -983,11 +983,11 @@ function loaded() { properties.color.green + "," + properties.color.blue + ")"; } - if (properties.type === "Model" || - properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { + //if (properties.type === "Model" || + // properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { elCanCastShadow = properties.canCastShadow; - } + //} if (properties.type === "Model") { elModelURL.value = properties.modelURL; From 7e99570824418cc1a57b72cad44ddd8c386a1bff Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Thu, 15 Feb 2018 07:54:55 -0800 Subject: [PATCH 030/260] Fixed Ubuntu warnings. --- libraries/entities/src/ModelEntityItem.cpp | 1 - libraries/graphics/src/graphics/Light.cpp | 2 +- libraries/graphics/src/graphics/Light.h | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 7c14d8a4a0..b1edd47a67 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -619,7 +619,6 @@ void ModelEntityItem::setColor(const rgbColor& value) { }); } -#pragma optimize("", off) void ModelEntityItem::setColor(const xColor& value) { withWriteLock([&] { _color[RED_INDEX] = value.red; diff --git a/libraries/graphics/src/graphics/Light.cpp b/libraries/graphics/src/graphics/Light.cpp index 50601299dd..76d8a6030a 100755 --- a/libraries/graphics/src/graphics/Light.cpp +++ b/libraries/graphics/src/graphics/Light.cpp @@ -69,7 +69,7 @@ void Light::setCastShadows(const bool castShadows) { _castShadows = castShadows; } -const bool Light::getCastShadows() const { +bool Light::getCastShadows() const { return _castShadows; } diff --git a/libraries/graphics/src/graphics/Light.h b/libraries/graphics/src/graphics/Light.h index ebe22d5593..bb9fb3e5b9 100755 --- a/libraries/graphics/src/graphics/Light.h +++ b/libraries/graphics/src/graphics/Light.h @@ -104,7 +104,7 @@ public: const Vec3& getDirection() const; void setCastShadows(const bool castShadows); - const bool getCastShadows() const; + bool getCastShadows() const; void setOrientation(const Quat& orientation); const glm::quat& getOrientation() const { return _transform.getRotation(); } From cb9327e03020b7cd6965c73cf7db2177eb46e7d9 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 10 Jan 2018 13:09:22 -0800 Subject: [PATCH 031/260] Add entity file sync and domain content backups --- .clang-format | 10 +- assignment-client/CMakeLists.txt | 1 + assignment-client/src/Agent.cpp | 1 - .../src/entities/EntityServer.cpp | 2 - assignment-client/src/entities/EntityServer.h | 13 +- assignment-client/src/octree/OctreeServer.cpp | 201 ++++++------ assignment-client/src/octree/OctreeServer.h | 21 +- .../src/scripts/EntityScriptServer.cpp | 2 +- domain-server/CMakeLists.txt | 12 +- .../src/DomainContentBackupManager.cpp | 303 ++++++++++++++++++ .../src/DomainContentBackupManager.h | 88 +++++ domain-server/src/DomainServer.cpp | 213 +++++++++++- domain-server/src/DomainServer.h | 28 +- interface/src/Application.cpp | 8 +- interface/src/ui/DomainConnectionModel.cpp | 2 +- libraries/entities/src/EntityTree.cpp | 10 + libraries/image/CMakeLists.txt | 3 +- libraries/networking/src/LimitedNodeList.cpp | 1 + libraries/networking/src/ThreadedAssignment.h | 6 +- libraries/networking/src/udt/PacketHeaders.h | 7 + libraries/networking/src/udt/Socket.cpp | 4 +- libraries/octree/CMakeLists.txt | 1 + libraries/octree/src/Octree.cpp | 54 +++- libraries/octree/src/Octree.h | 14 +- libraries/octree/src/OctreePersistThread.cpp | 102 +++--- libraries/octree/src/OctreePersistThread.h | 11 +- libraries/octree/src/OctreeUtils.cpp | 77 +++++ libraries/octree/src/OctreeUtils.h | 23 ++ libraries/shared/src/SharedUtil.cpp | 6 +- libraries/shared/src/SharedUtil.h | 1 + 30 files changed, 1026 insertions(+), 199 deletions(-) create mode 100644 domain-server/src/DomainContentBackupManager.cpp create mode 100644 domain-server/src/DomainContentBackupManager.h diff --git a/.clang-format b/.clang-format index f000a27017..507b1eb232 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 +AllowShortFunctionsOnASingleLine: InlineOnly +BreakConstructorInitializers: BeforeColon BreakConstructorInitializersBeforeComma: true IndentCaseLabels: true -ReflowComments: false +ReflowComments: false Cpp11BracedListStyle: false ContinuationIndentWidth: 4 ConstructorInitializerAllOnOneLineOrOnePerLine: false diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index c73e8e1d34..3de4c5fd3f 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -6,6 +6,7 @@ setup_hifi_project(Core Gui Network Script Quick WebSockets) if (APPLE) set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "@executable_path/../Frameworks") endif () +set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "/testing/") setup_memory_debugger() 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/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/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 42494ea7ee..e78f9f108b 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,19 @@ void OctreeServer::readConfiguration() { _persistFilePath = getMyDefaultPersistFilename(); } + // 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); + } + qDebug() << "persistFilePath=" << _persistFilePath; + qDebug() << "persisAbsoluteFilePath=" << _persistAbsoluteFilePath; _persistAsFileType = "json.gz"; @@ -1200,20 +1134,90 @@ 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 (OctreeUtils::readOctreeDataInfoFromFile(_persistAbsoluteFilePath, &data)) { + 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) { + 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::RawOctreeData data; + qCDebug(octree_server) << "Reading octree data from" << _persistAbsoluteFilePath; + if (OctreeUtils::readOctreeDataInfoFromFile(_persistAbsoluteFilePath, &data)) { + if (data.id.isNull()) { + qCDebug(octree_server) << "Current octree data has a null id, updating"; + data.id = QUuid::createUuid(); + data.version = 0; + + QFile file(_persistAbsoluteFilePath); + if (file.open(QIODevice::WriteOnly)) { + auto entityData = data.toByteArray(); + file.write(entityData); + file.close(); + } else { + qCDebug(octree_server) << "Failed to update octree data"; + } + } + } + } + beginRunning(replaceData); +} + +void OctreeServer::beginRunning(QByteArray replaceData) { + if (_state == OctreeServerState::Running) { + qCWarning(octree_server) << "Server is already running"; + return; + } + + _state = OctreeServerState::Running; 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 +1237,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 +1321,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..6f77920ee0 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,9 @@ 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 handleOctreeFileReplacement(QSharedPointer message); + //void handleOctreeFileReplacementFromURL(QSharedPointer message); + void handleOctreeDataFileReply(QSharedPointer message); void removeSendThread(); protected: @@ -159,11 +172,13 @@ 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); + //void replaceContentFromMessageData(QByteArray content); int _argc; const char** _argv; 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 c1e275e4d3..0e958b9537 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -22,7 +22,17 @@ setup_memory_debugger() symlink_or_copy_directory_beside_target(${_SHOULD_SYMLINK_RESOURCES} "${CMAKE_CURRENT_SOURCE_DIR}/resources" "resources") # link the shared hifi libraries -link_hifi_libraries(embedded-webserver networking shared avatars) +link_hifi_libraries(embedded-webserver networking shared avatars octree) + +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/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp new file mode 100644 index 0000000000..0eca10f8af --- /dev/null +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -0,0 +1,303 @@ +// +// DomainContentBackupManager.cpp +// libraries/octree/src +// +// Created by Brad Hefta-Gaub on 8/21/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 +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "DomainServer.h" +#include "DomainContentBackupManager.h" +const int DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds + +// Backup format looks like: daily_backup-TIMESTAMP.zip +const static QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" }; +const static QString DATETIME_FORMAT_RE("\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}"); + +void DomainContentBackupManager::addCreateBackupHandler(CreateBackupHandler handler) { + _backupHandlers.push_back(handler); +} + +DomainContentBackupManager::DomainContentBackupManager(const QString& backupDirectory, + const QJsonObject& settings, + int persistInterval, + bool debugTimestampNow) + : _backupDirectory(backupDirectory), + _persistInterval(persistInterval), + _initialLoadComplete(false), + _loadTimeUSecs(0), + _lastCheck(0), + _debugTimestampNow(debugTimestampNow), + _lastTimeDebug(0) { + parseSettings(settings); +} + +void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { + qDebug() << settings << settings["backups"] << settings["backups"].isArray(); + if (settings["backups"].isArray()) { + const QJsonArray& backupRules = settings["backups"].toArray(); + qCDebug(domain_server) << "BACKUP RULES:"; + + for (const QJsonValue& value : backupRules) { + QJsonObject obj = value.toObject(); + + int interval = 0; + int count = 0; + + QJsonValue intervalVal = obj["backupInterval"]; + if (intervalVal.isString()) { + interval = intervalVal.toString().toInt(); + } else { + interval = intervalVal.toInt(); + } + + QJsonValue countVal = obj["maxBackupVersions"]; + if (countVal.isString()) { + count = countVal.toString().toInt(); + } else { + count = countVal.toInt(); + } + + auto name = obj["Name"].toString(); + auto format = obj["format"].toString(); + format = name.replace(" ", "_").toLower() + "-"; + + 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 << newRule; + } + } else { + qCDebug(domain_server) << "BACKUP RULES: NONE"; + } +} + +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; +} + +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)); + + int64_t now = usecTimestampNow(); + int64_t sinceLastSave = now - _lastCheck; + int64_t intervalToCheck = _persistInterval * MSECS_TO_USECS; + + if (sinceLastSave > intervalToCheck) { + _lastCheck = now; + persist(); + } + } + + // if we were asked to debugTimestampNow do that now... + if (_debugTimestampNow) { + + quint64 now = usecTimestampNow(); + quint64 sinceLastDebug = now - _lastTimeDebug; + quint64 DEBUG_TIMESTAMP_INTERVAL = 600000000; // every 10 minutes + + if (sinceLastDebug > DEBUG_TIMESTAMP_INTERVAL) { + _lastTimeDebug = usecTimestampNow(true); // ask for debug output + } + } + + return isStillRunning(); +} + +void DomainContentBackupManager::aboutToFinish() { + qCDebug(domain_server) << "Persist thread about to finish..."; + persist(); +} + +void DomainContentBackupManager::persist() { + QDir backupDir { _backupDirectory }; + backupDir.mkpath("."); + + // create our "lock" file to indicate we're saving. + QString lockFileName = _backupDirectory + "/running.lock"; + + std::ofstream lockFile(qPrintable(lockFileName), std::ios::out | std::ios::binary); + if (lockFile.is_open()) { + backup(); + + lockFile.close(); + remove(qPrintable(lockFileName)); + } +} + +bool DomainContentBackupManager::getMostRecentBackup(const QString& format, + QString& mostRecentBackupFileName, + QDateTime& mostRecentBackupTime) { + QRegExp formatRE { QRegExp::escape(format) + "(" + DATETIME_FORMAT_RE + ")" + "\\.zip" }; + + QStringList filters; + filters << 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::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({ rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); + + int backupsToDelete = matchingFiles.length() - rule.maxBackupVersions; + 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 rolling 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..."; + + auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); + auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; + auto zip = new QuaZip(_backupDirectory + "/" + fileName); + zip->open(QuaZip::mdAdd); + + for (auto& handler : _backupHandlers) { + handler(zip); + } + + zip->close(); + + qDebug() << "Created backup: " << fileName; + + removeOldBackupVersions(rule); + + if (rule.maxBackupVersions > 0) { + // Execute backup + auto result = true; + if (result) { + qCDebug(domain_server) << "DONE backing up persist file..."; + rule.lastBackupSeconds = nowSeconds; + } else { + qCDebug(domain_server) << "ERROR in backing up persist file..."; + perror("ERROR in backing up persist file"); + } + } else { + qCDebug(domain_server) << "This backup rule" << rule.name << " has Max Rolled Backup Versions less than 1 [" + << rule.maxBackupVersions << "]." + << " There are no backups to be done..."; + } + } else { + qCDebug(domain_server) << "Backup not needed for this rule [" << rule.name << "]..."; + } + } +} diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h new file mode 100644 index 0000000000..20408fe486 --- /dev/null +++ b/domain-server/src/DomainContentBackupManager.h @@ -0,0 +1,88 @@ +// +// DomainContentBackupManager.h +// libraries/octree/src +// +// Created by Brad Hefta-Gaub on 8/21/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 +// + +#ifndef hifi_DomainContentBackupManager_h +#define hifi_DomainContentBackupManager_h + +#include +#include +#include + +#include +#include + +#include + +using BackupResult = std::vector; +using CreateBackupHandler = std::function; +using RecoverBackupHandler = std::function; + +class DomainContentBackupManager : public GenericThread { + Q_OBJECT +public: + class BackupRule { + public: + QString name; + int intervalSeconds; + QString extensionFormat; + int maxBackupVersions; + qint64 lastBackupSeconds; + }; + + static const int DEFAULT_PERSIST_INTERVAL; + + DomainContentBackupManager(const QString& rootBackupDirectory, + const QJsonObject& settings, + int persistInterval = DEFAULT_PERSIST_INTERVAL, + bool debugTimestampNow = false); + + void addCreateBackupHandler(CreateBackupHandler handler); + bool isInitialLoadComplete() const { return _initialLoadComplete; } + int64_t getLoadElapsedTime() const { return _loadTimeUSecs; } + + void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist + + void replaceData(QByteArray data); + +signals: + void loadCompleted(); + +protected: + /// Implements generic processing behavior for this thread. + bool process() override; + + void persist(); + void backup(); + void removeOldBackupVersions(const BackupRule& rule); + bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); + int64_t getMostRecentBackupTimeInSecs(const QString& format); + void parseSettings(const QJsonObject& settings); + +private: + QString _backupDirectory; + std::vector _backupHandlers; + int _persistInterval; + bool _initialLoadComplete; + + int64_t _loadTimeUSecs; + + time_t _lastPersistTime; + int64_t _lastCheck; + bool _wantBackup{ true }; + QVector _backupRules; + + bool _debugTimestampNow; + int64_t _lastTimeDebug; +}; + +#endif // hifi_DomainContentBackupManager_h diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 68a36195d9..e083710d35 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -47,7 +48,14 @@ #include "DomainServerNodeData.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; @@ -280,6 +288,30 @@ DomainServer::DomainServer(int argc, char* argv[]) : qDebug() << "Ignoring subnet in whitelist, invalid ip portion: " << subnet; } } + + qDebug() << "Starting persist thread"; + if (QDir(getEntitiesDirPath()).mkpath(".")) { + qCDebug(domain_server) << "Created entities data directory"; + } + maybeHandleReplacementEntityFile(); + auto entitiesFilePath = getEntitiesFilePath(); + _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.responseObjectForType("6")["entity_server_settings"].toObject())); + _contentManager->addCreateBackupHandler([entitiesFilePath](QuaZip* zip) { + qDebug() << "Creating a backup from handler"; + + QFile entitiesFile { entitiesFilePath }; + + if (entitiesFile.open(QIODevice::ReadOnly)) { + QuaZipFile zipFile { zip }; + zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("models.json.gz", entitiesFilePath)); + zipFile.write(entitiesFile.readAll()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qDebug() << "Failed to write entities file to backup:" << zipFile.getZipError(); + } + } + }); + _contentManager->initialize(true); } void DomainServer::parseCommandLine() { @@ -352,6 +384,11 @@ DomainServer::~DomainServer() { // destroy the LimitedNodeList before the DomainServer QCoreApplication is down DependencyManager::destroy(); + + if (_contentManager) { + _contentManager->aboutToFinish(); + _contentManager->terminating(); + } } void DomainServer::queuedQuit(QString quitMessage, int exitCode) { @@ -691,6 +728,12 @@ 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); @@ -1605,6 +1648,7 @@ void DomainServer::sendHeartbeatToIceServer() { qWarning() << "Waiting for keypair generation to complete before sending ICE heartbeat."; if (!limitedNodeList->getSessionUUID().isNull()) { + qDebug() << "generating keypair"; accountManager->generateNewDomainKeypair(limitedNodeList->getSessionUUID()); } else { qWarning() << "Attempting to send ICE server heartbeat with no domain ID. This is not supported"; @@ -1695,10 +1739,88 @@ 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(); + + QFile f(filePath); + if (f.open(QIODevice::WriteOnly)) { + f.write(data); + OctreeUtils::RawOctreeData octreeData; + if (OctreeUtils::readOctreeDataInfoFromData(data, &octreeData)) { + qCDebug(domain_server) << "Wrote new entiteis file" << octreeData.id << octreeData.version; + } else { + qCDebug(domain_server) << "Failed to read new octree data info"; + } + } else { + qCDebug(domain_server) << "Failed to write new entities file"; + } +} + +QString DomainServer::getContentBackupDir() { + return PathUtils::getAppDataFilePath("backup"); +} + +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(); + + bool remoteHasExistingData { false }; + QUuid id; + int version; + message->readPrimitive(&remoteHasExistingData); + if (remoteHasExistingData) { + auto idData = message->read(16); + 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(); + + //QFile file(entityFilePath); + auto reply = NLPacketList::create(PacketType::OctreeDataFileReply, QByteArray(), true, true); + OctreeUtils::RawOctreeData data; + if (OctreeUtils::readOctreeDataInfoFromFile(entityFilePath, &data)) { + 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"; + 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) { @@ -3105,9 +3227,64 @@ void DomainServer::setupGroupCacheRefresh() { } } +void DomainServer::maybeHandleReplacementEntityFile() { + QFile replacementFile(getEntitiesReplacementFilePath()); + if (replacementFile.exists()) { + qCDebug(domain_server) << "Replacing existing entity date with replacement file"; + QFile currentFile(getEntitiesFilePath()); + if (currentFile.exists()) { + if (currentFile.remove()) { + qCDebug(domain_server) << "Removed existing entity file"; + } else { + qCWarning(domain_server) << "Failled to remove existing entity file"; + } + } + if (replacementFile.rename(getEntitiesFilePath())) { + qCDebug(domain_server) << "Successfully updated entities data file with replacement file"; + } else { + qCWarning(domain_server) << "Failed to update entities data file with replacement file"; + } + } +} + void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) { // enumerate the nodes and find any octree type servers with active sockets + //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::RawOctreeData data; + if (OctreeUtils::readOctreeDataInfoFromData(jsonOctree, &data)) { + data.id = QUuid::createUuid(); + data.version = 0; + + 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"; + } + + + return; auto limitedNodeList = DependencyManager::get(); limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) { return node->getType() == NodeType::EntityServer && node->getActiveSocket(); @@ -3121,3 +3298,37 @@ void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) { limitedNodeList->sendPacketList(std::move(octreeFilePacketList), *octreeNode); }); } + +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); + 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..ee0350665e 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -32,9 +32,14 @@ #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/interface/src/Application.cpp b/interface/src/Application.cpp index 5a340f471e..c031c0e8d4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6240,13 +6240,15 @@ bool Application::askToReplaceDomainContent(const QString& url) { // 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(); + auto limitedNodeList = DependencyManager::get(); + const auto& domainHandler = limitedNodeList->getDomainHandler(); limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) { return node->getType() == NodeType::EntityServer && node->getActiveSocket(); - }, [&urlData, limitedNodeList](const SharedNodePointer& octreeNode) { + }, [&urlData, limitedNodeList, &domainHandler](const SharedNodePointer& octreeNode) { auto octreeFilePacket = NLPacket::create(PacketType::OctreeFileReplacementFromUrl, urlData.size(), true); octreeFilePacket->write(urlData); - limitedNodeList->sendPacket(std::move(octreeFilePacket), *octreeNode); + qDebug() << "WRiting url data: " << urlData; + limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr()); }); auto addressManager = DependencyManager::get(); addressManager->handleLookupString(DOMAIN_SPAWNING_POINT); 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/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 60bcc85575..4f96a6d072 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2244,6 +2244,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); @@ -2256,6 +2258,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/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/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 2343695914..3516fe948a 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -326,6 +326,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/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 5757cea496..7cd02608a1 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 diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 55643985c8..019ae07c16 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -328,7 +328,7 @@ void Socket::checkForReadyReadBackup() { void Socket::readPendingDatagrams() { int packetSizeWithHeader = -1; - while ((packetSizeWithHeader = _udpSocket.pendingDatagramSize()) != -1) { + while ((packetSizeWithHeader = _udpSocket.pendingDatagramSize()) > 0) { // we're reading a packet so re-start the readyRead backup timer _readyReadBackupTimer->start(); @@ -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/CMakeLists.txt b/libraries/octree/CMakeLists.txt index bea036add3..228779dbba 100644 --- a/libraries/octree/CMakeLists.txt +++ b/libraries/octree/CMakeLists.txt @@ -1,3 +1,4 @@ set(TARGET_NAME octree) +include_directories(system /usr/local/Cellar/qt5/5.9.1/include) setup_hifi_library() link_hifi_libraries(shared networking) diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index c63ff2f560..23352a548c 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1757,6 +1757,19 @@ bool Octree::readJSONFromStream(uint64_t streamLength, QDataStream& inputStream, QVariant asVariant = asDocument.toVariant(); QVariantMap asMap = asVariant.toMap(); bool success = readFromMap(asMap); + /* + if (success) { + if (asMap.contains("DataVersion") && asMap.contains("Id")) { + bool versionOk; + auto dataVersion = asMap["DataVersion"].toLongLong(&versionOk); + if (versionOk) { + auto id = asMap["Id"].toUuid(); + _persistDataVersion = dataVersion; + _persistID = id; + } + } + } + */ delete[] rawData; return success; } @@ -1778,11 +1791,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 +1813,35 @@ 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; - } +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; } else { - jsonDataForFile = jsonData; + qDebug() <<"Did gzip!"; + } + + 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 +1852,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..1b9495717b 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -283,8 +283,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 +328,11 @@ public: virtual void dumpTree() { } virtual void pruneTree() { } + void setEntityVersionInfo(QUuid id, int64_t dataVersion) { + _persistID = id; + _persistDataVersion = dataVersion; + } + virtual void resetEditStats() { } virtual quint64 getAverageDecodeTime() const { return 0; } virtual quint64 getAverageLookupTime() const { return 0; } @@ -359,6 +366,9 @@ protected: OctreeElementPointer _rootElement = nullptr; + QUuid _persistID { QUuid::createUuid() }; + int _persistDataVersion { 0 }; + bool _isDirty; bool _shouldReaverage; bool _stopImport; diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index ea6bd28fc4..9c9a4d40db 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -31,18 +31,19 @@ #include "OctreeLogging.h" #include "OctreePersistThread.h" +#include "OctreeUtils.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 +53,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 +134,56 @@ 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()) { + sendLatestEntityDataToDS(); + } else { + replaceData(_replacementData); + _replacementData.clear(); + } + + OctreeUtils::RawOctreeData data; + if (OctreeUtils::readOctreeDataInfoFromFile(_filename, &data)) { + _tree->setEntityVersionInfo(data.id, data.version); + } + + bool persistentFileRead; _tree->withWriteLock([&] { PerformanceWarning warn(true, "Loading Octree File", true); @@ -199,7 +206,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 +214,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(); @@ -272,7 +279,6 @@ bool OctreePersistThread::process() { return isStillRunning(); // keep running till they terminate us } - void OctreePersistThread::aboutToFinish() { qCDebug(octree) << "Persist thread about to finish..."; persist(); @@ -319,6 +325,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 +476,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..3fdad2c3f7 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..d8925a10ca 100644 --- a/libraries/octree/src/OctreeUtils.cpp +++ b/libraries/octree/src/OctreeUtils.cpp @@ -16,7 +16,11 @@ #include #include +#include +#include +#include +#include float calculateRenderAccuracy(const glm::vec3& position, const AABox& bounds, @@ -75,3 +79,76 @@ float getOrthographicAccuracySize(float octreeSizeScale, int boundaryLevelAdjust const float smallestSize = 0.01f; return (smallestSize * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT) / boundaryDistanceForRenderLevel(boundaryLevelAdjust, octreeSizeScale); } + +bool OctreeUtils::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 (path.endsWith(".json.gz")) { + if (!gunzip(data, jsonData)) { + qCritical() << "json File not in gzip format: " << path; + return false; + } + } else { + jsonData = data; + } + + *doc = QJsonDocument::fromJson(jsonData); + return !doc->isNull(); +} + +bool readOctreeDataInfoFromJSON(QJsonObject root, OctreeUtils::RawOctreeData* octreeData) { + if (root.contains("Id") && root.contains("DataVersion")) { + octreeData->id = root["Id"].toVariant().toUuid(); + octreeData->version = root["DataVersion"].toInt(); + } + if (root.contains("Entities")) { + octreeData->octreeData = root["Entities"].toArray(); + } + return true; +} + +bool OctreeUtils::readOctreeDataInfoFromData(QByteArray data, OctreeUtils::RawOctreeData* octreeData) { + 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, octreeData); +} + +bool OctreeUtils::readOctreeDataInfoFromFile(QString path, OctreeUtils::RawOctreeData* octreeData) { + QJsonDocument doc; + if (!OctreeUtils::readOctreeFile(path, &doc)) { + return false; + } + + auto root = doc.object(); + return readOctreeDataInfoFromJSON(root, octreeData); +} + +QByteArray OctreeUtils::RawOctreeData::toByteArray() { + QJsonObject obj { + { "DataVersion", QJsonValue((qint64)version) }, + { "Id", QJsonValue(id.toString()) }, + { "Version", QJsonValue(5) }, + { "Entities", octreeData } + }; + + QJsonDocument doc; + doc.setObject(obj); + + return doc.toJson(); +} diff --git a/libraries/octree/src/OctreeUtils.h b/libraries/octree/src/OctreeUtils.h index 0f87bb6f68..e5c7b617cd 100644 --- a/libraries/octree/src/OctreeUtils.h +++ b/libraries/octree/src/OctreeUtils.h @@ -14,7 +14,30 @@ #include "OctreeConstants.h" +#include +#include + class AABox; +class QJsonDocument; + +namespace OctreeUtils { + +// RawOctreeData is an intermediate format between JSON and a fully deserialized Octree. +class RawOctreeData { +public: + QUuid id { QUuid() }; + int64_t version { -1 }; + + QJsonArray octreeData; + + QByteArray toByteArray(); +}; + +bool readOctreeFile(QString path, QJsonDocument* doc); +bool readOctreeDataInfoFromData(QByteArray data, RawOctreeData* octreeData); +bool readOctreeDataInfoFromFile(QString path, RawOctreeData* octreeData); + +} /// 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/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 8e5c30711c..f46d0768c1 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -105,7 +105,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 +771,10 @@ QString formatUsecTime(double usecs) { return formatUsecTime(usecs); } +QString formatSecTime(qint64 secs) { + return formatUsecTime(secs * 1000000); +} + QString formatSecondsElapsed(float seconds) { QString result; diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index 5a1e48d9c0..7f9fb026f8 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -216,6 +216,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); From fc8e7a0841a875a2b7886784aa407547834326da Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 6 Feb 2018 14:33:01 -0800 Subject: [PATCH 032/260] Add target_zlib to DS CMakeLists.txt --- domain-server/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/domain-server/CMakeLists.txt b/domain-server/CMakeLists.txt index 0e958b9537..a578be5ff6 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -24,6 +24,8 @@ symlink_or_copy_directory_beside_target(${_SHOULD_SYMLINK_RESOURCES} "${CMAKE_CU # link the shared hifi libraries link_hifi_libraries(embedded-webserver networking shared avatars octree) +target_zlib() + add_dependency_external_projects(quazip) find_package(QuaZip REQUIRED) From ff5be2d690c9cbe3e5bd313daad68976f091d83f Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 7 Feb 2018 09:27:51 -0800 Subject: [PATCH 033/260] Fix entity data ID sometimes being reset --- domain-server/src/DomainServer.cpp | 5 ++++- libraries/octree/src/OctreePersistThread.cpp | 11 +++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index e083710d35..edb3fe77dd 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1802,7 +1802,7 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointerwritePrimitive(false); } else { - qCDebug(domain_server) << "Sending newer octree data to ES"; + 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); @@ -3312,6 +3312,9 @@ void DomainServer::handleOctreeFileReplacementFromURLRequest(QSharedPointererror(); if (networkError == QNetworkReply::NoError) { diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index 9c9a4d40db..d51bd540bc 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -171,15 +171,13 @@ bool OctreePersistThread::process() { quint64 loadStarted = usecTimestampNow(); qCDebug(octree) << "loading Octrees from file: " << _filename << "..."; - if (_replacementData.isNull()) { - sendLatestEntityDataToDS(); - } else { + if (!_replacementData.isNull()) { replaceData(_replacementData); - _replacementData.clear(); } OctreeUtils::RawOctreeData data; if (OctreeUtils::readOctreeDataInfoFromFile(_filename, &data)) { + qDebug() << "Setting entity version info to: " << data.id << data.version; _tree->setEntityVersionInfo(data.id, data.version); } @@ -244,6 +242,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(); } From 1b7b4eee50064fbeaf062fe810225993dc417fed Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 12 Feb 2018 11:46:45 -0800 Subject: [PATCH 034/260] Fix entity data not being gzipped when adding id+version --- assignment-client/src/octree/OctreeServer.cpp | 2 +- libraries/octree/src/OctreeUtils.cpp | 12 ++++++++++++ libraries/octree/src/OctreeUtils.h | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index e78f9f108b..6704786c36 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -1193,7 +1193,7 @@ void OctreeServer::handleOctreeDataFileReply(QSharedPointer mes QFile file(_persistAbsoluteFilePath); if (file.open(QIODevice::WriteOnly)) { - auto entityData = data.toByteArray(); + auto entityData = data.toGzippedByteArray(); file.write(entityData); file.close(); } else { diff --git a/libraries/octree/src/OctreeUtils.cpp b/libraries/octree/src/OctreeUtils.cpp index d8925a10ca..e068e83b23 100644 --- a/libraries/octree/src/OctreeUtils.cpp +++ b/libraries/octree/src/OctreeUtils.cpp @@ -152,3 +152,15 @@ QByteArray OctreeUtils::RawOctreeData::toByteArray() { 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; +} \ No newline at end of file diff --git a/libraries/octree/src/OctreeUtils.h b/libraries/octree/src/OctreeUtils.h index e5c7b617cd..18b0d27883 100644 --- a/libraries/octree/src/OctreeUtils.h +++ b/libraries/octree/src/OctreeUtils.h @@ -31,6 +31,7 @@ public: QJsonArray octreeData; QByteArray toByteArray(); + QByteArray toGzippedByteArray(); }; bool readOctreeFile(QString path, QJsonDocument* doc); From 2a667fcd60271c6a55147968aec8e0bae70d1bfe Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 13 Feb 2018 11:36:22 -0800 Subject: [PATCH 035/260] Cleanup entity -> ds persist --- assignment-client/CMakeLists.txt | 1 - assignment-client/src/octree/OctreeServer.cpp | 19 +++++--- assignment-client/src/octree/OctreeServer.h | 2 - .../src/DomainContentBackupManager.cpp | 18 +------ domain-server/src/DomainServer.cpp | 48 +++++++------------ interface/src/Application.cpp | 1 - libraries/entities/src/EntityTree.cpp | 2 +- libraries/octree/CMakeLists.txt | 1 - libraries/octree/src/Octree.cpp | 13 ----- libraries/octree/src/Octree.h | 2 + libraries/octree/src/OctreePersistThread.cpp | 1 + libraries/octree/src/OctreeUtils.cpp | 10 ++++ libraries/octree/src/OctreeUtils.h | 6 ++- 13 files changed, 49 insertions(+), 75 deletions(-) diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 3de4c5fd3f..c73e8e1d34 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -6,7 +6,6 @@ setup_hifi_project(Core Gui Network Script Quick WebSockets) if (APPLE) set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "@executable_path/../Frameworks") endif () -set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "/testing/") setup_memory_debugger() diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 6704786c36..05d070606a 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -1044,12 +1044,13 @@ void OctreeServer::readConfiguration() { // 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); + } else { + _persistAbsoluteFilePath = persistPath.absolutePath(); } qDebug() << "persistFilePath=" << _persistFilePath; @@ -1174,6 +1175,11 @@ void OctreeServer::domainSettingsRequestComplete() { } 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; @@ -1188,8 +1194,7 @@ void OctreeServer::handleOctreeDataFileReply(QSharedPointer mes if (OctreeUtils::readOctreeDataInfoFromFile(_persistAbsoluteFilePath, &data)) { if (data.id.isNull()) { qCDebug(octree_server) << "Current octree data has a null id, updating"; - data.id = QUuid::createUuid(); - data.version = 0; + data.resetIdAndVersion(); QFile file(_persistAbsoluteFilePath); if (file.open(QIODevice::WriteOnly)) { @@ -1202,17 +1207,17 @@ void OctreeServer::handleOctreeDataFileReply(QSharedPointer mes } } } + + _state = OctreeServerState::Running; beginRunning(replaceData); } void OctreeServer::beginRunning(QByteArray replaceData) { - if (_state == OctreeServerState::Running) { - qCWarning(octree_server) << "Server is already running"; + if (_state != OctreeServerState::Running) { + qCWarning(octree_server) << "Server is not running"; return; } - _state = OctreeServerState::Running; - auto nodeList = DependencyManager::get(); // we need to ask the DS about agents so we can ping/reply with them diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 6f77920ee0..eab71647e3 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -149,8 +149,6 @@ 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(); diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 0eca10f8af..4f544d7011 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -279,23 +279,9 @@ void DomainContentBackupManager::backup() { qDebug() << "Created backup: " << fileName; - removeOldBackupVersions(rule); + rule.lastBackupSeconds = nowSeconds; - if (rule.maxBackupVersions > 0) { - // Execute backup - auto result = true; - if (result) { - qCDebug(domain_server) << "DONE backing up persist file..."; - rule.lastBackupSeconds = nowSeconds; - } else { - qCDebug(domain_server) << "ERROR in backing up persist file..."; - perror("ERROR in backing up persist file"); - } - } else { - qCDebug(domain_server) << "This backup rule" << rule.name << " has Max Rolled Backup Versions less than 1 [" - << rule.maxBackupVersions << "]." - << " There are no backups to be done..."; - } + removeOldBackupVersions(rule); } else { qCDebug(domain_server) << "Backup not needed for this rule [" << rule.name << "]..."; } diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index edb3fe77dd..8f0e26375e 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -289,7 +289,6 @@ DomainServer::DomainServer(int argc, char* argv[]) : } } - qDebug() << "Starting persist thread"; if (QDir(getEntitiesDirPath()).mkpath(".")) { qCDebug(domain_server) << "Created entities data directory"; } @@ -1785,7 +1784,8 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointerreadPrimitive(&remoteHasExistingData); if (remoteHasExistingData) { - auto idData = message->read(16); + 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 << ")"; @@ -1794,7 +1794,6 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointer(); - 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); - - qDebug() << "Sending an octree file replacement of" << octreeFile.size() << "bytes to" << octreeNode; - - limitedNodeList->sendPacketList(std::move(octreeFilePacketList), *octreeNode); - }); } void DomainServer::handleOctreeFileReplacementFromURLRequest(QSharedPointer message) { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c031c0e8d4..bc44bb4cf0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6247,7 +6247,6 @@ bool Application::askToReplaceDomainContent(const QString& url) { }, [&urlData, limitedNodeList, &domainHandler](const SharedNodePointer& octreeNode) { auto octreeFilePacket = NLPacket::create(PacketType::OctreeFileReplacementFromUrl, urlData.size(), true); octreeFilePacket->write(urlData); - qDebug() << "WRiting url data: " << urlData; limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr()); }); auto addressManager = DependencyManager::get(); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 4f96a6d072..f632bcf140 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2244,7 +2244,7 @@ bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer if (! entityDescription.contains("Entities")) { entityDescription["Entities"] = QVariantList(); } - entityDescription["DataVersion"] = ++_persistDataVersion; + entityDescription["DataVersion"] = _persistDataVersion; entityDescription["Id"] = _persistID; QScriptEngine scriptEngine; RecurseOctreeToMapOperator theOperator(entityDescription, element, &scriptEngine, skipDefaultValues, diff --git a/libraries/octree/CMakeLists.txt b/libraries/octree/CMakeLists.txt index 228779dbba..bea036add3 100644 --- a/libraries/octree/CMakeLists.txt +++ b/libraries/octree/CMakeLists.txt @@ -1,4 +1,3 @@ set(TARGET_NAME octree) -include_directories(system /usr/local/Cellar/qt5/5.9.1/include) setup_hifi_library() link_hifi_libraries(shared networking) diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 23352a548c..d62cbad765 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1757,19 +1757,6 @@ bool Octree::readJSONFromStream(uint64_t streamLength, QDataStream& inputStream, QVariant asVariant = asDocument.toVariant(); QVariantMap asMap = asVariant.toMap(); bool success = readFromMap(asMap); - /* - if (success) { - if (asMap.contains("DataVersion") && asMap.contains("Id")) { - bool versionOk; - auto dataVersion = asMap["DataVersion"].toLongLong(&versionOk); - if (versionOk) { - auto id = asMap["Id"].toUuid(); - _persistDataVersion = dataVersion; - _persistID = id; - } - } - } - */ delete[] rawData; return success; } diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 1b9495717b..8954e53f8b 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -341,6 +341,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); diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index d51bd540bc..a669b7d3bb 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -311,6 +311,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"; diff --git a/libraries/octree/src/OctreeUtils.cpp b/libraries/octree/src/OctreeUtils.cpp index e068e83b23..85ea3beb86 100644 --- a/libraries/octree/src/OctreeUtils.cpp +++ b/libraries/octree/src/OctreeUtils.cpp @@ -80,6 +80,9 @@ float getOrthographicAccuracySize(float octreeSizeScale, int boundaryLevelAdjust return (smallestSize * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT) / boundaryDistanceForRenderLevel(boundaryLevelAdjust, octreeSizeScale); } +// 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 OctreeUtils::readOctreeFile(QString path, QJsonDocument* doc) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { @@ -129,6 +132,8 @@ bool OctreeUtils::readOctreeDataInfoFromData(QByteArray data, OctreeUtils::RawOc return readOctreeDataInfoFromJSON(root, octreeData); } +// Reads octree file and parses it into a RawOctreeData object. +// Returns false if readOctreeFile fails. bool OctreeUtils::readOctreeDataInfoFromFile(QString path, OctreeUtils::RawOctreeData* octreeData) { QJsonDocument doc; if (!OctreeUtils::readOctreeFile(path, &doc)) { @@ -163,4 +168,9 @@ QByteArray OctreeUtils::RawOctreeData::toGzippedByteArray() { } return gzData; +} + +void OctreeUtils::RawOctreeData::resetIdAndVersion() { + id = QUuid::createUuid(); + version = OctreeUtils::INITIAL_VERSION; } \ No newline at end of file diff --git a/libraries/octree/src/OctreeUtils.h b/libraries/octree/src/OctreeUtils.h index 18b0d27883..6fb0e62bcb 100644 --- a/libraries/octree/src/OctreeUtils.h +++ b/libraries/octree/src/OctreeUtils.h @@ -22,14 +22,18 @@ class QJsonDocument; namespace OctreeUtils { +using Version = int64_t; +constexpr Version INITIAL_VERSION = 0; + // RawOctreeData is an intermediate format between JSON and a fully deserialized Octree. class RawOctreeData { public: QUuid id { QUuid() }; - int64_t version { -1 }; + Version version { -1 }; QJsonArray octreeData; + void resetIdAndVersion(); QByteArray toByteArray(); QByteArray toGzippedByteArray(); }; From 0bbbff95cd2bd33bd6a0cad70ce351d9e14454c6 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 14 Feb 2018 11:38:18 -0800 Subject: [PATCH 036/260] Fix replacement octree data not working --- assignment-client/src/octree/OctreeServer.h | 2 -- domain-server/src/DomainServer.cpp | 22 ++++++++++++++------- libraries/octree/src/OctreeUtils.cpp | 8 ++------ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index eab71647e3..e7efc731f2 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -176,8 +176,6 @@ protected: 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/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8f0e26375e..3eb1f21da0 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -3234,15 +3234,23 @@ void DomainServer::maybeHandleReplacementEntityFile() { } else { qCDebug(domain_server) << "Replacing existing entity date with replacement file"; - data.resetIdAndVersion(); - auto gzippedData = data.toGzippedByteArray(); - - QFile currentFile(getEntitiesFilePath()); - if (!currentFile.open(QIODevice::WriteOnly)) { + 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) - << "Failed to update entities data file with replacement file, unable to open entities file for writing"; + << "Unable to remove replacement file, bailing"; } else { - currentFile.write(gzippedData); + data.resetIdAndVersion(); + auto gzippedData = data.toGzippedByteArray(); + + 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); + } } } } diff --git a/libraries/octree/src/OctreeUtils.cpp b/libraries/octree/src/OctreeUtils.cpp index 85ea3beb86..739c2661b3 100644 --- a/libraries/octree/src/OctreeUtils.cpp +++ b/libraries/octree/src/OctreeUtils.cpp @@ -93,12 +93,7 @@ bool OctreeUtils::readOctreeFile(QString path, QJsonDocument* doc) { QByteArray data = file.readAll(); QByteArray jsonData; - if (path.endsWith(".json.gz")) { - if (!gunzip(data, jsonData)) { - qCritical() << "json File not in gzip format: " << path; - return false; - } - } else { + if (!gunzip(data, jsonData)) { jsonData = data; } @@ -173,4 +168,5 @@ QByteArray OctreeUtils::RawOctreeData::toGzippedByteArray() { void OctreeUtils::RawOctreeData::resetIdAndVersion() { id = QUuid::createUuid(); version = OctreeUtils::INITIAL_VERSION; + qDebug() << "Reset octree data to: " << id << version; } \ No newline at end of file From 11b7fb89a903f044356b275f45cfeda21e883665 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 6 Feb 2018 15:37:48 -0800 Subject: [PATCH 037/260] Integrate new backup systems --- domain-server/src/BackupHandler.h | 110 ++++++++++++++++++ domain-server/src/BackupSupervisor.cpp | 94 +++++++++------ domain-server/src/BackupSupervisor.h | 66 ++++++++++- .../src/DomainContentBackupManager.cpp | 15 ++- .../src/DomainContentBackupManager.h | 17 +-- domain-server/src/DomainServer.cpp | 20 +--- domain-server/src/DomainServer.h | 2 + 7 files changed, 251 insertions(+), 73 deletions(-) create mode 100644 domain-server/src/BackupHandler.h diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h new file mode 100644 index 0000000000..c8e90025f8 --- /dev/null +++ b/domain-server/src/BackupHandler.h @@ -0,0 +1,110 @@ +// +// BackupHandler.h +// assignment-client +// +// 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 + +#include + +class BackupHandler { +public: + template + BackupHandler(T x) : _self(std::make_shared>(std::move(x))) {} + + void loadBackup(const QuaZip& zip) { + _self->loadBackup(zip); + } + void createBackup(QuaZip& zip) const { + _self->createBackup(zip); + } + void recoverBackup(const QuaZip& zip) const { + _self->recoverBackup(zip); + } + void deleteBackup(const QuaZip& zip) { + _self->deleteBackup(zip); + } + void consolidateBackup(QuaZip& zip) const { + _self->consolidateBackup(zip); + } + +private: + struct Concept { + virtual ~Concept() = default; + + virtual void loadBackup(const QuaZip& zip) = 0; + virtual void createBackup(QuaZip& zip) const = 0; + virtual void recoverBackup(const QuaZip& zip) const = 0; + virtual void deleteBackup(const QuaZip& zip) = 0; + virtual void consolidateBackup(QuaZip& zip) const = 0; + }; + + template + struct Model : Concept { + Model(T x) : data(std::move(x)) {} + + void loadBackup(const QuaZip& zip) { + data.loadBackup(zip); + } + void createBackup(QuaZip& zip) const { + data.createBackup(zip); + } + void recoverBackup(const QuaZip& zip) const { + data.recoverBackup(zip); + } + void deleteBackup(const QuaZip& zip) { + data.deleteBackup(zip); + } + void consolidateBackup(QuaZip& zip) const { + data.consolidateBackup(zip); + } + + T data; + }; + + std::shared_ptr _self; +}; + +#include +class EntitiesBackupHandler { +public: + EntitiesBackupHandler(QString entitiesFilePath) : _entitiesFilePath(entitiesFilePath) {} + + void loadBackup(const QuaZip& zip) {} + + void createBackup(QuaZip& zip) const { + qDebug() << "Creating a backup from handler"; + + QFile entitiesFile { _entitiesFilePath }; + + if (entitiesFile.open(QIODevice::ReadOnly)) { + QuaZipFile zipFile { &zip }; + zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("models.json.gz", _entitiesFilePath)); + zipFile.write(entitiesFile.readAll()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); + } + } + } + + void recoverBackup(const QuaZip& zip) const {} + void deleteBackup(const QuaZip& zip) {} + void consolidateBackup(QuaZip& zip) const {} + +private: + QString _entitiesFilePath; +}; + +#endif /* hifi_BackupHandler_h */ diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp index 03ad5de558..95fb1c6a6d 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/BackupSupervisor.cpp @@ -40,6 +40,39 @@ BackupSupervisor::BackupSupervisor() { } loadAllBackups(); + + static constexpr int MAPPINGS_REFRESH_INTERVAL = 30 * 1000; + _mappingsRefreshTimer.setInterval(MAPPINGS_REFRESH_INTERVAL); + _mappingsRefreshTimer.setTimerType(Qt::CoarseTimer); + _mappingsRefreshTimer.setSingleShot(false); + QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &BackupSupervisor::refreshMappings); + _mappingsRefreshTimer.start(); +} + +void BackupSupervisor::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(); + + qDebug() << "Refreshed" << mappings.size() << "asset mappings!"; + + _currentMappings.clear(); + for (const auto& mapping : mappings) { + _currentMappings.insert({ mapping.first, mapping.second.hash }); + } + _lastMappingsRefresh = usecTimestampNow(); + } else { + qCritical() << "Could not refresh asset server mappings."; + qCritical() << " Error:" << request->getErrorString(); + } + + request->deleteLater(); + }); + + request->start(); } void BackupSupervisor::loadAllBackups() { @@ -138,35 +171,26 @@ void BackupSupervisor::backupAssetServer() { return; } - auto assetClient = DependencyManager::get(); - auto request = assetClient->createGetAllMappingsRequest(); + if (_lastMappingsRefresh == 0) { + qWarning() << "Current mappings not yet loaded, "; + return; + } - 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(); - }); + static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; + if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { + qWarning() << "Backing up asset mappings that appear old."; + } startBackup(); - request->start(); + + if (!writeBackupFile(_currentMappings)) { + finishBackup(); + return; + } + + assert(!_backups.empty()); + const auto& mappings = _backups.back().mappings; + backupMissingFiles(mappings); } void BackupSupervisor::backupMissingFiles(const AssetUtils::Mappings& mappings) { @@ -193,7 +217,7 @@ void BackupSupervisor::backupNextMissingFile() { auto assetClient = DependencyManager::get(); auto assetRequest = assetClient->createRequest(hash); - connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) { + QObject::connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) { if (request->getError() == AssetRequest::NoError) { qDebug() << "Got" << request->getHash(); @@ -213,7 +237,7 @@ void BackupSupervisor::backupNextMissingFile() { assetRequest->start(); } -bool BackupSupervisor::writeBackupFile(const AssetUtils::AssetMappings& mappings) { +bool BackupSupervisor::writeBackupFile(const AssetUtils::Mappings& mappings) { auto filename = MAPPINGS_PREFIX + QDateTime::currentDateTimeUtc().toString(Qt::ISODate) + ".json"; QFile file { PathUtils::getAppDataPath() + BACKUPS_DIR + filename }; if (!file.open(QFile::WriteOnly)) { @@ -224,9 +248,9 @@ bool BackupSupervisor::writeBackupFile(const AssetUtils::AssetMappings& mappings 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); + backup.mappings[mapping.first] = mapping.second; + _assetsInBackups.insert(mapping.second); + jsonObject.insert(mapping.first, mapping.second); } QJsonDocument document(jsonObject); @@ -262,7 +286,7 @@ void BackupSupervisor::restoreAssetServer(int backupIndex) { auto assetClient = DependencyManager::get(); auto request = assetClient->createGetAllMappingsRequest(); - connect(request, &GetAllMappingsRequest::finished, this, [this, backupIndex](GetAllMappingsRequest* request) { + QObject::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); @@ -332,7 +356,7 @@ void BackupSupervisor::restoreNextAsset() { auto assetClient = DependencyManager::get(); auto request = assetClient->createUpload(assetFilename); - connect(request, &AssetUpload::finished, this, [this](AssetUpload* request) { + QObject::connect(request, &AssetUpload::finished, this, [this](AssetUpload* request) { if (request->getError() != AssetUpload::NoError) { qCritical() << "Failed to restore asset:" << request->getFilename(); qCritical() << " Error:" << request->getErrorString(); @@ -350,7 +374,7 @@ 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) { + QObject::connect(request, &SetMappingRequest::finished, this, [this](SetMappingRequest* request) { if (request->getError() != MappingRequest::NoError) { qCritical() << "Failed to set mapping:" << request->getPath(); qCritical() << " Error:" << request->getErrorString(); @@ -369,7 +393,7 @@ void BackupSupervisor::updateMappings() { _mappingsLeftToSet.clear(); auto request = assetClient->createDeleteMappingsRequest(_mappingsLeftToDelete); - connect(request, &DeleteMappingsRequest::finished, this, [this](DeleteMappingsRequest* request) { + QObject::connect(request, &DeleteMappingsRequest::finished, this, [this](DeleteMappingsRequest* request) { if (request->getError() != MappingRequest::NoError) { qCritical() << "Failed to delete mappings"; qCritical() << " Error:" << request->getErrorString(); diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index 067abdc25c..dd293c7fd5 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -16,11 +16,17 @@ #include #include +#include +#include +#include #include +#include #include +class QuaZip; + struct AssetServerBackup { std::string filePath; AssetUtils::Mappings mappings; @@ -42,7 +48,12 @@ public: bool backupInProgress() const { return _backupInProgress; } bool restoreInProgress() const { return _restoreInProgress; } + AssetUtils::Mappings getCurrentMappings() const { return _currentMappings; } + quint64 getLastRefreshTimestamp() const { return _lastMappingsRefresh; } + private: + void refreshMappings(); + void loadAllBackups(); bool loadBackup(const QString& backupFile); @@ -50,7 +61,7 @@ private: void finishBackup() { _backupInProgress = false; } void backupMissingFiles(const AssetUtils::Mappings& mappings); void backupNextMissingFile(); - bool writeBackupFile(const AssetUtils::AssetMappings& mappings); + bool writeBackupFile(const AssetUtils::Mappings& mappings); bool writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data); void startRestore() { _restoreInProgress = true; } @@ -64,6 +75,10 @@ private: QString _backupsDirectory; QString _assetsDirectory; + + quint64 _lastMappingsRefresh { 0 }; + AssetUtils::Mappings _currentMappings; + // Internal storage for backups on disk bool _allBackupsLoadedSuccessfully { false }; std::vector _backups; @@ -80,6 +95,55 @@ private: std::vector> _mappingsLeftToSet; AssetUtils::AssetPathList _mappingsLeftToDelete; int _mappingRequestsInFlight { 0 }; + + QTimer _mappingsRefreshTimer; +}; + + +#include +class AssetsBackupHandler { +public: + AssetsBackupHandler(BackupSupervisor* backupSupervisor) : _backupSupervisor(backupSupervisor) {} + + void loadBackup(const QuaZip& zip) {} + + void createBackup(QuaZip& zip) const { + quint64 lastRefreshTimestamp = _backupSupervisor->getLastRefreshTimestamp(); + AssetUtils::Mappings mappings = _backupSupervisor->getCurrentMappings(); + + if (lastRefreshTimestamp == 0) { + qWarning() << "Current mappings not yet loaded, "; + return; + } + + static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; + if (usecTimestampNow() - lastRefreshTimestamp > MAX_REFRESH_TIME) { + qWarning() << "Backing up asset mappings that appear old."; + } + + QJsonObject jsonObject; + for (const auto& mapping : mappings) { + jsonObject.insert(mapping.first, mapping.second); + } + QJsonDocument document(jsonObject); + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("mappings.json"))) { + qDebug() << "testCreate(): outFile.open()"; + } + zipFile.write(document.toJson()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); + } + } + + void recoverBackup(const QuaZip& zip) const {} + void deleteBackup(const QuaZip& zip) {} + void consolidateBackup(QuaZip& zip) const {} + +private: + BackupSupervisor* _backupSupervisor; }; #endif /* hifi_BackupSupervisor_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 4f544d7011..39ae63bc16 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -37,8 +37,8 @@ const int DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // const static QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" }; const static QString DATETIME_FORMAT_RE("\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}"); -void DomainContentBackupManager::addCreateBackupHandler(CreateBackupHandler handler) { - _backupHandlers.push_back(handler); +void DomainContentBackupManager::addBackupHandler(BackupHandler handler) { + _backupHandlers.push_back(std::move(handler)); } DomainContentBackupManager::DomainContentBackupManager(const QString& backupDirectory, @@ -48,7 +48,6 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire : _backupDirectory(backupDirectory), _persistInterval(persistInterval), _initialLoadComplete(false), - _loadTimeUSecs(0), _lastCheck(0), _debugTimestampNow(debugTimestampNow), _lastTimeDebug(0) { @@ -268,14 +267,14 @@ void DomainContentBackupManager::backup() { auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; - auto zip = new QuaZip(_backupDirectory + "/" + fileName); - zip->open(QuaZip::mdAdd); + QuaZip zip(_backupDirectory + "/" + fileName); + zip.open(QuaZip::mdAdd); - for (auto& handler : _backupHandlers) { - handler(zip); + for (const auto& handler : _backupHandlers) { + handler.createBackup(zip); } - zip->close(); + zip.close(); qDebug() << "Created backup: " << fileName; diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 20408fe486..67fc51f8f3 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -15,17 +15,11 @@ #define hifi_DomainContentBackupManager_h #include -#include #include -#include -#include +#include -#include - -using BackupResult = std::vector; -using CreateBackupHandler = std::function; -using RecoverBackupHandler = std::function; +#include "BackupHandler.h" class DomainContentBackupManager : public GenericThread { Q_OBJECT @@ -46,9 +40,8 @@ public: int persistInterval = DEFAULT_PERSIST_INTERVAL, bool debugTimestampNow = false); - void addCreateBackupHandler(CreateBackupHandler handler); + void addBackupHandler(BackupHandler handler); bool isInitialLoadComplete() const { return _initialLoadComplete; } - int64_t getLoadElapsedTime() const { return _loadTimeUSecs; } void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist @@ -70,12 +63,10 @@ protected: private: QString _backupDirectory; - std::vector _backupHandlers; + std::vector _backupHandlers; int _persistInterval; bool _initialLoadComplete; - int64_t _loadTimeUSecs; - time_t _lastPersistTime; int64_t _lastCheck; bool _wantBackup{ true }; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 3eb1f21da0..ed14bf3bdc 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -45,6 +45,7 @@ #include #include +#include "BackupSupervisor.h" #include "DomainServerNodeData.h" #include "NodeConnectionData.h" @@ -293,23 +294,10 @@ DomainServer::DomainServer(int argc, char* argv[]) : qCDebug(domain_server) << "Created entities data directory"; } maybeHandleReplacementEntityFile(); - auto entitiesFilePath = getEntitiesFilePath(); + _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.responseObjectForType("6")["entity_server_settings"].toObject())); - _contentManager->addCreateBackupHandler([entitiesFilePath](QuaZip* zip) { - qDebug() << "Creating a backup from handler"; - - QFile entitiesFile { entitiesFilePath }; - - if (entitiesFile.open(QIODevice::ReadOnly)) { - QuaZipFile zipFile { zip }; - zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("models.json.gz", entitiesFilePath)); - zipFile.write(entitiesFile.readAll()); - zipFile.close(); - if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "Failed to write entities file to backup:" << zipFile.getZipError(); - } - } - }); + _contentManager->addBackupHandler(EntitiesBackupHandler(getEntitiesFilePath())); + _contentManager->addBackupHandler(AssetsBackupHandler(&_backupSupervisor)); _contentManager->initialize(true); } diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index ee0350665e..645327225b 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -275,6 +275,8 @@ private: QHash> _pendingOAuthConnections; QThread _assetClientThread; + + BackupSupervisor _backupSupervisor; }; From a6447da64c6fcc0b950564c2fac16cfbbefb611a Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 12 Feb 2018 16:11:42 -0800 Subject: [PATCH 038/260] More Asset Backup work --- domain-server/src/BackupHandler.h | 55 +- domain-server/src/BackupSupervisor.cpp | 489 ++++++++++-------- domain-server/src/BackupSupervisor.h | 96 +--- .../src/DomainContentBackupManager.cpp | 60 ++- .../src/DomainContentBackupManager.h | 20 +- domain-server/src/DomainServer.cpp | 4 +- domain-server/src/DomainServer.h | 2 - 7 files changed, 359 insertions(+), 367 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index c8e90025f8..5c859165b7 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -21,21 +21,21 @@ class BackupHandler { public: template - BackupHandler(T x) : _self(std::make_shared>(std::move(x))) {} + BackupHandler(T* x) : _self(new Model(x)) {} - void loadBackup(const QuaZip& zip) { + void loadBackup(QuaZip& zip) { _self->loadBackup(zip); } - void createBackup(QuaZip& zip) const { + void createBackup(QuaZip& zip) { _self->createBackup(zip); } - void recoverBackup(const QuaZip& zip) const { + void recoverBackup(QuaZip& zip) { _self->recoverBackup(zip); } - void deleteBackup(const QuaZip& zip) { + void deleteBackup(QuaZip& zip) { _self->deleteBackup(zip); } - void consolidateBackup(QuaZip& zip) const { + void consolidateBackup(QuaZip& zip) { _self->consolidateBackup(zip); } @@ -43,37 +43,37 @@ private: struct Concept { virtual ~Concept() = default; - virtual void loadBackup(const QuaZip& zip) = 0; - virtual void createBackup(QuaZip& zip) const = 0; - virtual void recoverBackup(const QuaZip& zip) const = 0; - virtual void deleteBackup(const QuaZip& zip) = 0; - virtual void consolidateBackup(QuaZip& zip) const = 0; + virtual void loadBackup(QuaZip& zip) = 0; + virtual void createBackup(QuaZip& zip) = 0; + virtual void recoverBackup(QuaZip& zip) = 0; + virtual void deleteBackup(QuaZip& zip) = 0; + virtual void consolidateBackup(QuaZip& zip) = 0; }; template struct Model : Concept { - Model(T x) : data(std::move(x)) {} + Model(T* x) : data(x) {} - void loadBackup(const QuaZip& zip) { - data.loadBackup(zip); + void loadBackup(QuaZip& zip) { + data->loadBackup(zip); } - void createBackup(QuaZip& zip) const { - data.createBackup(zip); + void createBackup(QuaZip& zip) { + data->createBackup(zip); } - void recoverBackup(const QuaZip& zip) const { - data.recoverBackup(zip); + void recoverBackup(QuaZip& zip) { + data->recoverBackup(zip); } - void deleteBackup(const QuaZip& zip) { - data.deleteBackup(zip); + void deleteBackup(QuaZip& zip) { + data->deleteBackup(zip); } - void consolidateBackup(QuaZip& zip) const { - data.consolidateBackup(zip); + void consolidateBackup(QuaZip& zip) { + data->consolidateBackup(zip); } - T data; + std::unique_ptr data; }; - std::shared_ptr _self; + std::unique_ptr _self; }; #include @@ -81,12 +81,13 @@ class EntitiesBackupHandler { public: EntitiesBackupHandler(QString entitiesFilePath) : _entitiesFilePath(entitiesFilePath) {} - void loadBackup(const QuaZip& zip) {} + void loadBackup(QuaZip& zip) {} void createBackup(QuaZip& zip) const { qDebug() << "Creating a backup from handler"; QFile entitiesFile { _entitiesFilePath }; + qDebug() << entitiesFile.size(); if (entitiesFile.open(QIODevice::ReadOnly)) { QuaZipFile zipFile { &zip }; @@ -99,8 +100,8 @@ public: } } - void recoverBackup(const QuaZip& zip) const {} - void deleteBackup(const QuaZip& zip) {} + void recoverBackup(QuaZip& zip) const {} + void deleteBackup(QuaZip& zip) {} void consolidateBackup(QuaZip& zip) const {} private: diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp index 95fb1c6a6d..e0f0378fd4 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/BackupSupervisor.cpp @@ -13,6 +13,9 @@ #include #include +#include + +#include #include #include @@ -20,33 +23,231 @@ #include #include -const QString BACKUPS_DIR = "backups/"; -const QString ASSETS_DIR = "files/"; -const QString MAPPINGS_PREFIX = "mappings-"; +const QString ASSETS_DIR = "/assets/"; +const QString MAPPINGS_FILE = "mappings.json"; using namespace std; -BackupSupervisor::BackupSupervisor() { - _backupsDirectory = PathUtils::getAppDataPath() + BACKUPS_DIR; - QDir backupDir { _backupsDirectory }; - if (!backupDir.exists()) { - backupDir.mkpath("."); - } +Q_DECLARE_LOGGING_CATEGORY(backup_supervisor) +Q_LOGGING_CATEGORY(backup_supervisor, "hifi.backup-supervisor"); - _assetsDirectory = PathUtils::getAppDataPath() + BACKUPS_DIR + ASSETS_DIR; +BackupSupervisor::BackupSupervisor(const QString& backupDirectory) { + _assetsDirectory = backupDirectory + ASSETS_DIR; QDir assetsDir { _assetsDirectory }; if (!assetsDir.exists()) { assetsDir.mkpath("."); } - loadAllBackups(); + refreshAssetsOnDisk(); - static constexpr int MAPPINGS_REFRESH_INTERVAL = 30 * 1000; - _mappingsRefreshTimer.setInterval(MAPPINGS_REFRESH_INTERVAL); _mappingsRefreshTimer.setTimerType(Qt::CoarseTimer); - _mappingsRefreshTimer.setSingleShot(false); + _mappingsRefreshTimer.setSingleShot(true); QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &BackupSupervisor::refreshMappings); - _mappingsRefreshTimer.start(); + + auto nodeList = DependencyManager::get(); + QObject::connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, [this](SharedNodePointer node) { + if (node->getType() == NodeType::AssetServer) { + // Give the Asset Server some time to bootup. + static constexpr int ASSET_SERVER_BOOTUP_MARGIN = 1 * 1000; + _mappingsRefreshTimer.start(ASSET_SERVER_BOOTUP_MARGIN); + } + }); +} + + +void BackupSupervisor::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 BackupSupervisor::refreshAssetsInBackups() { + _assetsInBackups.clear(); + for (const auto& backup : _backups) { + for (const auto& mapping : backup.mappings) { + _assetsInBackups.insert(mapping.second); + } + } +} + +void BackupSupervisor::checkForMissingAssets() { + vector missingAssets; + set_difference(begin(_assetsInBackups), end(_assetsInBackups), + begin(_assetsOnDisk), end(_assetsOnDisk), + back_inserter(missingAssets)); + if (missingAssets.size() > 0) { + qCWarning(backup_supervisor) << "Found" << missingAssets.size() << "assets missing."; + } +} + +void BackupSupervisor::checkForAssetsToDelete() { + vector deprecatedAssets; + set_difference(begin(_assetsOnDisk), end(_assetsOnDisk), + begin(_assetsInBackups), end(_assetsInBackups), + back_inserter(deprecatedAssets)); + + if (deprecatedAssets.size() > 0) { + qCDebug(backup_supervisor) << "Found" << deprecatedAssets.size() << "assets to delete."; + if (_allBackupsLoadedSuccessfully) { + for (const auto& hash : deprecatedAssets) { + QFile::remove(_assetsDirectory + hash); + } + } else { + qCWarning(backup_supervisor) << "Some backups did not load properly, aborting deleting for safety."; + } + } +} + +void BackupSupervisor::loadBackup(QuaZip& zip) { + _backups.push_back({ zip.getZipName().toStdString(), {}, false }); + auto& backup = _backups.back(); + + if (!zip.setCurrentFile(MAPPINGS_FILE)) { + qCCritical(backup_supervisor) << "Failed to find" << MAPPINGS_FILE << "while recovering backup"; + qCCritical(backup_supervisor) << " Error:" << zip.getZipError(); + backup.corruptedBackup = true; + _allBackupsLoadedSuccessfully = false; + return; + } + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QFile::ReadOnly)) { + qCCritical(backup_supervisor) << "Could not open backup file:" << zip.getZipName(); + qCCritical(backup_supervisor) << " Error:" << zip.getZipError(); + backup.corruptedBackup = true; + _allBackupsLoadedSuccessfully = false; + return; + } + + QJsonParseError error; + auto document = QJsonDocument::fromJson(zipFile.readAll(), &error); + if (document.isNull() || !document.isObject()) { + qCCritical(backup_supervisor) << "Could not parse backup file to JSON object:" << zip.getZipName(); + qCCritical(backup_supervisor) << " Error:" << error.errorString(); + backup.corruptedBackup = true; + _allBackupsLoadedSuccessfully = false; + 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(backup_supervisor) << "Corrupted mapping in backup file" << zip.getZipName() << ":" << it.key(); + backup.corruptedBackup = true; + _allBackupsLoadedSuccessfully = false; + return; + } + + backup.mappings[assetPath] = assetHash; + _assetsInBackups.insert(assetHash); + } + + return; +} + +void BackupSupervisor::createBackup(QuaZip& zip) { + qDebug() << Q_FUNC_INFO; + if (operationInProgress()) { + qCWarning(backup_supervisor) << "There is already an operation in progress."; + return; + } + + if (_lastMappingsRefresh == 0) { + qCWarning(backup_supervisor) << "Current mappings not yet loaded."; + return; + } + + static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; + if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { + qCWarning(backup_supervisor) << "Backing up asset mappings that appear old."; + } + + AssetServerBackup backup; + backup.filePath = zip.getZipName().toStdString(); + + QJsonObject jsonObject; + for (const auto& mapping : _currentMappings) { + backup.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(backup_supervisor) << "testCreate(): outFile.open()"; + return; + } + zipFile.write(document.toJson()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qCDebug(backup_supervisor) << "testCreate(): outFile.close(): " << zipFile.getZipError(); + return; + } + _backups.push_back(backup); +} + +void BackupSupervisor::recoverBackup(QuaZip& zip) { + if (operationInProgress()) { + qCWarning(backup_supervisor) << "There is already a backup/restore in progress."; + return; + } + + if (_lastMappingsRefresh == 0) { + qCWarning(backup_supervisor) << "Current mappings not yet loaded."; + return; + } + + static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; + if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { + qCWarning(backup_supervisor) << "Backing up asset mappings that appear old."; + } + + startOperation(); + + auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { + return value.filePath == zip.getZipName().toStdString(); + }); + if (it == end(_backups)) { + qCDebug(backup_supervisor) << "Could not find backup"; + stopOperation(); + return; + } + + const auto& newMappings = it->mappings; + computeServerStateDifference(_currentMappings, newMappings); + + restoreAllAssets(); +} + +void BackupSupervisor::deleteBackup(QuaZip& zip) { + if (operationInProgress()) { + qCWarning(backup_supervisor) << "There is a backup/restore in progress."; + return; + } + + auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { + return value.filePath == zip.getZipName().toStdString(); + }); + if (it == end(_backups)) { + qCDebug(backup_supervisor) << "Could not find backup"; + return; + } + + refreshAssetsInBackups(); + checkForAssetsToDelete(); +} + +void BackupSupervisor::consolidateBackup(QuaZip& zip) { + } void BackupSupervisor::refreshMappings() { @@ -57,179 +258,69 @@ void BackupSupervisor::refreshMappings() { if (request->getError() == MappingRequest::NoError) { const auto& mappings = request->getMappings(); - qDebug() << "Refreshed" << mappings.size() << "asset mappings!"; + qCDebug(backup_supervisor) << "Refreshed" << mappings.size() << "asset mappings!"; _currentMappings.clear(); for (const auto& mapping : mappings) { _currentMappings.insert({ mapping.first, mapping.second.hash }); } _lastMappingsRefresh = usecTimestampNow(); + + downloadMissingFiles(_currentMappings); } else { - qCritical() << "Could not refresh asset server mappings."; - qCritical() << " Error:" << request->getErrorString(); + qCCritical(backup_supervisor) << "Could not refresh asset server mappings."; + qCCritical(backup_supervisor) << " 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 BackupSupervisor::loadAllBackups() { - _backups.clear(); - _assetsInBackups.clear(); - _assetsOnDisk.clear(); - _allBackupsLoadedSuccessfully = true; +void BackupSupervisor::downloadMissingFiles(const AssetUtils::Mappings& mappings) { + auto wasEmpty = _assetsLeftToRequest.empty(); - 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; - } - - if (_lastMappingsRefresh == 0) { - qWarning() << "Current mappings not yet loaded, "; - return; - } - - static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; - if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { - qWarning() << "Backing up asset mappings that appear old."; - } - - startBackup(); - - if (!writeBackupFile(_currentMappings)) { - finishBackup(); - return; - } - - assert(!_backups.empty()); - const auto& mappings = _backups.back().mappings; - backupMissingFiles(mappings); -} - -void BackupSupervisor::backupMissingFiles(const AssetUtils::Mappings& mappings) { - _assetsLeftToRequest.reserve(mappings.size()); - for (auto& mapping : mappings) { + for (const auto& mapping : mappings) { const auto& hash = mapping.second; if (_assetsOnDisk.find(hash) == end(_assetsOnDisk)) { - _assetsLeftToRequest.push_back(hash); + _assetsLeftToRequest.insert(hash); } } - backupNextMissingFile(); + // If we were empty, that means no download chain was already going, start one. + if (wasEmpty) { + downloadNextMissingFile(); + } } -void BackupSupervisor::backupNextMissingFile() { +void BackupSupervisor::downloadNextMissingFile() { if (_assetsLeftToRequest.empty()) { - finishBackup(); return; } - - auto hash = _assetsLeftToRequest.back(); - _assetsLeftToRequest.pop_back(); + 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) { - qDebug() << "Got" << request->getHash(); + qCDebug(backup_supervisor) << "Backing up asset" << request->getHash(); bool success = writeAssetFile(request->getHash(), request->getData()); if (!success) { - qCritical() << "Failed to write asset file" << request->getHash(); + qCCritical(backup_supervisor) << "Failed to write asset file" << request->getHash(); } } else { - qCritical() << "Failed to backup asset" << request->getHash(); + qCCritical(backup_supervisor) << "Failed to backup asset" << request->getHash(); } - backupNextMissingFile(); + _assetsLeftToRequest.erase(request->getHash()); + downloadNextMissingFile(); request->deleteLater(); }); @@ -237,73 +328,27 @@ void BackupSupervisor::backupNextMissingFile() { assetRequest->start(); } -bool BackupSupervisor::writeBackupFile(const AssetUtils::Mappings& 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; - _assetsInBackups.insert(mapping.second); - jsonObject.insert(mapping.first, mapping.second); - } - - 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(); + qCCritical(backup_supervisor) << "Could not open backup file" << file.fileName(); return false; } - file.write(data); + auto bytesWritten = file.write(data); + if (bytesWritten != data.size()) { + qCCritical(backup_supervisor) << "Could not write data to file" << file.fileName(); + file.remove(); + return false; + } _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(); - - QObject::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, +void BackupSupervisor::computeServerStateDifference(const AssetUtils::Mappings& currentMappings, const AssetUtils::Mappings& newMappings) { _mappingsLeftToSet.reserve((int)newMappings.size()); _assetsLeftToUpload.reserve((int)newMappings.size()); @@ -312,7 +357,7 @@ void BackupSupervisor::computeServerStateDifference(const AssetUtils::AssetMappi set currentAssets; for (const auto& currentMapping : currentMappings) { const auto& currentPath = currentMapping.first; - const auto& currentHash = currentMapping.second.hash; + const auto& currentHash = currentMapping.second; if (newMappings.find(currentPath) == end(newMappings)) { _mappingsLeftToDelete.push_back(currentPath); @@ -325,7 +370,7 @@ void BackupSupervisor::computeServerStateDifference(const AssetUtils::AssetMappi const auto& newHash = newMapping.second; auto it = currentMappings.find(newPath); - if (it == end(currentMappings) || it->second.hash != newHash) { + if (it == end(currentMappings) || it->second != newHash) { _mappingsLeftToSet.push_back({ newPath, newHash }); } if (currentAssets.find(newHash) == end(currentAssets)) { @@ -333,9 +378,9 @@ void BackupSupervisor::computeServerStateDifference(const AssetUtils::AssetMappi } } - qDebug() << "Mappings to set:" << _mappingsLeftToSet.size(); - qDebug() << "Mappings to del:" << _mappingsLeftToDelete.size(); - qDebug() << "Assets to upload:" << _assetsLeftToUpload.size(); + qCDebug(backup_supervisor) << "Mappings to set:" << _mappingsLeftToSet.size(); + qCDebug(backup_supervisor) << "Mappings to del:" << _mappingsLeftToDelete.size(); + qCDebug(backup_supervisor) << "Assets to upload:" << _assetsLeftToUpload.size(); } void BackupSupervisor::restoreAllAssets() { @@ -358,8 +403,8 @@ void BackupSupervisor::restoreNextAsset() { QObject::connect(request, &AssetUpload::finished, this, [this](AssetUpload* request) { if (request->getError() != AssetUpload::NoError) { - qCritical() << "Failed to restore asset:" << request->getFilename(); - qCritical() << " Error:" << request->getErrorString(); + qCCritical(backup_supervisor) << "Failed to restore asset:" << request->getFilename(); + qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); } restoreNextAsset(); @@ -376,12 +421,12 @@ void BackupSupervisor::updateMappings() { auto request = assetClient->createSetMappingRequest(mapping.first, mapping.second); QObject::connect(request, &SetMappingRequest::finished, this, [this](SetMappingRequest* request) { if (request->getError() != MappingRequest::NoError) { - qCritical() << "Failed to set mapping:" << request->getPath(); - qCritical() << " Error:" << request->getErrorString(); + qCCritical(backup_supervisor) << "Failed to set mapping:" << request->getPath(); + qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); } if (--_mappingRequestsInFlight == 0) { - finishRestore(); + stopOperation(); } request->deleteLater(); @@ -395,12 +440,12 @@ void BackupSupervisor::updateMappings() { auto request = assetClient->createDeleteMappingsRequest(_mappingsLeftToDelete); QObject::connect(request, &DeleteMappingsRequest::finished, this, [this](DeleteMappingsRequest* request) { if (request->getError() != MappingRequest::NoError) { - qCritical() << "Failed to delete mappings"; - qCritical() << " Error:" << request->getErrorString(); + qCCritical(backup_supervisor) << "Failed to delete mappings"; + qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); } if (--_mappingRequestsInFlight == 0) { - finishRestore(); + stopOperation(); } request->deleteLater(); @@ -410,15 +455,3 @@ void BackupSupervisor::updateMappings() { 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 index dd293c7fd5..a89be66742 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -37,48 +37,45 @@ class BackupSupervisor : public QObject { Q_OBJECT public: - BackupSupervisor(); + BackupSupervisor(const QString& backupDirectory); - void backupAssetServer(); - void restoreAssetServer(int backupIndex); - bool deleteBackup(int backupIndex); + void loadBackup(QuaZip& zip); + void createBackup(QuaZip& zip); + void recoverBackup(QuaZip& zip); + void deleteBackup(QuaZip& zip); + void consolidateBackup(QuaZip& zip); - const std::vector& getBackups() const { return _backups; }; - - bool backupInProgress() const { return _backupInProgress; } - bool restoreInProgress() const { return _restoreInProgress; } - - AssetUtils::Mappings getCurrentMappings() const { return _currentMappings; } - quint64 getLastRefreshTimestamp() const { return _lastMappingsRefresh; } + bool operationInProgress() const { return _operationInProgress; } private: void refreshMappings(); - void loadAllBackups(); - bool loadBackup(const QString& backupFile); + void refreshAssetsInBackups(); + void refreshAssetsOnDisk(); + void checkForMissingAssets(); + void checkForAssetsToDelete(); - void startBackup() { _backupInProgress = true; } - void finishBackup() { _backupInProgress = false; } - void backupMissingFiles(const AssetUtils::Mappings& mappings); - void backupNextMissingFile(); - bool writeBackupFile(const AssetUtils::Mappings& mappings); + void startOperation() { _operationInProgress = true; } + void stopOperation() { _operationInProgress = false; } + + void downloadMissingFiles(const AssetUtils::Mappings& mappings); + void downloadNextMissingFile(); bool writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data); - void startRestore() { _restoreInProgress = true; } - void finishRestore() { _restoreInProgress = false; } - void computeServerStateDifference(const AssetUtils::AssetMappings& currentMappings, + void computeServerStateDifference(const AssetUtils::Mappings& currentMappings, const AssetUtils::Mappings& newMappings); void restoreAllAssets(); void restoreNextAsset(); void updateMappings(); - QString _backupsDirectory; QString _assetsDirectory; - + QTimer _mappingsRefreshTimer; quint64 _lastMappingsRefresh { 0 }; AssetUtils::Mappings _currentMappings; + bool _operationInProgress { false }; + // Internal storage for backups on disk bool _allBackupsLoadedSuccessfully { false }; std::vector _backups; @@ -86,64 +83,13 @@ private: std::set _assetsOnDisk; // Internal storage for backup in progress - bool _backupInProgress { false }; - std::vector _assetsLeftToRequest; + std::set _assetsLeftToRequest; // Internal storage for restore in progress - bool _restoreInProgress { false }; std::vector _assetsLeftToUpload; std::vector> _mappingsLeftToSet; AssetUtils::AssetPathList _mappingsLeftToDelete; int _mappingRequestsInFlight { 0 }; - - QTimer _mappingsRefreshTimer; -}; - - -#include -class AssetsBackupHandler { -public: - AssetsBackupHandler(BackupSupervisor* backupSupervisor) : _backupSupervisor(backupSupervisor) {} - - void loadBackup(const QuaZip& zip) {} - - void createBackup(QuaZip& zip) const { - quint64 lastRefreshTimestamp = _backupSupervisor->getLastRefreshTimestamp(); - AssetUtils::Mappings mappings = _backupSupervisor->getCurrentMappings(); - - if (lastRefreshTimestamp == 0) { - qWarning() << "Current mappings not yet loaded, "; - return; - } - - static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; - if (usecTimestampNow() - lastRefreshTimestamp > MAX_REFRESH_TIME) { - qWarning() << "Backing up asset mappings that appear old."; - } - - QJsonObject jsonObject; - for (const auto& mapping : mappings) { - jsonObject.insert(mapping.first, mapping.second); - } - QJsonDocument document(jsonObject); - - QuaZipFile zipFile { &zip }; - if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("mappings.json"))) { - qDebug() << "testCreate(): outFile.open()"; - } - zipFile.write(document.toJson()); - zipFile.close(); - if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); - } - } - - void recoverBackup(const QuaZip& zip) const {} - void deleteBackup(const QuaZip& zip) {} - void consolidateBackup(QuaZip& zip) const {} - -private: - BackupSupervisor* _backupSupervisor; }; #endif /* hifi_BackupSupervisor_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 39ae63bc16..7c1c7f2cd7 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -47,10 +47,7 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire bool debugTimestampNow) : _backupDirectory(backupDirectory), _persistInterval(persistInterval), - _initialLoadComplete(false), - _lastCheck(0), - _debugTimestampNow(debugTimestampNow), - _lastTimeDebug(0) { + _lastCheck(usecTimestampNow()) { parseSettings(settings); } @@ -101,7 +98,7 @@ void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { qCDebug(domain_server) << " lastBackup: NEVER"; } - _backupRules << newRule; + _backupRules.push_back(newRule); } } else { qCDebug(domain_server) << "BACKUP RULES: NONE"; @@ -123,6 +120,10 @@ int64_t DomainContentBackupManager::getMostRecentBackupTimeInSecs(const QString& return mostRecentBackupInSecs; } +void DomainContentBackupManager::setup() { + load(); +} + bool DomainContentBackupManager::process() { if (isStillRunning()) { constexpr int64_t MSECS_TO_USECS = 1000; @@ -139,18 +140,6 @@ bool DomainContentBackupManager::process() { } } - // if we were asked to debugTimestampNow do that now... - if (_debugTimestampNow) { - - quint64 now = usecTimestampNow(); - quint64 sinceLastDebug = now - _lastTimeDebug; - quint64 DEBUG_TIMESTAMP_INTERVAL = 600000000; // every 10 minutes - - if (sinceLastDebug > DEBUG_TIMESTAMP_INTERVAL) { - _lastTimeDebug = usecTimestampNow(true); // ask for debug output - } - } - return isStillRunning(); } @@ -250,6 +239,36 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) } } +void DomainContentBackupManager::load() { + QDir backupDir { _backupDirectory }; + if (backupDir.exists()) { + + auto matchingFiles = backupDir.entryInfoList({ "backup-*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); + + for (const auto& file : matchingFiles) { + QFile backupFile { file.absoluteFilePath() }; + if (!backupFile.open(QIODevice::ReadOnly)) { + qCritical() << "Could not open file:" << file.absoluteFilePath(); + qCritical() << " ERROR:" << backupFile.errorString(); + continue; + } + + QuaZip zip { &backupFile }; + if (!zip.open(QuaZip::mdUnzip)) { + qCritical() << "Could not open backup archive:" << file.absoluteFilePath(); + qCritical() << " ERROR:" << zip.getZipError(); + continue; + } + + for (auto& handler : _backupHandlers) { + handler.loadBackup(zip); + } + + zip.close(); + } + } +} + void DomainContentBackupManager::backup() { auto nowDateTime = QDateTime::currentDateTime(); auto nowSeconds = nowDateTime.toSecsSinceEpoch(); @@ -268,9 +287,12 @@ void DomainContentBackupManager::backup() { auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; QuaZip zip(_backupDirectory + "/" + fileName); - zip.open(QuaZip::mdAdd); + if (!zip.open(QuaZip::mdAdd)) { + qDebug() << "Could not open archive"; + } - for (const auto& handler : _backupHandlers) { + for (auto& handler : _backupHandlers) { + qDebug() << "Backup handler"; handler.createBackup(zip); } diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 67fc51f8f3..bb1d4f0116 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -41,20 +41,18 @@ public: bool debugTimestampNow = false); void addBackupHandler(BackupHandler handler); - bool isInitialLoadComplete() const { return _initialLoadComplete; } void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist void replaceData(QByteArray data); -signals: - void loadCompleted(); - protected: /// Implements generic processing behavior for this thread. - bool process() override; + virtual void setup() override; + virtual bool process() override; void persist(); + void load(); void backup(); void removeOldBackupVersions(const BackupRule& rule); bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); @@ -64,16 +62,10 @@ protected: private: QString _backupDirectory; std::vector _backupHandlers; - int _persistInterval; - bool _initialLoadComplete; + int _persistInterval { 0 }; - time_t _lastPersistTime; - int64_t _lastCheck; - bool _wantBackup{ true }; - QVector _backupRules; - - bool _debugTimestampNow; - int64_t _lastTimeDebug; + int64_t _lastCheck { 0 }; + std::vector _backupRules; }; #endif // hifi_DomainContentBackupManager_h diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index ed14bf3bdc..06d3549ff8 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -296,8 +296,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : maybeHandleReplacementEntityFile(); _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.responseObjectForType("6")["entity_server_settings"].toObject())); - _contentManager->addBackupHandler(EntitiesBackupHandler(getEntitiesFilePath())); - _contentManager->addBackupHandler(AssetsBackupHandler(&_backupSupervisor)); + _contentManager->addBackupHandler(new EntitiesBackupHandler(getEntitiesFilePath())); + _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); _contentManager->initialize(true); } diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 645327225b..ee0350665e 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -275,8 +275,6 @@ private: QHash> _pendingOAuthConnections; QThread _assetClientThread; - - BackupSupervisor _backupSupervisor; }; From 272f95efa294f24ddf91cc1b36af5cf278557925 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 13 Feb 2018 18:13:07 -0800 Subject: [PATCH 039/260] Specify wich packet can ignore verification at DS level --- libraries/networking/src/LimitedNodeList.cpp | 6 ++++-- libraries/networking/src/udt/PacketHeaders.h | 17 +++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 3516fe948a..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()); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 7cd02608a1..b263823fa4 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -187,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; From c41ad1a699c79f6e9a1ec2bd7b6dbe7a2a0ba33f Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 13 Feb 2018 15:59:51 -0800 Subject: [PATCH 040/260] Add consolidate --- domain-server/src/BackupSupervisor.cpp | 46 +++++++++++++++++-- domain-server/src/BackupSupervisor.h | 2 +- .../src/DomainContentBackupManager.cpp | 28 +++++++++++ .../src/DomainContentBackupManager.h | 1 + 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp index e0f0378fd4..4cb42787ba 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/BackupSupervisor.cpp @@ -104,7 +104,7 @@ void BackupSupervisor::checkForAssetsToDelete() { } void BackupSupervisor::loadBackup(QuaZip& zip) { - _backups.push_back({ zip.getZipName().toStdString(), {}, false }); + _backups.push_back({ zip.getZipName(), {}, false }); auto& backup = _backups.back(); if (!zip.setCurrentFile(MAPPINGS_FILE)) { @@ -171,7 +171,7 @@ void BackupSupervisor::createBackup(QuaZip& zip) { } AssetServerBackup backup; - backup.filePath = zip.getZipName().toStdString(); + backup.filePath = zip.getZipName(); QJsonObject jsonObject; for (const auto& mapping : _currentMappings) { @@ -214,7 +214,7 @@ void BackupSupervisor::recoverBackup(QuaZip& zip) { startOperation(); auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { - return value.filePath == zip.getZipName().toStdString(); + return value.filePath == zip.getZipName(); }); if (it == end(_backups)) { qCDebug(backup_supervisor) << "Could not find backup"; @@ -235,7 +235,7 @@ void BackupSupervisor::deleteBackup(QuaZip& zip) { } auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { - return value.filePath == zip.getZipName().toStdString(); + return value.filePath == zip.getZipName(); }); if (it == end(_backups)) { qCDebug(backup_supervisor) << "Could not find backup"; @@ -247,6 +247,44 @@ void BackupSupervisor::deleteBackup(QuaZip& zip) { } void BackupSupervisor::consolidateBackup(QuaZip& zip) { + if (operationInProgress()) { + qCWarning(backup_supervisor) << "There is a backup/restore in progress."; + return; + } + QFileInfo zipInfo(zip.getZipName()); + + auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { + QFileInfo info(value.filePath); + return info.fileName() == zipInfo.fileName(); + }); + if (it == end(_backups)) { + qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName(); + 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(backup_supervisor) << "Could not open asset file" << file.fileName(); + continue; + } + + QuaZipFile zipFile { &zip }; + static const QString ZIP_ASSETS_FOLDER = "files/"; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ZIP_ASSETS_FOLDER + hash))) { + qCDebug(backup_supervisor) << "testCreate(): outFile.open()"; + continue; + } + zipFile.write(file.readAll()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qCDebug(backup_supervisor) << "testCreate(): outFile.close(): " << zipFile.getZipError(); + continue; + } + } } diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index a89be66742..d0f6e52ac6 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -28,7 +28,7 @@ class QuaZip; struct AssetServerBackup { - std::string filePath; + QString filePath; AssetUtils::Mappings mappings; bool corruptedBackup; }; diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 7c1c7f2cd7..3f9b3f20d8 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -308,3 +308,31 @@ void DomainContentBackupManager::backup() { } } } + +void DomainContentBackupManager::consolidate(QString fileName) { + QDir backupDir { _backupDirectory }; + if (backupDir.exists()) { + auto filePath = backupDir.absoluteFilePath(fileName); + + auto copyFilePath = QDir::tempPath() + "/" + fileName; + + auto copySuccess = QFile::copy(filePath, copyFilePath); + if (!copySuccess) { + qCritical() << "Failed to create full backup."; + return; + } + + QuaZip zip(copyFilePath); + if (!zip.open(QuaZip::mdAdd)) { + qCritical() << "Could not open backup archive:" << filePath; + qCritical() << " ERROR:" << zip.getZipError(); + return; + } + + for (auto& handler : _backupHandlers) { + handler.consolidateBackup(zip); + } + + zip.close(); + } +} diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index bb1d4f0116..69163b4ead 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -54,6 +54,7 @@ protected: void persist(); void load(); void backup(); + void consolidate(QString fileName); void removeOldBackupVersions(const BackupRule& rule); bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); int64_t getMostRecentBackupTimeInSecs(const QString& format); From d4b4c55673744e144dcb54938ff75cdb0e9e169f Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 13 Feb 2018 18:38:13 -0800 Subject: [PATCH 041/260] Remove unecessary debug --- domain-server/src/BackupHandler.h | 3 --- domain-server/src/BackupSupervisor.cpp | 1 - domain-server/src/DomainContentBackupManager.cpp | 1 - 3 files changed, 5 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 5c859165b7..332afe22f7 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -84,10 +84,7 @@ public: void loadBackup(QuaZip& zip) {} void createBackup(QuaZip& zip) const { - qDebug() << "Creating a backup from handler"; - QFile entitiesFile { _entitiesFilePath }; - qDebug() << entitiesFile.size(); if (entitiesFile.open(QIODevice::ReadOnly)) { QuaZipFile zipFile { &zip }; diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp index 4cb42787ba..832fc8a3ff 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/BackupSupervisor.cpp @@ -154,7 +154,6 @@ void BackupSupervisor::loadBackup(QuaZip& zip) { } void BackupSupervisor::createBackup(QuaZip& zip) { - qDebug() << Q_FUNC_INFO; if (operationInProgress()) { qCWarning(backup_supervisor) << "There is already an operation in progress."; return; diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 3f9b3f20d8..56571f1b8c 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -292,7 +292,6 @@ void DomainContentBackupManager::backup() { } for (auto& handler : _backupHandlers) { - qDebug() << "Backup handler"; handler.createBackup(zip); } From 69298246c4726a80aef6eda423024cc0a4ec4d22 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 14 Feb 2018 15:44:51 -0800 Subject: [PATCH 042/260] CR --- domain-server/src/BackupHandler.h | 2 +- domain-server/src/BackupSupervisor.cpp | 32 ++++++++----------- domain-server/src/BackupSupervisor.h | 1 - .../src/DomainContentBackupManager.cpp | 9 ++++-- domain-server/src/DomainServer.cpp | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 332afe22f7..4643d183b2 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -1,6 +1,6 @@ // // BackupHandler.h -// assignment-client +// domain-server/src // // Created by Clement Brisset on 2/5/18. // Copyright 2018 High Fidelity, Inc. diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp index 832fc8a3ff..869f85c6cc 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/BackupSupervisor.cpp @@ -31,12 +31,11 @@ using namespace std; Q_DECLARE_LOGGING_CATEGORY(backup_supervisor) Q_LOGGING_CATEGORY(backup_supervisor, "hifi.backup-supervisor"); -BackupSupervisor::BackupSupervisor(const QString& backupDirectory) { - _assetsDirectory = backupDirectory + ASSETS_DIR; - QDir assetsDir { _assetsDirectory }; - if (!assetsDir.exists()) { - assetsDir.mkpath("."); - } +BackupSupervisor::BackupSupervisor(const QString& backupDirectory) : + _assetsDirectory(backupDirectory + ASSETS_DIR) +{ + // Make sure the asset directory exists. + QDir(_assetsDirectory).mkpath("."); refreshAssetsOnDisk(); @@ -166,7 +165,7 @@ void BackupSupervisor::createBackup(QuaZip& zip) { static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { - qCWarning(backup_supervisor) << "Backing up asset mappings that appear old."; + qCWarning(backup_supervisor) << "Backing up asset mappings that might be stale."; } AssetServerBackup backup; @@ -182,13 +181,13 @@ void BackupSupervisor::createBackup(QuaZip& zip) { QuaZipFile zipFile { &zip }; if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(MAPPINGS_FILE))) { - qCDebug(backup_supervisor) << "testCreate(): outFile.open()"; + qCDebug(backup_supervisor) << "Could not open zip file:" << zipFile.getZipError(); return; } zipFile.write(document.toJson()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qCDebug(backup_supervisor) << "testCreate(): outFile.close(): " << zipFile.getZipError(); + qCDebug(backup_supervisor) << "Could not close zip file: " << zipFile.getZipError(); return; } _backups.push_back(backup); @@ -207,7 +206,7 @@ void BackupSupervisor::recoverBackup(QuaZip& zip) { static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { - qCWarning(backup_supervisor) << "Backing up asset mappings that appear old."; + qCWarning(backup_supervisor) << "Current asset mappings that might be stale."; } startOperation(); @@ -216,7 +215,7 @@ void BackupSupervisor::recoverBackup(QuaZip& zip) { return value.filePath == zip.getZipName(); }); if (it == end(_backups)) { - qCDebug(backup_supervisor) << "Could not find backup"; + qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName() << "to restore."; stopOperation(); return; } @@ -237,7 +236,7 @@ void BackupSupervisor::deleteBackup(QuaZip& zip) { return value.filePath == zip.getZipName(); }); if (it == end(_backups)) { - qCDebug(backup_supervisor) << "Could not find backup"; + qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName() << "to delete."; return; } @@ -257,7 +256,7 @@ void BackupSupervisor::consolidateBackup(QuaZip& zip) { return info.fileName() == zipInfo.fileName(); }); if (it == end(_backups)) { - qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName(); + qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName() << "to consolidate."; return; } @@ -274,13 +273,13 @@ void BackupSupervisor::consolidateBackup(QuaZip& zip) { QuaZipFile zipFile { &zip }; static const QString ZIP_ASSETS_FOLDER = "files/"; if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ZIP_ASSETS_FOLDER + hash))) { - qCDebug(backup_supervisor) << "testCreate(): outFile.open()"; + qCDebug(backup_supervisor) << "Could not open zip file:" << zipFile.getZipError(); continue; } zipFile.write(file.readAll()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qCDebug(backup_supervisor) << "testCreate(): outFile.close(): " << zipFile.getZipError(); + qCDebug(backup_supervisor) << "Could not close zip file: " << zipFile.getZipError(); continue; } } @@ -294,9 +293,6 @@ void BackupSupervisor::refreshMappings() { QObject::connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) { if (request->getError() == MappingRequest::NoError) { const auto& mappings = request->getMappings(); - - qCDebug(backup_supervisor) << "Refreshed" << mappings.size() << "asset mappings!"; - _currentMappings.clear(); for (const auto& mapping : mappings) { _currentMappings.insert({ mapping.first, mapping.second.hash }); diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index d0f6e52ac6..9fedcca19b 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -21,7 +21,6 @@ #include #include -#include #include diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 56571f1b8c..ed5d99f927 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -47,7 +47,11 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire bool debugTimestampNow) : _backupDirectory(backupDirectory), _persistInterval(persistInterval), - _lastCheck(usecTimestampNow()) { + _lastCheck(usecTimestampNow()) +{ + // Make sure the backup directory exists. + QDir(_backupDirectory).mkpath("."); + parseSettings(settings); } @@ -288,7 +292,8 @@ void DomainContentBackupManager::backup() { auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; QuaZip zip(_backupDirectory + "/" + fileName); if (!zip.open(QuaZip::mdAdd)) { - qDebug() << "Could not open archive"; + qDebug() << "Could not open backup archive:" << zip.getZipName(); + qDebug() << " ERROR:" << zip.getZipError(); } for (auto& handler : _backupHandlers) { diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 06d3549ff8..8ccba3d942 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -295,7 +295,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : } maybeHandleReplacementEntityFile(); - _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.responseObjectForType("6")["entity_server_settings"].toObject())); + _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); _contentManager->addBackupHandler(new EntitiesBackupHandler(getEntitiesFilePath())); _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); _contentManager->initialize(true); From e63b692d80f584eac5d70751b9e01d2a1a8b8cf9 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 7 Feb 2018 16:53:29 -0800 Subject: [PATCH 043/260] Add BackupHandler for entity file backups --- domain-server/src/BackupHandler.h | 34 ++++++++++++-- domain-server/src/BackupSupervisor.h | 47 +++++++++++++++++++ .../src/DomainContentBackupManager.cpp | 31 ++++++++++-- .../src/DomainContentBackupManager.h | 5 ++ domain-server/src/DomainServer.cpp | 2 + libraries/entities/src/EntityTree.cpp | 1 + 6 files changed, 113 insertions(+), 7 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 4643d183b2..b790591bea 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -83,7 +83,10 @@ public: void loadBackup(QuaZip& zip) {} - void createBackup(QuaZip& zip) const { + // Create a skeleton backup + void createBackup(QuaZip& zip) { + qDebug() << "Creating a backup from handler"; + QFile entitiesFile { _entitiesFilePath }; if (entitiesFile.open(QIODevice::ReadOnly)) { @@ -97,9 +100,32 @@ public: } } - void recoverBackup(QuaZip& zip) const {} - void deleteBackup(QuaZip& zip) {} - void consolidateBackup(QuaZip& zip) const {} + // Recover from a full backup + void recoverBackup(QuaZip& zip) { + if (!zip.setCurrentFile("models.json.gz")) { + qWarning() << "Failed to find models.json.gz while recovering backup"; + return; + } + QuaZipFile zipFile { &zip }; + zipFile.open(QIODevice::ReadOnly); + auto data = zipFile.readAll(); + + QFile entitiesFile { _entitiesFilePath }; + + if (entitiesFile.open(QIODevice::WriteOnly)) { + entitiesFile.write(data); + } + + zipFile.close(); + } + + // Delete a skeleton backup + void deleteBackup(QuaZip& zip) { + } + + // Create a full backup + void consolidateBackup(QuaZip& zip) { + } private: QString _entitiesFilePath; diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index 9fedcca19b..1023622971 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -91,4 +91,51 @@ private: int _mappingRequestsInFlight { 0 }; }; + +#include +class AssetsBackupHandler { +public: + AssetsBackupHandler(BackupSupervisor* backupSupervisor) : _backupSupervisor(backupSupervisor) {} + + void loadBackup(QuaZip& zip) {} + + void createBackup(QuaZip& zip) { + quint64 lastRefreshTimestamp = _backupSupervisor->getLastRefreshTimestamp(); + AssetUtils::Mappings mappings = _backupSupervisor->getCurrentMappings(); + + if (lastRefreshTimestamp == 0) { + qWarning() << "Current mappings not yet loaded, "; + return; + } + + static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; + if (usecTimestampNow() - lastRefreshTimestamp > MAX_REFRESH_TIME) { + qWarning() << "Backing up asset mappings that appear old."; + } + + QJsonObject jsonObject; + for (const auto& mapping : mappings) { + jsonObject.insert(mapping.first, mapping.second); + } + QJsonDocument document(jsonObject); + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("mappings.json"))) { + qDebug() << "testCreate(): outFile.open()"; + } + zipFile.write(document.toJson()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); + } + } + + void recoverBackup(QuaZip& zip) {} + void deleteBackup(QuaZip& zip) {} + void consolidateBackup(QuaZip& zip) {} + +private: + BackupSupervisor* _backupSupervisor; +}; + #endif /* hifi_BackupSupervisor_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index ed5d99f927..b0a80531f8 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -216,10 +216,35 @@ bool DomainContentBackupManager::getMostRecentBackup(const QString& format, return bestBackupFound; } +bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { + qDebug() << "Recoving from" << backupName; + + QDir backupDir { _backupDirectory }; + QFile backupFile { backupDir.filePath(backupName) }; + if (backupFile.open(QIODevice::ReadOnly)) { + QuaZip zip { &backupFile }; + if (!zip.open(QuaZip::Mode::mdUnzip)) { + qWarning() << "Failed to unzip file: " << backupName; + backupFile.close(); + return false; + } + + for (auto& handler : _backupHandlers) { + handler.recoverBackup(zip); + } + + backupFile.close(); + } + + qDebug() << "Successfully recovered from " << backupName; + + return true; +} + 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 << "..."; + qCDebug(domain_server) << "Rolling old backup versions for rule" << rule.name; auto matchingFiles = backupDir.entryInfoList({ rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); @@ -235,11 +260,11 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) } } - qCDebug(domain_server) << "Done rolling old backup versions..."; + 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..."; + << " No need to roll backups"; } } diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 69163b4ead..d0dd9cf2c6 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -46,6 +46,11 @@ public: void replaceData(QByteArray data); + bool recoverFromBackup(const QString& backupName); + +signals: + void loadCompleted(); + protected: /// Implements generic processing behavior for this thread. virtual void setup() override; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8ccba3d942..fe6a303e08 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -299,6 +299,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : _contentManager->addBackupHandler(new EntitiesBackupHandler(getEntitiesFilePath())); _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); _contentManager->initialize(true); + + _contentManager->recoverFromBackup("backup-daily_rolling-2018-02-06_15-13-50.zip"); } void DomainServer::parseCommandLine() { diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index f632bcf140..fc3e793b45 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2246,6 +2246,7 @@ bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer } entityDescription["DataVersion"] = _persistDataVersion; entityDescription["Id"] = _persistID; + qDebug() << "Writing to map: " << _persistDataVersion << _persistID; QScriptEngine scriptEngine; RecurseOctreeToMapOperator theOperator(entityDescription, element, &scriptEngine, skipDefaultValues, skipThoseWithBadParents, _myAvatar); From 8b07e7e28ff8eea62fb60bf81c73bdb05c10fc36 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 8 Feb 2018 22:13:52 -0800 Subject: [PATCH 044/260] Add backup DS APIs Add backup apis --- domain-server/src/BackupHandler.h | 11 +- domain-server/src/BackupSupervisor.h | 47 ------ .../src/DomainContentBackupManager.cpp | 144 ++++++++++++++---- .../src/DomainContentBackupManager.h | 18 ++- domain-server/src/DomainServer.cpp | 72 ++++++++- .../embedded-webserver/src/HTTPConnection.cpp | 53 ++++++- .../embedded-webserver/src/HTTPConnection.h | 6 + .../embedded-webserver/src/HTTPManager.cpp | 15 +- 8 files changed, 270 insertions(+), 96 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index b790591bea..ad1fc6b793 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -95,7 +95,7 @@ public: zipFile.write(entitiesFile.readAll()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); + qDebug() << "Failed to zip models.json.gz: " << zipFile.getZipError(); } } } @@ -107,7 +107,10 @@ public: return; } QuaZipFile zipFile { &zip }; - zipFile.open(QIODevice::ReadOnly); + if (!zipFile.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open models.json.gz in backup"; + return; + } auto data = zipFile.readAll(); QFile entitiesFile { _entitiesFilePath }; @@ -117,6 +120,10 @@ public: } zipFile.close(); + + if (zipFile.getZipError() != UNZ_OK) { + qDebug() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + } } // Delete a skeleton backup diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index 1023622971..9fedcca19b 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -91,51 +91,4 @@ private: int _mappingRequestsInFlight { 0 }; }; - -#include -class AssetsBackupHandler { -public: - AssetsBackupHandler(BackupSupervisor* backupSupervisor) : _backupSupervisor(backupSupervisor) {} - - void loadBackup(QuaZip& zip) {} - - void createBackup(QuaZip& zip) { - quint64 lastRefreshTimestamp = _backupSupervisor->getLastRefreshTimestamp(); - AssetUtils::Mappings mappings = _backupSupervisor->getCurrentMappings(); - - if (lastRefreshTimestamp == 0) { - qWarning() << "Current mappings not yet loaded, "; - return; - } - - static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; - if (usecTimestampNow() - lastRefreshTimestamp > MAX_REFRESH_TIME) { - qWarning() << "Backing up asset mappings that appear old."; - } - - QJsonObject jsonObject; - for (const auto& mapping : mappings) { - jsonObject.insert(mapping.first, mapping.second); - } - QJsonDocument document(jsonObject); - - QuaZipFile zipFile { &zip }; - if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("mappings.json"))) { - qDebug() << "testCreate(): outFile.open()"; - } - zipFile.write(document.toJson()); - zipFile.close(); - if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); - } - } - - void recoverBackup(QuaZip& zip) {} - void deleteBackup(QuaZip& zip) {} - void consolidateBackup(QuaZip& zip) {} - -private: - BackupSupervisor* _backupSupervisor; -}; - #endif /* hifi_BackupSupervisor_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index b0a80531f8..29f6b7948f 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include "DomainServer.h" #include "DomainContentBackupManager.h" @@ -36,7 +37,8 @@ const int DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // // Backup format looks like: daily_backup-TIMESTAMP.zip const static QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" }; const static 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-" }; void DomainContentBackupManager::addBackupHandler(BackupHandler handler) { _backupHandlers.push_back(std::move(handler)); } @@ -83,7 +85,7 @@ void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { auto name = obj["Name"].toString(); auto format = obj["format"].toString(); - format = name.replace(" ", "_").toLower() + "-"; + format = name.replace(" ", "_").toLower(); qCDebug(domain_server) << " Name:" << name; qCDebug(domain_server) << " format:" << format; @@ -129,6 +131,14 @@ void DomainContentBackupManager::setup() { } bool DomainContentBackupManager::process() { + if (!_initialLoadComplete) { + QDir backupDir { _backupDirectory }; + if (!backupDir.exists()) { + backupDir.mkpath("."); + } + _initialLoadComplete = true; + } + if (isStillRunning()) { constexpr int64_t MSECS_TO_USECS = 1000; constexpr int64_t USECS_TO_SLEEP = 10 * MSECS_TO_USECS; // every 10ms @@ -140,7 +150,7 @@ bool DomainContentBackupManager::process() { if (sinceLastSave > intervalToCheck) { _lastCheck = now; - persist(); + backup(); } } @@ -149,32 +159,18 @@ bool DomainContentBackupManager::process() { void DomainContentBackupManager::aboutToFinish() { qCDebug(domain_server) << "Persist thread about to finish..."; - persist(); -} - -void DomainContentBackupManager::persist() { - QDir backupDir { _backupDirectory }; - backupDir.mkpath("."); - - // create our "lock" file to indicate we're saving. - QString lockFileName = _backupDirectory + "/running.lock"; - - std::ofstream lockFile(qPrintable(lockFileName), std::ios::out | std::ios::binary); - if (lockFile.is_open()) { - backup(); - - lockFile.close(); - remove(qPrintable(lockFileName)); - } + backup(); + qCDebug(domain_server) << "Persist thread done with about to finish..."; + _stopThread = true; } bool DomainContentBackupManager::getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime) { - QRegExp formatRE { QRegExp::escape(format) + "(" + DATETIME_FORMAT_RE + ")" + "\\.zip" }; + QRegExp formatRE { AUTOMATIC_BACKUP_PREFIX + QRegExp::escape(format) + "\\-(" + DATETIME_FORMAT_RE + ")" + "\\.zip" }; QStringList filters; - filters << format + "*.zip"; + filters << AUTOMATIC_BACKUP_PREFIX + format + "*.zip"; bool bestBackupFound = false; QString bestBackupFile; @@ -216,7 +212,32 @@ bool DomainContentBackupManager::getMostRecentBackup(const QString& format, return bestBackupFound; } +bool DomainContentBackupManager::deleteBackup(const QString& backupName) { + if (QThread::currentThread() != thread()) { + bool result{ false }; + BLOCKING_INVOKE_METHOD(this, "deleteBackup", + Q_RETURN_ARG(bool, result), + Q_ARG(const QString&, backupName)); + return result; + } + + QDir backupDir { _backupDirectory }; + QFile backupFile { backupDir.filePath(backupName) }; + if (backupFile.remove()) { + return true; + } + return false; +} + bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { + if (QThread::currentThread() != thread()) { + bool result{ false }; + BLOCKING_INVOKE_METHOD(this, "recoverFromBackup", + Q_RETURN_ARG(bool, result), + Q_ARG(const QString&, backupName)); + return result; + } + qDebug() << "Recoving from" << backupName; QDir backupDir { _backupDirectory }; @@ -226,7 +247,6 @@ bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { if (!zip.open(QuaZip::Mode::mdUnzip)) { qWarning() << "Failed to unzip file: " << backupName; backupFile.close(); - return false; } for (auto& handler : _backupHandlers) { @@ -234,11 +254,43 @@ bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { } backupFile.close(); + qDebug() << "Successfully recovered from " << backupName; + return true; + } else { + qWarning() << "Invalid id: " << backupName; + return false; + } +} + +std::vector DomainContentBackupManager::getAllBackups() { + std::vector backups; + + 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" }; + + 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()) { + continue; + } + + BackupItemInfo backup { fileInfo.fileName(), name, fileInfo.absoluteFilePath(), createdAt, type == MANUAL_BACKUP_PREFIX }; + backups.push_back(backup); + } } - qDebug() << "Successfully recovered from " << backupName; - - return true; + return backups; } void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) { @@ -247,9 +299,10 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) qCDebug(domain_server) << "Rolling old backup versions for rule" << rule.name; auto matchingFiles = - backupDir.entryInfoList({ rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); + backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); int backupsToDelete = matchingFiles.length() - rule.maxBackupVersions; + 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); @@ -313,6 +366,7 @@ void DomainContentBackupManager::backup() { qCDebug(domain_server) << "Time since last backup [" << secondsSinceLastBackup << "] for rule [" << rule.name << "] exceeds backup interval [" << rule.intervalSeconds << "] doing backup now..."; +<<<<<<< HEAD auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; QuaZip zip(_backupDirectory + "/" + fileName); @@ -323,11 +377,17 @@ void DomainContentBackupManager::backup() { for (auto& handler : _backupHandlers) { handler.createBackup(zip); +======= + 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; +>>>>>>> dd86471a42... Add backup DS APIs } - zip.close(); - - qDebug() << "Created backup: " << fileName; + qDebug() << "Created backup: " << path; rule.lastBackupSeconds = nowSeconds; @@ -365,3 +425,27 @@ void DomainContentBackupManager::consolidate(QString fileName) { zip.close(); } } + +void DomainContentBackupManager::createManualBackup(const QString& name) { + createBackup(MANUAL_BACKUP_PREFIX, name); +} + +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(zip); + } + + zip.close(); + + return { true, path }; +} diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index d0dd9cf2c6..461d4dd794 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -21,6 +21,14 @@ #include "BackupHandler.h" +struct BackupItemInfo { + QString id; + QString name; + QString absolutePath; + QDateTime createdAt; + bool isManualBackup; +}; + class DomainContentBackupManager : public GenericThread { Q_OBJECT public: @@ -41,12 +49,18 @@ public: bool debugTimestampNow = false); void addBackupHandler(BackupHandler handler); + bool isInitialLoadComplete() const { return _initialLoadComplete; } + std::vector getAllBackups(); void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist void replaceData(QByteArray data); + void createManualBackup(const QString& name); + +public slots: bool recoverFromBackup(const QString& backupName); + bool deleteBackup(const QString& backupName); signals: void loadCompleted(); @@ -56,7 +70,6 @@ protected: virtual void setup() override; virtual bool process() override; - void persist(); void load(); void backup(); void consolidate(QString fileName); @@ -65,10 +78,13 @@ protected: int64_t getMostRecentBackupTimeInSecs(const QString& format); void parseSettings(const QJsonObject& settings); + std::pair createBackup(const QString& prefix, const QString& name); + private: QString _backupDirectory; std::vector _backupHandlers; int _persistInterval { 0 }; + bool _initialLoadComplete { false }; int64_t _lastCheck { 0 }; std::vector _backupRules; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index fe6a303e08..1949c40566 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -300,7 +300,10 @@ DomainServer::DomainServer(int argc, char* argv[]) : _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); _contentManager->initialize(true); - _contentManager->recoverFromBackup("backup-daily_rolling-2018-02-06_15-13-50.zip"); + qDebug() << "Existing backups:"; + for (auto& backup : _contentManager->getAllBackups()) { + qDebug() << " Backup: " << backup.name << backup.createdAt; + } } void DomainServer::parseCommandLine() { @@ -1736,6 +1739,12 @@ void DomainServer::processOctreeDataPersistMessage(QSharedPointerreadAll(); 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); @@ -1746,12 +1755,12 @@ void DomainServer::processOctreeDataPersistMessage(QSharedPointerrespond(HTTPConnection::StatusCode200, nodesDocument.toJson(), qPrintable(JSON_MIME_TYPE)); + return true; + } else if (url.path().startsWith(URI_API_BACKUPS_RECOVER)) { + auto id = url.path().mid(QString(URI_API_BACKUPS_RECOVER).length()); + _contentManager->recoverFromBackup(id); + QJsonObject rootJSON; + rootJSON["success"] = true; + QJsonDocument docJSON(rootJSON); + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + return true; + } else if (url.path() == URI_API_BACKUPS) { + QJsonObject rootJSON; + QJsonArray backupsJSON; + + auto backups = _contentManager->getAllBackups(); + + for (const auto& backup : backups) { + QJsonObject obj; + obj["id"] = backup.id; + obj["name"] = backup.name; + obj["createdAtMillis"] = backup.createdAt.toMSecsSinceEpoch(); + obj["isManualBackup"] = backup.isManualBackup; + backupsJSON.push_back(obj); + } + + rootJSON["backups"] = backupsJSON; + QJsonDocument docJSON(rootJSON); + + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); return true; } else if (url.path() == URI_RESTART) { connection->respond(HTTPConnection::StatusCode200); @@ -2213,6 +2254,20 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; + } else if (url.path() == URI_API_BACKUPS) { + qDebug() << "GOt request to create a backup:"; + auto params = connection->parseUrlEncodedForm(); + auto it = params.find("name"); + if (it == params.end()) { + connection->respond(HTTPConnection::StatusCode400, "Bad request, missing `name`"); + return true; + } + + _contentManager->createManualBackup(it.value()); + + connection->respond(HTTPConnection::StatusCode200); + return true; + } else if (url.path() == "/domain_settings") { auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); if (!accessTokenVariant) { @@ -2311,7 +2366,16 @@ 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 success = _contentManager->deleteBackup(id); + QJsonObject rootJSON; + rootJSON["success"] = success; + QJsonDocument docJSON(rootJSON); + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + 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 diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index a61bc95f8b..6496cc3f68 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -133,12 +133,33 @@ QList HTTPConnection::parseFormData() const { } void HTTPConnection::respond(const char* code, const QByteArray& content, const char* contentType, const Headers& headers) { + QByteArray data(content); + auto device { std::unique_ptr(new QBuffer()) }; + device->setBuffer(new QByteArray(content)); + if (device->open(QIODevice::ReadOnly)) { + respond(code, std::move(device), contentType, headers); + } else { + qCritical() << "Error opening QBuffer to respond to " << _requestUrl.path(); + } +} + +void HTTPConnection::respond(const char* code, std::unique_ptr device, const char* contentType, const Headers& headers) { + _responseDevice = std::move(device); + _socket->write("HTTP/1.1 "); + + if (_responseDevice->isSequential()) { + qWarning() << "Error responding to HTTPConnection: sequential IO devices not supported"; + _socket->write(StatusCode500); + _socket->write("\r\n"); + _socket->disconnect(SIGNAL(readyRead()), this); + _socket->disconnectFromHost(); + return; + } + _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,6 +167,8 @@ void HTTPConnection::respond(const char* code, const QByteArray& content, const _socket->write(it.value()); _socket->write("\r\n"); } + + int csize = _responseDevice->size(); if (csize > 0) { _socket->write("Content-Length: "); _socket->write(QByteArray::number(csize)); @@ -157,20 +180,35 @@ void HTTPConnection::respond(const char* code, const QByteArray& content, const } _socket->write("Connection: close\r\n\r\n"); - if (csize > 0) { - _socket->write(content); + if (_responseDevice->atEnd()) { + _socket->disconnectFromHost(); + } else { + constexpr size_t HTTP_RESPONSE_CHUNK_SIZE = 1024 * 10; + int totalToBeWritten = csize; + connect(_socket, &QTcpSocket::bytesWritten, this, [this, totalToBeWritten](size_t bytes) mutable { + 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 - _socket->disconnect(SIGNAL(readyRead()), this); - - _socket->disconnectFromHost(); + disconnect(_socket, &QTcpSocket::readyRead, this, nullptr); } 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")) { @@ -249,6 +287,7 @@ void HTTPConnection::readContent() { if (_socket->bytesAvailable() < size) { return; } + qDebug() << "Reading content"; _socket->read(_requestContent.data(), size); _socket->disconnect(this, SLOT(readContent())); diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index 966fc26949..9c435b14a0 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.h +++ b/libraries/embedded-webserver/src/HTTPConnection.h @@ -87,6 +87,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: @@ -127,6 +130,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; } } From dd398da2e0baa1306ce754edd73a70e49144be6e Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 14 Feb 2018 15:36:50 -0800 Subject: [PATCH 045/260] Update DS to use promises for backup APIs --- .../src/DomainContentBackupManager.cpp | 50 ++++++++++--------- .../src/DomainContentBackupManager.h | 6 ++- domain-server/src/DomainServer.cpp | 29 ++++++----- 3 files changed, 48 insertions(+), 37 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 29f6b7948f..5c4d70d7ad 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -212,54 +212,58 @@ bool DomainContentBackupManager::getMostRecentBackup(const QString& format, return bestBackupFound; } -bool DomainContentBackupManager::deleteBackup(const QString& backupName) { +void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, const QString& backupName) { if (QThread::currentThread() != thread()) { - bool result{ false }; - BLOCKING_INVOKE_METHOD(this, "deleteBackup", - Q_RETURN_ARG(bool, result), - Q_ARG(const QString&, backupName)); - return result; + QMetaObject::invokeMethod(this, "deleteBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(const QString&, backupName)); + return; } + bool success { false }; QDir backupDir { _backupDirectory }; QFile backupFile { backupDir.filePath(backupName) }; if (backupFile.remove()) { - return true; + success = true; } - return false; + promise->resolve({ + { "success", success } + }); } -bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { +void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, const QString& backupName) { if (QThread::currentThread() != thread()) { - bool result{ false }; - BLOCKING_INVOKE_METHOD(this, "recoverFromBackup", - Q_RETURN_ARG(bool, result), - Q_ARG(const QString&, backupName)); - return result; + QMetaObject::invokeMethod(this, "recoverFromBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(const QString&, backupName)); + return; } qDebug() << "Recoving from" << backupName; + bool success { false }; QDir backupDir { _backupDirectory }; QFile backupFile { backupDir.filePath(backupName) }; if (backupFile.open(QIODevice::ReadOnly)) { QuaZip zip { &backupFile }; if (!zip.open(QuaZip::Mode::mdUnzip)) { qWarning() << "Failed to unzip file: " << backupName; - backupFile.close(); + success = false; + } else { + for (auto& handler : _backupHandlers) { + handler.recoverBackup(zip); + } + + qDebug() << "Successfully recovered from " << backupName; + success = true; } - - for (auto& handler : _backupHandlers) { - handler.recoverBackup(zip); - } - backupFile.close(); - qDebug() << "Successfully recovered from " << backupName; - return true; } else { + success = false; qWarning() << "Invalid id: " << backupName; - return false; } + + promise->resolve({ + { "success", success } + }); } std::vector DomainContentBackupManager::getAllBackups() { diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 461d4dd794..6d6f07a19e 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -21,6 +21,8 @@ #include "BackupHandler.h" +#include + struct BackupItemInfo { QString id; QString name; @@ -59,8 +61,8 @@ public: void createManualBackup(const QString& name); public slots: - bool recoverFromBackup(const QString& backupName); - bool deleteBackup(const QString& backupName); + void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); + void deleteBackup(MiniPromise::Promise promise, const QString& backupName); signals: void loadCompleted(); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 1949c40566..0e057972ca 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2124,11 +2124,14 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; } else if (url.path().startsWith(URI_API_BACKUPS_RECOVER)) { auto id = url.path().mid(QString(URI_API_BACKUPS_RECOVER).length()); - _contentManager->recoverFromBackup(id); - QJsonObject rootJSON; - rootJSON["success"] = true; - QJsonDocument docJSON(rootJSON); - connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + auto deferred = makePromise("recoverFromBackup"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + rootJSON["success"] = result["success"].toBool(); + QJsonDocument docJSON(rootJSON); + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + }); + _contentManager->recoverFromBackup(deferred, id); return true; } else if (url.path() == URI_API_BACKUPS) { QJsonObject rootJSON; @@ -2368,11 +2371,15 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url if (url.path().startsWith(URI_API_BACKUPS_ID)) { auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length()); - auto success = _contentManager->deleteBackup(id); - QJsonObject rootJSON; - rootJSON["success"] = success; - QJsonDocument docJSON(rootJSON); - connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + auto deferred = makePromise("deleteBackup"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + rootJSON["success"] = result["success"].toBool(); + QJsonDocument docJSON(rootJSON); + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + }); + _contentManager->deleteBackup(deferred, id); + return true; } else if (nodeDeleteRegex.indexIn(url.path()) != -1) { @@ -3310,8 +3317,6 @@ void DomainServer::maybeHandleReplacementEntityFile() { } void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) { - // enumerate the nodes and find any octree type servers with active sockets - //Assume we have compressed data auto compressedOctree = octreeFile; QByteArray jsonOctree; From 80b03b904610911577d58d21d4d59de0cc93b39a Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 14 Feb 2018 16:09:16 -0800 Subject: [PATCH 046/260] Make backup directory in content manager const --- domain-server/src/DomainContentBackupManager.cpp | 13 ------------- domain-server/src/DomainContentBackupManager.h | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 5c4d70d7ad..159d6d6e95 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -370,25 +370,12 @@ void DomainContentBackupManager::backup() { qCDebug(domain_server) << "Time since last backup [" << secondsSinceLastBackup << "] for rule [" << rule.name << "] exceeds backup interval [" << rule.intervalSeconds << "] doing backup now..."; -<<<<<<< HEAD - auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); - auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; - QuaZip zip(_backupDirectory + "/" + fileName); - if (!zip.open(QuaZip::mdAdd)) { - qDebug() << "Could not open backup archive:" << zip.getZipName(); - qDebug() << " ERROR:" << zip.getZipError(); - } - - for (auto& handler : _backupHandlers) { - handler.createBackup(zip); -======= 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; ->>>>>>> dd86471a42... Add backup DS APIs } qDebug() << "Created backup: " << path; diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 6d6f07a19e..792695acce 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -83,7 +83,7 @@ protected: std::pair createBackup(const QString& prefix, const QString& name); private: - QString _backupDirectory; + const QString _backupDirectory; std::vector _backupHandlers; int _persistInterval { 0 }; bool _initialLoadComplete { false }; From 8a69c69bec02016ebdea92ea19e50d15cf9e1b74 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 14 Feb 2018 16:49:23 -0800 Subject: [PATCH 047/260] CR --- domain-server/src/BackupHandler.h | 15 ++++++------ .../src/DomainContentBackupManager.cpp | 23 +++++-------------- .../src/DomainContentBackupManager.h | 2 -- domain-server/src/DomainServer.cpp | 16 +++++++------ .../embedded-webserver/src/HTTPConnection.cpp | 1 - libraries/entities/src/EntityTree.cpp | 1 - 6 files changed, 23 insertions(+), 35 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index ad1fc6b793..3e25af83e8 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -79,14 +79,14 @@ private: #include class EntitiesBackupHandler { public: - EntitiesBackupHandler(QString entitiesFilePath) : _entitiesFilePath(entitiesFilePath) {} + EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) + : _entitiesFilePath(entitiesFilePath) + , _entitiesReplacementFilePath {} void loadBackup(QuaZip& zip) {} // Create a skeleton backup void createBackup(QuaZip& zip) { - qDebug() << "Creating a backup from handler"; - QFile entitiesFile { _entitiesFilePath }; if (entitiesFile.open(QIODevice::ReadOnly)) { @@ -95,7 +95,7 @@ public: zipFile.write(entitiesFile.readAll()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + qCritical() << "Failed to zip models.json.gz: " << zipFile.getZipError(); } } } @@ -108,12 +108,12 @@ public: } QuaZipFile zipFile { &zip }; if (!zipFile.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open models.json.gz in backup"; + qCritical() << "Failed to open models.json.gz in backup"; return; } auto data = zipFile.readAll(); - QFile entitiesFile { _entitiesFilePath }; + QFile entitiesFile { _entitiesReplacementFilePath }; if (entitiesFile.open(QIODevice::WriteOnly)) { entitiesFile.write(data); @@ -122,7 +122,7 @@ public: zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + qCritical() << "Failed to zip models.json.gz: " << zipFile.getZipError(); } } @@ -136,6 +136,7 @@ public: private: QString _entitiesFilePath; + QString _entitiesReplacementFilePath; }; #endif /* hifi_BackupHandler_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 159d6d6e95..6f311613d5 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -35,10 +35,10 @@ const int DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds // Backup format looks like: daily_backup-TIMESTAMP.zip -const static QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" }; -const static 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 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-" }; void DomainContentBackupManager::addBackupHandler(BackupHandler handler) { _backupHandlers.push_back(std::move(handler)); } @@ -131,14 +131,6 @@ void DomainContentBackupManager::setup() { } bool DomainContentBackupManager::process() { - if (!_initialLoadComplete) { - QDir backupDir { _backupDirectory }; - if (!backupDir.exists()) { - backupDir.mkpath("."); - } - _initialLoadComplete = true; - } - if (isStillRunning()) { constexpr int64_t MSECS_TO_USECS = 1000; constexpr int64_t USECS_TO_SLEEP = 10 * MSECS_TO_USECS; // every 10ms @@ -219,12 +211,9 @@ void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, cons return; } - bool success { false }; QDir backupDir { _backupDirectory }; QFile backupFile { backupDir.filePath(backupName) }; - if (backupFile.remove()) { - success = true; - } + auto success = backupFile.remove(); promise->resolve({ { "success", success } }); @@ -237,7 +226,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, return; } - qDebug() << "Recoving from" << backupName; + qDebug() << "Recovering from" << backupName; bool success { false }; QDir backupDir { _backupDirectory }; diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 792695acce..cfeae9c8b9 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -51,7 +51,6 @@ public: bool debugTimestampNow = false); void addBackupHandler(BackupHandler handler); - bool isInitialLoadComplete() const { return _initialLoadComplete; } std::vector getAllBackups(); void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist @@ -86,7 +85,6 @@ private: const QString _backupDirectory; std::vector _backupHandlers; int _persistInterval { 0 }; - bool _initialLoadComplete { false }; int64_t _lastCheck { 0 }; std::vector _backupRules; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 0e057972ca..718c5ff402 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -296,7 +296,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : maybeHandleReplacementEntityFile(); _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); - _contentManager->addBackupHandler(new EntitiesBackupHandler(getEntitiesFilePath())); + _contentManager->addBackupHandler(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath())); _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); _contentManager->initialize(true); @@ -1936,7 +1936,6 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url 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 URI_API_BACKUPS_CREATE = "/api/backups"; 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}"; @@ -2127,9 +2126,11 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url auto deferred = makePromise("recoverFromBackup"); deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { QJsonObject rootJSON; - rootJSON["success"] = result["success"].toBool(); + auto success = result["success"].toBool(); + rootJSON["success"] = success; QJsonDocument docJSON(rootJSON); - connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); }); _contentManager->recoverFromBackup(deferred, id); return true; @@ -2258,7 +2259,6 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; } else if (url.path() == URI_API_BACKUPS) { - qDebug() << "GOt request to create a backup:"; auto params = connection->parseUrlEncodedForm(); auto it = params.find("name"); if (it == params.end()) { @@ -2374,9 +2374,11 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url auto deferred = makePromise("deleteBackup"); deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { QJsonObject rootJSON; - rootJSON["success"] = result["success"].toBool(); + auto success = result["success"].toBool(); + rootJSON["success"] = success; QJsonDocument docJSON(rootJSON); - connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); }); _contentManager->deleteBackup(deferred, id); diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index 6496cc3f68..1368a9f54c 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -287,7 +287,6 @@ void HTTPConnection::readContent() { if (_socket->bytesAvailable() < size) { return; } - qDebug() << "Reading content"; _socket->read(_requestContent.data(), size); _socket->disconnect(this, SLOT(readContent())); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index fc3e793b45..f632bcf140 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2246,7 +2246,6 @@ bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer } entityDescription["DataVersion"] = _persistDataVersion; entityDescription["Id"] = _persistID; - qDebug() << "Writing to map: " << _persistDataVersion << _persistID; QScriptEngine scriptEngine; RecurseOctreeToMapOperator theOperator(entityDescription, element, &scriptEngine, skipDefaultValues, skipThoseWithBadParents, _myAvatar); From b6240e8622c8591ee57972abc5a2c719b608395b Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 14 Feb 2018 17:02:11 -0800 Subject: [PATCH 048/260] Move backup recover API to POST --- domain-server/src/DomainServer.cpp | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 718c5ff402..416c8e39b6 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2120,19 +2120,6 @@ 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().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 (url.path() == URI_API_BACKUPS) { QJsonObject rootJSON; @@ -2279,8 +2266,21 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url } } 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) { From f2b6823748cf0c257aa421a3cbf1718c65788245 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 08:20:19 -0800 Subject: [PATCH 049/260] Fix initializer in EntitiesBackupHandler --- domain-server/src/BackupHandler.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 3e25af83e8..7d1a0bdb24 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -81,7 +81,7 @@ class EntitiesBackupHandler { public: EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : _entitiesFilePath(entitiesFilePath) - , _entitiesReplacementFilePath {} + , _entitiesReplacementFilePath(entitiesReplacementFilePath) {} void loadBackup(QuaZip& zip) {} From 2cfa91be0688ead85dd702b6e43f1fd534727900 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 08:48:43 -0800 Subject: [PATCH 050/260] Add memory include to HTTPConnection --- libraries/embedded-webserver/src/HTTPConnection.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index 9c435b14a0..a020dfdca9 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; From b832e118cc0adec0392949fcfee1b880de76af45 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 09:14:20 -0800 Subject: [PATCH 051/260] Add 'override' to BackupHandler methods --- domain-server/src/BackupHandler.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 7d1a0bdb24..045fcedc71 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -54,19 +54,19 @@ private: struct Model : Concept { Model(T* x) : data(x) {} - void loadBackup(QuaZip& zip) { + void loadBackup(QuaZip& zip) override { data->loadBackup(zip); } - void createBackup(QuaZip& zip) { + void createBackup(QuaZip& zip) override { data->createBackup(zip); } - void recoverBackup(QuaZip& zip) { + void recoverBackup(QuaZip& zip) override { data->recoverBackup(zip); } - void deleteBackup(QuaZip& zip) { + void deleteBackup(QuaZip& zip) override { data->deleteBackup(zip); } - void consolidateBackup(QuaZip& zip) { + void consolidateBackup(QuaZip& zip) override { data->consolidateBackup(zip); } From 145a8b385b7432dc906c9f1faefd9f12ebbd4435 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 09:36:59 -0800 Subject: [PATCH 052/260] Move HTTP_RESPONSE_CHUNK_SIZE into lambda for http response --- libraries/embedded-webserver/src/HTTPConnection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index 1368a9f54c..6d0126b3d1 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -183,9 +183,9 @@ void HTTPConnection::respond(const char* code, std::unique_ptr device if (_responseDevice->atEnd()) { _socket->disconnectFromHost(); } else { - constexpr size_t HTTP_RESPONSE_CHUNK_SIZE = 1024 * 10; int totalToBeWritten = csize; 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()) { From 1aba89b908cc29565b600beff5d4539f27496d19 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 09:38:47 -0800 Subject: [PATCH 053/260] Fix style of init list --- domain-server/src/BackupHandler.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 045fcedc71..5fea3be6af 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -79,9 +79,9 @@ private: #include class EntitiesBackupHandler { public: - EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) - : _entitiesFilePath(entitiesFilePath) - , _entitiesReplacementFilePath(entitiesReplacementFilePath) {} + EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : + _entitiesFilePath(entitiesFilePath), + _entitiesReplacementFilePath(entitiesReplacementFilePath) {} void loadBackup(QuaZip& zip) {} From 4b2e907ada0ce5b64606d9fbf2e4c42b793e4d0c Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 11:02:14 -0800 Subject: [PATCH 054/260] Update entities recover backup to reset id and version --- domain-server/src/BackupHandler.h | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 5fea3be6af..f2735e5adf 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -77,6 +77,8 @@ private: }; #include +#include + class EntitiesBackupHandler { public: EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : @@ -111,18 +113,27 @@ public: qCritical() << "Failed to open models.json.gz in backup"; return; } - auto data = zipFile.readAll(); + auto rawData = zipFile.readAll(); + + zipFile.close(); + + OctreeUtils::RawOctreeData data; + if (!OctreeUtils::readOctreeDataInfoFromData(rawData, &data)) { + qCritical() << "Unable to parse octree data during backup recovery"; + return; + } + + data.resetIdAndVersion(); + + if (zipFile.getZipError() != UNZ_OK) { + qCritical() << "Failed to unzip models.json.gz: " << zipFile.getZipError(); + return; + } QFile entitiesFile { _entitiesReplacementFilePath }; if (entitiesFile.open(QIODevice::WriteOnly)) { - entitiesFile.write(data); - } - - zipFile.close(); - - if (zipFile.getZipError() != UNZ_OK) { - qCritical() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + entitiesFile.write(data.toGzippedByteArray()); } } From df809f5a3eb2b80bc73f39789554bf60dde3332a Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 11:02:29 -0800 Subject: [PATCH 055/260] Cleanup logging for backup cleanup --- .../src/DomainContentBackupManager.cpp | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 6f311613d5..347923a282 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -295,18 +295,21 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); int backupsToDelete = matchingFiles.length() - rule.maxBackupVersions; - 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(); + 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"; } - - 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 << "]." From efb2473fcf19737f63032331a632f8cca9672337 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 11:02:55 -0800 Subject: [PATCH 056/260] Updaet createManualBackup to defer response until creation is done --- domain-server/src/DomainContentBackupManager.cpp | 16 ++++++++++++++-- domain-server/src/DomainContentBackupManager.h | 3 +-- domain-server/src/DomainServer.cpp | 12 ++++++++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 347923a282..66655ea966 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -409,8 +409,20 @@ void DomainContentBackupManager::consolidate(QString fileName) { } } -void DomainContentBackupManager::createManualBackup(const QString& name) { - createBackup(MANUAL_BACKUP_PREFIX, name); +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; + } + + bool success; + 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) { diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index cfeae9c8b9..5cf8d4698f 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -57,9 +57,8 @@ public: void replaceData(QByteArray data); - void createManualBackup(const QString& name); - public slots: + void createManualBackup(MiniPromise::Promise promise, const QString& name); void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); void deleteBackup(MiniPromise::Promise promise, const QString& backupName); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 416c8e39b6..da8527bf16 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2253,9 +2253,17 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; } - _contentManager->createManualBackup(it.value()); + 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()); - connection->respond(HTTPConnection::StatusCode200); return true; } else if (url.path() == "/domain_settings") { From 145a0df082654b960a225c276ff1328ccac67a29 Mon Sep 17 00:00:00 2001 From: humbletim Date: Thu, 15 Feb 2018 14:14:07 -0500 Subject: [PATCH 057/260] interim checkin --- interface/src/Application.cpp | 4 +- interface/src/ui/overlays/Base3DOverlay.h | 4 +- interface/src/ui/overlays/ModelOverlay.cpp | 2 +- interface/src/ui/overlays/ModelOverlay.h | 2 +- interface/src/ui/overlays/Shape3DOverlay.cpp | 6 +- interface/src/ui/overlays/Shape3DOverlay.h | 2 +- libraries/entities-renderer/CMakeLists.txt | 2 +- .../src/RenderableEntityItem.h | 4 +- .../src/RenderableModelEntityItem.cpp | 19 +- .../src/RenderableModelEntityItem.h | 3 +- .../graphics-scripting/BufferViewHelpers.cpp | 416 +++++++++- .../graphics-scripting/BufferViewHelpers.h | 35 +- .../BufferViewScripting.cpp | 4 +- .../src/graphics-scripting/Forward.h | 107 +++ .../GraphicsScriptingUtil.cpp | 3 + .../GraphicsScriptingUtil.h | 85 +++ .../ModelScriptingInterface.cpp | 152 ++-- .../ModelScriptingInterface.h | 10 +- .../src/graphics-scripting/ScriptableMesh.cpp | 714 +++++++++--------- .../src/graphics-scripting/ScriptableMesh.h | 198 +++-- .../src/graphics-scripting/ScriptableModel.h | 81 +- 21 files changed, 1219 insertions(+), 634 deletions(-) create mode 100644 libraries/graphics-scripting/src/graphics-scripting/Forward.h create mode 100644 libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.cpp create mode 100644 libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f24969ce60..bdef2f456b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -600,7 +600,9 @@ public: QString error; scriptable::ModelProviderPointer provider; - if (auto entityInterface = getEntityModelProvider(static_cast(uuid))) { + if (uuid.isNull()) { + provider = nullptr; + } else if (auto entityInterface = getEntityModelProvider(static_cast(uuid))) { provider = entityInterface; } else if (auto overlayInterface = getOverlayModelProvider(static_cast(uuid))) { provider = overlayInterface; diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 0c8bc5aacb..6ccad338c9 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -13,7 +13,7 @@ #include #include -#include +#include #include "Overlay.h" namespace model { class Mesh; } @@ -37,7 +37,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::ScriptableModel getScriptableModel(bool* ok = nullptr) override { return scriptable::ModelProvider::modelUnavailableError(ok); } + virtual scriptable::ScriptableModelBase getScriptableModel(bool* ok = nullptr) override { return scriptable::ModelProvider::modelUnavailableError(ok); } // TODO: consider implementing registration points in this class glm::vec3 getCenter() const { return getWorldPosition(); } diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 5a80ca1abf..e007591ce0 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -630,7 +630,7 @@ uint32_t ModelOverlay::fetchMetaSubItems(render::ItemIDs& subItems) const { return 0; } -scriptable::ScriptableModel ModelOverlay::getScriptableModel(bool* ok) { +scriptable::ScriptableModelBase ModelOverlay::getScriptableModel(bool* ok) { if (!_model || !_model->isLoaded()) { return Base3DOverlay::getScriptableModel(ok); } diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index 32d9a08c70..8dc386c733 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -59,7 +59,7 @@ public: void setDrawInFront(bool drawInFront) override; void setDrawHUDLayer(bool drawHUDLayer) override; - virtual scriptable::ScriptableModel getScriptableModel(bool* ok = nullptr) override; + virtual scriptable::ScriptableModelBase getScriptableModel(bool* ok = nullptr) override; protected: Transform evalRenderTransform() override; diff --git a/interface/src/ui/overlays/Shape3DOverlay.cpp b/interface/src/ui/overlays/Shape3DOverlay.cpp index 8bb3d16888..54423feef6 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.cpp +++ b/interface/src/ui/overlays/Shape3DOverlay.cpp @@ -180,15 +180,15 @@ Transform Shape3DOverlay::evalRenderTransform() { return transform; } -scriptable::ScriptableModel Shape3DOverlay::getScriptableModel(bool* ok) { +scriptable::ScriptableModelBase Shape3DOverlay::getScriptableModel(bool* ok) { auto geometryCache = DependencyManager::get(); auto vertexColor = ColorUtils::toVec3(_color); - scriptable::ScriptableModel result; + scriptable::ScriptableModelBase result; result.metadata = { { "origin", "Shape3DOverlay::"+shapeStrings[_shape] }, { "overlayID", getID() }, }; - result.meshes << geometryCache->meshFromShape(_shape, vertexColor); + result.append(geometryCache->meshFromShape(_shape, vertexColor), {{ "shape", shapeStrings[_shape] }}); if (ok) { *ok = true; } diff --git a/interface/src/ui/overlays/Shape3DOverlay.h b/interface/src/ui/overlays/Shape3DOverlay.h index 34f82af278..f5246d95ac 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.h +++ b/interface/src/ui/overlays/Shape3DOverlay.h @@ -37,7 +37,7 @@ public: void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; - virtual scriptable::ScriptableModel getScriptableModel(bool* ok = nullptr) override; + virtual scriptable::ScriptableModelBase getScriptableModel(bool* ok = nullptr) override; protected: Transform evalRenderTransform() override; diff --git a/libraries/entities-renderer/CMakeLists.txt b/libraries/entities-renderer/CMakeLists.txt index 27ea04f642..3aa561f927 100644 --- a/libraries/entities-renderer/CMakeLists.txt +++ b/libraries/entities-renderer/CMakeLists.txt @@ -13,7 +13,7 @@ include_hifi_library_headers(fbx) include_hifi_library_headers(entities) include_hifi_library_headers(avatars) include_hifi_library_headers(controllers) -include_hifi_library_headers(graphics-scripting) # for ScriptableModel.h +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 f07b67fbd0..74759f4fe4 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -17,7 +17,7 @@ #include #include "AbstractViewStateInterface.h" #include "EntitiesRendererLogging.h" -#include +#include class EntityTreeRenderer; @@ -55,7 +55,7 @@ public: const uint64_t& getUpdateTime() const { return _updateTime; } - virtual scriptable::ScriptableModel getScriptableModel(bool* ok = nullptr) override { return scriptable::ModelProvider::modelUnavailableError(ok); } + virtual scriptable::ScriptableModelBase getScriptableModel(bool* ok = nullptr) override { return scriptable::ModelProvider::modelUnavailableError(ok); } 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 7b022fefac..3d6714a400 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -950,12 +950,9 @@ QStringList RenderableModelEntityItem::getJointNames() const { return result; } - -scriptable::ScriptableModel render::entities::ModelEntityRenderer::getScriptableModel(bool* ok) { +scriptable::ScriptableModelBase render::entities::ModelEntityRenderer::getScriptableModel(bool* ok) { ModelPointer model; - withReadLock([&] { - model = _model; - }); + withReadLock([&] { model = _model; }); if (!model || !model->isLoaded()) { return scriptable::ModelProvider::modelUnavailableError(ok); @@ -964,6 +961,18 @@ scriptable::ScriptableModel render::entities::ModelEntityRenderer::getScriptable return _model->getScriptableModel(ok); } +bool render::entities::ModelEntityRenderer::replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointer newModel, int meshIndex, int partIndex) { + qCDebug(entitiesrenderer) << "REPLACING RenderableModelEntityItem" << newModel->objectName(); + ModelPointer model; + withReadLock([&] { model = _model; }); + + if (!model || !model->isLoaded()) { + return false; + } + + return _model->replaceScriptableModelMeshPart(newModel, meshIndex, partIndex); +} + void RenderableModelEntityItem::simulateRelayedJoints() { ModelPointer model = getModel(); if (model && model->isLoaded()) { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 3e952cb9a7..ffb83d3609 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -140,7 +140,8 @@ class ModelEntityRenderer : public TypedEntityRenderer #include +#include + +#include +#include + +#include #include +#include namespace glm { using hvec2 = glm::tvec2; using hvec4 = glm::tvec4; } //#define DEBUG_BUFFERVIEW_SCRIPTING -#ifdef DEBUG_BUFFERVIEW_SCRIPTING +//#ifdef DEBUG_BUFFERVIEW_SCRIPTING #include "DebugNames.h" - QLoggingCategory bufferview_helpers{"hifi.bufferview"}; -#endif +//#endif namespace { + QLoggingCategory bufferhelper_logging{"hifi.bufferview"}; const std::array XYZW = {{ "x", "y", "z", "w" }}; const std::array ZERO123 = {{ "0", "1", "2", "3" }}; } +gpu::BufferView buffer_helpers::getBufferView(graphics::MeshPointer mesh, gpu::Stream::Slot slot) { + return slot == gpu::Stream::POSITION ? mesh->getVertexBuffer() : mesh->getAttributeBuffer(slot); +} +QMap buffer_helpers::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 }, +}; + + template QVariant getBufferViewElement(const gpu::BufferView& view, quint32 index, bool asArray = false) { return glmVecToVariant(view.get(index), asArray); @@ -61,14 +86,14 @@ static void packNormalAndTangent(glm::vec3 normal, glm::vec3 tangent, glm::uint3 packedTangent = tangentStruct.pack; } -bool bufferViewElementFromVariant(const gpu::BufferView& view, quint32 index, const QVariant& v) { +bool buffer_helpers::fromVariant(const gpu::BufferView& view, quint32 index, const QVariant& v) { const auto& element = view._element; const auto vecN = element.getScalarCount(); const auto dataType = element.getType(); const auto byteLength = element.getSize(); const auto BYTES_PER_ELEMENT = byteLength / vecN; #ifdef DEBUG_BUFFERVIEW_SCRIPTING - qCDebug(bufferview_helpers) << "bufferViewElementFromVariant" << index << DebugNames::stringFrom(dataType) << BYTES_PER_ELEMENT << vecN; + qCDebug(bufferhelper_logging) << "bufferViewElementFromVariant" << index << DebugNames::stringFrom(dataType) << BYTES_PER_ELEMENT << vecN; #endif if (BYTES_PER_ELEMENT == 1) { switch(vecN) { @@ -122,16 +147,34 @@ bool bufferViewElementFromVariant(const gpu::BufferView& view, quint32 index, co return false; } -QVariant bufferViewElementToVariant(const gpu::BufferView& view, quint32 index, bool asArray, const char* hint) { +bool boundsCheck(const gpu::BufferView& view, quint32 index) { + const auto byteLength = view._element.getSize(); + return ( + index < view.getNumElements() && + index * byteLength < (view._size - 1) * byteLength + ); +} + +QVariant buffer_helpers::toVariant(const gpu::BufferView& view, quint32 index, bool asArray, const char* hint) { const auto& element = view._element; const auto vecN = element.getScalarCount(); const auto dataType = element.getType(); const auto byteLength = element.getSize(); const auto BYTES_PER_ELEMENT = byteLength / vecN; Q_ASSERT(index < view.getNumElements()); - Q_ASSERT(index * vecN * BYTES_PER_ELEMENT < (view._size - vecN * BYTES_PER_ELEMENT)); + if (!boundsCheck(view, index)) { + // sanity checks + auto byteOffset = index * vecN * BYTES_PER_ELEMENT; + auto maxByteOffset = (view._size - 1) * vecN * BYTES_PER_ELEMENT; + if (byteOffset > maxByteOffset) { + qDebug() << "bufferViewElementToVariant -- byteOffset out of range " << byteOffset << " < " << maxByteOffset << DebugNames::stringFrom(dataType); + qDebug() << "bufferViewElementToVariant -- index: " << index << "numElements" << view.getNumElements(); + qDebug() << "bufferViewElementToVariant -- vecN: " << vecN << "byteLength" << byteLength << "BYTES_PER_ELEMENT" << BYTES_PER_ELEMENT; + } + Q_ASSERT(byteOffset <= maxByteOffset); + } #ifdef DEBUG_BUFFERVIEW_SCRIPTING - qCDebug(bufferview_helpers) << "bufferViewElementToVariant" << index << DebugNames::stringFrom(dataType) << BYTES_PER_ELEMENT << vecN; + qCDebug(bufferhelper_logging) << "bufferViewElementToVariant" << index << DebugNames::stringFrom(dataType) << BYTES_PER_ELEMENT << vecN; #endif if (BYTES_PER_ELEMENT == 1) { switch(vecN) { @@ -221,22 +264,137 @@ const T glmVecFromVariant(const QVariant& v) { } template -gpu::BufferView bufferViewFromVector(QVector elements, gpu::Element elementType) { +gpu::BufferView buffer_helpers::fromVector(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 buffer_helpers::fromVector(const QVector& elements, const gpu::Element& elementType) { return fromVector(elements, elementType); } +template<> gpu::BufferView buffer_helpers::fromVector(const QVector& elements, const gpu::Element& elementType) { return fromVector(elements, elementType); } -template<> gpu::BufferView bufferViewFromVector(QVector elements, gpu::Element elementType) { return bufferViewFromVector(elements, elementType); } -template<> gpu::BufferView bufferViewFromVector(QVector elements, gpu::Element elementType) { return bufferViewFromVector(elements, elementType); } +template struct getVec4;// { static T get(const gpu::BufferView& view, quint32 index, const char *hint); }; +template struct getScalar;// { static T get(const gpu::BufferView& view, quint32 index, const char *hint); }; -gpu::BufferView cloneBufferView(const gpu::BufferView& input) { +struct gotter { + static float error(const QString& name, const gpu::BufferView& view, quint32 index, const char *hint) { + qDebug() << QString("gotter:: unhandled type=%1(element=%2(%3)) size=%4(per=%5) vec%6 hint=%7 #%8") + .arg(name) + .arg(DebugNames::stringFrom(view._element.getType())) + .arg(view._element.getType()) + .arg(view._element.getSize()) + .arg(view._element.getSize() / view._element.getScalarCount()) + .arg(view._element.getScalarCount()) + .arg(hint) + .arg(view.getNumElements()); + Q_ASSERT(false); + assert(false); + return NAN; + } +}; +template struct getScalar : gotter { + static T get(const gpu::BufferView& view, quint32 index, const char *hint) { 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::unpackSnorm1x8(view.get(index))); + default: break; + } return T(error("getScalar", view, index, hint)); + } +}; + +template struct getVec2 : gotter { static T get(const gpu::BufferView& view, quint32 index, const char *hint) { 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 glm::unpackSnorm2x8(view.get(index)); + default: break; + } return T(error("getVec2", view, index, hint)); }}; + + +template struct getVec3 : gotter { static T get(const gpu::BufferView& view, quint32 index, const char *hint) { 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: + case gpu::NUINT8: + case gpu::NINT2_10_10_10: + if (view._element.getSize() == sizeof(glm::int32)) { + return getVec4::get(view, index, hint); + } + default: break; + } return T(error("getVec3", view, index, hint)); }}; + +template struct getVec4 : gotter { static T get(const gpu::BufferView& view, quint32 index, const char *hint) { + assert(view._element.getSize() == sizeof(glm::int32)); + 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: break; + case gpu::NUINT8: 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: return glm::unpackSnorm4x8(view.get(index)); + case gpu::NINT2_10_10_10: return glm::unpackSnorm3x10_1x2(view.get(index)); + } return T(error("getVec4", view, index, hint)); }}; + + +template +struct getVec { + static QVector __to_vector__(const gpu::BufferView& view, const char *hint) { + QVector result; + const quint32 count = (quint32)view.getNumElements(); + result.resize(count); + for (quint32 i = 0; i < count; i++) { + result[i] = FUNC::get(view, i, hint); + } + return result; + } + static T __to_scalar__(const gpu::BufferView& view, quint32 index, const char *hint) { + assert(boundsCheck(view, index)); + return FUNC::get(view, index, hint); + } +}; + +template <> QVector buffer_helpers::toVector(const gpu::BufferView& view, const char *hint) { return getVec,int>::__to_vector__(view, hint); } +template <> QVector buffer_helpers::toVector(const gpu::BufferView& view, const char *hint) { return getVec,glm::vec2>::__to_vector__(view, hint); } +template <> QVector buffer_helpers::toVector(const gpu::BufferView& view, const char *hint) { return getVec,glm::vec3>::__to_vector__(view, hint); } +template <> QVector buffer_helpers::toVector(const gpu::BufferView& view, const char *hint) { return getVec,glm::vec4>::__to_vector__(view, hint); } + + +template <> int buffer_helpers::convert(const gpu::BufferView& view, quint32 index, const char *hint) { return getVec,int>::__to_scalar__(view, index, hint); } +template <> glm::vec2 buffer_helpers::convert(const gpu::BufferView& view, quint32 index, const char *hint) { return getVec,glm::vec2>::__to_scalar__(view, index, hint); } +template <> glm::vec3 buffer_helpers::convert(const gpu::BufferView& view, quint32 index, const char *hint) { return getVec,glm::vec3>::__to_scalar__(view, index, hint); } +template <> glm::vec4 buffer_helpers::convert(const gpu::BufferView& view, quint32 index, const char *hint) { return getVec,glm::vec4>::__to_scalar__(view, index, hint); } + +gpu::BufferView buffer_helpers::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 resizedBufferView(const gpu::BufferView& input, quint32 numElements) { +gpu::BufferView buffer_helpers::resize(const gpu::BufferView& input, quint32 numElements) { auto effectiveSize = input._buffer->getSize() / input.getNumElements(); qDebug() << "resize input" << input.getNumElements() << input._buffer->getSize() << "effectiveSize" << effectiveSize; auto vsize = input._element.getSize() * numElements; @@ -248,3 +406,235 @@ gpu::BufferView resizedBufferView(const gpu::BufferView& input, quint32 numEleme qDebug() << "resized output" << output.getNumElements() << output._buffer->getSize(); return output; } + +graphics::MeshPointer buffer_helpers::cloneMesh(graphics::MeshPointer mesh) { + auto clone = std::make_shared(); + //[](graphics::Mesh* blah) { + //qCDebug(bufferhelper_logging) << "--- DELETING MESH POINTER" << blah; + // delete blah; + //}); + clone->displayName = (QString::fromStdString(mesh->displayName) + "-clone").toStdString(); + //qCInfo(bufferhelper_logging) << "+++ ALLOCATED MESH POINTER ScriptableMesh::cloneMesh" << clone->displayName << clone.get() << !!mesh; + clone->setIndexBuffer(buffer_helpers::clone(mesh->getIndexBuffer())); + clone->setPartBuffer(buffer_helpers::clone(mesh->getPartBuffer())); + auto attributeViews = buffer_helpers::gatherBufferViews(mesh); + for (const auto& a : attributeViews) { + auto& view = a.second; + auto slot = buffer_helpers::ATTRIBUTES[a.first]; + auto points = buffer_helpers::clone(view); + if (slot == gpu::Stream::POSITION) { + clone->setVertexBuffer(points); + } else { + clone->addAttribute(slot, points); + } + } + return clone; +} + + +/// --- buffer view <-> variant helpers + +namespace { + // expand the corresponding attribute buffer (creating it if needed) so that it matches POSITIONS size and specified element type + gpu::BufferView _expandedAttributeBuffer(const graphics::MeshPointer mesh, gpu::Stream::Slot slot) { + gpu::BufferView bufferView = buffer_helpers::getBufferView(mesh, slot); + const auto& elementType = bufferView._element; + //auto vecN = element.getScalarCount(); + //auto type = element.getType(); + //gpu::Element elementType = getVecNElement(type, vecN); + + gpu::Size elementSize = elementType.getSize(); + auto nPositions = mesh->getNumVertices(); + auto vsize = nPositions * elementSize; + auto diffTypes = (elementType.getType() != bufferView._element.getType() || + elementType.getSize() > bufferView._element.getSize() || + elementType.getScalarCount() > bufferView._element.getScalarCount() || + vsize > bufferView._size + ); + QString hint = QString("%1").arg(slot); +#ifdef DEBUG_BUFFERVIEW_SCRIPTING + hint = DebugNames::stringFrom(slot); +#endif +#ifdef DEV_BUILD + auto beforeCount = bufferView.getNumElements(); + auto beforeTotal = bufferView._size; +#endif + if (bufferView.getNumElements() < nPositions || diffTypes) { + if (!bufferView._buffer || bufferView.getNumElements() == 0) { + qCInfo(bufferhelper_logging).nospace() << "ScriptableMesh -- adding missing mesh attribute '" << hint << "' for BufferView"; + gpu::Byte *data = new gpu::Byte[vsize]; + memset(data, 0, vsize); + auto buffer = new gpu::Buffer(vsize, (gpu::Byte*)data); + delete[] data; + bufferView = gpu::BufferView(buffer, elementType); + mesh->addAttribute(slot, bufferView); + } else { + qCInfo(bufferhelper_logging) << "ScriptableMesh -- resizing Buffer current:" << hint << bufferView._buffer->getSize() << "wanted:" << vsize; + bufferView._element = elementType; + bufferView._buffer->resize(vsize); + bufferView._size = bufferView._buffer->getSize(); + } + } +#ifdef DEV_BUILD + auto afterCount = bufferView.getNumElements(); + auto afterTotal = bufferView._size; + if (beforeTotal != afterTotal || beforeCount != afterCount) { + QString typeName = QString("%1").arg(bufferView._element.getType()); +#ifdef DEBUG_BUFFERVIEW_SCRIPTING + typeName = DebugNames::stringFrom(bufferView._element.getType()); +#endif + qCDebug(bufferhelper_logging, "NOTE:: _expandedAttributeBuffer.%s vec%d %s (before count=%lu bytes=%lu // after count=%lu bytes=%lu)", + hint.toStdString().c_str(), bufferView._element.getScalarCount(), + typeName.toStdString().c_str(), beforeCount, beforeTotal, afterCount, afterTotal); + } +#endif + return bufferView; + } + + gpu::BufferView expandAttributeToMatchPositions(graphics::MeshPointer mesh, gpu::Stream::Slot slot) { + if (slot == gpu::Stream::POSITION) { + return buffer_helpers::getBufferView(mesh, slot); + } + return _expandedAttributeBuffer(mesh, slot); + } +} + +std::map buffer_helpers::gatherBufferViews(graphics::MeshPointer mesh, const QStringList& expandToMatchPositions) { + std::map attributeViews; + if (!mesh) { + return attributeViews; + } + for (const auto& a : buffer_helpers::ATTRIBUTES.toStdMap()) { + auto name = a.first; + auto slot = a.second; + auto view = getBufferView(mesh, slot); + auto beforeCount = view.getNumElements(); + auto beforeTotal = view._size; + if (expandToMatchPositions.contains(name)) { + expandAttributeToMatchPositions(mesh, slot); + } + if (beforeCount > 0) { + auto element = view._element; + auto vecN = element.getScalarCount(); + //auto type = element.getType(); + QString typeName = QString("%1").arg(element.getType()); +#ifdef DEBUG_BUFFERVIEW_SCRIPTING + typeName = DebugNames::stringFrom(element.getType()); +#endif + + attributeViews[name] = getBufferView(mesh, slot); + +#if DEV_BUILD + auto afterTotal = attributeViews[name]._size; + auto afterCount = attributeViews[name].getNumElements(); + if (beforeTotal != afterTotal || beforeCount != afterCount) { + qCDebug(bufferhelper_logging, "NOTE:: gatherBufferViews.%s vec%d %s (before count=%lu bytes=%lu // after count=%lu bytes=%lu)", + name.toStdString().c_str(), vecN, typeName.toStdString().c_str(), beforeCount, beforeTotal, afterCount, afterTotal); + } +#endif + } + } + return attributeViews; +} + + +bool buffer_helpers::recalculateNormals(graphics::MeshPointer mesh) { + qCInfo(bufferhelper_logging) << "Recalculating normals" << !!mesh; + if (!mesh) { + return false; + } + buffer_helpers::gatherBufferViews(mesh, { "normal", "color" }); // ensures #normals >= #positions + auto normals = mesh->getAttributeBuffer(gpu::Stream::NORMAL); + auto verts = mesh->getVertexBuffer(); + auto indices = mesh->getIndexBuffer(); + auto esize = indices._element.getSize(); + auto numPoints = indices.getNumElements(); + const auto TRIANGLE = 3; + quint32 numFaces = (quint32)numPoints / TRIANGLE; + //QVector faces; + QVector faceNormals; + QMap> vertexToFaces; + //faces.resize(numFaces); + faceNormals.resize(numFaces); + auto numNormals = normals.getNumElements(); + qCInfo(bufferhelper_logging) << QString("numFaces: %1, numNormals: %2, numPoints: %3").arg(numFaces).arg(numNormals).arg(numPoints); + if (normals.getNumElements() != verts.getNumElements()) { + return false; + } + for (quint32 i = 0; i < numFaces; i++) { + quint32 I = TRIANGLE * i; + quint32 i0 = esize == 4 ? indices.get(I+0) : indices.get(I+0); + quint32 i1 = esize == 4 ? indices.get(I+1) : indices.get(I+1); + quint32 i2 = esize == 4 ? indices.get(I+2) : indices.get(I+2); + + Triangle face = { + verts.get(i1), + verts.get(i2), + verts.get(i0) + }; + faceNormals[i] = face.getNormal(); + if (glm::isnan(faceNormals[i].x)) { + qCInfo(bufferhelper_logging) << i << i0 << i1 << i2 << glmVecToVariant(face.v0) << glmVecToVariant(face.v1) << glmVecToVariant(face.v2); + break; + } + vertexToFaces[glm::to_string(face.v0).c_str()] << i; + vertexToFaces[glm::to_string(face.v1).c_str()] << i; + vertexToFaces[glm::to_string(face.v2).c_str()] << i; + } + for (quint32 j = 0; j < numNormals; j++) { + //auto v = verts.get(j); + glm::vec3 normal { 0.0f, 0.0f, 0.0f }; + QString key { glm::to_string(verts.get(j)).c_str() }; + const auto& faces = vertexToFaces.value(key); + if (faces.size()) { + for (const auto i : faces) { + normal += faceNormals[i]; + } + normal *= 1.0f / (float)faces.size(); + } else { + static int logged = 0; + if (logged++ < 10) { + qCInfo(bufferhelper_logging) << "no faces for key!?" << key; + } + normal = verts.get(j); + } + if (glm::isnan(normal.x)) { + static int logged = 0; + if (logged++ < 10) { + qCInfo(bufferhelper_logging) << "isnan(normal.x)" << j << glmVecToVariant(normal); + } + break; + } + normals.edit(j) = glm::normalize(normal); + } + return true; +} + +QVariant buffer_helpers::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 buffer_helpers::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 buffer_helpers::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()) }, + }; +} diff --git a/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.h b/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.h index d0d42ca419..d963fd4b22 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.h +++ b/libraries/graphics-scripting/src/graphics-scripting/BufferViewHelpers.h @@ -7,6 +7,8 @@ #pragma once #include +#include +#include namespace gpu { class BufferView; @@ -15,11 +17,34 @@ namespace gpu { template QVariant glmVecToVariant(const T& v, bool asArray = false); template const T glmVecFromVariant(const QVariant& v); -QVariant bufferViewElementToVariant(const gpu::BufferView& view, quint32 index, bool asArray = false, const char* hint = ""); -bool bufferViewElementFromVariant(const gpu::BufferView& view, quint32 index, const QVariant& v); -template gpu::BufferView bufferViewFromVector(QVector elements, gpu::Element elementType); +namespace graphics { + class Mesh; + using MeshPointer = std::shared_ptr; +} -gpu::BufferView cloneBufferView(const gpu::BufferView& input); -gpu::BufferView resizedBufferView(const gpu::BufferView& input, quint32 numElements); +class Extents; +class AABox; +struct buffer_helpers { + static graphics::MeshPointer cloneMesh(graphics::MeshPointer mesh); + static QMap ATTRIBUTES; + static std::map gatherBufferViews(graphics::MeshPointer mesh, const QStringList& expandToMatchPositions = QStringList()); + static bool recalculateNormals(graphics::MeshPointer meshProxy); + static gpu::BufferView getBufferView(graphics::MeshPointer mesh, quint8 slot); + + static QVariant toVariant(const Extents& box); + static QVariant toVariant(const AABox& box); + static QVariant toVariant(const glm::mat4& mat4); + static QVariant toVariant(const gpu::BufferView& view, quint32 index, bool asArray = false, const char* hint = ""); + + static bool fromVariant(const gpu::BufferView& view, quint32 index, const QVariant& v); + + template static gpu::BufferView fromVector(const QVector& elements, const gpu::Element& elementType); + + template static QVector toVector(const gpu::BufferView& view, const char *hint = ""); + template static T convert(const gpu::BufferView& view, quint32 index, const char* hint = ""); + + static gpu::BufferView clone(const gpu::BufferView& input); + static gpu::BufferView resize(const gpu::BufferView& input, quint32 numElements); +}; diff --git a/libraries/graphics-scripting/src/graphics-scripting/BufferViewScripting.cpp b/libraries/graphics-scripting/src/graphics-scripting/BufferViewScripting.cpp index 367c0589e9..ab6f2c92be 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/BufferViewScripting.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/BufferViewScripting.cpp @@ -26,7 +26,7 @@ QScriptValue getBufferViewElement(QScriptEngine* js, const gpu::BufferView& view } QScriptValue bufferViewElementToScriptValue(QScriptEngine* engine, const gpu::BufferView& view, quint32 index, bool asArray, const char* hint) { - QVariant result = bufferViewElementToVariant(view, index, asArray, hint); + QVariant result = buffer_helpers::toVariant(view, index, asArray, hint); if (!result.isValid()) { return QScriptValue::NullValue; } @@ -39,7 +39,7 @@ void setBufferViewElement(const gpu::BufferView& view, quint32 index, const QScr } bool bufferViewElementFromScriptValue(const QScriptValue& v, const gpu::BufferView& view, quint32 index) { - return bufferViewElementFromVariant(view, index, v.toVariant()); + return buffer_helpers::fromVariant(view, index, v.toVariant()); } // 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..15973b5852 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/Forward.h @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace graphics { + class Mesh; +} +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 mesh; + MeshPointer ownedMesh; + QVariantMap metadata; + ScriptableMeshBase(WeakModelProviderPointer provider, ScriptableModelBasePointer model, WeakMeshPointer mesh, const QVariantMap& metadata); + ScriptableMeshBase(WeakMeshPointer mesh = WeakMeshPointer()); + ScriptableMeshBase(MeshPointer mesh, const QVariantMap& metadata); + ScriptableMeshBase(const ScriptableMeshBase& other) { *this = other; } + ScriptableMeshBase& operator=(const ScriptableMeshBase& view); + virtual ~ScriptableMeshBase(); + Q_INVOKABLE const scriptable::MeshPointer getMeshPointer() const { return mesh.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 + QVariantMap metadata; + QVector meshes; + + ScriptableModelBase(QObject* parent = nullptr) : QObject(parent) {} + ScriptableModelBase(const ScriptableModelBase& other) { *this = other; } + ScriptableModelBase& operator=(const ScriptableModelBase& other) { + provider = other.provider; + objectID = other.objectID; + metadata = other.metadata; + for (auto& mesh : other.meshes) { + append(mesh); + } + return *this; + } + virtual ~ScriptableModelBase(); + + void mixin(const QVariantMap& other); + void append(const ScriptableModelBase& other, const QVariantMap& modelMetadata = QVariantMap()); + void append(scriptable::WeakMeshPointer mesh, const QVariantMap& metadata = QVariantMap()); + void append(const ScriptableMeshBase& mesh, const QVariantMap& metadata = QVariantMap()); + // 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: + QVariantMap metadata{ { "providerType", "unknown" } }; + static scriptable::ScriptableModelBase modelUnavailableError(bool* ok) { if (ok) { *ok = false; } return {}; } + virtual scriptable::ScriptableModelBase getScriptableModel(bool* ok = nullptr) = 0; + + virtual bool replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointer model, int meshIndex, int partIndex) { return false; } + }; + + // mixin class for resolving UUIDs into a corresponding ModelProvider + class ModelProviderFactory : public Dependency { + public: + virtual scriptable::ModelProviderPointer lookupModelProvider(QUuid uuid) = 0; + }; + + using uint32 = quint32; + class ScriptableModel; + using ScriptableModelPointer = QPointer; + class ScriptableMesh; + using ScriptableMeshPointer = QPointer; + class ScriptableMeshPart; + using ScriptableMeshPartPointer = QPointer; + bool registerMetaTypes(QScriptEngine* engine); +} 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..aabf83ff66 --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.cpp @@ -0,0 +1,3 @@ +#include "GraphicsScriptingUtil.h" + +Q_LOGGING_CATEGORY(graphics_scripting, "hifi.scripting.graphics") 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..a536fc413c --- /dev/null +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +Q_DECLARE_LOGGING_CATEGORY(graphics_scripting) + +namespace scriptable { + // derive current context's C++ QObject (based on current JS "this" value) + template T this_qobject_cast(QScriptEngine* engine) { + auto context = engine ? engine->currentContext() : nullptr; + return qscriptvalue_cast(context ? context->thisObject() : QScriptValue::NullValue); + } + // JS => QPointer + template QPointer qpointer_qobject_cast(const QScriptValue& value) { + auto obj = value.toQObject(); + qCInfo(graphics_scripting) << "qpointer_qobject_cast" << obj << value.toString(); + if (auto tmp = qobject_cast(obj)) { + return QPointer(tmp); + } + if (auto tmp = static_cast(obj)) { + return QPointer(tmp); + } + return nullptr; + } + inline QString toDebugString(QObject* tmp) { + 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())); + } + + // C++ > QtOwned instance + template std::shared_ptr make_qtowned(Rest... rest) { + T* tmp = new T(rest...); + qCInfo(graphics_scripting) << "scriptable::make_qtowned" << toDebugString(tmp); + QString debug = toDebugString(tmp); + if (tmp) { + tmp->metadata["__ownership__"] = QScriptEngine::QtOwnership; + QObject::connect(tmp, &QObject::destroyed, [=]() { qCInfo(graphics_scripting) << "-------- ~scriptable::make_qtowned" << debug; }); + auto ptr = std::shared_ptr(tmp, [debug](T* tmp) { + //qDebug() << "~std::shared_ptr" << debug; + delete tmp; + }); + return ptr; + } else { + return std::shared_ptr(tmp); + } + } + // C++ > ScriptOwned JS instance + template QPointer make_scriptowned(Rest... rest) { + T* tmp = new T(rest...); + qCInfo(graphics_scripting) << "scriptable::make_scriptowned" << toDebugString(tmp); + if (tmp) { + tmp->metadata["__ownership__"] = QScriptEngine::ScriptOwnership; + //auto blah = (DeleterFunction)[](void* delme) { }; + return add_scriptowned_destructor(tmp); + } else { + return QPointer(tmp); + } + } + // C++ > ScriptOwned JS instance + template QPointer add_scriptowned_destructor(T* tmp) { + QString debug = toDebugString(tmp); + if (tmp) { + QObject::connect(tmp, &QObject::destroyed, [=]() { + qCInfo(graphics_scripting) << "-------- ~scriptable::make_scriptowned" << debug;// << !!customDeleter; + //if (customDeleter) { + // customDeleter(tmp); + //} + }); + } else { + qCInfo(graphics_scripting) << "add_scriptowned_destructor -- not connecting to null value" << debug; + } + return QPointer(tmp); + } +} diff --git a/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.cpp index ab85fb8265..ab9403a8ed 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.cpp @@ -26,12 +26,11 @@ #include "BufferViewScripting.h" #include "ScriptableMesh.h" +#include "GraphicsScriptingUtil.h" #include "ModelScriptingInterface.moc" -namespace { - QLoggingCategory model_scripting { "hifi.model.scripting" }; -} +#include "RegisteredMetaTypes.h" ModelScriptingInterface::ModelScriptingInterface(QObject* parent) : QObject(parent) { if (auto scriptEngine = qobject_cast(parent)) { @@ -39,8 +38,51 @@ ModelScriptingInterface::ModelScriptingInterface(QObject* parent) : QObject(pare } } -void ModelScriptingInterface::getMeshes(QUuid uuid, QScriptValue scopeOrCallback, QScriptValue methodOrName) { - auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); +bool ModelScriptingInterface::updateMeshes(QUuid uuid, const scriptable::ScriptableMeshPointer mesh, int meshIndex, int partIndex) { + auto model = scriptable::make_qtowned(); + if (mesh) { + model->append(*mesh); + } + return updateMeshes(uuid, model.get()); +} + +bool ModelScriptingInterface::updateMeshes(QUuid uuid, const scriptable::ScriptableModelPointer model) { + auto appProvider = DependencyManager::get(); + qCDebug(graphics_scripting) << "appProvider" << appProvider.data(); + scriptable::ModelProviderPointer provider = appProvider ? appProvider->lookupModelProvider(uuid) : nullptr; + QString providerType = provider ? provider->metadata.value("providerType").toString() : QString(); + if (providerType.isEmpty()) { + providerType = "unknown"; + } + bool success = false; + if (provider) { + qCDebug(graphics_scripting) << "fetching meshes from " << providerType << "..."; + auto scriptableMeshes = provider->getScriptableModel(&success); + qCDebug(graphics_scripting) << "//fetched meshes from " << providerType << "success:" <operator scriptable::ScriptableModelBasePointer(); + qCDebug(graphics_scripting) << "as base" << base; + if (base) { + //auto meshes = model->getConstMeshes(); + success = provider->replaceScriptableModelMeshPart(base, -1, -1); + + // for (uint32_t m = 0; success && m < meshes.size(); m++) { + // const auto& mesh = meshes.at(m); + // for (int p = 0; success && p < mesh->getNumParts(); p++) { + // qCDebug(graphics_scripting) << "provider->replaceScriptableModelMeshPart" << "meshIndex" << m << "partIndex" << p; + // success = provider->replaceScriptableModelMeshPart(base, m, p); + // //if (!success) { + // qCDebug(graphics_scripting) << "//provider->replaceScriptableModelMeshPart" << "meshIndex" << m << "partIndex" << p << success; + // } + // } + } + } + } + return success; +} + +void ModelScriptingInterface::getMeshes(QUuid uuid, QScriptValue callback) { + auto handler = scriptable::jsBindCallback(callback); Q_ASSERT(handler.engine() == this->engine()); QPointer engine = dynamic_cast(handler.engine()); @@ -49,18 +91,23 @@ void ModelScriptingInterface::getMeshes(QUuid uuid, QScriptValue scopeOrCallback QString error; auto appProvider = DependencyManager::get(); - qDebug() << "appProvider" << appProvider.data(); + qCDebug(graphics_scripting) << "appProvider" << appProvider.data(); scriptable::ModelProviderPointer provider = appProvider ? appProvider->lookupModelProvider(uuid) : nullptr; QString providerType = provider ? provider->metadata.value("providerType").toString() : QString(); if (providerType.isEmpty()) { providerType = "unknown"; } if (provider) { - qCDebug(model_scripting) << "fetching meshes from " << providerType << "..."; + qCDebug(graphics_scripting) << "fetching meshes from " << providerType << "..."; auto scriptableMeshes = provider->getScriptableModel(&success); - qCDebug(model_scripting) << "//fetched meshes from " << providerType << "success:" <(scriptableMeshes); + QString debugString = scriptable::toDebugString(meshes); + QObject::connect(meshes, &QObject::destroyed, this, [=]() { + qCDebug(graphics_scripting) << "///fetched meshes" << debugString; + }); + if (meshes->objectName().isEmpty()) { meshes->setObjectName(providerType+"::meshes"); } @@ -75,20 +122,20 @@ void ModelScriptingInterface::getMeshes(QUuid uuid, QScriptValue scopeOrCallback } if (!error.isEmpty()) { - qCWarning(model_scripting) << "ModelScriptingInterface::getMeshes ERROR" << error; + qCWarning(graphics_scripting) << "ModelScriptingInterface::getMeshes ERROR" << error; callScopedHandlerObject(handler, engine->makeError(error), QScriptValue::NullValue); } else { - callScopedHandlerObject(handler, QScriptValue::NullValue, engine->newQObject(meshes, QScriptEngine::ScriptOwnership)); + callScopedHandlerObject(handler, QScriptValue::NullValue, engine->toScriptValue(meshes)); } } QString ModelScriptingInterface::meshToOBJ(const scriptable::ScriptableModel& _in) { const auto& in = _in.getConstMeshes(); - qCDebug(model_scripting) << "meshToOBJ" << in.size(); + qCDebug(graphics_scripting) << "meshToOBJ" << in.size(); if (in.size()) { QList meshes; foreach (auto meshProxy, in) { - qCDebug(model_scripting) << "meshToOBJ" << meshProxy; + qCDebug(graphics_scripting) << "meshToOBJ" << meshProxy; if (meshProxy) { meshes.append(getMeshPointer(meshProxy)); } @@ -207,7 +254,7 @@ QScriptValue ModelScriptingInterface::appendMeshes(scriptable::ScriptableModel _ (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); - return engine()->toScriptValue(scriptable::ScriptableMeshPointer(new scriptable::ScriptableMesh(nullptr, result))); + return engine()->toScriptValue(scriptable::make_scriptowned(result)); } QScriptValue ModelScriptingInterface::transformMesh(scriptable::ScriptableMeshPointer meshProxy, glm::mat4 transform) { @@ -220,8 +267,7 @@ QScriptValue ModelScriptingInterface::transformMesh(scriptable::ScriptableMeshPo [&](glm::vec3 color){ return color; }, [&](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, [&](uint32_t index){ return index; }); - scriptable::ScriptableMeshPointer resultProxy = scriptable::ScriptableMeshPointer(new scriptable::ScriptableMesh(nullptr, result)); - return engine()->toScriptValue(resultProxy); + return engine()->toScriptValue(scriptable::make_scriptowned(result)); } QScriptValue ModelScriptingInterface::getVertexCount(scriptable::ScriptableMeshPointer meshProxy) { @@ -270,7 +316,7 @@ QScriptValue ModelScriptingInterface::newMesh(const QVector& vertices sizeof(glm::vec3), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); mesh->addAttribute(gpu::Stream::NORMAL, normalBufferView); } else { - qCWarning(model_scripting, "ModelScriptingInterface::newMesh normals must be same length as vertices"); + qCWarning(graphics_scripting, "ModelScriptingInterface::newMesh normals must be same length as vertices"); } // indices (faces) @@ -300,54 +346,10 @@ QScriptValue ModelScriptingInterface::newMesh(const QVector& vertices - scriptable::ScriptableMeshPointer meshProxy = scriptable::ScriptableMeshPointer(new scriptable::ScriptableMesh(nullptr, mesh)); - return engine()->toScriptValue(meshProxy); + return engine()->toScriptValue(scriptable::make_scriptowned(mesh)); } namespace { - QScriptValue meshPointerToScriptValue(QScriptEngine* engine, scriptable::ScriptableMeshPointer const &in) { - if (!in) { - return QScriptValue::NullValue; - } - return engine->newQObject(in, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); - } - - void meshPointerFromScriptValue(const QScriptValue& value, scriptable::ScriptableMeshPointer &out) { - auto obj = value.toQObject(); - qDebug() << "meshPointerFromScriptValue" << obj; - if (auto tmp = qobject_cast(obj)) { - out = tmp; - } - // FIXME: Why does above cast not work on Win32!? - if (!out) { - if (auto smp = static_cast(obj)) { - qDebug() << "meshPointerFromScriptValue2" << smp; - out = smp; - } - } - } - - QScriptValue modelPointerToScriptValue(QScriptEngine* engine, const scriptable::ScriptableModelPointer &in) { - return engine->newQObject(in, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); - // QScriptValue result = engine->newArray(); - // int i = 0; - // foreach(auto& mesh, in->getMeshes()) { - // result.setProperty(i++, meshPointerToScriptValue(engine, mesh)); - // } - // return result; - } - - void modelPointerFromScriptValue(const QScriptValue& value, scriptable::ScriptableModelPointer &out) { - const auto length = value.property("length").toInt32(); - qCDebug(model_scripting) << "in modelPointerFromScriptValue, length =" << length; - for (int i = 0; i < length; i++) { - if (const auto meshProxy = qobject_cast(value.property(i).toQObject())) { - out->meshes.append(meshProxy->getMeshPointer()); - } else { - qCDebug(model_scripting) << "null meshProxy" << i; - } - } - } // FIXME: MESHFACES: // QScriptValue meshFaceToScriptValue(QScriptEngine* engine, const mesh::MeshFace &meshFace) { @@ -365,29 +367,11 @@ namespace { // qScriptValueToSequence(array, result); // } - QScriptValue qVectorUInt32ToScriptValue(QScriptEngine* engine, const QVector& vector) { - return qScriptValueFromSequence(engine, vector); - } - - void qVectorUInt32FromScriptValue(const QScriptValue& array, QVector& result) { - qScriptValueToSequence(array, result); - } } -int meshUint32 = qRegisterMetaType(); -namespace mesh { - int meshUint32 = qRegisterMetaType(); -} -int qVectorMeshUint32 = qRegisterMetaType>(); void ModelScriptingInterface::registerMetaTypes(QScriptEngine* engine) { - qScriptRegisterSequenceMetaType>(engine); - qScriptRegisterSequenceMetaType>(engine); - - qScriptRegisterMetaType(engine, qVectorUInt32ToScriptValue, qVectorUInt32FromScriptValue); - qScriptRegisterMetaType(engine, meshPointerToScriptValue, meshPointerFromScriptValue); - qScriptRegisterMetaType(engine, modelPointerToScriptValue, modelPointerFromScriptValue); - + scriptable::registerMetaTypes(engine); // FIXME: MESHFACES: remove if MeshFace is not needed anywhere // qScriptRegisterSequenceMetaType(engine); // qScriptRegisterMetaType(engine, meshFaceToScriptValue, meshFaceFromScriptValue); @@ -395,7 +379,7 @@ void ModelScriptingInterface::registerMetaTypes(QScriptEngine* engine) { } MeshPointer ModelScriptingInterface::getMeshPointer(const scriptable::ScriptableMesh& meshProxy) { - return meshProxy._mesh;//getMeshPointer(&meshProxy); + return meshProxy.getMeshPointer(); } MeshPointer ModelScriptingInterface::getMeshPointer(scriptable::ScriptableMesh& meshProxy) { return getMeshPointer(&meshProxy); @@ -406,7 +390,7 @@ MeshPointer ModelScriptingInterface::getMeshPointer(scriptable::ScriptableMeshPo if (context()){ context()->throwError("expected meshProxy as first parameter"); } else { - qDebug() << "expected meshProxy as first parameter"; + qCDebug(graphics_scripting) << "expected meshProxy as first parameter"; } return result; } @@ -415,7 +399,7 @@ MeshPointer ModelScriptingInterface::getMeshPointer(scriptable::ScriptableMeshPo if (context()) { context()->throwError("expected valid meshProxy as first parameter"); } else { - qDebug() << "expected valid meshProxy as first parameter"; + qCDebug(graphics_scripting) << "expected valid meshProxy as first parameter"; } return result; } diff --git a/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.h b/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.h index eac4df3216..fa7b885014 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.h +++ b/libraries/graphics-scripting/src/graphics-scripting/ModelScriptingInterface.h @@ -15,19 +15,17 @@ #include #include -#include - #include #include #include "ScriptableMesh.h" #include + class ModelScriptingInterface : public QObject, public QScriptable, public Dependency { Q_OBJECT public: ModelScriptingInterface(QObject* parent = nullptr); - static void registerMetaTypes(QScriptEngine* engine); public slots: /**jsdoc @@ -36,7 +34,9 @@ public slots: * @function ModelScriptingInterface.getMeshes * @param {EntityID} entityID The ID of the entity whose meshes are to be retrieve */ - void getMeshes(QUuid uuid, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue()); + void getMeshes(QUuid uuid, QScriptValue callback); + bool updateMeshes(QUuid uuid, const scriptable::ScriptableModelPointer model); + bool updateMeshes(QUuid uuid, const scriptable::ScriptableMeshPointer mesh, int meshIndex=0, int partIndex=0); QString meshToOBJ(const scriptable::ScriptableModel& in); @@ -48,6 +48,8 @@ public slots: QScriptValue getVertexCount(scriptable::ScriptableMeshPointer meshProxy); QScriptValue getVertex(scriptable::ScriptableMeshPointer meshProxy, quint32 vertexIndex); + static void registerMetaTypes(QScriptEngine* engine); + private: scriptable::MeshPointer getMeshPointer(scriptable::ScriptableMeshPointer meshProxy); scriptable::MeshPointer getMeshPointer(scriptable::ScriptableMesh& meshProxy); diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp index 1b16a6d263..b83b901acd 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.cpp @@ -9,17 +9,16 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "GraphicsScriptingUtil.h" #include "ScriptableMesh.h" #include #include #include -#include #include #include #include #include -#include #include "ScriptableMesh.moc" @@ -29,49 +28,45 @@ #include "OBJWriter.h" -QLoggingCategory mesh_logging { "hifi.scripting.mesh" }; - -// FIXME: unroll/resolve before PR -using namespace scriptable; -QMap ScriptableMesh::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 }, -}; - - -QString scriptable::ScriptableModel::toString() const { - return QString("[ScriptableModel%1%2]") - .arg(objectID.isNull() ? "" : " objectID="+objectID.toString()) - .arg(objectName().isEmpty() ? "" : " name=" +objectName()); +namespace scriptable { + // QScriptValue jsBindCallback(QScriptValue callback); + // template QPointer qpointer_qobject_cast(const QScriptValue& value); + // template T this_qobject_cast(QScriptEngine* engine); + // template QPointer make_scriptowned(Rest... rest); } -const QVector scriptable::ScriptableModel::getConstMeshes() const { - QVector out; - for(const auto& mesh : meshes) { - const scriptable::ScriptableMeshPointer m = scriptable::ScriptableMeshPointer(new scriptable::ScriptableMesh(const_cast(this), mesh)); - out << m; +scriptable::ScriptableMeshPart::ScriptableMeshPart(scriptable::ScriptableMeshPointer parentMesh, int partIndex) + : parentMesh(parentMesh), partIndex(partIndex) { + setObjectName(QString("%1.part[%2]").arg(parentMesh ? parentMesh->objectName() : "").arg(partIndex)); +} + +scriptable::ScriptableMesh::ScriptableMesh(const ScriptableMeshBase& other) + : ScriptableMeshBase(other) { + auto mesh = getMeshPointer(); + QString name = mesh ? QString::fromStdString(mesh->modelName) : ""; + if (name.isEmpty()) { + name = mesh ? QString::fromStdString(mesh->displayName) : ""; } - return out; + auto parentModel = getParentModel(); + setObjectName(QString("%1#%2").arg(parentModel ? parentModel->objectName() : "").arg(name)); } -QVector scriptable::ScriptableModel::getMeshes() { - QVector out; - for(auto& mesh : meshes) { - scriptable::ScriptableMeshPointer m{new scriptable::ScriptableMesh(this, mesh)}; - out << m; + +QVector scriptable::ScriptableMesh::getMeshParts() const { + QVector out; + for (quint32 i = 0; i < getNumParts(); i++) { + out << scriptable::make_scriptowned(getSelf(), i); } return out; } -quint32 ScriptableMesh::getNumVertices() const { +quint32 scriptable::ScriptableMesh::getNumIndices() const { + if (auto mesh = getMeshPointer()) { + return (quint32)mesh->getNumIndices(); + } + return 0; +} + +quint32 scriptable::ScriptableMesh::getNumVertices() const { if (auto mesh = getMeshPointer()) { return (quint32)mesh->getNumVertices(); } @@ -87,16 +82,10 @@ quint32 ScriptableMesh::getNumVertices() const { // return glm::vec3(NAN); // } -namespace { - gpu::BufferView getBufferView(scriptable::MeshPointer mesh, gpu::Stream::Slot slot) { - return slot == gpu::Stream::POSITION ? mesh->getVertexBuffer() : mesh->getAttributeBuffer(slot); - } -} - -QVector ScriptableMesh::findNearbyIndices(const glm::vec3& origin, float epsilon) const { +QVector scriptable::ScriptableMesh::findNearbyIndices(const glm::vec3& origin, float epsilon) const { QVector result; if (auto mesh = getMeshPointer()) { - const auto& pos = getBufferView(mesh, gpu::Stream::POSITION); + const auto& pos = buffer_helpers::getBufferView(mesh, gpu::Stream::POSITION); const uint32_t num = (uint32_t)pos.getNumElements(); for (uint32_t i = 0; i < num; i++) { const auto& position = pos.get(i); @@ -108,40 +97,45 @@ QVector ScriptableMesh::findNearbyIndices(const glm::vec3& origin, floa return result; } -QVector ScriptableMesh::getIndices() const { +QVector scriptable::ScriptableMesh::getIndices() const { QVector result; if (auto mesh = getMeshPointer()) { - qCDebug(mesh_logging, "getTriangleIndices mesh %p", mesh.get()); + qCDebug(graphics_scripting, "getTriangleIndices mesh %p", mesh.get()); gpu::BufferView indexBufferView = mesh->getIndexBuffer(); if (quint32 count = (quint32)indexBufferView.getNumElements()) { result.resize(count); - auto buffer = indexBufferView._buffer; - if (indexBufferView._element.getSize() == 4) { + switch(indexBufferView._element.getType()) { + case gpu::UINT32: // memcpy(result.data(), buffer->getData(), result.size()*sizeof(quint32)); for (quint32 i = 0; i < count; i++) { result[i] = indexBufferView.get(i); } - } else { + break; + case gpu::UINT16: for (quint32 i = 0; i < count; i++) { result[i] = indexBufferView.get(i); } + break; + default: + assert(false); + Q_ASSERT(false); } } } return result; } -quint32 ScriptableMesh::getNumAttributes() const { +quint32 scriptable::ScriptableMesh::getNumAttributes() const { if (auto mesh = getMeshPointer()) { return (quint32)mesh->getNumAttributes(); } return 0; } -QVector ScriptableMesh::getAttributeNames() const { +QVector scriptable::ScriptableMesh::getAttributeNames() const { QVector result; if (auto mesh = getMeshPointer()) { - for (const auto& a : ATTRIBUTES.toStdMap()) { - auto bufferView = getBufferView(mesh, a.second); + for (const auto& a : buffer_helpers::ATTRIBUTES.toStdMap()) { + auto bufferView = buffer_helpers::getBufferView(mesh, a.second); if (bufferView.getNumElements() > 0) { result << a.first; } @@ -151,55 +145,49 @@ QVector ScriptableMesh::getAttributeNames() const { } // override -QVariantMap ScriptableMesh::getVertexAttributes(quint32 vertexIndex) const { +QVariantMap scriptable::ScriptableMesh::getVertexAttributes(quint32 vertexIndex) const { return getVertexAttributes(vertexIndex, getAttributeNames()); } -bool ScriptableMesh::setVertexAttributes(quint32 vertexIndex, QVariantMap attributes) { - //qDebug() << "setVertexAttributes" << vertexIndex << attributes; - for (auto& a : gatherBufferViews(getMeshPointer())) { +bool scriptable::ScriptableMesh::setVertexAttributes(quint32 vertexIndex, QVariantMap attributes) { + //qCInfo(graphics_scripting) << "setVertexAttributes" << vertexIndex << attributes; + metadata["last-modified"] = QDateTime::currentDateTime().toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate); + for (auto& a : buffer_helpers::gatherBufferViews(getMeshPointer())) { const auto& name = a.first; const auto& value = attributes.value(name); if (value.isValid()) { auto& view = a.second; - //qCDebug(mesh_logging) << "setVertexAttributes" << vertexIndex << name; - bufferViewElementFromVariant(view, vertexIndex, value); + //qCDebug(graphics_scripting) << "setVertexAttributes" << vertexIndex << name; + buffer_helpers::fromVariant(view, vertexIndex, value); } else { - //qCDebug(mesh_logging) << "(skipping) setVertexAttributes" << vertexIndex << name; + //qCDebug(graphics_scripting) << "(skipping) setVertexAttributes" << vertexIndex << name; } } return true; } -int ScriptableMesh::_getSlotNumber(const QString& attributeName) const { +int scriptable::ScriptableMesh::_getSlotNumber(const QString& attributeName) const { if (auto mesh = getMeshPointer()) { - return ATTRIBUTES.value(attributeName, -1); + return buffer_helpers::ATTRIBUTES.value(attributeName, -1); } return -1; } -QVariantMap ScriptableMesh::getMeshExtents() const { +QVariantMap scriptable::ScriptableMesh::getMeshExtents() const { auto mesh = getMeshPointer(); auto box = mesh ? mesh->evalPartsBound(0, (int)mesh->getNumParts()) : AABox(); - return { - { "brn", glmVecToVariant(box.getCorner()) }, - { "tfl", glmVecToVariant(box.calcTopFarLeft()) }, - { "center", glmVecToVariant(box.calcCenter()) }, - { "min", glmVecToVariant(box.getMinimumPoint()) }, - { "max", glmVecToVariant(box.getMaximumPoint()) }, - { "dimensions", glmVecToVariant(box.getDimensions()) }, - }; + return buffer_helpers::toVariant(box).toMap(); } -quint32 ScriptableMesh::getNumParts() const { +quint32 scriptable::ScriptableMesh::getNumParts() const { if (auto mesh = getMeshPointer()) { return (quint32)mesh->getNumParts(); } return 0; } -QVariantMap ScriptableMesh::scaleToFit(float unitScale) { +QVariantMap scriptable::ScriptableMeshPart::scaleToFit(float unitScale) { if (auto mesh = getMeshPointer()) { auto box = mesh->evalPartsBound(0, (int)mesh->getNumParts()); auto center = box.calcCenter(); @@ -208,10 +196,10 @@ QVariantMap ScriptableMesh::scaleToFit(float unitScale) { } return {}; } -QVariantMap ScriptableMesh::translate(const glm::vec3& translation) { +QVariantMap scriptable::ScriptableMeshPart::translate(const glm::vec3& translation) { return transform(glm::translate(translation)); } -QVariantMap ScriptableMesh::scale(const glm::vec3& scale, const glm::vec3& origin) { +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; @@ -219,10 +207,10 @@ QVariantMap ScriptableMesh::scale(const glm::vec3& scale, const glm::vec3& origi } return {}; } -QVariantMap ScriptableMesh::rotateDegrees(const glm::vec3& eulerAngles, const glm::vec3& origin) { +QVariantMap scriptable::ScriptableMeshPart::rotateDegrees(const glm::vec3& eulerAngles, const glm::vec3& origin) { return rotate(glm::quat(glm::radians(eulerAngles)), origin); } -QVariantMap ScriptableMesh::rotate(const glm::quat& rotation, const glm::vec3& 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; @@ -230,184 +218,61 @@ QVariantMap ScriptableMesh::rotate(const glm::quat& rotation, const glm::vec3& o } return {}; } -QVariantMap ScriptableMesh::transform(const glm::mat4& transform) { +QVariantMap scriptable::ScriptableMeshPart::transform(const glm::mat4& transform) { if (auto mesh = getMeshPointer()) { - const auto& pos = getBufferView(mesh, gpu::Stream::POSITION); + const auto& pos = buffer_helpers::getBufferView(mesh, gpu::Stream::POSITION); const uint32_t num = (uint32_t)pos.getNumElements(); for (uint32_t i = 0; i < num; i++) { auto& position = pos.edit(i); position = transform * glm::vec4(position, 1.0f); } + return parentMesh->getMeshExtents(); } - return getMeshExtents(); + return {}; } -QVariantList ScriptableMesh::getAttributeValues(const QString& attributeName) const { +QVariantList scriptable::ScriptableMesh::getAttributeValues(const QString& attributeName) const { QVariantList result; auto slotNum = _getSlotNumber(attributeName); if (slotNum >= 0) { auto slot = (gpu::Stream::Slot)slotNum; - const auto& bufferView = getBufferView(getMeshPointer(), slot); + const auto& bufferView = buffer_helpers::getBufferView(getMeshPointer(), slot); if (auto len = bufferView.getNumElements()) { bool asArray = bufferView._element.getType() != gpu::FLOAT; for (quint32 i = 0; i < len; i++) { - result << bufferViewElementToVariant(bufferView, i, asArray, attributeName.toStdString().c_str()); + result << buffer_helpers::toVariant(bufferView, i, asArray, attributeName.toStdString().c_str()); } } } return result; } -QVariantMap ScriptableMesh::getVertexAttributes(quint32 vertexIndex, QVector names) const { +QVariantMap scriptable::ScriptableMesh::getVertexAttributes(quint32 vertexIndex, QVector names) const { QVariantMap result; auto mesh = getMeshPointer(); if (!mesh || vertexIndex >= getNumVertices()) { return result; } - for (const auto& a : ATTRIBUTES.toStdMap()) { + for (const auto& a : buffer_helpers::ATTRIBUTES.toStdMap()) { auto name = a.first; if (!names.contains(name)) { continue; } auto slot = a.second; - const gpu::BufferView& bufferView = getBufferView(mesh, slot); + const gpu::BufferView& bufferView = buffer_helpers::getBufferView(mesh, slot); if (vertexIndex < bufferView.getNumElements()) { bool asArray = bufferView._element.getType() != gpu::FLOAT; - result[name] = bufferViewElementToVariant(bufferView, vertexIndex, asArray, name.toStdString().c_str()); + result[name] = buffer_helpers::toVariant(bufferView, vertexIndex, asArray, name.toStdString().c_str()); } } return result; } -/// --- buffer view <-> variant helpers - -namespace { - // expand the corresponding attribute buffer (creating it if needed) so that it matches POSITIONS size and specified element type - gpu::BufferView _expandedAttributeBuffer(const scriptable::MeshPointer mesh, gpu::Stream::Slot slot, const gpu::Element& elementType) { - gpu::Size elementSize = elementType.getSize(); - gpu::BufferView bufferView = getBufferView(mesh, slot); - auto nPositions = mesh->getNumVertices(); - auto vsize = nPositions * elementSize; - auto diffTypes = (elementType.getType() != bufferView._element.getType() || - elementType.getSize() > bufferView._element.getSize() || - elementType.getScalarCount() > bufferView._element.getScalarCount() || - vsize > bufferView._size - ); - auto hint = DebugNames::stringFrom(slot); - -#ifdef DEV_BUILD - auto beforeCount = bufferView.getNumElements(); - auto beforeTotal = bufferView._size; -#endif - if (bufferView.getNumElements() < nPositions || diffTypes) { - if (!bufferView._buffer || bufferView.getNumElements() == 0) { - qCInfo(mesh_logging).nospace() << "ScriptableMesh -- adding missing mesh attribute '" << hint << "' for BufferView"; - gpu::Byte *data = new gpu::Byte[vsize]; - memset(data, 0, vsize); - auto buffer = new gpu::Buffer(vsize, (gpu::Byte*)data); - delete[] data; - bufferView = gpu::BufferView(buffer, elementType); - mesh->addAttribute(slot, bufferView); - } else { - qCInfo(mesh_logging) << "ScriptableMesh -- resizing Buffer current:" << hint << bufferView._buffer->getSize() << "wanted:" << vsize; - bufferView._element = elementType; - bufferView._buffer->resize(vsize); - bufferView._size = bufferView._buffer->getSize(); - } - } -#ifdef DEV_BUILD - auto afterCount = bufferView.getNumElements(); - auto afterTotal = bufferView._size; - if (beforeTotal != afterTotal || beforeCount != afterCount) { - auto typeName = DebugNames::stringFrom(bufferView._element.getType()); - qCDebug(mesh_logging, "NOTE:: _expandedAttributeBuffer.%s vec%d %s (before count=%lu bytes=%lu // after count=%lu bytes=%lu)", - hint.toStdString().c_str(), bufferView._element.getScalarCount(), - typeName.toStdString().c_str(), beforeCount, beforeTotal, afterCount, afterTotal); - } -#endif - return bufferView; - } - const gpu::Element UNUSED{ gpu::SCALAR, gpu::UINT8, gpu::RAW }; - - gpu::Element getVecNElement(gpu::Type T, int N) { - switch(N) { - case 2: return { gpu::VEC2, T, gpu::XY }; - case 3: return { gpu::VEC3, T, gpu::XYZ }; - case 4: return { gpu::VEC4, T, gpu::XYZW }; - } - Q_ASSERT(false); - return UNUSED; - } - - gpu::BufferView expandAttributeToMatchPositions(scriptable::MeshPointer mesh, gpu::Stream::Slot slot) { - if (slot == gpu::Stream::POSITION) { - return getBufferView(mesh, slot); - } - return _expandedAttributeBuffer(mesh, slot, getVecNElement(gpu::FLOAT, 3)); - } -} - -std::map ScriptableMesh::gatherBufferViews(scriptable::MeshPointer mesh, const QStringList& expandToMatchPositions) { - std::map attributeViews; - if (!mesh) { - return attributeViews; - } - for (const auto& a : ScriptableMesh::ATTRIBUTES.toStdMap()) { - auto name = a.first; - auto slot = a.second; - if (expandToMatchPositions.contains(name)) { - expandAttributeToMatchPositions(mesh, slot); - } - auto view = getBufferView(mesh, slot); - auto beforeCount = view.getNumElements(); - if (beforeCount > 0) { - auto element = view._element; - auto vecN = element.getScalarCount(); - auto type = element.getType(); - QString typeName = DebugNames::stringFrom(element.getType()); - auto beforeTotal = view._size; - - attributeViews[name] = _expandedAttributeBuffer(mesh, slot, getVecNElement(type, vecN)); - -#if DEV_BUILD - auto afterTotal = attributeViews[name]._size; - auto afterCount = attributeViews[name].getNumElements(); - if (beforeTotal != afterTotal || beforeCount != afterCount) { - qCDebug(mesh_logging, "NOTE:: gatherBufferViews.%s vec%d %s (before count=%lu bytes=%lu // after count=%lu bytes=%lu)", - name.toStdString().c_str(), vecN, typeName.toStdString().c_str(), beforeCount, beforeTotal, afterCount, afterTotal); - } -#endif - } - } - return attributeViews; -} - -QScriptValue ScriptableModel::mapAttributeValues(QScriptValue scopeOrCallback, QScriptValue methodOrName) { - auto context = scopeOrCallback.engine()->currentContext(); - auto _in = context->thisObject(); - qCInfo(mesh_logging) << "mapAttributeValues" << _in.toVariant().typeName() << _in.toVariant().toString() << _in.toQObject(); - auto model = qscriptvalue_cast(_in); - QVector in = model.getMeshes(); - if (in.size()) { - foreach (scriptable::ScriptableMeshPointer meshProxy, in) { - meshProxy->mapAttributeValues(scopeOrCallback, methodOrName); - } - return _in; - } else if (auto meshProxy = qobject_cast(_in.toQObject())) { - return meshProxy->mapAttributeValues(scopeOrCallback, methodOrName); - } else { - context->throwError("invalid ModelProxy || MeshProxyPointer"); - } - return false; -} - - - -QScriptValue ScriptableMesh::mapAttributeValues(QScriptValue scopeOrCallback, QScriptValue methodOrName) { +quint32 scriptable::ScriptableMesh::mapAttributeValues(QScriptValue _callback) { auto mesh = getMeshPointer(); if (!mesh) { - return false; + return 0; } - auto scopedHandler = makeScopedHandlerObject(scopeOrCallback, methodOrName); + auto scopedHandler = jsBindCallback(_callback); // input buffers gpu::BufferView positions = mesh->getVertexBuffer(); @@ -417,20 +282,25 @@ QScriptValue ScriptableMesh::mapAttributeValues(QScriptValue scopeOrCallback, QS // 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(); // cache value to avoid resolving each iteration - auto meshPart = thisObject();//js->toScriptValue(meshProxy); - + 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; + qCInfo(graphics_scripting) << "mapAttributeValues" << mesh.get() << js->currentContext()->thisObject().toQObject(); auto obj = js->newObject(); - auto attributeViews = ScriptableMesh::gatherBufferViews(mesh, { "normal", "color" }); - for (uint32_t i=0; i < nPositions; i++) { + auto attributeViews = buffer_helpers::gatherBufferViews(mesh, { "normal", "color" }); + metadata["last-modified"] = QDateTime::currentDateTime().toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate); + uint32_t i = 0; + for (; i < nPositions; i++) { for (const auto& a : attributeViews) { bool asArray = a.second._element.getType() != gpu::FLOAT; obj.setProperty(a.first, bufferViewElementToScriptValue(js, a.second, i, asArray, a.first.toStdString().c_str())); } auto result = callback.call(scope, { obj, i, meshPart }); if (js->hasUncaughtException()) { - context()->throwValue(js->uncaughtException()); - return false; + js->currentContext()->throwValue(js->uncaughtException()); + return i; } if (result.isBool() && !result.toBool()) { @@ -450,15 +320,19 @@ QScriptValue ScriptableMesh::mapAttributeValues(QScriptValue scopeOrCallback, QS } } } - return thisObject(); + return i; } -QScriptValue ScriptableMesh::unrollVertices(bool recalcNormals) { +quint32 scriptable::ScriptableMeshPart::mapAttributeValues(QScriptValue callback) { + return parentMesh ? parentMesh->mapAttributeValues(callback) : 0; +} + +bool scriptable::ScriptableMeshPart::unrollVertices(bool recalcNormals) { auto meshProxy = this; auto mesh = getMeshPointer(); - qCInfo(mesh_logging) << "ScriptableMesh::unrollVertices" << !!mesh<< !!meshProxy; + qCInfo(graphics_scripting) << "ScriptableMeshPart::unrollVertices" << !!mesh<< !!meshProxy; if (!mesh) { - return QScriptValue(); + return false; } auto positions = mesh->getVertexBuffer(); @@ -467,8 +341,9 @@ QScriptValue ScriptableMesh::unrollVertices(bool recalcNormals) { auto buffer = new gpu::Buffer(); buffer->resize(numPoints * sizeof(uint32_t)); auto newindices = gpu::BufferView(buffer, { gpu::SCALAR, gpu::UINT32, gpu::INDEX }); - qCInfo(mesh_logging) << "ScriptableMesh::unrollVertices numPoints" << numPoints; - auto attributeViews = ScriptableMesh::gatherBufferViews(mesh); + metadata["last-modified"] = QDateTime::currentDateTime().toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate); + qCInfo(graphics_scripting) << "ScriptableMeshPart::unrollVertices numPoints" << numPoints; + auto attributeViews = buffer_helpers::gatherBufferViews(mesh); for (const auto& a : attributeViews) { auto& view = a.second; auto sz = view._element.getSize(); @@ -477,19 +352,21 @@ QScriptValue ScriptableMesh::unrollVertices(bool recalcNormals) { auto points = gpu::BufferView(buffer, view._element); auto src = (uint8_t*)view._buffer->getData(); auto dest = (uint8_t*)points._buffer->getData(); - auto slot = ScriptableMesh::ATTRIBUTES[a.first]; - qCInfo(mesh_logging) << "ScriptableMesh::unrollVertices buffer" << a.first; - qCInfo(mesh_logging) << "ScriptableMesh::unrollVertices source" << view.getNumElements(); - qCInfo(mesh_logging) << "ScriptableMesh::unrollVertices dest" << points.getNumElements(); - qCInfo(mesh_logging) << "ScriptableMesh::unrollVertices sz" << sz << src << dest << slot; + auto slot = buffer_helpers::ATTRIBUTES[a.first]; + if (0) { + qCInfo(graphics_scripting) << "ScriptableMeshPart::unrollVertices buffer" << a.first; + qCInfo(graphics_scripting) << "ScriptableMeshPart::unrollVertices source" << view.getNumElements(); + qCInfo(graphics_scripting) << "ScriptableMeshPart::unrollVertices dest" << points.getNumElements(); + qCInfo(graphics_scripting) << "ScriptableMeshPart::unrollVertices sz" << sz << src << dest << slot; + } auto esize = indices._element.getSize(); const char* hint= a.first.toStdString().c_str(); for(quint32 i = 0; i < numPoints; i++) { quint32 index = esize == 4 ? indices.get(i) : indices.get(i); newindices.edit(i) = i; - bufferViewElementFromVariant( + buffer_helpers::fromVariant( points, i, - bufferViewElementToVariant(view, index, false, hint) + buffer_helpers::toVariant(view, index, false, hint) ); } if (slot == gpu::Stream::POSITION) { @@ -505,62 +382,70 @@ QScriptValue ScriptableMesh::unrollVertices(bool recalcNormals) { return true; } -bool ScriptableMesh::replaceMeshData(scriptable::ScriptableMeshPointer src, const QVector& attributeNames) { +bool scriptable::ScriptableMeshPart::replaceMeshData(scriptable::ScriptableMeshPartPointer src, const QVector& attributeNames) { auto target = getMeshPointer(); auto source = src ? src->getMeshPointer() : nullptr; if (!target || !source) { - context()->throwError("ScriptableMesh::replaceMeshData -- expected dest and src to be valid mesh proxy pointers"); + 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->getAttributeNames() : attributeNames; + QVector attributes = attributeNames.isEmpty() ? src->parentMesh->getAttributeNames() : attributeNames; - //qCInfo(mesh_logging) << "ScriptableMesh::replaceMeshData -- source:" << source->displayName << "target:" << target->displayName << "attributes:" << attributes; + qCInfo(graphics_scripting) << "ScriptableMeshPart::replaceMeshData -- " << + "source:" << QString::fromStdString(source->displayName) << + "target:" << QString::fromStdString(target->displayName) << + "attributes:" << attributes; + + metadata["last-modified"] = QDateTime::currentDateTime().toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate); // remove attributes only found on target mesh, unless user has explicitly specified the relevant attribute names if (attributeNames.isEmpty()) { - auto attributeViews = ScriptableMesh::gatherBufferViews(target); + auto attributeViews = buffer_helpers::gatherBufferViews(target); for (const auto& a : attributeViews) { - auto slot = ScriptableMesh::ATTRIBUTES[a.first]; + auto slot = buffer_helpers::ATTRIBUTES[a.first]; if (!attributes.contains(a.first)) { - //qCInfo(mesh_logging) << "ScriptableMesh::replaceMeshData -- pruning target attribute" << a.first << slot; + qCInfo(graphics_scripting) << "ScriptableMesh::replaceMeshData -- pruning target attribute" << a.first << slot; target->removeAttribute(slot); } } } - target->setVertexBuffer(cloneBufferView(source->getVertexBuffer())); - target->setIndexBuffer(cloneBufferView(source->getIndexBuffer())); - target->setPartBuffer(cloneBufferView(source->getPartBuffer())); + 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 = ScriptableMesh::ATTRIBUTES[a]; + auto slot = buffer_helpers::ATTRIBUTES[a]; if (slot == gpu::Stream::POSITION) { continue; } - // auto& before = target->getAttributeBuffer(slot); + auto& before = target->getAttributeBuffer(slot); auto& input = source->getAttributeBuffer(slot); if (input.getNumElements() == 0) { - //qCInfo(mesh_logging) << "ScriptableMesh::replaceMeshData buffer is empty -- pruning" << a << slot; + qCInfo(graphics_scripting) << "ScriptableMeshPart::replaceMeshData buffer is empty -- pruning" << a << slot; target->removeAttribute(slot); } else { - // if (before.getNumElements() == 0) { - // qCInfo(mesh_logging) << "ScriptableMesh::replaceMeshData target buffer is empty -- adding" << a << slot; - // } else { - // qCInfo(mesh_logging) << "ScriptableMesh::replaceMeshData target buffer exists -- updating" << a << slot; - // } - target->addAttribute(slot, cloneBufferView(input)); + 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; + } + target->addAttribute(slot, buffer_helpers::clone(input)); } - // auto& after = target->getAttributeBuffer(slot); - // qCInfo(mesh_logging) << "ScriptableMesh::replaceMeshData" << a << slot << before.getNumElements() << " -> " << after.getNumElements(); + auto& after = target->getAttributeBuffer(slot); + qCInfo(graphics_scripting) << "ScriptableMeshPart::replaceMeshData" << a << slot << before.getNumElements() << " -> " << after.getNumElements(); } return true; } -bool ScriptableMesh::dedupeVertices(float epsilon) { - scriptable::ScriptableMeshPointer meshProxy = this; +bool scriptable::ScriptableMeshPart::dedupeVertices(float epsilon) { auto mesh = getMeshPointer(); if (!mesh) { return false; @@ -573,6 +458,7 @@ bool ScriptableMesh::dedupeVertices(float epsilon) { uniqueVerts.reserve((int)numPositions); QMap remapIndices; + metadata["last-modified"] = QDateTime::currentDateTime().toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate); for (quint32 i = 0; i < numPositions; i++) { const quint32 numUnique = uniqueVerts.size(); const auto& position = positions.get(i); @@ -590,7 +476,7 @@ bool ScriptableMesh::dedupeVertices(float epsilon) { } } - qCInfo(mesh_logging) << "//VERTS before" << numPositions << "after" << uniqueVerts.size(); + qCInfo(graphics_scripting) << "//VERTS before" << numPositions << "after" << uniqueVerts.size(); auto indices = mesh->getIndexBuffer(); auto numIndices = indices.getNumElements(); @@ -600,34 +486,34 @@ bool ScriptableMesh::dedupeVertices(float epsilon) { for (quint32 i = 0; i < numIndices; i++) { quint32 index = esize == 4 ? indices.get(i) : indices.get(i); if (remapIndices.contains(index)) { - //qCInfo(mesh_logging) << i << index << "->" << remapIndices[index]; + //qCInfo(graphics_scripting) << i << index << "->" << remapIndices[index]; newIndices << remapIndices[index]; } else { - qCInfo(mesh_logging) << i << index << "!remapIndices[index]"; + qCInfo(graphics_scripting) << i << index << "!remapIndices[index]"; } } - mesh->setIndexBuffer(bufferViewFromVector(newIndices, { gpu::SCALAR, gpu::UINT32, gpu::INDEX })); - mesh->setVertexBuffer(bufferViewFromVector(uniqueVerts, { gpu::VEC3, gpu::FLOAT, gpu::XYZ })); + mesh->setIndexBuffer(buffer_helpers::fromVector(newIndices, { gpu::SCALAR, gpu::UINT32, gpu::INDEX })); + mesh->setVertexBuffer(buffer_helpers::fromVector(uniqueVerts, { gpu::VEC3, gpu::FLOAT, gpu::XYZ })); - auto attributeViews = ScriptableMesh::gatherBufferViews(mesh); + auto attributeViews = buffer_helpers::gatherBufferViews(mesh); quint32 numUniqueVerts = uniqueVerts.size(); for (const auto& a : attributeViews) { auto& view = a.second; - auto slot = ScriptableMesh::ATTRIBUTES[a.first]; + auto slot = buffer_helpers::ATTRIBUTES[a.first]; if (slot == gpu::Stream::POSITION) { continue; } - qCInfo(mesh_logging) << "ScriptableMesh::dedupeVertices" << a.first << slot << view.getNumElements(); - auto newView = resizedBufferView(view, numUniqueVerts); - qCInfo(mesh_logging) << a.first << "before: #" << view.getNumElements() << "after: #" << newView.getNumElements(); + qCInfo(graphics_scripting) << "ScriptableMeshPart::dedupeVertices" << a.first << slot << view.getNumElements(); + auto newView = buffer_helpers::resize(view, numUniqueVerts); + qCInfo(graphics_scripting) << a.first << "before: #" << view.getNumElements() << "after: #" << newView.getNumElements(); quint32 numElements = (quint32)view.getNumElements(); for (quint32 i = 0; i < numElements; i++) { quint32 fromVertexIndex = i; quint32 toVertexIndex = remapIndices.contains(fromVertexIndex) ? remapIndices[fromVertexIndex] : fromVertexIndex; - bufferViewElementFromVariant( + buffer_helpers::fromVariant( newView, toVertexIndex, - bufferViewElementToVariant(view, fromVertexIndex, false, "dedupe") + buffer_helpers::toVariant(view, fromVertexIndex, false, "dedupe") ); } mesh->addAttribute(slot, newView); @@ -635,120 +521,202 @@ bool ScriptableMesh::dedupeVertices(float epsilon) { return true; } -QScriptValue ScriptableMesh::cloneMesh(bool recalcNormals) { +scriptable::ScriptableMeshPointer scriptable::ScriptableMesh::cloneMesh(bool recalcNormals) { auto mesh = getMeshPointer(); if (!mesh) { - return QScriptValue::NullValue; + qCInfo(graphics_scripting) << "ScriptableMesh::cloneMesh -- !meshPointer"; + return nullptr; } - graphics::MeshPointer clone(new graphics::Mesh()); - clone->displayName = mesh->displayName + "-clone"; - qCInfo(mesh_logging) << "ScriptableMesh::cloneMesh" << !!mesh; - if (!mesh) { - return QScriptValue::NullValue; - } - - clone->setIndexBuffer(cloneBufferView(mesh->getIndexBuffer())); - clone->setPartBuffer(cloneBufferView(mesh->getPartBuffer())); - auto attributeViews = ScriptableMesh::gatherBufferViews(mesh); - for (const auto& a : attributeViews) { - auto& view = a.second; - auto slot = ScriptableMesh::ATTRIBUTES[a.first]; - qCInfo(mesh_logging) << "ScriptableMesh::cloneVertices buffer" << a.first << slot; - auto points = cloneBufferView(view); - qCInfo(mesh_logging) << "ScriptableMesh::cloneVertices source" << view.getNumElements(); - qCInfo(mesh_logging) << "ScriptableMesh::cloneVertices dest" << points.getNumElements(); - if (slot == gpu::Stream::POSITION) { - clone->setVertexBuffer(points); - } else { - clone->addAttribute(slot, points); - } - } - - auto result = scriptable::ScriptableMeshPointer(new ScriptableMesh(nullptr, clone)); + qCInfo(graphics_scripting) << "ScriptableMesh::cloneMesh..."; + auto clone = buffer_helpers::cloneMesh(mesh); + + qCInfo(graphics_scripting) << "ScriptableMesh::cloneMesh..."; if (recalcNormals) { - result->recalculateNormals(); + buffer_helpers::recalculateNormals(clone); } - return engine()->toScriptValue(result); + qCDebug(graphics_scripting) << clone.get();// << metadata; + auto meshPointer = scriptable::make_scriptowned(provider, model, clone, metadata); + clone.reset(); // free local reference + qCInfo(graphics_scripting) << "========= ScriptableMesh::cloneMesh..." << meshPointer << meshPointer->ownedMesh.use_count(); + //scriptable::MeshPointer* ppMesh = new scriptable::MeshPointer(); + //*ppMesh = clone; + + if (meshPointer) { + scriptable::WeakMeshPointer delme = meshPointer->mesh; + QString debugString = scriptable::toDebugString(meshPointer); + QObject::connect(meshPointer, &QObject::destroyed, meshPointer, [=]() { + qCWarning(graphics_scripting) << "*************** cloneMesh/Destroy"; + qCWarning(graphics_scripting) << "*************** " << debugString << delme.lock().get(); + if (!delme.expired()) { + qCWarning(graphics_scripting) << "cloneMesh -- potential memory leak..." << debugString << delme.lock().get(); + } + }); + } + + meshPointer->metadata["last-modified"] = QDateTime::currentDateTime().toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate); + return scriptable::ScriptableMeshPointer(meshPointer); } -bool ScriptableMesh::recalculateNormals() { - scriptable::ScriptableMeshPointer meshProxy = this; - qCInfo(mesh_logging) << "Recalculating normals" << !!meshProxy; - auto mesh = getMeshPointer(); - if (!mesh) { - return false; - } - ScriptableMesh::gatherBufferViews(mesh, { "normal", "color" }); // ensures #normals >= #positions - auto normals = mesh->getAttributeBuffer(gpu::Stream::NORMAL); - auto verts = mesh->getVertexBuffer(); - auto indices = mesh->getIndexBuffer(); - auto esize = indices._element.getSize(); - auto numPoints = indices.getNumElements(); - const auto TRIANGLE = 3; - quint32 numFaces = (quint32)numPoints / TRIANGLE; - //QVector faces; - QVector faceNormals; - QMap> vertexToFaces; - //faces.resize(numFaces); - faceNormals.resize(numFaces); - auto numNormals = normals.getNumElements(); - qCInfo(mesh_logging) << QString("numFaces: %1, numNormals: %2, numPoints: %3").arg(numFaces).arg(numNormals).arg(numPoints); - if (normals.getNumElements() != verts.getNumElements()) { - return false; - } - for (quint32 i = 0; i < numFaces; i++) { - quint32 I = TRIANGLE * i; - quint32 i0 = esize == 4 ? indices.get(I+0) : indices.get(I+0); - quint32 i1 = esize == 4 ? indices.get(I+1) : indices.get(I+1); - quint32 i2 = esize == 4 ? indices.get(I+2) : indices.get(I+2); - - Triangle face = { - verts.get(i1), - verts.get(i2), - verts.get(i0) - }; - faceNormals[i] = face.getNormal(); - if (glm::isnan(faceNormals[i].x)) { - qCInfo(mesh_logging) << i << i0 << i1 << i2 << vec3toVariant(face.v0) << vec3toVariant(face.v1) << vec3toVariant(face.v2); - break; - } - vertexToFaces[glm::to_string(face.v0).c_str()] << i; - vertexToFaces[glm::to_string(face.v1).c_str()] << i; - vertexToFaces[glm::to_string(face.v2).c_str()] << i; - } - for (quint32 j = 0; j < numNormals; j++) { - //auto v = verts.get(j); - glm::vec3 normal { 0.0f, 0.0f, 0.0f }; - QString key { glm::to_string(verts.get(j)).c_str() }; - const auto& faces = vertexToFaces.value(key); - if (faces.size()) { - for (const auto i : faces) { - normal += faceNormals[i]; - } - normal *= 1.0f / (float)faces.size(); - } else { - static int logged = 0; - if (logged++ < 10) { - qCInfo(mesh_logging) << "no faces for key!?" << key; - } - normal = verts.get(j); - } - if (glm::isnan(normal.x)) { - static int logged = 0; - if (logged++ < 10) { - qCInfo(mesh_logging) << "isnan(normal.x)" << j << vec3toVariant(normal); - } - break; - } - normals.edit(j) = glm::normalize(normal); - } - return true; +scriptable::ScriptableMeshBase::ScriptableMeshBase(scriptable::WeakModelProviderPointer provider, scriptable::ScriptableModelBasePointer model, scriptable::WeakMeshPointer mesh, const QVariantMap& metadata) + : provider(provider), model(model), mesh(mesh), metadata(metadata) {} +scriptable::ScriptableMeshBase::ScriptableMeshBase(scriptable::WeakMeshPointer mesh) : scriptable::ScriptableMeshBase(scriptable::WeakModelProviderPointer(), nullptr, mesh, QVariantMap()) { } +scriptable::ScriptableMeshBase::ScriptableMeshBase(scriptable::MeshPointer mesh, const QVariantMap& metadata) + : ScriptableMeshBase(WeakModelProviderPointer(), nullptr, mesh, metadata) { + ownedMesh = mesh; +} +//scriptable::ScriptableMeshBase::ScriptableMeshBase(const scriptable::ScriptableMeshBase& other) { *this = other; } +scriptable::ScriptableMeshBase& scriptable::ScriptableMeshBase::operator=(const scriptable::ScriptableMeshBase& view) { + provider = view.provider; + model = view.model; + mesh = view.mesh; + ownedMesh = view.ownedMesh; + metadata = view.metadata; + return *this; +} + scriptable::ScriptableMeshBase::~ScriptableMeshBase() { + ownedMesh.reset(); + qCInfo(graphics_scripting) << "//~ScriptableMeshBase" << this << "ownedMesh:" << ownedMesh.use_count() << "mesh:" << mesh.use_count(); } -QString ScriptableMesh::toOBJ() { +scriptable::ScriptableMesh::~ScriptableMesh() { + ownedMesh.reset(); + qCInfo(graphics_scripting) << "//~ScriptableMesh" << this << "ownedMesh:" << ownedMesh.use_count() << "mesh:" << mesh.use_count(); +} + +QString scriptable::ScriptableMeshPart::toOBJ() { if (!getMeshPointer()) { - context()->throwError(QString("null mesh")); + if (context()) { + context()->throwError(QString("null mesh")); + } else { + qCWarning(graphics_scripting) << "null mesh"; + } + return QString(); } return writeOBJToString({ getMeshPointer() }); } +namespace { + template + QScriptValue qObjectToScriptValue(QScriptEngine* engine, const T& object) { + if (!object) { + return QScriptValue::NullValue; + } + auto ownership = object->metadata.value("__ownership__"); + return engine->newQObject( + object, + ownership.isValid() ? static_cast(ownership.toInt()) : QScriptEngine::QtOwnership + //, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects + ); + } + + QScriptValue meshPointerToScriptValue(QScriptEngine* engine, const scriptable::ScriptableMeshPointer& in) { + return qObjectToScriptValue(engine, in); + } + QScriptValue meshPartPointerToScriptValue(QScriptEngine* engine, const scriptable::ScriptableMeshPartPointer& in) { + return qObjectToScriptValue(engine, in); + } + QScriptValue modelPointerToScriptValue(QScriptEngine* engine, const scriptable::ScriptableModelPointer& in) { + return qObjectToScriptValue(engine, in); + } + + void meshPointerFromScriptValue(const QScriptValue& value, scriptable::ScriptableMeshPointer &out) { + out = scriptable::qpointer_qobject_cast(value); + } + void modelPointerFromScriptValue(const QScriptValue& value, scriptable::ScriptableModelPointer &out) { + out = scriptable::qpointer_qobject_cast(value); + } + void meshPartPointerFromScriptValue(const QScriptValue& value, scriptable::ScriptableMeshPartPointer &out) { + out = scriptable::qpointer_qobject_cast(value); + } + + // FIXME: MESHFACES: + // QScriptValue meshFaceToScriptValue(QScriptEngine* engine, const mesh::MeshFace &meshFace) { + // QScriptValue obj = engine->newObject(); + // obj.setProperty("vertices", qVectorIntToScriptValue(engine, meshFace.vertexIndices)); + // return obj; + // } + // void meshFaceFromScriptValue(const QScriptValue &object, mesh::MeshFace& meshFaceResult) { + // qScriptValueToSequence(object.property("vertices"), meshFaceResult.vertexIndices); + // } + // QScriptValue qVectorMeshFaceToScriptValue(QScriptEngine* engine, const QVector& vector) { + // return qScriptValueFromSequence(engine, vector); + // } + // void qVectorMeshFaceFromScriptValue(const QScriptValue& array, QVector& result) { + // qScriptValueToSequence(array, result); + // } + + QScriptValue qVectorUInt32ToScriptValue(QScriptEngine* engine, const QVector& vector) { + return qScriptValueFromSequence(engine, vector); + } + + void qVectorUInt32FromScriptValue(const QScriptValue& array, QVector& result) { + qScriptValueToSequence(array, result); + } + + QVector metaTypeIds{ + qRegisterMetaType("uint32"), + qRegisterMetaType("scriptable::uint32"), + qRegisterMetaType>(), + qRegisterMetaType>("QVector"), + qRegisterMetaType(), + qRegisterMetaType(), + qRegisterMetaType(), + }; +} + +namespace scriptable { + bool registerMetaTypes(QScriptEngine* engine) { + qScriptRegisterSequenceMetaType>(engine); + qScriptRegisterSequenceMetaType>(engine); + qScriptRegisterSequenceMetaType>(engine); + + qScriptRegisterMetaType(engine, qVectorUInt32ToScriptValue, qVectorUInt32FromScriptValue); + qScriptRegisterMetaType(engine, modelPointerToScriptValue, modelPointerFromScriptValue); + qScriptRegisterMetaType(engine, meshPointerToScriptValue, meshPointerFromScriptValue); + qScriptRegisterMetaType(engine, meshPartPointerToScriptValue, meshPartPointerFromScriptValue); + + return metaTypeIds.size(); + } + // callback helper that lets C++ method signatures remain simple (ie: taking a single callback argument) while + // still supporting extended Qt signal-like (scope, "methodName") and (scope, function(){}) "this" binding conventions + QScriptValue jsBindCallback(QScriptValue callback) { + if (callback.isObject() && callback.property("callback").isFunction()) { + return callback; + } + auto engine = callback.engine(); + auto context = engine ? engine->currentContext() : nullptr; + auto length = context ? context->argumentCount() : 0; + QScriptValue scope = context ? context->thisObject() : QScriptValue::NullValue; + QScriptValue method; + qCInfo(graphics_scripting) << "jsBindCallback" << engine << length << scope.toQObject() << method.toString(); + int i = 0; + for (; context && i < length; i++) { + if (context->argument(i).strictlyEquals(callback)) { + method = context->argument(i+1); + } + } + if (method.isFunction() || method.isString()) { + scope = callback; + } else { + method = callback; + } + qCInfo(graphics_scripting) << "scope:" << scope.toQObject() << "method:" << method.toString(); + return ::makeScopedHandlerObject(scope, method); + } +} + +bool scriptable::GraphicsScriptingInterface::updateMeshPart(ScriptableMeshPointer mesh, ScriptableMeshPartPointer part) { + Q_ASSERT(mesh); + Q_ASSERT(part); + Q_ASSERT(part->parentMesh); + auto tmp = exportMeshPart(mesh, part->partIndex); + if (part->parentMesh == mesh) { + qCInfo(graphics_scripting) << "updateMeshPart -- update via clone" << mesh << part; + tmp->replaceMeshData(part->cloneMeshPart()); + return false; + } else { + qCInfo(graphics_scripting) << "updateMeshPart -- update via inplace" << mesh << part; + tmp->replaceMeshData(part); + return true; + } +} diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h index 257285fa90..c655167c2b 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h @@ -11,142 +11,188 @@ #include +//#include #include +#include #include #include -namespace graphics { - class Mesh; -} -namespace gpu { - class BufferView; -} namespace scriptable { - class ScriptableMeshPart; - using ScriptableMeshPartPointer = QPointer; - class ScriptableMesh : public QObject, QScriptable { + + QScriptValue jsBindCallback(QScriptValue callback); + class ScriptableMesh : public ScriptableMeshBase, QScriptable { Q_OBJECT public: - Q_PROPERTY(quint32 numParts READ getNumParts) - Q_PROPERTY(quint32 numAttributes READ getNumAttributes) - Q_PROPERTY(quint32 numVertices READ getNumVertices) - Q_PROPERTY(quint32 numIndices READ getNumIndices) - Q_PROPERTY(QVariantMap metadata MEMBER _metadata) + Q_PROPERTY(uint32 numParts READ getNumParts) + Q_PROPERTY(uint32 numAttributes READ getNumAttributes) + Q_PROPERTY(uint32 numVertices READ getNumVertices) + Q_PROPERTY(uint32 numIndices READ getNumIndices) + Q_PROPERTY(QVariantMap metadata MEMBER metadata) Q_PROPERTY(QVector attributeNames READ getAttributeNames) + Q_PROPERTY(QVector parts READ getMeshParts) + Q_PROPERTY(bool valid READ hasValidMesh) + bool hasValidMesh() const { return (bool)getMeshPointer(); } + Q_PROPERTY(bool validOwned READ hasValidOwnedMesh) + bool hasValidOwnedMesh() const { return (bool)getOwnedMeshPointer(); } - static QMap ATTRIBUTES; - static std::map gatherBufferViews(MeshPointer mesh, const QStringList& expandToMatchPositions = QStringList()); + operator const ScriptableMeshBase*() const { return (qobject_cast(this)); } + ScriptableMesh(scriptable::MeshPointer mesh) : ScriptableMeshBase(mesh) { ownedMesh = mesh; } + ScriptableMesh(WeakModelProviderPointer provider, ScriptableModelBasePointer model, MeshPointer mesh, const QVariantMap& metadata) + : ScriptableMeshBase(provider, model, mesh, metadata) { ownedMesh = mesh; } + //ScriptableMesh& operator=(const ScriptableMesh& other) { model=other.model; mesh=other.mesh; metadata=other.metadata; return *this; }; + //ScriptableMesh() : QObject(), model(nullptr) {} + //ScriptableMesh(const ScriptableMesh& other) : QObject(), model(other.model), mesh(other.mesh), metadata(other.metadata) {} + ScriptableMesh(const ScriptableMeshBase& other); + ScriptableMesh(const ScriptableMesh& other) : ScriptableMeshBase(other) {}; + virtual ~ScriptableMesh(); - ScriptableMesh& operator=(const ScriptableMesh& other) { _model=other._model; _mesh=other._mesh; _metadata=other._metadata; return *this; }; - ScriptableMesh() : QObject(), _model(nullptr) {} - ScriptableMesh(ScriptableModelPointer parent, scriptable::MeshPointer mesh) : QObject(), _model(parent), _mesh(mesh) {} - ScriptableMesh(const ScriptableMesh& other) : QObject(), _model(other._model), _mesh(other._mesh), _metadata(other._metadata) {} - ~ScriptableMesh() { qDebug() << "~ScriptableMesh" << this; } - - scriptable::MeshPointer getMeshPointer() const { return _mesh; } + Q_INVOKABLE const scriptable::ScriptableModelPointer getParentModel() const { return qobject_cast(model); } + Q_INVOKABLE const scriptable::MeshPointer getOwnedMeshPointer() const { return ownedMesh; } + scriptable::ScriptableMeshPointer getSelf() const { return const_cast(this); } public slots: - quint32 getNumParts() const; - quint32 getNumVertices() const; - quint32 getNumAttributes() const; - quint32 getNumIndices() const { return 0; } + uint32 getNumParts() const; + uint32 getNumVertices() const; + uint32 getNumAttributes() const; + uint32 getNumIndices() const; QVector getAttributeNames() const; + QVector getMeshParts() const; - QVariantMap getVertexAttributes(quint32 vertexIndex) const; - QVariantMap getVertexAttributes(quint32 vertexIndex, QVector attributes) const; + QVariantMap getVertexAttributes(uint32 vertexIndex) const; + QVariantMap getVertexAttributes(uint32 vertexIndex, QVector attributes) const; - QVector getIndices() const; - QVector findNearbyIndices(const glm::vec3& origin, float epsilon = 1e-6) const; + QVector getIndices() const; + QVector findNearbyIndices(const glm::vec3& origin, float epsilon = 1e-6) const; QVariantMap getMeshExtents() const; - bool setVertexAttributes(quint32 vertexIndex, QVariantMap attributes); - QVariantMap scaleToFit(float unitScale); + bool setVertexAttributes(uint32 vertexIndex, QVariantMap attributes); QVariantList getAttributeValues(const QString& attributeName) const; int _getSlotNumber(const QString& attributeName) const; + scriptable::ScriptableMeshPointer cloneMesh(bool recalcNormals = false); + public: + operator bool() const { return !mesh.expired(); } + + public slots: + // QScriptEngine-specific wrappers + uint32 mapAttributeValues(QScriptValue callback); + }; + + // TODO: part-specific wrapper for working with raw geometries + class ScriptableMeshPart : public QObject, QScriptable { + Q_OBJECT + public: + Q_PROPERTY(uint32 partIndex MEMBER partIndex CONSTANT) + Q_PROPERTY(int numElementsPerFace MEMBER _elementsPerFace CONSTANT) + Q_PROPERTY(QString topology MEMBER _topology CONSTANT) + + Q_PROPERTY(uint32 numFaces READ getNumFaces) + Q_PROPERTY(uint32 numAttributes READ getNumAttributes) + Q_PROPERTY(uint32 numVertices READ getNumVertices) + Q_PROPERTY(uint32 numIndices READ getNumIndices) + Q_PROPERTY(QVector attributeNames READ getAttributeNames) + + Q_PROPERTY(QVariantMap metadata MEMBER metadata) + + //Q_PROPERTY(scriptable::ScriptableMeshPointer parentMesh MEMBER parentMesh CONSTANT HIDE) + + ScriptableMeshPart(scriptable::ScriptableMeshPointer parentMesh, int partIndex); + ScriptableMeshPart& operator=(const ScriptableMeshPart& view) { parentMesh=view.parentMesh; return *this; }; + ScriptableMeshPart(const ScriptableMeshPart& other) : parentMesh(other.parentMesh), partIndex(other.partIndex) {} + ~ScriptableMeshPart() { qDebug() << "~ScriptableMeshPart" << this; } + + public slots: + scriptable::ScriptableMeshPointer getParentMesh() const { return parentMesh; } + uint32 getNumAttributes() const { return parentMesh ? parentMesh->getNumAttributes() : 0; } + uint32 getNumVertices() const { return parentMesh ? parentMesh->getNumVertices() : 0; } + uint32 getNumIndices() const { return parentMesh ? parentMesh->getNumIndices() : 0; } + uint32 getNumFaces() const { return parentMesh ? parentMesh->getNumIndices() / _elementsPerFace : 0; } + QVector getAttributeNames() const { return parentMesh ? parentMesh->getAttributeNames() : QVector(); } + QVector getFace(uint32 faceIndex) const { + auto inds = parentMesh ? parentMesh->getIndices() : QVector(); + return faceIndex+2 < (uint32)inds.size() ? inds.mid(faceIndex*3, 3) : QVector(); + } + 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); - public: - operator bool() const { return _mesh != nullptr; } - ScriptableModelPointer _model; - scriptable::MeshPointer _mesh; - QVariantMap _metadata; + bool unrollVertices(bool recalcNormals = false); + bool dedupeVertices(float epsilon = 1e-6); + bool recalculateNormals() { return buffer_helpers::recalculateNormals(getMeshPointer()); } + + bool replaceMeshData(scriptable::ScriptableMeshPartPointer source, const QVector& attributeNames = QVector()); + scriptable::ScriptableMeshPartPointer cloneMeshPart(bool recalcNormals = false) { + if (parentMesh) { + if (auto clone = parentMesh->cloneMesh(recalcNormals)) { + return clone->getMeshParts().value(partIndex); + } + } + return nullptr; + } + QString toOBJ(); public slots: // QScriptEngine-specific wrappers - QScriptValue mapAttributeValues(QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue()); - bool dedupeVertices(float epsilon = 1e-6); - bool recalculateNormals(); - QScriptValue cloneMesh(bool recalcNormals = true); - QScriptValue unrollVertices(bool recalcNormals = true); - bool replaceMeshData(scriptable::ScriptableMeshPointer source, const QVector& attributeNames = QVector()); - QString toOBJ(); - }; - - // TODO: part-specific wrapper for working with raw geometries - class ScriptableMeshPart : public QObject { - Q_OBJECT - public: - Q_PROPERTY(QString topology READ getTopology) - Q_PROPERTY(quint32 numFaces READ getNumFaces) - - ScriptableMeshPart& operator=(const ScriptableMeshPart& view) { parentMesh=view.parentMesh; return *this; }; - ScriptableMeshPart(const ScriptableMeshPart& other) : parentMesh(other.parentMesh) {} - ScriptableMeshPart() {} - ~ScriptableMeshPart() { qDebug() << "~ScriptableMeshPart" << this; } - - public slots: - QString getTopology() const { return "triangles"; } - quint32 getNumFaces() const { return parentMesh.getIndices().size() / 3; } - QVector getFace(quint32 faceIndex) const { - auto inds = parentMesh.getIndices(); - return faceIndex+2 < (quint32)inds.size() ? inds.mid(faceIndex*3, 3) : QVector(); - } + uint32 mapAttributeValues(QScriptValue callback); public: - scriptable::ScriptableMesh parentMesh; - int partIndex; + scriptable::ScriptableMeshPointer parentMesh; + uint32 partIndex; + QVariantMap metadata; + protected: + int _elementsPerFace{ 3 }; + QString _topology{ "triangles" }; + scriptable::MeshPointer getMeshPointer() const { return parentMesh ? parentMesh->getMeshPointer() : nullptr; } }; - class GraphicsScriptingInterface : public QObject { + class GraphicsScriptingInterface : public QObject, QScriptable { Q_OBJECT public: GraphicsScriptingInterface(QObject* parent = nullptr) : QObject(parent) {} GraphicsScriptingInterface(const GraphicsScriptingInterface& other) {} - public slots: - ScriptableMeshPart exportMeshPart(ScriptableMesh mesh, int part) { return {}; } + public slots: + ScriptableMeshPartPointer exportMeshPart(ScriptableMeshPointer mesh, int part=0) { + return ScriptableMeshPartPointer(new ScriptableMeshPart(mesh, part)); + } + bool updateMeshPart(ScriptableMeshPointer mesh, ScriptableMeshPartPointer part); }; + + // callback helper that lets C++ method signatures remain simple (ie: taking a single callback argument) while + // still supporting extended Qt signal-like (scope, "methodName") and (scope, function(){}) "this" binding conventions + QScriptValue jsBindCallback(QScriptValue callback); + + // derive a corresponding C++ class instance from the current script engine's thisObject + template T this_qobject_cast(QScriptEngine* engine); } Q_DECLARE_METATYPE(scriptable::ScriptableMeshPointer) Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(scriptable::ScriptableMeshPartPointer) +Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(scriptable::GraphicsScriptingInterface) // FIXME: MESHFACES: faces were supported in the original Model.* API -- are they still needed/used/useful for anything yet? #include namespace mesh { - using uint32 = quint32; class MeshFace; using MeshFaces = QVector; class MeshFace { public: MeshFace() {} - MeshFace(QVector vertexIndices) : vertexIndices(vertexIndices) {} + MeshFace(QVector vertexIndices) : vertexIndices(vertexIndices) {} ~MeshFace() {} - QVector vertexIndices; + QVector vertexIndices; // TODO -- material... }; }; Q_DECLARE_METATYPE(mesh::MeshFace) Q_DECLARE_METATYPE(QVector) -Q_DECLARE_METATYPE(mesh::uint32) -Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(scriptable::uint32) +Q_DECLARE_METATYPE(QVector) diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h index 4ba5a993b1..97a73ddd61 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.h @@ -1,56 +1,24 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include +#include "Forward.h" -#include - -namespace graphics { - class Mesh; -} -namespace gpu { - class BufferView; -} class QScriptValue; - namespace scriptable { - using Mesh = graphics::Mesh; - using MeshPointer = std::shared_ptr; - - class ScriptableModel; - using ScriptableModelPointer = QPointer; - class ScriptableMesh; - using ScriptableMeshPointer = QPointer; - - // abstract container for holding one or more scriptable meshes - class ScriptableModel : public QObject { + class ScriptableModel : public ScriptableModelBase { Q_OBJECT public: - QUuid objectID; - QVariantMap metadata; - QVector meshes; - - Q_PROPERTY(QVector meshes READ getMeshes) Q_PROPERTY(QUuid objectID MEMBER objectID CONSTANT) Q_PROPERTY(QVariantMap metadata MEMBER metadata CONSTANT) - Q_INVOKABLE QString toString() const; + Q_PROPERTY(uint32 numMeshes READ getNumMeshes) + Q_PROPERTY(QVector meshes READ getMeshes) - ScriptableModel(QObject* parent = nullptr) : QObject(parent) {} - ScriptableModel(const ScriptableModel& other) : objectID(other.objectID), metadata(other.metadata), meshes(other.meshes) {} - ScriptableModel& operator=(const ScriptableModel& view) { objectID = view.objectID; metadata = view.metadata; meshes = view.meshes; return *this; } - ~ScriptableModel() { qDebug() << "~ScriptableModel" << this; } - - void mixin(const ScriptableModel& other) { - for (const auto& key : other.metadata.keys()) { metadata[key] = other.metadata[key]; } - for (const auto& mesh : other.meshes) { meshes << mesh; } - } + 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; } + virtual ~ScriptableModel() { qDebug() << "~ScriptableModel" << this; } + Q_INVOKABLE scriptable::ScriptableModelPointer cloneModel(const QVariantMap& options = QVariantMap()); // TODO: in future accessors for these could go here // QVariantMap shapes; // QVariantMap materials; @@ -58,28 +26,23 @@ namespace scriptable { QVector getMeshes(); const QVector getConstMeshes() const; + operator scriptable::ScriptableModelBasePointer() { + QPointer p; + p = qobject_cast(this); + return p; + } + // QScriptEngine-specific wrappers - Q_INVOKABLE QScriptValue mapAttributeValues(QScriptValue scopeOrCallback, QScriptValue methodOrName); + Q_INVOKABLE uint32 mapAttributeValues(QScriptValue callback); + Q_INVOKABLE QString toString() const; + Q_INVOKABLE uint32 getNumMeshes() { return meshes.size(); } }; - // mixin class for Avatar/Entity/Overlay Rendering that expose their in-memory graphics::Meshes - class ModelProvider { - public: - QVariantMap metadata; - static scriptable::ScriptableModel modelUnavailableError(bool* ok) { if (ok) { *ok = false; } return {}; } - virtual scriptable::ScriptableModel getScriptableModel(bool* ok = nullptr) = 0; - }; - using ModelProviderPointer = std::shared_ptr; - - // mixin class for Application to resolve UUIDs into a corresponding ModelProvider - class ModelProviderFactory : public Dependency { - public: - virtual scriptable::ModelProviderPointer lookupModelProvider(QUuid uuid) = 0; - }; } Q_DECLARE_METATYPE(scriptable::MeshPointer) -Q_DECLARE_METATYPE(scriptable::ScriptableModel) +Q_DECLARE_METATYPE(scriptable::WeakMeshPointer) Q_DECLARE_METATYPE(scriptable::ScriptableModelPointer) - +Q_DECLARE_METATYPE(scriptable::ScriptableModelBase) +Q_DECLARE_METATYPE(scriptable::ScriptableModelBasePointer) From ce93b9a1f4ea001587a0d8ecc39d4473326e1cf8 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 14 Feb 2018 16:48:37 -0800 Subject: [PATCH 058/260] Simplify BackupHandler pattern --- domain-server/src/BackupHandler.h | 64 +++---------------- domain-server/src/BackupSupervisor.h | 4 +- .../src/DomainContentBackupManager.cpp | 11 ++-- .../src/DomainContentBackupManager.h | 4 +- domain-server/src/DomainServer.cpp | 4 +- 5 files changed, 22 insertions(+), 65 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index f2735e5adf..eb9c35f236 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -18,68 +18,22 @@ #include -class BackupHandler { +class BackupHandlerInterface { public: - template - BackupHandler(T* x) : _self(new Model(x)) {} + virtual ~BackupHandlerInterface() = default; - void loadBackup(QuaZip& zip) { - _self->loadBackup(zip); - } - void createBackup(QuaZip& zip) { - _self->createBackup(zip); - } - void recoverBackup(QuaZip& zip) { - _self->recoverBackup(zip); - } - void deleteBackup(QuaZip& zip) { - _self->deleteBackup(zip); - } - void consolidateBackup(QuaZip& zip) { - _self->consolidateBackup(zip); - } - -private: - struct Concept { - virtual ~Concept() = default; - - virtual void loadBackup(QuaZip& zip) = 0; - virtual void createBackup(QuaZip& zip) = 0; - virtual void recoverBackup(QuaZip& zip) = 0; - virtual void deleteBackup(QuaZip& zip) = 0; - virtual void consolidateBackup(QuaZip& zip) = 0; - }; - - template - struct Model : Concept { - Model(T* x) : data(x) {} - - void loadBackup(QuaZip& zip) override { - data->loadBackup(zip); - } - void createBackup(QuaZip& zip) override { - data->createBackup(zip); - } - void recoverBackup(QuaZip& zip) override { - data->recoverBackup(zip); - } - void deleteBackup(QuaZip& zip) override { - data->deleteBackup(zip); - } - void consolidateBackup(QuaZip& zip) override { - data->consolidateBackup(zip); - } - - std::unique_ptr data; - }; - - std::unique_ptr _self; + virtual void loadBackup(QuaZip& zip) = 0; + virtual void createBackup(QuaZip& zip) = 0; + virtual void recoverBackup(QuaZip& zip) = 0; + virtual void deleteBackup(QuaZip& zip) = 0; + virtual void consolidateBackup(QuaZip& zip) = 0; }; +using BackupHandlerPointer = std::unique_ptr; #include #include -class EntitiesBackupHandler { +class EntitiesBackupHandler : public BackupHandlerInterface { public: EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : _entitiesFilePath(entitiesFilePath), diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index 9fedcca19b..0d0d21a174 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -24,6 +24,8 @@ #include +#include "BackupHandler.h" + class QuaZip; struct AssetServerBackup { @@ -32,7 +34,7 @@ struct AssetServerBackup { bool corruptedBackup; }; -class BackupSupervisor : public QObject { +class BackupSupervisor : public QObject, public BackupHandlerInterface { Q_OBJECT public: diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 66655ea966..2b990b170e 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -39,7 +39,8 @@ 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-" }; -void DomainContentBackupManager::addBackupHandler(BackupHandler handler) { + +void DomainContentBackupManager::addBackupHandler(BackupHandlerPointer handler) { _backupHandlers.push_back(std::move(handler)); } @@ -238,7 +239,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, success = false; } else { for (auto& handler : _backupHandlers) { - handler.recoverBackup(zip); + handler->recoverBackup(zip); } qDebug() << "Successfully recovered from " << backupName; @@ -339,7 +340,7 @@ void DomainContentBackupManager::load() { } for (auto& handler : _backupHandlers) { - handler.loadBackup(zip); + handler->loadBackup(zip); } zip.close(); @@ -402,7 +403,7 @@ void DomainContentBackupManager::consolidate(QString fileName) { } for (auto& handler : _backupHandlers) { - handler.consolidateBackup(zip); + handler->consolidateBackup(zip); } zip.close(); @@ -437,7 +438,7 @@ std::pair DomainContentBackupManager::createBackup(const QString& } for (auto& handler : _backupHandlers) { - handler.createBackup(zip); + handler->createBackup(zip); } zip.close(); diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 5cf8d4698f..a3606929d5 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -50,7 +50,7 @@ public: int persistInterval = DEFAULT_PERSIST_INTERVAL, bool debugTimestampNow = false); - void addBackupHandler(BackupHandler handler); + void addBackupHandler(BackupHandlerPointer handler); std::vector getAllBackups(); void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist @@ -82,7 +82,7 @@ protected: private: const QString _backupDirectory; - std::vector _backupHandlers; + std::vector _backupHandlers; int _persistInterval { 0 }; int64_t _lastCheck { 0 }; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index da8527bf16..a8ceebd6e7 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -296,8 +296,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : maybeHandleReplacementEntityFile(); _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); - _contentManager->addBackupHandler(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath())); - _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); + _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); + _contentManager->addBackupHandler(BackupHandlerPointer(new BackupSupervisor(getContentBackupDir()))); _contentManager->initialize(true); qDebug() << "Existing backups:"; From 4482f9c83c67def4fef5444a52f6d85e531ce53f Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 14 Feb 2018 16:49:12 -0800 Subject: [PATCH 059/260] Queue all requests until the AS is fully setup --- assignment-client/src/assets/AssetServer.cpp | 45 +++++++++++++++++--- assignment-client/src/assets/AssetServer.h | 5 +++ domain-server/src/BackupSupervisor.cpp | 5 +-- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 1ae65290ff..0be557bccd 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); @@ -417,6 +415,43 @@ void AssetServer::completeSetup() { PathUtils::removeTemporaryApplicationDirs(); PathUtils::removeTemporaryApplicationDirs("Oven"); + + // We're fully setup, remove the request queueing and replay all requests + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); + packetReceiver.unregisterListener(this); + 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) { + _queuedRequests.push_back({ packet, senderNode }); +} + +void AssetServer::replayRequests() { + for (const auto& request : _queuedRequests) { + 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: + qWarning() << "Unknown queued request type:" << request.first->getType(); + break; + } + } + _queuedRequests.clear(); } void AssetServer::cleanupUnmappedFiles() { diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h index c6336a3a4d..b8aac800ed 100644 --- a/assignment-client/src/assets/AssetServer.h +++ b/assignment-client/src/assets/AssetServer.h @@ -49,6 +49,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); @@ -57,6 +58,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); @@ -120,6 +123,8 @@ private: QHash> _pendingBakes; QThreadPool _bakingTaskPool; + QVector, SharedNodePointer>> _queuedRequests; + bool _wasColorTextureCompressionEnabled { false }; bool _wasGrayscaleTextureCompressionEnabled { false }; bool _wasNormalTextureCompressionEnabled { false }; diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp index 869f85c6cc..0cbded4e43 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/BackupSupervisor.cpp @@ -46,9 +46,8 @@ BackupSupervisor::BackupSupervisor(const QString& backupDirectory) : auto nodeList = DependencyManager::get(); QObject::connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, [this](SharedNodePointer node) { if (node->getType() == NodeType::AssetServer) { - // Give the Asset Server some time to bootup. - static constexpr int ASSET_SERVER_BOOTUP_MARGIN = 1 * 1000; - _mappingsRefreshTimer.start(ASSET_SERVER_BOOTUP_MARGIN); + // run immediately for the first time. + _mappingsRefreshTimer.start(0); } }); } From d8d05fe0456831cd9dadde546c3635f09eafa46c Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 14 Feb 2018 17:24:00 -0800 Subject: [PATCH 060/260] Rename backup supervisor --- ...Supervisor.cpp => AssetsBackupHandler.cpp} | 42 +++++++++---------- ...ckupSupervisor.h => AssetsBackupHandler.h} | 24 +++++------ domain-server/src/DomainServer.cpp | 4 +- domain-server/src/DomainServer.h | 2 +- 4 files changed, 36 insertions(+), 36 deletions(-) rename domain-server/src/{BackupSupervisor.cpp => AssetsBackupHandler.cpp} (93%) rename domain-server/src/{BackupSupervisor.h => AssetsBackupHandler.h} (85%) diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/AssetsBackupHandler.cpp similarity index 93% rename from domain-server/src/BackupSupervisor.cpp rename to domain-server/src/AssetsBackupHandler.cpp index 0cbded4e43..3dc4851762 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -1,5 +1,5 @@ // -// BackupSupervisor.cpp +// AssetsBackupHandler.cpp // domain-server/src // // Created by Clement Brisset on 1/12/18. @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "BackupSupervisor.h" +#include "AssetsBackupHandler.h" #include #include @@ -31,7 +31,7 @@ using namespace std; Q_DECLARE_LOGGING_CATEGORY(backup_supervisor) Q_LOGGING_CATEGORY(backup_supervisor, "hifi.backup-supervisor"); -BackupSupervisor::BackupSupervisor(const QString& backupDirectory) : +AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory) : _assetsDirectory(backupDirectory + ASSETS_DIR) { // Make sure the asset directory exists. @@ -41,7 +41,7 @@ BackupSupervisor::BackupSupervisor(const QString& backupDirectory) : _mappingsRefreshTimer.setTimerType(Qt::CoarseTimer); _mappingsRefreshTimer.setSingleShot(true); - QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &BackupSupervisor::refreshMappings); + QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &AssetsBackupHandler::refreshMappings); auto nodeList = DependencyManager::get(); QObject::connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, [this](SharedNodePointer node) { @@ -53,7 +53,7 @@ BackupSupervisor::BackupSupervisor(const QString& backupDirectory) : } -void BackupSupervisor::refreshAssetsOnDisk() { +void AssetsBackupHandler::refreshAssetsOnDisk() { QDir assetsDir { _assetsDirectory }; auto assetNames = assetsDir.entryList(QDir::Files); @@ -64,7 +64,7 @@ void BackupSupervisor::refreshAssetsOnDisk() { } -void BackupSupervisor::refreshAssetsInBackups() { +void AssetsBackupHandler::refreshAssetsInBackups() { _assetsInBackups.clear(); for (const auto& backup : _backups) { for (const auto& mapping : backup.mappings) { @@ -73,7 +73,7 @@ void BackupSupervisor::refreshAssetsInBackups() { } } -void BackupSupervisor::checkForMissingAssets() { +void AssetsBackupHandler::checkForMissingAssets() { vector missingAssets; set_difference(begin(_assetsInBackups), end(_assetsInBackups), begin(_assetsOnDisk), end(_assetsOnDisk), @@ -83,7 +83,7 @@ void BackupSupervisor::checkForMissingAssets() { } } -void BackupSupervisor::checkForAssetsToDelete() { +void AssetsBackupHandler::checkForAssetsToDelete() { vector deprecatedAssets; set_difference(begin(_assetsOnDisk), end(_assetsOnDisk), begin(_assetsInBackups), end(_assetsInBackups), @@ -101,7 +101,7 @@ void BackupSupervisor::checkForAssetsToDelete() { } } -void BackupSupervisor::loadBackup(QuaZip& zip) { +void AssetsBackupHandler::loadBackup(QuaZip& zip) { _backups.push_back({ zip.getZipName(), {}, false }); auto& backup = _backups.back(); @@ -151,7 +151,7 @@ void BackupSupervisor::loadBackup(QuaZip& zip) { return; } -void BackupSupervisor::createBackup(QuaZip& zip) { +void AssetsBackupHandler::createBackup(QuaZip& zip) { if (operationInProgress()) { qCWarning(backup_supervisor) << "There is already an operation in progress."; return; @@ -192,7 +192,7 @@ void BackupSupervisor::createBackup(QuaZip& zip) { _backups.push_back(backup); } -void BackupSupervisor::recoverBackup(QuaZip& zip) { +void AssetsBackupHandler::recoverBackup(QuaZip& zip) { if (operationInProgress()) { qCWarning(backup_supervisor) << "There is already a backup/restore in progress."; return; @@ -225,7 +225,7 @@ void BackupSupervisor::recoverBackup(QuaZip& zip) { restoreAllAssets(); } -void BackupSupervisor::deleteBackup(QuaZip& zip) { +void AssetsBackupHandler::deleteBackup(QuaZip& zip) { if (operationInProgress()) { qCWarning(backup_supervisor) << "There is a backup/restore in progress."; return; @@ -243,7 +243,7 @@ void BackupSupervisor::deleteBackup(QuaZip& zip) { checkForAssetsToDelete(); } -void BackupSupervisor::consolidateBackup(QuaZip& zip) { +void AssetsBackupHandler::consolidateBackup(QuaZip& zip) { if (operationInProgress()) { qCWarning(backup_supervisor) << "There is a backup/restore in progress."; return; @@ -285,7 +285,7 @@ void BackupSupervisor::consolidateBackup(QuaZip& zip) { } -void BackupSupervisor::refreshMappings() { +void AssetsBackupHandler::refreshMappings() { auto assetClient = DependencyManager::get(); auto request = assetClient->createGetAllMappingsRequest(); @@ -314,7 +314,7 @@ void BackupSupervisor::refreshMappings() { request->start(); } -void BackupSupervisor::downloadMissingFiles(const AssetUtils::Mappings& mappings) { +void AssetsBackupHandler::downloadMissingFiles(const AssetUtils::Mappings& mappings) { auto wasEmpty = _assetsLeftToRequest.empty(); for (const auto& mapping : mappings) { @@ -330,7 +330,7 @@ void BackupSupervisor::downloadMissingFiles(const AssetUtils::Mappings& mappings } } -void BackupSupervisor::downloadNextMissingFile() { +void AssetsBackupHandler::downloadNextMissingFile() { if (_assetsLeftToRequest.empty()) { return; } @@ -360,7 +360,7 @@ void BackupSupervisor::downloadNextMissingFile() { assetRequest->start(); } -bool BackupSupervisor::writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data) { +bool AssetsBackupHandler::writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data) { QDir assetsDir { _assetsDirectory }; QFile file { assetsDir.filePath(hash) }; if (!file.open(QFile::WriteOnly)) { @@ -380,7 +380,7 @@ bool BackupSupervisor::writeAssetFile(const AssetUtils::AssetHash& hash, const Q return true; } -void BackupSupervisor::computeServerStateDifference(const AssetUtils::Mappings& currentMappings, +void AssetsBackupHandler::computeServerStateDifference(const AssetUtils::Mappings& currentMappings, const AssetUtils::Mappings& newMappings) { _mappingsLeftToSet.reserve((int)newMappings.size()); _assetsLeftToUpload.reserve((int)newMappings.size()); @@ -415,11 +415,11 @@ void BackupSupervisor::computeServerStateDifference(const AssetUtils::Mappings& qCDebug(backup_supervisor) << "Assets to upload:" << _assetsLeftToUpload.size(); } -void BackupSupervisor::restoreAllAssets() { +void AssetsBackupHandler::restoreAllAssets() { restoreNextAsset(); } -void BackupSupervisor::restoreNextAsset() { +void AssetsBackupHandler::restoreNextAsset() { if (_assetsLeftToUpload.empty()) { updateMappings(); return; @@ -447,7 +447,7 @@ void BackupSupervisor::restoreNextAsset() { request->start(); } -void BackupSupervisor::updateMappings() { +void AssetsBackupHandler::updateMappings() { auto assetClient = DependencyManager::get(); for (const auto& mapping : _mappingsLeftToSet) { auto request = assetClient->createSetMappingRequest(mapping.first, mapping.second); diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/AssetsBackupHandler.h similarity index 85% rename from domain-server/src/BackupSupervisor.h rename to domain-server/src/AssetsBackupHandler.h index 0d0d21a174..184f30ab9b 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/AssetsBackupHandler.h @@ -1,5 +1,5 @@ // -// BackupSupervisor.h +// AssetsBackupHandler.h // domain-server/src // // Created by Clement Brisset on 1/12/18. @@ -9,8 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_BackupSupervisor_h -#define hifi_BackupSupervisor_h +#ifndef hifi_AssetsBackupHandler_h +#define hifi_AssetsBackupHandler_h #include #include @@ -28,17 +28,11 @@ class QuaZip; -struct AssetServerBackup { - QString filePath; - AssetUtils::Mappings mappings; - bool corruptedBackup; -}; - -class BackupSupervisor : public QObject, public BackupHandlerInterface { +class AssetsBackupHandler : public QObject, public BackupHandlerInterface { Q_OBJECT public: - BackupSupervisor(const QString& backupDirectory); + AssetsBackupHandler(const QString& backupDirectory); void loadBackup(QuaZip& zip); void createBackup(QuaZip& zip); @@ -75,6 +69,12 @@ private: quint64 _lastMappingsRefresh { 0 }; AssetUtils::Mappings _currentMappings; + struct AssetServerBackup { + QString filePath; + AssetUtils::Mappings mappings; + bool corruptedBackup; + }; + bool _operationInProgress { false }; // Internal storage for backups on disk @@ -93,4 +93,4 @@ private: int _mappingRequestsInFlight { 0 }; }; -#endif /* hifi_BackupSupervisor_h */ +#endif /* hifi_AssetsBackupHandler_h */ diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index a8ceebd6e7..11b6a2d441 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -45,7 +45,7 @@ #include #include -#include "BackupSupervisor.h" +#include "AssetsBackupHandler.h" #include "DomainServerNodeData.h" #include "NodeConnectionData.h" @@ -297,7 +297,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); - _contentManager->addBackupHandler(BackupHandlerPointer(new BackupSupervisor(getContentBackupDir()))); + _contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir()))); _contentManager->initialize(true); qDebug() << "Existing backups:"; diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index ee0350665e..afe2a1cc7c 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -26,7 +26,7 @@ #include #include -#include "BackupSupervisor.h" +#include "AssetsBackupHandler.h" #include "DomainGatekeeper.h" #include "DomainMetadata.h" #include "DomainServerSettingsManager.h" From 9fca92facd27f12a0daff1455b0dc560fda4db46 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 14 Feb 2018 18:11:55 -0800 Subject: [PATCH 061/260] Move EntitiesBackupHandler to its own file --- domain-server/src/AssetsBackupHandler.h | 3 - domain-server/src/BackupHandler.h | 79 +------------------ .../src/DomainContentBackupManager.cpp | 6 +- .../src/DomainContentBackupManager.h | 1 + domain-server/src/DomainServer.cpp | 1 + domain-server/src/EntitiesBackupHandler.cpp | 73 +++++++++++++++++ domain-server/src/EntitiesBackupHandler.h | 42 ++++++++++ 7 files changed, 124 insertions(+), 81 deletions(-) create mode 100644 domain-server/src/EntitiesBackupHandler.cpp create mode 100644 domain-server/src/EntitiesBackupHandler.h diff --git a/domain-server/src/AssetsBackupHandler.h b/domain-server/src/AssetsBackupHandler.h index 184f30ab9b..b78206b7b1 100644 --- a/domain-server/src/AssetsBackupHandler.h +++ b/domain-server/src/AssetsBackupHandler.h @@ -21,13 +21,10 @@ #include #include - #include #include "BackupHandler.h" -class QuaZip; - class AssetsBackupHandler : public QObject, public BackupHandlerInterface { Q_OBJECT diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index eb9c35f236..8599dafb29 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -14,9 +14,7 @@ #include -#include - -#include +class QuaZip; class BackupHandlerInterface { public: @@ -28,80 +26,7 @@ public: virtual void deleteBackup(QuaZip& zip) = 0; virtual void consolidateBackup(QuaZip& zip) = 0; }; + using BackupHandlerPointer = std::unique_ptr; -#include -#include - -class EntitiesBackupHandler : public BackupHandlerInterface { -public: - EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : - _entitiesFilePath(entitiesFilePath), - _entitiesReplacementFilePath(entitiesReplacementFilePath) {} - - void loadBackup(QuaZip& zip) {} - - // Create a skeleton backup - void createBackup(QuaZip& zip) { - QFile entitiesFile { _entitiesFilePath }; - - if (entitiesFile.open(QIODevice::ReadOnly)) { - QuaZipFile zipFile { &zip }; - zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("models.json.gz", _entitiesFilePath)); - zipFile.write(entitiesFile.readAll()); - zipFile.close(); - if (zipFile.getZipError() != UNZ_OK) { - qCritical() << "Failed to zip models.json.gz: " << zipFile.getZipError(); - } - } - } - - // Recover from a full backup - void recoverBackup(QuaZip& zip) { - if (!zip.setCurrentFile("models.json.gz")) { - qWarning() << "Failed to find models.json.gz while recovering backup"; - return; - } - QuaZipFile zipFile { &zip }; - if (!zipFile.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open models.json.gz in backup"; - return; - } - auto rawData = zipFile.readAll(); - - zipFile.close(); - - OctreeUtils::RawOctreeData data; - if (!OctreeUtils::readOctreeDataInfoFromData(rawData, &data)) { - qCritical() << "Unable to parse octree data during backup recovery"; - return; - } - - data.resetIdAndVersion(); - - if (zipFile.getZipError() != UNZ_OK) { - qCritical() << "Failed to unzip models.json.gz: " << zipFile.getZipError(); - return; - } - - QFile entitiesFile { _entitiesReplacementFilePath }; - - if (entitiesFile.open(QIODevice::WriteOnly)) { - entitiesFile.write(data.toGzippedByteArray()); - } - } - - // Delete a skeleton backup - void deleteBackup(QuaZip& zip) { - } - - // Create a full backup - void consolidateBackup(QuaZip& zip) { - } - -private: - QString _entitiesFilePath; - QString _entitiesReplacementFilePath; -}; - #endif /* hifi_BackupHandler_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 2b990b170e..f39737c92e 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "DomainContentBackupManager.h" + #include #include @@ -25,13 +27,15 @@ #include #include +#include + #include #include #include #include #include "DomainServer.h" -#include "DomainContentBackupManager.h" + const int DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds // Backup format looks like: daily_backup-TIMESTAMP.zip diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index a3606929d5..1e1b2360a8 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -16,6 +16,7 @@ #include #include +#include #include diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 11b6a2d441..4c72423f74 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -47,6 +47,7 @@ #include "AssetsBackupHandler.h" #include "DomainServerNodeData.h" +#include "EntitiesBackupHandler.h" #include "NodeConnectionData.h" #include diff --git a/domain-server/src/EntitiesBackupHandler.cpp b/domain-server/src/EntitiesBackupHandler.cpp new file mode 100644 index 0000000000..a95d68b007 --- /dev/null +++ b/domain-server/src/EntitiesBackupHandler.cpp @@ -0,0 +1,73 @@ +// +// 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) +{ +} + +void EntitiesBackupHandler::createBackup(QuaZip& zip) { + QFile entitiesFile { _entitiesFilePath }; + + if (entitiesFile.open(QIODevice::ReadOnly)) { + QuaZipFile zipFile { &zip }; + zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("models.json.gz", _entitiesFilePath)); + zipFile.write(entitiesFile.readAll()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qCritical() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + } + } +} + +void EntitiesBackupHandler::recoverBackup(QuaZip& zip) { + if (!zip.setCurrentFile("models.json.gz")) { + qWarning() << "Failed to find models.json.gz while recovering backup"; + return; + } + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open models.json.gz in backup"; + return; + } + auto rawData = zipFile.readAll(); + + zipFile.close(); + + OctreeUtils::RawOctreeData data; + if (!OctreeUtils::readOctreeDataInfoFromData(rawData, &data)) { + qCritical() << "Unable to parse octree data during backup recovery"; + return; + } + + data.resetIdAndVersion(); + + if (zipFile.getZipError() != UNZ_OK) { + qCritical() << "Failed to unzip models.json.gz: " << 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..6f66483a87 --- /dev/null +++ b/domain-server/src/EntitiesBackupHandler.h @@ -0,0 +1,42 @@ +// +// 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 + +#include "BackupHandler.h" + +class EntitiesBackupHandler : public BackupHandlerInterface { +public: + EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath); + + void loadBackup(QuaZip& zip) {} + + // Create a skeleton backup + void createBackup(QuaZip& zip); + + // Recover from a full backup + void recoverBackup(QuaZip& zip); + + // Delete a skeleton backup + void deleteBackup(QuaZip& zip) {} + + // Create a full backup + void consolidateBackup(QuaZip& zip) {} + +private: + QString _entitiesFilePath; + QString _entitiesReplacementFilePath; +}; + +#endif /* hifi_EntitiesBackupHandler_h */ From d6e281408144f0db5bb102ca4a0b99dd460cd63f Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 11:25:07 -0800 Subject: [PATCH 062/260] Write assets to disk when recovering full backup --- domain-server/src/AssetsBackupHandler.cpp | 152 ++++++++++++++-------- domain-server/src/AssetsBackupHandler.h | 1 + 2 files changed, 99 insertions(+), 54 deletions(-) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index 3dc4851762..a9f56a0c5b 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -23,13 +24,14 @@ #include #include -const QString ASSETS_DIR = "/assets/"; -const QString MAPPINGS_FILE = "mappings.json"; +static const QString ASSETS_DIR = "/assets/"; +static const QString MAPPINGS_FILE = "mappings.json"; +static const QString ZIP_ASSETS_FOLDER = "files"; using namespace std; -Q_DECLARE_LOGGING_CATEGORY(backup_supervisor) -Q_LOGGING_CATEGORY(backup_supervisor, "hifi.backup-supervisor"); +Q_DECLARE_LOGGING_CATEGORY(asset_backup) +Q_LOGGING_CATEGORY(asset_backup, "hifi.asset-backup"); AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory) : _assetsDirectory(backupDirectory + ASSETS_DIR) @@ -39,6 +41,10 @@ AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory) : refreshAssetsOnDisk(); + setupRefreshTimer(); +} + +void AssetsBackupHandler::setupRefreshTimer() { _mappingsRefreshTimer.setTimerType(Qt::CoarseTimer); _mappingsRefreshTimer.setSingleShot(true); QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &AssetsBackupHandler::refreshMappings); @@ -50,9 +56,14 @@ AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory) : _mappingsRefreshTimer.start(0); } }); + QObject::connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, [this](SharedNodePointer node) { + if (node->getType() == NodeType::AssetServer) { + // run immediately for the first time. + _mappingsRefreshTimer.stop(); + } + }); } - void AssetsBackupHandler::refreshAssetsOnDisk() { QDir assetsDir { _assetsDirectory }; auto assetNames = assetsDir.entryList(QDir::Files); @@ -79,7 +90,7 @@ void AssetsBackupHandler::checkForMissingAssets() { begin(_assetsOnDisk), end(_assetsOnDisk), back_inserter(missingAssets)); if (missingAssets.size() > 0) { - qCWarning(backup_supervisor) << "Found" << missingAssets.size() << "assets missing."; + qCWarning(asset_backup) << "Found" << missingAssets.size() << "backup assets missing from disk."; } } @@ -90,24 +101,26 @@ void AssetsBackupHandler::checkForAssetsToDelete() { back_inserter(deprecatedAssets)); if (deprecatedAssets.size() > 0) { - qCDebug(backup_supervisor) << "Found" << deprecatedAssets.size() << "assets to delete."; + qCDebug(asset_backup) << "Found" << deprecatedAssets.size() << "backup assets to delete from disk."; if (_allBackupsLoadedSuccessfully) { for (const auto& hash : deprecatedAssets) { QFile::remove(_assetsDirectory + hash); } } else { - qCWarning(backup_supervisor) << "Some backups did not load properly, aborting deleting for safety."; + qCWarning(asset_backup) << "Some backups did not load properly, aborting delete operation for safety."; } } } void AssetsBackupHandler::loadBackup(QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + _backups.push_back({ zip.getZipName(), {}, false }); auto& backup = _backups.back(); if (!zip.setCurrentFile(MAPPINGS_FILE)) { - qCCritical(backup_supervisor) << "Failed to find" << MAPPINGS_FILE << "while recovering backup"; - qCCritical(backup_supervisor) << " Error:" << zip.getZipError(); + qCCritical(asset_backup) << "Failed to find" << MAPPINGS_FILE << "while loading backup"; + qCCritical(asset_backup) << " Error:" << zip.getZipError(); backup.corruptedBackup = true; _allBackupsLoadedSuccessfully = false; return; @@ -115,8 +128,8 @@ void AssetsBackupHandler::loadBackup(QuaZip& zip) { QuaZipFile zipFile { &zip }; if (!zipFile.open(QFile::ReadOnly)) { - qCCritical(backup_supervisor) << "Could not open backup file:" << zip.getZipName(); - qCCritical(backup_supervisor) << " Error:" << zip.getZipError(); + qCCritical(asset_backup) << "Could not unzip backup file for load:" << zip.getZipName(); + qCCritical(asset_backup) << " Error:" << zip.getZipError(); backup.corruptedBackup = true; _allBackupsLoadedSuccessfully = false; return; @@ -125,8 +138,8 @@ void AssetsBackupHandler::loadBackup(QuaZip& zip) { QJsonParseError error; auto document = QJsonDocument::fromJson(zipFile.readAll(), &error); if (document.isNull() || !document.isObject()) { - qCCritical(backup_supervisor) << "Could not parse backup file to JSON object:" << zip.getZipName(); - qCCritical(backup_supervisor) << " Error:" << error.errorString(); + qCCritical(asset_backup) << "Could not parse backup file to JSON object for load:" << zip.getZipName(); + qCCritical(asset_backup) << " Error:" << error.errorString(); backup.corruptedBackup = true; _allBackupsLoadedSuccessfully = false; return; @@ -138,33 +151,37 @@ void AssetsBackupHandler::loadBackup(QuaZip& zip) { const auto& assetHash = it.value().toString(); if (!AssetUtils::isValidHash(assetHash)) { - qCCritical(backup_supervisor) << "Corrupted mapping in backup file" << zip.getZipName() << ":" << it.key(); + qCCritical(asset_backup) << "Corrupted mapping in loading backup file" << zip.getZipName() << ":" << it.key(); backup.corruptedBackup = true; _allBackupsLoadedSuccessfully = false; - return; + continue; } backup.mappings[assetPath] = assetHash; _assetsInBackups.insert(assetHash); } + checkForMissingAssets(); + checkForAssetsToDelete(); return; } void AssetsBackupHandler::createBackup(QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + if (operationInProgress()) { - qCWarning(backup_supervisor) << "There is already an operation in progress."; + qCWarning(asset_backup) << "There is already an operation in progress."; return; } if (_lastMappingsRefresh == 0) { - qCWarning(backup_supervisor) << "Current mappings not yet loaded."; + qCWarning(asset_backup) << "Current mappings not yet loaded."; return; } static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { - qCWarning(backup_supervisor) << "Backing up asset mappings that might be stale."; + qCWarning(asset_backup) << "Backing up asset mappings that might be stale."; } AssetServerBackup backup; @@ -180,43 +197,65 @@ void AssetsBackupHandler::createBackup(QuaZip& zip) { QuaZipFile zipFile { &zip }; if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(MAPPINGS_FILE))) { - qCDebug(backup_supervisor) << "Could not open zip file:" << zipFile.getZipError(); + qCDebug(asset_backup) << "Could not open zip file:" << zipFile.getZipError(); return; } zipFile.write(document.toJson()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qCDebug(backup_supervisor) << "Could not close zip file: " << zipFile.getZipError(); + qCDebug(asset_backup) << "Could not close zip file: " << zipFile.getZipError(); return; } _backups.push_back(backup); } void AssetsBackupHandler::recoverBackup(QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + if (operationInProgress()) { - qCWarning(backup_supervisor) << "There is already a backup/restore in progress."; + qCWarning(asset_backup) << "There is already a backup/restore in progress."; return; } if (_lastMappingsRefresh == 0) { - qCWarning(backup_supervisor) << "Current mappings not yet loaded."; + qCWarning(asset_backup) << "Current mappings not yet loaded."; return; } static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { - qCWarning(backup_supervisor) << "Current asset mappings that might be stale."; + qCWarning(asset_backup) << "Current asset mappings that might be stale."; } - startOperation(); - auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { return value.filePath == zip.getZipName(); }); if (it == end(_backups)) { - qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName() << "to restore."; - stopOperation(); - return; + qCDebug(asset_backup) << "Could not find backup" << zip.getZipName() << "to restore."; + + loadBackup(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(MAPPINGS_FILE)) { + 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()); + } + } } const auto& newMappings = it->mappings; @@ -226,8 +265,10 @@ void AssetsBackupHandler::recoverBackup(QuaZip& zip) { } void AssetsBackupHandler::deleteBackup(QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + if (operationInProgress()) { - qCWarning(backup_supervisor) << "There is a backup/restore in progress."; + qCWarning(asset_backup) << "There is a backup/restore in progress."; return; } @@ -235,7 +276,7 @@ void AssetsBackupHandler::deleteBackup(QuaZip& zip) { return value.filePath == zip.getZipName(); }); if (it == end(_backups)) { - qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName() << "to delete."; + qCDebug(asset_backup) << "Could not find backup" << zip.getZipName() << "to delete."; return; } @@ -244,8 +285,10 @@ void AssetsBackupHandler::deleteBackup(QuaZip& zip) { } void AssetsBackupHandler::consolidateBackup(QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + if (operationInProgress()) { - qCWarning(backup_supervisor) << "There is a backup/restore in progress."; + qCWarning(asset_backup) << "There is a backup/restore in progress."; return; } QFileInfo zipInfo(zip.getZipName()); @@ -255,7 +298,7 @@ void AssetsBackupHandler::consolidateBackup(QuaZip& zip) { return info.fileName() == zipInfo.fileName(); }); if (it == end(_backups)) { - qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName() << "to consolidate."; + qCDebug(asset_backup) << "Could not find backup" << zip.getZipName() << "to consolidate."; return; } @@ -265,20 +308,19 @@ void AssetsBackupHandler::consolidateBackup(QuaZip& zip) { QDir assetsDir { _assetsDirectory }; QFile file { assetsDir.filePath(hash) }; if (!file.open(QFile::ReadOnly)) { - qCCritical(backup_supervisor) << "Could not open asset file" << file.fileName(); + qCCritical(asset_backup) << "Could not open asset file" << file.fileName(); continue; } QuaZipFile zipFile { &zip }; - static const QString ZIP_ASSETS_FOLDER = "files/"; - if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ZIP_ASSETS_FOLDER + hash))) { - qCDebug(backup_supervisor) << "Could not open zip file:" << zipFile.getZipError(); + 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(backup_supervisor) << "Could not close zip file: " << zipFile.getZipError(); + qCDebug(asset_backup) << "Could not close zip file: " << zipFile.getZipError(); continue; } } @@ -300,8 +342,8 @@ void AssetsBackupHandler::refreshMappings() { downloadMissingFiles(_currentMappings); } else { - qCCritical(backup_supervisor) << "Could not refresh asset server mappings."; - qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); + qCCritical(asset_backup) << "Could not refresh asset server mappings."; + qCCritical(asset_backup) << " Error:" << request->getErrorString(); } request->deleteLater(); @@ -341,14 +383,14 @@ void AssetsBackupHandler::downloadNextMissingFile() { QObject::connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) { if (request->getError() == AssetRequest::NoError) { - qCDebug(backup_supervisor) << "Backing up asset" << request->getHash(); + qCDebug(asset_backup) << "Backing up asset" << request->getHash(); bool success = writeAssetFile(request->getHash(), request->getData()); if (!success) { - qCCritical(backup_supervisor) << "Failed to write asset file" << request->getHash(); + qCCritical(asset_backup) << "Failed to write asset file" << request->getHash(); } } else { - qCCritical(backup_supervisor) << "Failed to backup asset" << request->getHash(); + qCCritical(asset_backup) << "Failed to backup asset" << request->getHash(); } _assetsLeftToRequest.erase(request->getHash()); @@ -364,13 +406,13 @@ bool AssetsBackupHandler::writeAssetFile(const AssetUtils::AssetHash& hash, cons QDir assetsDir { _assetsDirectory }; QFile file { assetsDir.filePath(hash) }; if (!file.open(QFile::WriteOnly)) { - qCCritical(backup_supervisor) << "Could not open backup file" << file.fileName(); + qCCritical(asset_backup) << "Could not open asset file for write:" << file.fileName(); return false; } auto bytesWritten = file.write(data); if (bytesWritten != data.size()) { - qCCritical(backup_supervisor) << "Could not write data to file" << file.fileName(); + qCCritical(asset_backup) << "Could not write data to file" << file.fileName(); file.remove(); return false; } @@ -410,9 +452,9 @@ void AssetsBackupHandler::computeServerStateDifference(const AssetUtils::Mapping } } - qCDebug(backup_supervisor) << "Mappings to set:" << _mappingsLeftToSet.size(); - qCDebug(backup_supervisor) << "Mappings to del:" << _mappingsLeftToDelete.size(); - qCDebug(backup_supervisor) << "Assets to upload:" << _assetsLeftToUpload.size(); + 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() { @@ -420,6 +462,8 @@ void AssetsBackupHandler::restoreAllAssets() { } void AssetsBackupHandler::restoreNextAsset() { + startOperation(); + if (_assetsLeftToUpload.empty()) { updateMappings(); return; @@ -435,8 +479,8 @@ void AssetsBackupHandler::restoreNextAsset() { QObject::connect(request, &AssetUpload::finished, this, [this](AssetUpload* request) { if (request->getError() != AssetUpload::NoError) { - qCCritical(backup_supervisor) << "Failed to restore asset:" << request->getFilename(); - qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); + qCCritical(asset_backup) << "Failed to restore asset:" << request->getFilename(); + qCCritical(asset_backup) << " Error:" << request->getErrorString(); } restoreNextAsset(); @@ -453,8 +497,8 @@ void AssetsBackupHandler::updateMappings() { auto request = assetClient->createSetMappingRequest(mapping.first, mapping.second); QObject::connect(request, &SetMappingRequest::finished, this, [this](SetMappingRequest* request) { if (request->getError() != MappingRequest::NoError) { - qCCritical(backup_supervisor) << "Failed to set mapping:" << request->getPath(); - qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); + qCCritical(asset_backup) << "Failed to set mapping:" << request->getPath(); + qCCritical(asset_backup) << " Error:" << request->getErrorString(); } if (--_mappingRequestsInFlight == 0) { @@ -472,8 +516,8 @@ void AssetsBackupHandler::updateMappings() { auto request = assetClient->createDeleteMappingsRequest(_mappingsLeftToDelete); QObject::connect(request, &DeleteMappingsRequest::finished, this, [this](DeleteMappingsRequest* request) { if (request->getError() != MappingRequest::NoError) { - qCCritical(backup_supervisor) << "Failed to delete mappings"; - qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); + qCCritical(asset_backup) << "Failed to delete mappings"; + qCCritical(asset_backup) << " Error:" << request->getErrorString(); } if (--_mappingRequestsInFlight == 0) { diff --git a/domain-server/src/AssetsBackupHandler.h b/domain-server/src/AssetsBackupHandler.h index b78206b7b1..2ef454998e 100644 --- a/domain-server/src/AssetsBackupHandler.h +++ b/domain-server/src/AssetsBackupHandler.h @@ -40,6 +40,7 @@ public: bool operationInProgress() const { return _operationInProgress; } private: + void setupRefreshTimer(); void refreshMappings(); void refreshAssetsInBackups(); From 777862f253c32afcfa1bd0988a986da0d5b7c7da Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Thu, 15 Feb 2018 11:50:38 -0800 Subject: [PATCH 063/260] can cast shadow flag now works correctly (UI/JS aspects). --- libraries/entities/src/EntityItemProperties.cpp | 9 +++++++++ libraries/entities/src/EntityItemProperties.h | 2 +- scripts/system/html/js/entityProperties.js | 8 ++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 3c3c0742da..86404c6504 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -1506,6 +1506,15 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy APPEND_ENTITY_PROPERTY(PROP_SHAPE, properties.getShape()); } + // Only models and shapes (including cubes and spheres) can cast shadows + if (properties.getType() == EntityTypes::Model || + properties.getType() == EntityTypes::Shape || + properties.getType() == EntityTypes::Box || + properties.getType() == EntityTypes::Sphere) { + + APPEND_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, properties.getCanCastShadow()); + } + APPEND_ENTITY_PROPERTY(PROP_NAME, properties.getName()); APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, properties.getCollisionSoundURL()); APPEND_ENTITY_PROPERTY(PROP_ACTION_DATA, properties.getActionData()); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index dcec1a1f81..349d32f806 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -113,6 +113,7 @@ public: // bool _fooChanged { false }; DEFINE_PROPERTY(PROP_VISIBLE, Visible, visible, bool, ENTITY_ITEM_DEFAULT_VISIBLE); + DEFINE_PROPERTY(PROP_CAN_CAST_SHADOW, CanCastShadow, canCastShadow, bool, ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW); DEFINE_PROPERTY_REF_WITH_SETTER(PROP_POSITION, Position, position, glm::vec3, ENTITY_ITEM_ZERO_VEC3); DEFINE_PROPERTY_REF(PROP_DIMENSIONS, Dimensions, dimensions, glm::vec3, ENTITY_ITEM_DEFAULT_DIMENSIONS); DEFINE_PROPERTY_REF(PROP_ROTATION, Rotation, rotation, glm::quat, ENTITY_ITEM_DEFAULT_ROTATION); @@ -128,7 +129,6 @@ public: DEFINE_PROPERTY_REF(PROP_SCRIPT, Script, script, QString, ENTITY_ITEM_DEFAULT_SCRIPT); DEFINE_PROPERTY(PROP_SCRIPT_TIMESTAMP, ScriptTimestamp, scriptTimestamp, quint64, ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP); DEFINE_PROPERTY_REF(PROP_COLLISION_SOUND_URL, CollisionSoundURL, collisionSoundURL, QString, ENTITY_ITEM_DEFAULT_COLLISION_SOUND_URL); - DEFINE_PROPERTY(PROP_CAN_CAST_SHADOW, CanCastShadow, canCastShadow, bool, ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW); DEFINE_PROPERTY_REF(PROP_COLOR, Color, color, xColor, particle::DEFAULT_COLOR); DEFINE_PROPERTY_REF(PROP_COLOR_SPREAD, ColorSpread, colorSpread, xColor, particle::DEFAULT_COLOR_SPREAD); DEFINE_PROPERTY_REF(PROP_COLOR_START, ColorStart, colorStart, xColor, particle::DEFAULT_COLOR); diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index fca43c4665..1b4df84f40 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -983,11 +983,11 @@ function loaded() { properties.color.green + "," + properties.color.blue + ")"; } - //if (properties.type === "Model" || - // properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { + if (properties.type === "Model" || + properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { - elCanCastShadow = properties.canCastShadow; - //} + elCanCastShadow.checked = properties.canCastShadow; + } if (properties.type === "Model") { elModelURL.value = properties.modelURL; From df7a8389b3102c9c4dc4b0bb74168f97a16343ab Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Thu, 15 Feb 2018 11:51:06 -0800 Subject: [PATCH 064/260] Fixed possible crash. --- libraries/render-utils/src/RenderShadowTask.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index 24a14a697c..7806c95330 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -121,8 +121,7 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, con assert(lightStage); // Exit if current keylight does not cast shadows - bool castShadows = lightStage->getCurrentKeyLight()->getCastShadows(); - if (!castShadows) { + if (!lightStage->getCurrentKeyLight() || !lightStage->getCurrentKeyLight()->getCastShadows()) { return; } From cee0bbf8a5914be7e254af8911bc22caedf82583 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 15 Feb 2018 11:32:29 -0800 Subject: [PATCH 065/260] Revert "FBX node IDs aren't alphanumerically ordered per logical structure" This reverts commit a7ec4501e65396d4c7bf2316dd73188cea7a3c7d. Because remainingModels is a QSet, the order is not guaranteed. Therefore the same code iterating over the same items will sometimes have a different ordering. See docs for QSet, http://doc.qt.io/qt-5/qset.html This was bug was causing scrambled avatars, because both the transmitter and receiver of the AvatarData packets make the strong assumption that the joint orders are same. When they are not the avatar's appear scrambled. (cherry picked from commit f07b1fa4c52af97f9adab2ba6e9678a75fe6aa2b) --- libraries/fbx/src/FBXReader.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 4ed1ca38dc..14f12b5d1b 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1481,6 +1481,11 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } while (!remainingModels.isEmpty()) { QString first = *remainingModels.constBegin(); + foreach (const QString& id, remainingModels) { + if (id < first) { + first = id; + } + } QString topID = getTopModelID(_connectionParentMap, models, first, url); appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, models, remainingModels, modelIDs, true); } From 3297e39c144581788015b5ea4cd5bc52505fda0a Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 13:39:18 -0800 Subject: [PATCH 066/260] CR --- domain-server/src/AssetsBackupHandler.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index a9f56a0c5b..ae9cb58343 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -58,7 +58,6 @@ void AssetsBackupHandler::setupRefreshTimer() { }); QObject::connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, [this](SharedNodePointer node) { if (node->getType() == NodeType::AssetServer) { - // run immediately for the first time. _mappingsRefreshTimer.stop(); } }); @@ -240,7 +239,7 @@ void AssetsBackupHandler::recoverBackup(QuaZip& zip) { auto assetNames = zipDir.entryList(QDir::Files); for (const auto& asset : assetNames) { if (AssetUtils::isValidHash(asset)) { - if (!zip.setCurrentFile(MAPPINGS_FILE)) { + if (!zip.setCurrentFile(zipDir.filePath(asset))) { qCCritical(asset_backup) << "Failed to find" << asset << "while recovering backup"; qCCritical(asset_backup) << " Error:" << zip.getZipError(); continue; From 72e89851f3c2936bbf3da1f065fb3bc4fb4b919e Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Thu, 15 Feb 2018 14:16:56 -0800 Subject: [PATCH 067/260] Added READ_ENTITY_PROPERTY_TO_PROPERTIES --- libraries/entities/src/EntityItemProperties.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 86404c6504..78c6c6a6b3 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -1869,6 +1869,14 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int properties.getType() == EntityTypes::Box || properties.getType() == EntityTypes::Sphere) { READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE, QString, setShape); + } + + // Can cast shadow flag + if (properties.getType() == EntityTypes::Model || + properties.getType() == EntityTypes::Shape || + properties.getType() == EntityTypes::Box || + properties.getType() == EntityTypes::Sphere) { + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow); } From f624e1b464bd17755058f516e4ae114cc759041f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 15:10:51 -0800 Subject: [PATCH 068/260] add a content settings backup handler --- .../src/ContentSettingsBackupHandler.cpp | 66 +++++++++++++++++++ .../src/ContentSettingsBackupHandler.h | 35 ++++++++++ domain-server/src/DomainServer.cpp | 2 + .../src/DomainServerSettingsManager.h | 10 +-- domain-server/src/EntitiesBackupHandler.cpp | 14 ++-- 5 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 domain-server/src/ContentSettingsBackupHandler.cpp create mode 100644 domain-server/src/ContentSettingsBackupHandler.h diff --git a/domain-server/src/ContentSettingsBackupHandler.cpp b/domain-server/src/ContentSettingsBackupHandler.cpp new file mode 100644 index 0000000000..be470bdd9f --- /dev/null +++ b/domain-server/src/ContentSettingsBackupHandler.cpp @@ -0,0 +1,66 @@ +// +// 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(QuaZip& zip) { + + // grab the content settings as JSON,excluding default values and values hidden from backup + QJsonObject contentSettingsJSON = _settingsManager.settingsResponseObjectForType("", true, false, true, false, true); + + // make a QJSonDocument using the object + QJsonDocument contentSettingsDocument { contentSettingsJSON }; + + QuaZipFile zipFile { &zip }; + + zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(CONTENT_SETTINGS_BACKUP_FILENAME)); + zipFile.write(contentSettingsDocument.toJson()); + zipFile.close(); + + if (zipFile.getZipError() != UNZ_OK) { + qCritical().nospace() << "Failed to zip " << CONTENT_SETTINGS_BACKUP_FILENAME << ": " << zipFile.getZipError(); + } +} + +void ContentSettingsBackupHandler::recoverBackup(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"; + return; + } + +} diff --git a/domain-server/src/ContentSettingsBackupHandler.h b/domain-server/src/ContentSettingsBackupHandler.h new file mode 100644 index 0000000000..932b7c0c3f --- /dev/null +++ b/domain-server/src/ContentSettingsBackupHandler.h @@ -0,0 +1,35 @@ +// +// 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); + + void loadBackup(QuaZip& zip) {}; + + void createBackup(QuaZip& zip); + + void recoverBackup(QuaZip& zip); + + void deleteBackup(QuaZip& zip) {}; + + void consolidateBackup(QuaZip& zip) {}; +private: + DomainServerSettingsManager& _settingsManager; +}; + +#endif // hifi_ContentSettingsBackupHandler_h diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 4c72423f74..d3bc5fdff1 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -46,6 +46,7 @@ #include #include "AssetsBackupHandler.h" +#include "ContentSettingsBackupHandler.h" #include "DomainServerNodeData.h" #include "EntitiesBackupHandler.h" #include "NodeConnectionData.h" @@ -299,6 +300,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); _contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir()))); + _contentManager->addBackupHandler(BackupHandlerPointer(new ContentSettingsBackupHandler(_settingsManager))); _contentManager->initialize(true); qDebug() << "Existing backups:"; diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 9b2427b344..4316534685 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -111,6 +111,11 @@ public: void debugDumpGroupsState(); + QJsonObject settingsResponseObjectForType(const QString& typeValue, bool isAuthenticated = false, + bool includeDomainSettings = true, bool includeContentSettings = true, + bool includeDefaults = true, bool isForBackup = false); + bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType); + signals: void updateNodePermissions(); void settingsUpdated(); @@ -130,9 +135,6 @@ 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, @@ -143,8 +145,6 @@ private: void splitSettingsDescription(); - bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType); - double _descriptionVersion; QJsonArray _descriptionArray; diff --git a/domain-server/src/EntitiesBackupHandler.cpp b/domain-server/src/EntitiesBackupHandler.cpp index a95d68b007..6ad00d01c8 100644 --- a/domain-server/src/EntitiesBackupHandler.cpp +++ b/domain-server/src/EntitiesBackupHandler.cpp @@ -24,28 +24,30 @@ EntitiesBackupHandler::EntitiesBackupHandler(QString entitiesFilePath, QString e { } +static const QString ENTITIES_BACKUP_FILENAME = "models.json.gz"; + void EntitiesBackupHandler::createBackup(QuaZip& zip) { QFile entitiesFile { _entitiesFilePath }; if (entitiesFile.open(QIODevice::ReadOnly)) { QuaZipFile zipFile { &zip }; - zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("models.json.gz", _entitiesFilePath)); + zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ENTITIES_BACKUP_FILENAME, _entitiesFilePath)); zipFile.write(entitiesFile.readAll()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qCritical() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + qCritical().nospace() << "Failed to zip " << ENTITIES_BACKUP_FILENAME << ": " << zipFile.getZipError(); } } } void EntitiesBackupHandler::recoverBackup(QuaZip& zip) { - if (!zip.setCurrentFile("models.json.gz")) { - qWarning() << "Failed to find models.json.gz while recovering backup"; + 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 models.json.gz in backup"; + qCritical() << "Failed to open" << ENTITIES_BACKUP_FILENAME << "in backup"; return; } auto rawData = zipFile.readAll(); @@ -61,7 +63,7 @@ void EntitiesBackupHandler::recoverBackup(QuaZip& zip) { data.resetIdAndVersion(); if (zipFile.getZipError() != UNZ_OK) { - qCritical() << "Failed to unzip models.json.gz: " << zipFile.getZipError(); + qCritical().nospace() << "Failed to unzip " << ENTITIES_BACKUP_FILENAME << ": " << zipFile.getZipError(); return; } From f5cad5683dbb4028c76beafbdafddb249655d0b7 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 15:39:25 -0800 Subject: [PATCH 069/260] make sure backup handlers end up on the correct thread --- domain-server/src/DomainContentBackupManager.cpp | 3 --- domain-server/src/DomainServer.cpp | 12 ++++++++---- libraries/shared/src/GenericThread.cpp | 2 ++ libraries/shared/src/GenericThread.h | 1 + 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index f39737c92e..c68ff0c6ea 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -155,9 +155,6 @@ bool DomainContentBackupManager::process() { } void DomainContentBackupManager::aboutToFinish() { - qCDebug(domain_server) << "Persist thread about to finish..."; - backup(); - qCDebug(domain_server) << "Persist thread done with about to finish..."; _stopThread = true; } diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index d3bc5fdff1..599f09ae94 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -298,9 +298,13 @@ DomainServer::DomainServer(int argc, char* argv[]) : maybeHandleReplacementEntityFile(); _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); - _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); - _contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir()))); - _contentManager->addBackupHandler(BackupHandlerPointer(new ContentSettingsBackupHandler(_settingsManager))); + + 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); qDebug() << "Existing backups:"; @@ -382,7 +386,7 @@ DomainServer::~DomainServer() { if (_contentManager) { _contentManager->aboutToFinish(); - _contentManager->terminating(); + _contentManager->terminate(); } } diff --git a/libraries/shared/src/GenericThread.cpp b/libraries/shared/src/GenericThread.cpp index 2e126f12c9..50655820af 100644 --- a/libraries/shared/src/GenericThread.cpp +++ b/libraries/shared/src/GenericThread.cpp @@ -38,6 +38,8 @@ void GenericThread::initialize(bool isThreaded, QThread::Priority priority) { _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: From 52f576e6f6449d6f0b16d8b9e1e4753cbc38765a Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 15 Feb 2018 15:02:45 -0800 Subject: [PATCH 070/260] don't update avatar entities if the avatars ID is AVATAR_SELF_ID --- libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 86635cd3bf..1b1511e2c6 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -219,7 +219,7 @@ void Avatar::updateAvatarEntities() { return; } - if (getID() == QUuid()) { + if (getID() == QUuid() || getID() == AVATAR_SELF_ID) { return; // wait until MyAvatar gets an ID before doing this. } From e06c95f5863d3aeb67f1c18d624acc25743e605f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 15:51:49 -0800 Subject: [PATCH 071/260] make settings manager methods used for backup/restore thread safe --- .../src/DomainServerSettingsManager.cpp | 24 +++++++++++++++++++ .../src/DomainServerSettingsManager.h | 6 +++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 8febbd5769..5e3c8e9cee 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -1196,6 +1197,16 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection } bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType) { + + if (thread() != QThread::currentThread()) { + bool success; + QMetaObject::invokeMethod(this, "restoreSettingsFromObject", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, success), + Q_ARG(QJsonObject, settingsToRestore), + Q_ARG(SettingsType, settingsType)); + return success; + } + QJsonArray& filteredDescriptionArray = settingsType == DomainSettings ? _domainSettingsDescription : _contentSettingsDescription; @@ -1321,6 +1332,19 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt bool includeDefaults, bool isForBackup) { QJsonObject responseObject; + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "settingsResponseObjectForType", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QJsonObject, responseObject), + Q_ARG(const QString&, typeValue), + Q_ARG(bool, isAuthenticated), + Q_ARG(bool, includeDomainSettings), + Q_ARG(bool, includeContentSettings), + Q_ARG(bool, includeDefaults), + Q_ARG(bool, isForBackup)); + + return responseObject; + } + if (!typeValue.isEmpty() || isAuthenticated) { // convert the string type value to a QJsonValue QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt()); diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 4316534685..abc70751a8 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -111,10 +111,12 @@ public: void debugDumpGroupsState(); - QJsonObject settingsResponseObjectForType(const QString& typeValue, bool isAuthenticated = false, + /// thread safe method to retrieve a JSON representation of settings + Q_INVOKABLE QJsonObject settingsResponseObjectForType(const QString& typeValue, bool isAuthenticated = false, bool includeDomainSettings = true, bool includeContentSettings = true, bool includeDefaults = true, bool isForBackup = false); - bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType); + /// thread safe method to restore settings from a JSON object + Q_INVOKABLE bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType); signals: void updateNodePermissions(); From e71f2fa38788c8f74f5ede92749eab7f296a5743 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 16:03:03 -0800 Subject: [PATCH 072/260] use QtHelpers macro for blocking invoke --- .../src/DomainServerSettingsManager.cpp | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 5e3c8e9cee..5f71890898 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -33,6 +33,8 @@ #include #include //for KillAvatarReason #include +#include + #include "DomainServerNodeData.h" const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; @@ -1200,10 +1202,11 @@ bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settings if (thread() != QThread::currentThread()) { bool success; - QMetaObject::invokeMethod(this, "restoreSettingsFromObject", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(bool, success), - Q_ARG(QJsonObject, settingsToRestore), - Q_ARG(SettingsType, settingsType)); + + BLOCKING_INVOKE_METHOD(this, "restoreSettingsFromObject", + Q_RETURN_ARG(bool, success), + Q_ARG(QJsonObject, settingsToRestore), + Q_ARG(SettingsType, settingsType)); return success; } @@ -1333,14 +1336,15 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt QJsonObject responseObject; if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "settingsResponseObjectForType", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QJsonObject, responseObject), - Q_ARG(const QString&, typeValue), - Q_ARG(bool, isAuthenticated), - Q_ARG(bool, includeDomainSettings), - Q_ARG(bool, includeContentSettings), - Q_ARG(bool, includeDefaults), - Q_ARG(bool, isForBackup)); + + BLOCKING_INVOKE_METHOD(this, "settingsResponseObjectForType", + Q_RETURN_ARG(QJsonObject, responseObject), + Q_ARG(const QString&, typeValue), + Q_ARG(bool, isAuthenticated), + Q_ARG(bool, includeDomainSettings), + Q_ARG(bool, includeContentSettings), + Q_ARG(bool, includeDefaults), + Q_ARG(bool, isForBackup)); return responseObject; } From 9e99c5c744ea7621b37e4c0b7e36d84906ea82f4 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 11:49:05 -0800 Subject: [PATCH 073/260] Add restart of ES during backup recovery --- domain-server/src/DomainServer.cpp | 2 ++ domain-server/src/EntitiesBackupHandler.cpp | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 599f09ae94..2f8d8f6d03 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1785,6 +1785,8 @@ QString DomainServer::getEntitiesReplacementFilePath() { void DomainServer::processOctreeDataRequestMessage(QSharedPointer message) { qDebug() << "Got request for octree data from " << message->getSenderSockAddr(); + maybeHandleReplacementEntityFile(); + bool remoteHasExistingData { false }; QUuid id; int version; diff --git a/domain-server/src/EntitiesBackupHandler.cpp b/domain-server/src/EntitiesBackupHandler.cpp index 6ad00d01c8..deb92ee0f6 100644 --- a/domain-server/src/EntitiesBackupHandler.cpp +++ b/domain-server/src/EntitiesBackupHandler.cpp @@ -16,6 +16,7 @@ #include #include +#include #include EntitiesBackupHandler::EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : @@ -71,5 +72,13 @@ void EntitiesBackupHandler::recoverBackup(QuaZip& zip) { if (entitiesFile.open(QIODevice::WriteOnly)) { entitiesFile.write(data.toGzippedByteArray()); + entitiesFile.close(); + + auto nodeList = DependencyManager::get(); + nodeList->eachMatchingNode([](const SharedNodePointer& otherNode) -> bool { + return otherNode->getType() == NodeType::EntityServer; + }, [nodeList](const SharedNodePointer& otherNode) { + QMetaObject::invokeMethod(nodeList.data(), "killNodeWithUUID", Q_ARG(const QUuid&, otherNode->getUUID())); + }); } } From dd0b8a0c2fd3a22a82857c06e6027c56735222be Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 14:17:32 -0800 Subject: [PATCH 074/260] Add backup download API to DS --- .../src/DomainContentBackupManager.cpp | 78 +++++++++++++------ .../src/DomainContentBackupManager.h | 2 +- domain-server/src/DomainServer.cpp | 22 ++++++ 3 files changed, 76 insertions(+), 26 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index c68ff0c6ea..f56c41dacd 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -383,32 +383,60 @@ void DomainContentBackupManager::backup() { } } -void DomainContentBackupManager::consolidate(QString fileName) { - QDir backupDir { _backupDirectory }; - if (backupDir.exists()) { - auto filePath = backupDir.absoluteFilePath(fileName); - - auto copyFilePath = QDir::tempPath() + "/" + fileName; - - auto copySuccess = QFile::copy(filePath, copyFilePath); - if (!copySuccess) { - qCritical() << "Failed to create full backup."; - return; - } - - QuaZip zip(copyFilePath); - if (!zip.open(QuaZip::mdAdd)) { - qCritical() << "Could not open backup archive:" << filePath; - qCritical() << " ERROR:" << zip.getZipError(); - return; - } - - for (auto& handler : _backupHandlers) { - handler->consolidateBackup(zip); - } - - zip.close(); +void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise, QString fileName) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "consolidateBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(const 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(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) { diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 1e1b2360a8..9ec7eb9950 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -62,6 +62,7 @@ public slots: void createManualBackup(MiniPromise::Promise promise, const QString& name); void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); void deleteBackup(MiniPromise::Promise promise, const QString& backupName); + void consolidateBackup(MiniPromise::Promise promise, QString fileName); signals: void loadCompleted(); @@ -73,7 +74,6 @@ protected: void load(); void backup(); - void consolidate(QString fileName); void removeOldBackupVersions(const BackupRule& rule); bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); int64_t getMostRecentBackupTimeInSecs(const QString& format); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 2f8d8f6d03..b91e12a9cf 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2149,6 +2149,28 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url QJsonDocument docJSON(rootJSON); connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + 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); From 2942a53a1d51b488d7e8818e180c25abb335f0a2 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 15:55:14 -0800 Subject: [PATCH 075/260] Add recovery mode and full backup information to DS --- .../src/DomainContentBackupManager.cpp | 91 +++++++++++++++++-- .../src/DomainContentBackupManager.h | 14 +-- domain-server/src/DomainServer.cpp | 28 ++---- 3 files changed, 96 insertions(+), 37 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index f56c41dacd..54ea7e23d5 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -147,7 +147,24 @@ bool DomainContentBackupManager::process() { if (sinceLastSave > intervalToCheck) { _lastCheck = now; - backup(); + if (_isRecovering) { + bool anyHandlerIsRecovering { false }; + for (auto& handler : _backupHandlers) { + bool handlerIsRecovering { false }; + float progress { 0.0f }; + //std::tie = handler->getRecoveryStatus(); + if (handlerIsRecovering) { + anyHandlerIsRecovering = true; + emit recoveryCompleted(); + break; + } + } + _isRecovering = anyHandlerIsRecovering; + } + + if (!_isRecovering) { + backup(); + } } } @@ -213,6 +230,13 @@ void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, cons return; } + if (_isRecovering && backupName == _recoveryFilename) { + promise->resolve({ + { "success", false } + }); + return; + } + QDir backupDir { _backupDirectory }; QFile backupFile { backupDir.filePath(backupName) }; auto success = backupFile.remove(); @@ -222,6 +246,13 @@ void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, cons } 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)); @@ -239,11 +270,12 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, qWarning() << "Failed to unzip file: " << backupName; success = false; } else { + _isRecovering = true; for (auto& handler : _backupHandlers) { handler->recoverBackup(zip); } - qDebug() << "Successfully recovered from " << backupName; + qDebug() << "Successfully started recovering from " << backupName; success = true; } backupFile.close(); @@ -257,8 +289,11 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, }); } -std::vector DomainContentBackupManager::getAllBackups() { - std::vector backups; +void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise promise) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "getAllBackupInformation", Q_ARG(MiniPromise::Promise, promise)); + return; + } QDir backupDir { _backupDirectory }; auto matchingFiles = @@ -269,6 +304,8 @@ std::vector DomainContentBackupManager::getAllBackups() { QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")"; QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" }; + QVariantList backups; + for (const auto& fileInfo : matchingFiles) { auto fileName = fileInfo.fileName(); if (backupNameFormat.exactMatch(fileName)) { @@ -280,12 +317,50 @@ std::vector DomainContentBackupManager::getAllBackups() { continue; } - BackupItemInfo backup { fileInfo.fileName(), name, fileInfo.absoluteFilePath(), createdAt, type == MANUAL_BACKUP_PREFIX }; - backups.push_back(backup); + bool isAvailable { true }; + float availabilityProgress { 0.0f }; + for (auto& handler : _backupHandlers) { + bool handlerIsAvailable { false }; + float progress { 0.0f }; + //std::tie = handler->isAvailable(); + //isAvailable = isAvailable && !handlerIsAvailable); + //availabilityProgress += progress / _backupHandlers.size(); + } + + backups.push_back(QVariantMap({ + { "id", fileInfo.fileName() }, + { "name", name }, + { "createdAtMillis", createdAt.toMSecsSinceEpoch() }, + { "isAvailable", isAvailable }, + { "availabilityProgress", availabilityProgress }, + { "isManualBackup", type == MANUAL_BACKUP_PREFIX } + })); } } - return backups; + float recoveryProgress = 0.0f; + bool isRecovering = _isRecovering.load(); + if (_isRecovering) { + for (auto& handler : _backupHandlers) { + bool handlerIsRecovering { false }; + float progress { 0.0f }; + //std::tie = handler->getRecoveryStatus(); + recoveryProgress += progress / _backupHandlers.size(); + } + } + + QVariantMap status { + { "isRecovering", isRecovering }, + { "recoveringBackupId", _recoveryFilename }, + { "recoveryProgress", recoveryProgress } + }; + + QVariantMap info { + { "backups", backups }, + { "status", status } + }; + + promise->resolve(info); } void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) { @@ -433,6 +508,8 @@ void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise, return; } + qDebug() << "copyFilePath" << copyFilePath; + promise->resolve({ { "success", true }, { "backupFilePath", copyFilePath } diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 9ec7eb9950..790dff0fb4 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -24,14 +24,6 @@ #include -struct BackupItemInfo { - QString id; - QString name; - QString absolutePath; - QDateTime createdAt; - bool isManualBackup; -}; - class DomainContentBackupManager : public GenericThread { Q_OBJECT public: @@ -52,13 +44,13 @@ public: bool debugTimestampNow = false); void addBackupHandler(BackupHandlerPointer handler); - std::vector getAllBackups(); 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 getAllBackupInformation(MiniPromise::Promise promise); void createManualBackup(MiniPromise::Promise promise, const QString& name); void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); void deleteBackup(MiniPromise::Promise promise, const QString& backupName); @@ -66,6 +58,7 @@ public slots: signals: void loadCompleted(); + void recoveryCompleted(); protected: /// Implements generic processing behavior for this thread. @@ -86,6 +79,9 @@ private: std::vector _backupHandlers; int _persistInterval { 0 }; + std::atomic _isRecovering { false }; + QString _recoveryFilename { }; + int64_t _lastCheck { 0 }; std::vector _backupRules; }; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index b91e12a9cf..fe145b341b 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -307,10 +307,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : _contentManager->initialize(true); - qDebug() << "Existing backups:"; - for (auto& backup : _contentManager->getAllBackups()) { - qDebug() << " Backup: " << backup.name << backup.createdAt; - } + connect(_contentManager.get(), &DomainContentBackupManager::recoveryCompleted, this, &DomainServer::restart); } void DomainServer::parseCommandLine() { @@ -2131,24 +2128,13 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; } else if (url.path() == URI_API_BACKUPS) { - QJsonObject rootJSON; - QJsonArray backupsJSON; + auto deferred = makePromise("getAllBackupInformation"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonDocument docJSON(QJsonObject::fromVariantMap(result)); - auto backups = _contentManager->getAllBackups(); - - for (const auto& backup : backups) { - QJsonObject obj; - obj["id"] = backup.id; - obj["name"] = backup.name; - obj["createdAtMillis"] = backup.createdAt.toMSecsSinceEpoch(); - obj["isManualBackup"] = backup.isManualBackup; - backupsJSON.push_back(obj); - } - - rootJSON["backups"] = backupsJSON; - QJsonDocument docJSON(rootJSON); - - connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + }); + _contentManager->getAllBackupInformation(deferred); return true; } else if (url.path().startsWith(URI_API_BACKUPS_ID)) { auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length()); From 1120b12b8cb758578e16912961744a81a12b1880 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 15:58:39 -0800 Subject: [PATCH 076/260] Fix argument to isAvailable --- domain-server/src/DomainContentBackupManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 54ea7e23d5..eccc3e904d 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -322,7 +322,7 @@ void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise pr for (auto& handler : _backupHandlers) { bool handlerIsAvailable { false }; float progress { 0.0f }; - //std::tie = handler->isAvailable(); + //std::tie = handler->isAvailable(fileInfo.absoluteFilePath()); //isAvailable = isAvailable && !handlerIsAvailable); //availabilityProgress += progress / _backupHandlers.size(); } From a7ca5398990a9328856f1a1aca75e4397a78db3a Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 14 Feb 2018 16:48:37 -0800 Subject: [PATCH 077/260] Simplify BackupHandler pattern --- domain-server/src/BackupHandler.h | 1 - 1 file changed, 1 deletion(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 8599dafb29..960dde9b45 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -26,7 +26,6 @@ public: virtual void deleteBackup(QuaZip& zip) = 0; virtual void consolidateBackup(QuaZip& zip) = 0; }; - using BackupHandlerPointer = std::unique_ptr; #endif /* hifi_BackupHandler_h */ From b76e1b9750973416aa2a316e601e2c36057b829e Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 15:46:35 -0800 Subject: [PATCH 078/260] Add backup status getters --- domain-server/src/AssetsBackupHandler.cpp | 51 +++++++++++++++++++---- domain-server/src/AssetsBackupHandler.h | 21 +++++----- domain-server/src/BackupHandler.h | 5 +++ domain-server/src/EntitiesBackupHandler.h | 13 +++--- 4 files changed, 66 insertions(+), 24 deletions(-) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index ae9cb58343..e683c626ea 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -111,6 +111,42 @@ void AssetsBackupHandler::checkForAssetsToDelete() { } } + +std::pair AssetsBackupHandler::isAvailable(QString filePath) { + auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { + return value.filePath == filePath; + }); + if (it == end(_backups)) { + return { true, 1.0f }; + } + + float progress = (float)it->mappings.size(); + for (const auto& mapping : it->mappings) { + if (_assetsLeftToRequest.find(mapping.second) != end(_assetsLeftToRequest)) { + progress -= 1.0f; + } + } + progress /= (float)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(QuaZip& zip) { Q_ASSERT(QThread::currentThread() == thread()); @@ -451,6 +487,11 @@ void AssetsBackupHandler::computeServerStateDifference(const AssetUtils::Mapping } } + _numRestoreOperations = _assetsLeftToUpload.size() + _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(); @@ -461,8 +502,6 @@ void AssetsBackupHandler::restoreAllAssets() { } void AssetsBackupHandler::restoreNextAsset() { - startOperation(); - if (_assetsLeftToUpload.empty()) { updateMappings(); return; @@ -500,9 +539,7 @@ void AssetsBackupHandler::updateMappings() { qCCritical(asset_backup) << " Error:" << request->getErrorString(); } - if (--_mappingRequestsInFlight == 0) { - stopOperation(); - } + --_mappingRequestsInFlight; request->deleteLater(); }); @@ -519,9 +556,7 @@ void AssetsBackupHandler::updateMappings() { qCCritical(asset_backup) << " Error:" << request->getErrorString(); } - if (--_mappingRequestsInFlight == 0) { - stopOperation(); - } + --_mappingRequestsInFlight; request->deleteLater(); }); diff --git a/domain-server/src/AssetsBackupHandler.h b/domain-server/src/AssetsBackupHandler.h index 2ef454998e..1421ddd400 100644 --- a/domain-server/src/AssetsBackupHandler.h +++ b/domain-server/src/AssetsBackupHandler.h @@ -31,13 +31,16 @@ class AssetsBackupHandler : public QObject, public BackupHandlerInterface { public: AssetsBackupHandler(const QString& backupDirectory); - void loadBackup(QuaZip& zip); - void createBackup(QuaZip& zip); - void recoverBackup(QuaZip& zip); - void deleteBackup(QuaZip& zip); - void consolidateBackup(QuaZip& zip); + std::pair isAvailable(QString filePath) override; + std::pair getRecoveryStatus() override; - bool operationInProgress() const { return _operationInProgress; } + void loadBackup(QuaZip& zip) override; + void createBackup(QuaZip& zip) override; + void recoverBackup(QuaZip& zip) override; + void deleteBackup(QuaZip& zip) override; + void consolidateBackup(QuaZip& zip) override; + + bool operationInProgress() { return getRecoveryStatus().first; } private: void setupRefreshTimer(); @@ -48,9 +51,6 @@ private: void checkForMissingAssets(); void checkForAssetsToDelete(); - void startOperation() { _operationInProgress = true; } - void stopOperation() { _operationInProgress = false; } - void downloadMissingFiles(const AssetUtils::Mappings& mappings); void downloadNextMissingFile(); bool writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data); @@ -73,8 +73,6 @@ private: bool corruptedBackup; }; - bool _operationInProgress { false }; - // Internal storage for backups on disk bool _allBackupsLoadedSuccessfully { false }; std::vector _backups; @@ -89,6 +87,7 @@ private: 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 index 960dde9b45..d513820000 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -20,6 +20,11 @@ class BackupHandlerInterface { public: virtual ~BackupHandlerInterface() = default; + virtual std::pair isAvailable(QString filePath) = 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(QuaZip& zip) = 0; virtual void createBackup(QuaZip& zip) = 0; virtual void recoverBackup(QuaZip& zip) = 0; diff --git a/domain-server/src/EntitiesBackupHandler.h b/domain-server/src/EntitiesBackupHandler.h index 6f66483a87..4cff7b6a33 100644 --- a/domain-server/src/EntitiesBackupHandler.h +++ b/domain-server/src/EntitiesBackupHandler.h @@ -20,19 +20,22 @@ class EntitiesBackupHandler : public BackupHandlerInterface { public: EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath); - void loadBackup(QuaZip& zip) {} + std::pair isAvailable(QString filePath) override { return { true, 1.0f }; } + std::pair getRecoveryStatus() override { return { false, 1.0f }; } + + void loadBackup(QuaZip& zip) override {} // Create a skeleton backup - void createBackup(QuaZip& zip); + void createBackup(QuaZip& zip) override; // Recover from a full backup - void recoverBackup(QuaZip& zip); + void recoverBackup(QuaZip& zip) override; // Delete a skeleton backup - void deleteBackup(QuaZip& zip) {} + void deleteBackup(QuaZip& zip) override {} // Create a full backup - void consolidateBackup(QuaZip& zip) {} + void consolidateBackup(QuaZip& zip) override {} private: QString _entitiesFilePath; From 57410e4f1cc017c5aa23220fa96f6a445899d62a Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 16:22:06 -0800 Subject: [PATCH 079/260] Remove ES restart after restore --- domain-server/src/EntitiesBackupHandler.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/domain-server/src/EntitiesBackupHandler.cpp b/domain-server/src/EntitiesBackupHandler.cpp index deb92ee0f6..6ad00d01c8 100644 --- a/domain-server/src/EntitiesBackupHandler.cpp +++ b/domain-server/src/EntitiesBackupHandler.cpp @@ -16,7 +16,6 @@ #include #include -#include #include EntitiesBackupHandler::EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : @@ -72,13 +71,5 @@ void EntitiesBackupHandler::recoverBackup(QuaZip& zip) { if (entitiesFile.open(QIODevice::WriteOnly)) { entitiesFile.write(data.toGzippedByteArray()); - entitiesFile.close(); - - auto nodeList = DependencyManager::get(); - nodeList->eachMatchingNode([](const SharedNodePointer& otherNode) -> bool { - return otherNode->getType() == NodeType::EntityServer; - }, [nodeList](const SharedNodePointer& otherNode) { - QMetaObject::invokeMethod(nodeList.data(), "killNodeWithUUID", Q_ARG(const QUuid&, otherNode->getUUID())); - }); } } From 771e4cd9f4d5e8324813f41011717ea423327525 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 16:45:29 -0800 Subject: [PATCH 080/260] Hook up status and progress --- .../src/DomainContentBackupManager.cpp | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index eccc3e904d..37a8ecfce2 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -148,18 +148,15 @@ bool DomainContentBackupManager::process() { if (sinceLastSave > intervalToCheck) { _lastCheck = now; if (_isRecovering) { - bool anyHandlerIsRecovering { false }; - for (auto& handler : _backupHandlers) { - bool handlerIsRecovering { false }; - float progress { 0.0f }; - //std::tie = handler->getRecoveryStatus(); - if (handlerIsRecovering) { - anyHandlerIsRecovering = true; - emit recoveryCompleted(); - break; - } + using Value = std::vector::value_type; + bool isStillRecovering = std::any_of(begin(_backupHandlers), end(_backupHandlers), [](const Value& handler) { + return handler->getRecoveryStatus().first; + }); + + if (!isStillRecovering) { + _isRecovering = false; + emit recoveryCompleted(); } - _isRecovering = anyHandlerIsRecovering; } if (!_isRecovering) { @@ -320,11 +317,11 @@ void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise pr bool isAvailable { true }; float availabilityProgress { 0.0f }; for (auto& handler : _backupHandlers) { - bool handlerIsAvailable { false }; + bool handlerIsAvailable { true }; float progress { 0.0f }; - //std::tie = handler->isAvailable(fileInfo.absoluteFilePath()); - //isAvailable = isAvailable && !handlerIsAvailable); - //availabilityProgress += progress / _backupHandlers.size(); + std::tie(handlerIsAvailable, progress) = handler->isAvailable(fileInfo.absoluteFilePath()); + isAvailable &= handlerIsAvailable; + availabilityProgress += progress / _backupHandlers.size(); } backups.push_back(QVariantMap({ @@ -342,9 +339,7 @@ void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise pr bool isRecovering = _isRecovering.load(); if (_isRecovering) { for (auto& handler : _backupHandlers) { - bool handlerIsRecovering { false }; - float progress { 0.0f }; - //std::tie = handler->getRecoveryStatus(); + float progress = handler->getRecoveryStatus().second; recoveryProgress += progress / _backupHandlers.size(); } } From cae3e0a9dcceff2e182a012a6381743ca85905ba Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 16:59:17 -0800 Subject: [PATCH 081/260] Add status func to ContentSettingsBackupHandler --- domain-server/src/ContentSettingsBackupHandler.h | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/domain-server/src/ContentSettingsBackupHandler.h b/domain-server/src/ContentSettingsBackupHandler.h index 932b7c0c3f..8a81392513 100644 --- a/domain-server/src/ContentSettingsBackupHandler.h +++ b/domain-server/src/ContentSettingsBackupHandler.h @@ -19,15 +19,18 @@ class ContentSettingsBackupHandler : public BackupHandlerInterface { public: ContentSettingsBackupHandler(DomainServerSettingsManager& domainServerSettingsManager); - void loadBackup(QuaZip& zip) {}; + std::pair isAvailable(QString filePath) override { return { true, 1.0f }; } + std::pair getRecoveryStatus() override { return { false, 1.0f }; } - void createBackup(QuaZip& zip); + void loadBackup(QuaZip& zip) override {} - void recoverBackup(QuaZip& zip); + void createBackup(QuaZip& zip) override; - void deleteBackup(QuaZip& zip) {}; + void recoverBackup(QuaZip& zip) override; - void consolidateBackup(QuaZip& zip) {}; + void deleteBackup(QuaZip& zip) override {} + + void consolidateBackup(QuaZip& zip) override {} private: DomainServerSettingsManager& _settingsManager; }; From 2b85634a21abec80f8f689013c2696afb19e2dc9 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 17:03:55 -0800 Subject: [PATCH 082/260] Fix build error --- domain-server/src/BackupHandler.h | 2 ++ domain-server/src/EntitiesBackupHandler.h | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index d513820000..1bd40cd9e4 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -14,6 +14,8 @@ #include +#include + class QuaZip; class BackupHandlerInterface { diff --git a/domain-server/src/EntitiesBackupHandler.h b/domain-server/src/EntitiesBackupHandler.h index 4cff7b6a33..1a6110f1cd 100644 --- a/domain-server/src/EntitiesBackupHandler.h +++ b/domain-server/src/EntitiesBackupHandler.h @@ -12,8 +12,6 @@ #ifndef hifi_EntitiesBackupHandler_h #define hifi_EntitiesBackupHandler_h -#include - #include "BackupHandler.h" class EntitiesBackupHandler : public BackupHandlerInterface { From 697f0c443cb0f3606ca2facc53dc35bd5ca98ae0 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 17:43:02 -0800 Subject: [PATCH 083/260] Fix warning --- domain-server/src/AssetsBackupHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index e683c626ea..db39f2a731 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -487,7 +487,7 @@ void AssetsBackupHandler::computeServerStateDifference(const AssetUtils::Mapping } } - _numRestoreOperations = _assetsLeftToUpload.size() + _mappingsLeftToSet.size(); + _numRestoreOperations = (int)_assetsLeftToUpload.size() + (int)_mappingsLeftToSet.size(); if (!_mappingsLeftToDelete.empty()) { ++_numRestoreOperations; } From f6e9d2c6dd05358d3053795f81705c43bbde02b3 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 18:16:30 -0800 Subject: [PATCH 084/260] Fix race condition in Asset Server --- assignment-client/src/assets/AssetServer.cpp | 28 ++++++++++++++++---- assignment-client/src/assets/AssetServer.h | 5 +++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 0be557bccd..4c6cba2e74 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -416,9 +416,9 @@ void AssetServer::completeSetup() { PathUtils::removeTemporaryApplicationDirs(); PathUtils::removeTemporaryApplicationDirs("Oven"); - // We're fully setup, remove the request queueing and replay all requests + 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.unregisterListener(this); packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet"); packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo"); packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload"); @@ -428,11 +428,30 @@ void AssetServer::completeSetup() { } 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() { - for (const auto& request : _queuedRequests) { + RequestQueue queue; + { + QMutexLocker lock { &_queuedRequestsMutex }; + qSwap(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); @@ -447,11 +466,10 @@ void AssetServer::replayRequests() { handleAssetMappingOperation(request.first, request.second); break; default: - qWarning() << "Unknown queued request type:" << request.first->getType(); + qCWarning(asset_server) << "Unknown queued request type:" << request.first->getType(); break; } } - _queuedRequests.clear(); } void AssetServer::cleanupUnmappedFiles() { diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h index b8aac800ed..c85fb89175 100644 --- a/assignment-client/src/assets/AssetServer.h +++ b/assignment-client/src/assets/AssetServer.h @@ -123,7 +123,10 @@ private: QHash> _pendingBakes; QThreadPool _bakingTaskPool; - QVector, SharedNodePointer>> _queuedRequests; + QMutex _queuedRequestsMutex; + bool _isQueueingRequests { true }; + using RequestQueue = QVector, SharedNodePointer>>; + RequestQueue _queuedRequests; bool _wasColorTextureCompressionEnabled { false }; bool _wasGrayscaleTextureCompressionEnabled { false }; From b30f98d5414759a1eae88505812cb481bb813844 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 18:20:14 -0800 Subject: [PATCH 085/260] CR --- domain-server/src/DomainContentBackupManager.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 37a8ecfce2..5eb4e7627f 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -503,8 +503,6 @@ void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise, return; } - qDebug() << "copyFilePath" << copyFilePath; - promise->resolve({ { "success", true }, { "backupFilePath", copyFilePath } From 4bb8435ef884e760ca7374d25574a115ce1fa488 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 19:14:14 -0800 Subject: [PATCH 086/260] don't overwrite general description object with filtered one --- .../src/DomainServerSettingsManager.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 5f71890898..85d6a046b5 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -1199,7 +1199,7 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection } bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType) { - + if (thread() != QThread::currentThread()) { bool success; @@ -1210,8 +1210,8 @@ bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settings return success; } - QJsonArray& filteredDescriptionArray = settingsType == DomainSettings - ? _domainSettingsDescription : _contentSettingsDescription; + QJsonArray* filteredDescriptionArray = settingsType == DomainSettings + ? &_domainSettingsDescription : &_contentSettingsDescription; // grab a copy of the current config before restore, so that we can back out if something bad happens during QVariantMap preRestoreConfig = _configMap.getConfig(); @@ -1220,7 +1220,7 @@ bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settings // enumerate through the settings in the description // if we have one in the restore then use it, otherwise clear it from current settings - foreach(const QJsonValue& descriptionGroupValue, filteredDescriptionArray) { + foreach(const QJsonValue& descriptionGroupValue, *filteredDescriptionArray) { QJsonObject descriptionGroupObject = descriptionGroupValue.toObject(); QString groupKey = descriptionGroupObject[DESCRIPTION_NAME_KEY].toString(); QJsonArray descriptionGroupSettings = descriptionGroupObject[DESCRIPTION_SETTINGS_KEY].toArray(); @@ -1356,15 +1356,15 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt const QString AFFECTED_TYPES_JSON_KEY = "assignment-types"; // only enumerate the requested settings type (domain setting or content setting) - QJsonArray& filteredDescriptionArray = _descriptionArray; + QJsonArray* filteredDescriptionArray = &_descriptionArray; if (includeDomainSettings && !includeContentSettings) { - filteredDescriptionArray = _domainSettingsDescription; + filteredDescriptionArray = &_domainSettingsDescription; } else if (includeContentSettings && !includeDomainSettings) { - filteredDescriptionArray = _contentSettingsDescription; + filteredDescriptionArray = &_contentSettingsDescription; } // enumerate the groups in the potentially filtered object to find which settings to pass - foreach(const QJsonValue& groupValue, filteredDescriptionArray) { + foreach(const QJsonValue& groupValue, *filteredDescriptionArray) { QJsonObject groupObject = groupValue.toObject(); QString groupKey = groupObject[DESCRIPTION_NAME_KEY].toString(); QJsonArray groupSettingsArray = groupObject[DESCRIPTION_SETTINGS_KEY].toArray(); From 29349d7bb2c198e859038c542af794161e5cba16 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 19:18:51 -0800 Subject: [PATCH 087/260] fix for isAvailable boolean in AssetsBackupHandler --- domain-server/src/AssetsBackupHandler.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index db39f2a731..c365b942af 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -120,13 +120,20 @@ std::pair AssetsBackupHandler::isAvailable(QString filePath) { return { true, 1.0f }; } - float progress = (float)it->mappings.size(); + int mappingsMissing = 0; for (const auto& mapping : it->mappings) { if (_assetsLeftToRequest.find(mapping.second) != end(_assetsLeftToRequest)) { - progress -= 1.0f; + ++mappingsMissing; } } - progress /= (float)it->mappings.size(); + + if (mappingsMissing == 0) { + return { true, 1.0f }; + } + + float progress = (float)it->mappings.size(); + progress -= (float)mappingsMissing; + progress /= it->mappings.size(); return { false, progress }; } From 194c7f41015575a91b4aa64f1775cc533689b5ec Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Fri, 16 Feb 2018 09:28:15 -0800 Subject: [PATCH 088/260] WIP - for review. --- interface/src/avatar/MyAvatar.cpp | 8 +-- interface/src/avatar/MyAvatar.h | 2 - .../src/RenderableEntityItem.h | 1 + .../src/RenderableModelEntityItem.cpp | 4 ++ .../render-utils/src/CauterizedModel.cpp | 5 +- .../render-utils/src/MeshPartPayload.cpp | 10 +++- libraries/render-utils/src/MeshPartPayload.h | 4 +- libraries/render-utils/src/Model.cpp | 55 ++++++++++++++----- libraries/render-utils/src/Model.h | 6 ++ 9 files changed, 67 insertions(+), 28 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c25aaeeecd..97b8c71a32 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1112,6 +1112,7 @@ void MyAvatar::setEnableDebugDrawIKChains(bool isEnabled) { void MyAvatar::setEnableMeshVisible(bool isEnabled) { _skeletonModel->setVisibleInScene(isEnabled, qApp->getMain3DScene(), render::ItemKey::TAG_BITS_NONE, true); + _skeletonModel->setCanCastShadow(true, qApp->getMain3DScene()); } void MyAvatar::setEnableInverseKinematics(bool isEnabled) { @@ -1464,6 +1465,7 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { int skeletonModelChangeCount = _skeletonModelChangeCount; Avatar::setSkeletonModelURL(skeletonModelURL); _skeletonModel->setVisibleInScene(true, qApp->getMain3DScene(), render::ItemKey::TAG_BITS_NONE, true); + _skeletonModel->setCanCastShadow(true, qApp->getMain3DScene()); _headBoneSet.clear(); _cauterizationNeedsUpdate = true; @@ -1819,12 +1821,6 @@ void MyAvatar::attach(const QString& modelURL, const QString& jointName, Avatar::attach(modelURL, jointName, translation, rotation, scale, isSoft, allowDuplicates, useSaved); } -void MyAvatar::setVisibleInSceneIfReady(Model* model, const render::ScenePointer& scene, bool visible) { - if (model->isActive() && model->isRenderable()) { - model->setVisibleInScene(visible, scene, render::ItemKey::TAG_BITS_NONE, true); - } -} - void MyAvatar::initHeadBones() { int neckJointIndex = -1; if (_skeletonModel->isLoaded()) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 28af8b62fd..a62bc1a109 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -677,8 +677,6 @@ private: // These are made private for MyAvatar so that you will use the "use" methods instead virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; - void setVisibleInSceneIfReady(Model* model, const render::ScenePointer& scene, bool visiblity); - virtual void updatePalms() override {} void lateUpdatePalms(); diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index f8685df5da..a9bb12d087 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -124,6 +124,7 @@ protected: bool _isFading{ _entitiesShouldFadeFunction() }; bool _prevIsTransparent { false }; bool _visible { false }; + bool _canCastShadow { false }; bool _cauterized { false }; bool _moving { false }; bool _needsRenderUpdate { false }; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 56e3f96014..d3e1b62f5a 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1356,6 +1356,10 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce } // TODO? early exit here when not visible? + if (model->canCastShadow() != _canCastShadow) { + model->setCanCastShadow(_canCastShadow, scene); + } + if (_needsCollisionGeometryUpdate) { setCollisionMeshKey(entity->getCollisionMeshKey()); _needsCollisionGeometryUpdate = false; diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 54dfd96a00..74b278c2c1 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -204,6 +204,7 @@ void CauterizedModel::updateRenderItems() { bool isWireframe = self->isWireframe(); bool isVisible = self->isVisible(); + bool canCastShadow = self->canCastShadow(); bool isLayeredInFront = self->isLayeredInFront(); bool isLayeredInHUD = self->isLayeredInHUD(); bool enableCauterization = self->getEnableCauterization(); @@ -219,7 +220,7 @@ void CauterizedModel::updateRenderItems() { bool invalidatePayloadShapeKey = self->shouldInvalidatePayloadShapeKey(meshIndex); transaction.updateItem(itemID, [modelTransform, clusterTransforms, clusterTransformsCauterized, invalidatePayloadShapeKey, - isWireframe, isVisible, isLayeredInFront, isLayeredInHUD, enableCauterization](CauterizedMeshPartPayload& data) { + isWireframe, isVisible, isLayeredInFront, isLayeredInHUD, canCastShadow, enableCauterization](CauterizedMeshPartPayload& data) { data.updateClusterBuffer(clusterTransforms, clusterTransformsCauterized); Transform renderTransform = modelTransform; @@ -249,7 +250,7 @@ void CauterizedModel::updateRenderItems() { data.updateTransformForCauterizedMesh(renderTransform); data.setEnableCauterization(enableCauterization); - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, render::ItemKey::TAG_BITS_ALL); + data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, render::ItemKey::TAG_BITS_ALL); data.setLayer(isLayeredInFront, isLayeredInHUD); data.setShapeKey(invalidatePayloadShapeKey, isWireframe); }); diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index da11535396..84f6c6aca3 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -71,7 +71,7 @@ void MeshPartPayload::updateMaterial(graphics::MaterialPointer drawMaterial) { _drawMaterial = drawMaterial; } -void MeshPartPayload::updateKey(bool isVisible, bool isLayered, uint8_t tagBits, bool isGroupCulled) { +void MeshPartPayload::updateKey(bool isVisible, bool isLayered, bool canCastShadow, uint8_t tagBits, bool isGroupCulled) { ItemKey::Builder builder; builder.withTypeShape(); @@ -85,6 +85,10 @@ void MeshPartPayload::updateKey(bool isVisible, bool isLayered, uint8_t tagBits, builder.withLayered(); } + if (canCastShadow) { + builder.withShadowCaster(); + } + if (isGroupCulled) { builder.withSubMetaCulled(); } @@ -421,6 +425,10 @@ void ModelMeshPartPayload::updateKey(bool isVisible, bool isLayered, uint8_t tag builder.withLayered(); } + if (canCastShadow) { + builder.withShadowCaster(); + } + if (isGroupCulled) { builder.withSubMetaCulled(); } diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 40efc67572..e7996e94dc 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -33,7 +33,7 @@ public: typedef render::Payload Payload; typedef Payload::DataPointer Pointer; - virtual void updateKey(bool isVisible, bool isLayered, uint8_t tagBits, bool isGroupCulled = false); + virtual void updateKey(bool isVisible, bool isLayered, bool canCastShadow, uint8_t tagBits, bool isGroupCulled = false); virtual void updateMeshPart(const std::shared_ptr& drawMesh, int partIndex); @@ -99,7 +99,7 @@ public: using TransformType = glm::mat4; #endif - void updateKey(bool isVisible, bool isLayered, uint8_t tagBits, bool isGroupCulled = false) override; + void updateKey(bool isVisible, bool isLayered, bool canCastShadow, uint8_t tagBits, bool isGroupCulled = false) override; void updateClusterBuffer(const std::vector& clusterTransforms); void updateTransformForSkinnedMesh(const Transform& renderTransform, const Transform& boundTransform); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index bb8353c746..197008fc94 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -102,6 +102,7 @@ Model::Model(QObject* parent, SpatiallyNestable* spatiallyNestableOverride) : _snappedToRegistrationPoint(false), _url(HTTP_INVALID_COM), _isVisible(true), + _canCastShadow(false), _blendNumber(0), _appliedBlendNumber(0), _isWireframe(false) @@ -268,6 +269,7 @@ void Model::updateRenderItems() { bool isWireframe = self->isWireframe(); bool isVisible = self->isVisible(); + bool canCastShadow = self->canCastShadow(); uint8_t viewTagBits = self->getViewTagBits(); bool isLayeredInFront = self->isLayeredInFront(); bool isLayeredInHUD = self->isLayeredInHUD(); @@ -284,7 +286,7 @@ void Model::updateRenderItems() { transaction.updateItem(itemID, [modelTransform, clusterTransforms, invalidatePayloadShapeKey, isWireframe, isVisible, - viewTagBits, isLayeredInFront, + canCastShadow, viewTagBits, isLayeredInFront, isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { data.updateClusterBuffer(clusterTransforms); @@ -301,7 +303,7 @@ void Model::updateRenderItems() { } data.updateTransformForSkinnedMesh(renderTransform, modelTransform); - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled); + data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); data.setLayer(isLayeredInFront, isLayeredInHUD); data.setShapeKey(invalidatePayloadShapeKey, isWireframe); }); @@ -693,46 +695,68 @@ void Model::setVisibleInScene(bool isVisible, const render::ScenePointer& scene, bool isLayeredInFront = _isLayeredInFront; bool isLayeredInHUD = _isLayeredInHUD; - + bool canCastShadow = _canCastShadow; render::Transaction transaction; foreach (auto item, _modelMeshRenderItemsMap.keys()) { - transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, + transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, canCastShadow, isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled); + data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); }); } foreach(auto item, _collisionRenderItemsMap.keys()) { - transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, + transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, canCastShadow, isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled); + data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); }); } scene->enqueueTransaction(transaction); } } +void Model::setCanCastShadow(bool canCastShadow, const render::ScenePointer& scene) { + if (_canCastShadow != canCastShadow) { + _canCastShadow = canCastShadow; + + bool isVisible = _isVisible; + bool viewTagBits = _viewTagBits; + bool isLayeredInFront = _isLayeredInFront; + bool isLayeredInHUD = _isLayeredInHUD; + bool isGroupCulled = _isGroupCulled; + + render::Transaction transaction; + foreach (auto item, _modelMeshRenderItemsMap.keys()) { + transaction.updateItem(item, + [isVisible, viewTagBits, canCastShadow, isLayeredInFront, isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { + data.updateKey(isVisible, viewTagBits, canCastShadow, isLayeredInFront || isLayeredInHUD, isGroupCulled); + }); + } + + scene->enqueueTransaction(transaction); + } +} void Model::setLayeredInFront(bool isLayeredInFront, const render::ScenePointer& scene) { if (_isLayeredInFront != isLayeredInFront) { _isLayeredInFront = isLayeredInFront; bool isVisible = _isVisible; + bool canCastShadow = _canCastShadow; uint8_t viewTagBits = _viewTagBits; bool isLayeredInHUD = _isLayeredInHUD; bool isGroupCulled = _isGroupCulled; render::Transaction transaction; foreach(auto item, _modelMeshRenderItemsMap.keys()) { - transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, + transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, canCastShadow, isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled); + data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); data.setLayer(isLayeredInFront, isLayeredInHUD); }); } foreach(auto item, _collisionRenderItemsMap.keys()) { - transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, + transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, canCastShadow, isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled); + data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); data.setLayer(isLayeredInFront, isLayeredInHUD); }); } @@ -745,22 +769,23 @@ void Model::setLayeredInHUD(bool isLayeredInHUD, const render::ScenePointer& sce _isLayeredInHUD = isLayeredInHUD; bool isVisible = _isVisible; + bool canCastShadow = _canCastShadow; uint8_t viewTagBits = _viewTagBits; bool isLayeredInFront = _isLayeredInFront; bool isGroupCulled = _isGroupCulled; render::Transaction transaction; foreach(auto item, _modelMeshRenderItemsMap.keys()) { - transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, + transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, canCastShadow, isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled); + data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); data.setLayer(isLayeredInFront, isLayeredInHUD); }); } foreach(auto item, _collisionRenderItemsMap.keys()) { - transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, + transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, canCastShadow, isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled); + data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); data.setLayer(isLayeredInFront, isLayeredInHUD); }); } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 57d2798a66..c0d2c32f8f 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -87,6 +87,10 @@ public: // new Scene/Engine rendering support void setVisibleInScene(bool isVisible, const render::ScenePointer& scene, uint8_t viewTagBits, bool isGroupCulled); + + bool canCastShadow() const { return _canCastShadow; } + void setCanCastShadow(bool canCastShadow, const render::ScenePointer& scene); + void setLayeredInFront(bool isLayeredInFront, const render::ScenePointer& scene); void setLayeredInHUD(bool isLayeredInHUD, const render::ScenePointer& scene); bool needsFixupInScene() const; @@ -401,6 +405,8 @@ protected: bool _isVisible; uint8_t _viewTagBits{ render::ItemKey::TAG_BITS_ALL }; + bool _canCastShadow; + gpu::Buffers _blendedVertexBuffers; QVector > > _dilatedTextures; From 47924a58f9732e62ef79efa065bd944781193ebd Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Fri, 16 Feb 2018 10:14:46 -0800 Subject: [PATCH 089/260] WIP - for review. --- libraries/render-utils/src/MeshPartPayload.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 84f6c6aca3..178f5782dd 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -411,7 +411,7 @@ void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& render _worldBound.transform(boundTransform); } -void ModelMeshPartPayload::updateKey(bool isVisible, bool isLayered, uint8_t tagBits, bool isGroupCulled) { +void ModelMeshPartPayload::updateKey(bool isVisible, bool isLayered, bool canCastShadow, uint8_t tagBits, bool isGroupCulled) { ItemKey::Builder builder; builder.withTypeShape(); From efa55c0a63d09b4f484c9248c59854924ba56b4e Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 13:07:17 -0800 Subject: [PATCH 090/260] Update backup delete to not break rolling backups and remove unused asset files --- domain-server/src/AssetsBackupHandler.cpp | 6 +++--- domain-server/src/AssetsBackupHandler.h | 2 +- domain-server/src/BackupHandler.h | 2 +- .../src/ContentSettingsBackupHandler.h | 2 +- .../src/DomainContentBackupManager.cpp | 19 ++++++++++++++++--- .../src/DomainContentBackupManager.h | 1 + domain-server/src/EntitiesBackupHandler.h | 2 +- 7 files changed, 24 insertions(+), 10 deletions(-) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index c365b942af..694277910f 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -306,7 +306,7 @@ void AssetsBackupHandler::recoverBackup(QuaZip& zip) { restoreAllAssets(); } -void AssetsBackupHandler::deleteBackup(QuaZip& zip) { +void AssetsBackupHandler::deleteBackup(const QString& absoluteFilePath) { Q_ASSERT(QThread::currentThread() == thread()); if (operationInProgress()) { @@ -315,10 +315,10 @@ void AssetsBackupHandler::deleteBackup(QuaZip& zip) { } auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { - return value.filePath == zip.getZipName(); + return value.filePath == absoluteFilePath; }); if (it == end(_backups)) { - qCDebug(asset_backup) << "Could not find backup" << zip.getZipName() << "to delete."; + qCDebug(asset_backup) << "Could not find backup" << absoluteFilePath << "to delete."; return; } diff --git a/domain-server/src/AssetsBackupHandler.h b/domain-server/src/AssetsBackupHandler.h index 1421ddd400..a4b62f563d 100644 --- a/domain-server/src/AssetsBackupHandler.h +++ b/domain-server/src/AssetsBackupHandler.h @@ -37,7 +37,7 @@ public: void loadBackup(QuaZip& zip) override; void createBackup(QuaZip& zip) override; void recoverBackup(QuaZip& zip) override; - void deleteBackup(QuaZip& zip) override; + void deleteBackup(const QString& absoluteFilePath) override; void consolidateBackup(QuaZip& zip) override; bool operationInProgress() { return getRecoveryStatus().first; } diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 1bd40cd9e4..7d876cec01 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -30,7 +30,7 @@ public: virtual void loadBackup(QuaZip& zip) = 0; virtual void createBackup(QuaZip& zip) = 0; virtual void recoverBackup(QuaZip& zip) = 0; - virtual void deleteBackup(QuaZip& zip) = 0; + virtual void deleteBackup(const QString& absoluteFilePath) = 0; virtual void consolidateBackup(QuaZip& zip) = 0; }; using BackupHandlerPointer = std::unique_ptr; diff --git a/domain-server/src/ContentSettingsBackupHandler.h b/domain-server/src/ContentSettingsBackupHandler.h index 8a81392513..ba252c862c 100644 --- a/domain-server/src/ContentSettingsBackupHandler.h +++ b/domain-server/src/ContentSettingsBackupHandler.h @@ -28,7 +28,7 @@ public: void recoverBackup(QuaZip& zip) override; - void deleteBackup(QuaZip& zip) override {} + void deleteBackup(const QString& absoluteFilePath) override {} void consolidateBackup(QuaZip& zip) override {} private: diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 5eb4e7627f..bf388ce63e 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -89,8 +89,7 @@ void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { } auto name = obj["Name"].toString(); - auto format = obj["format"].toString(); - format = name.replace(" ", "_").toLower(); + auto format = name.replace(" ", "_").toLower(); qCDebug(domain_server) << " Name:" << name; qCDebug(domain_server) << " format:" << format; @@ -116,6 +115,12 @@ void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { } } +void DomainContentBackupManager::refreshBackupRules() { + for (auto& backup : _backupRules) { + backup.lastBackupSeconds = getMostRecentBackupTimeInSecs(backup.extensionFormat); + } +} + int64_t DomainContentBackupManager::getMostRecentBackupTimeInSecs(const QString& format) { int64_t mostRecentBackupInSecs = 0; @@ -235,8 +240,16 @@ void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, cons } QDir backupDir { _backupDirectory }; - QFile backupFile { backupDir.filePath(backupName) }; + auto absoluteFilePath { backupDir.filePath(backupName) }; + QFile backupFile { absoluteFilePath }; auto success = backupFile.remove(); + + refreshBackupRules(); + + for (auto& handler : _backupHandlers) { + handler->deleteBackup(absoluteFilePath); + } + promise->resolve({ { "success", success } }); diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 790dff0fb4..4ec3d7bcc7 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -68,6 +68,7 @@ protected: void load(); 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 parseSettings(const QJsonObject& settings); diff --git a/domain-server/src/EntitiesBackupHandler.h b/domain-server/src/EntitiesBackupHandler.h index 1a6110f1cd..c143fe5774 100644 --- a/domain-server/src/EntitiesBackupHandler.h +++ b/domain-server/src/EntitiesBackupHandler.h @@ -30,7 +30,7 @@ public: void recoverBackup(QuaZip& zip) override; // Delete a skeleton backup - void deleteBackup(QuaZip& zip) override {} + void deleteBackup(const QString& absoluteFilePath) override {} // Create a full backup void consolidateBackup(QuaZip& zip) override {} From bb8caa0ce3edccec0ed6d6d31c06f20643d4e508 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 13:07:45 -0800 Subject: [PATCH 091/260] Update HTTPConnection to not use QIODevice when given a QByteArray --- .../embedded-webserver/src/HTTPConnection.cpp | 72 ++++++++++--------- .../embedded-webserver/src/HTTPConnection.h | 1 + 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index 6d0126b3d1..3f6f8d64ee 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -133,57 +133,33 @@ QList HTTPConnection::parseFormData() const { } void HTTPConnection::respond(const char* code, const QByteArray& content, const char* contentType, const Headers& headers) { - QByteArray data(content); - auto device { std::unique_ptr(new QBuffer()) }; - device->setBuffer(new QByteArray(content)); - if (device->open(QIODevice::ReadOnly)) { - respond(code, std::move(device), contentType, headers); - } else { - qCritical() << "Error opening QBuffer to respond to " << _requestUrl.path(); - } + 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); - _socket->write("HTTP/1.1 "); - if (_responseDevice->isSequential()) { qWarning() << "Error responding to HTTPConnection: sequential IO devices not supported"; - _socket->write(StatusCode500); - _socket->write("\r\n"); + respondWithStatusAndHeaders(StatusCode500, contentType, headers, 0); _socket->disconnect(SIGNAL(readyRead()), this); _socket->disconnectFromHost(); return; } - _socket->write(code); - _socket->write("\r\n"); - - for (Headers::const_iterator it = headers.constBegin(), end = headers.constEnd(); - it != end; it++) { - _socket->write(it.key()); - _socket->write(": "); - _socket->write(it.value()); - _socket->write("\r\n"); - } - - int csize = _responseDevice->size(); - if (csize > 0) { - _socket->write("Content-Length: "); - _socket->write(QByteArray::number(csize)); - _socket->write("\r\n"); - - _socket->write("Content-Type: "); - _socket->write(contentType); - _socket->write("\r\n"); - } - _socket->write("Connection: close\r\n\r\n"); + int totalToBeWritten = _responseDevice->size(); + respondWithStatusAndHeaders(code, contentType, headers, totalToBeWritten); if (_responseDevice->atEnd()) { _socket->disconnectFromHost(); } else { - int totalToBeWritten = csize; connect(_socket, &QTcpSocket::bytesWritten, this, [this, totalToBeWritten](size_t bytes) mutable { constexpr size_t HTTP_RESPONSE_CHUNK_SIZE = 1024 * 10; if (!_responseDevice->atEnd()) { @@ -201,6 +177,32 @@ void HTTPConnection::respond(const char* code, std::unique_ptr device disconnect(_socket, &QTcpSocket::readyRead, this, nullptr); } +void HTTPConnection::respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, int64_t contentLength) { + _socket->write("HTTP/1.1 "); + + _socket->write(code); + _socket->write("\r\n"); + + for (Headers::const_iterator it = headers.constBegin(), end = headers.constEnd(); + it != end; it++) { + _socket->write(it.key()); + _socket->write(": "); + _socket->write(it.value()); + _socket->write("\r\n"); + } + + if (contentLength > 0) { + _socket->write("Content-Length: "); + _socket->write(QByteArray::number(contentLength)); + _socket->write("\r\n"); + + _socket->write("Content-Type: "); + _socket->write(contentType); + _socket->write("\r\n"); + } + _socket->write("Connection: close\r\n\r\n"); +} + void HTTPConnection::readRequest() { if (!_socket->canReadLine()) { return; diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index a020dfdca9..ec00864514 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.h +++ b/libraries/embedded-webserver/src/HTTPConnection.h @@ -105,6 +105,7 @@ protected slots: void readContent (); protected: + void respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, int64_t size); /// The parent HTTP manager HTTPManager* _parentManager; From ec3580f5964187fd898ce735e029544d57856970 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 13:08:57 -0800 Subject: [PATCH 092/260] Fix recovery ID not being recorded --- domain-server/src/DomainContentBackupManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index bf388ce63e..8ea3d2ba90 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -281,6 +281,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, success = false; } else { _isRecovering = true; + _recoveryFilename = backupName; for (auto& handler : _backupHandlers) { handler->recoverBackup(zip); } From 0230abea790ddb7917ec366ae9b7b87c9038d3c4 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 13:54:35 -0800 Subject: [PATCH 093/260] Fix backup loading not getting auto backups --- .../src/DomainContentBackupManager.cpp | 108 +++++++++++------- .../src/DomainContentBackupManager.h | 13 ++- domain-server/src/DomainServer.cpp | 4 +- 3 files changed, 79 insertions(+), 46 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 8ea3d2ba90..db924c4e4f 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -300,12 +300,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, }); } -void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise promise) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "getAllBackupInformation", Q_ARG(MiniPromise::Promise, promise)); - return; - } - +std::vector DomainContentBackupManager::getAllBackups() { QDir backupDir { _backupDirectory }; auto matchingFiles = backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + "*.zip", MANUAL_BACKUP_PREFIX + "*.zip" }, @@ -315,7 +310,7 @@ void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise pr QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")"; QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" }; - QVariantList backups; + std::vector backups; for (const auto& fileInfo : matchingFiles) { auto fileName = fileInfo.fileName(); @@ -338,17 +333,53 @@ void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise pr availabilityProgress += progress / _backupHandlers.size(); } - backups.push_back(QVariantMap({ - { "id", fileInfo.fileName() }, - { "name", name }, - { "createdAtMillis", createdAt.toMSecsSinceEpoch() }, - { "isAvailable", isAvailable }, - { "availabilityProgress", availabilityProgress }, - { "isManualBackup", type == MANUAL_BACKUP_PREFIX } - })); + backups.push_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, "getAllBackupInformation", Q_ARG(MiniPromise::Promise, promise)); + return; + } + + 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" }; + + auto backups = getAllBackups(); + + QVariantList variantBackups; + + for (auto& backup : backups) { + bool isAvailable { true }; + float availabilityProgress { 0.0f }; + for (auto& handler : _backupHandlers) { + bool handlerIsAvailable { true }; + float progress { 0.0f }; + std::tie(handlerIsAvailable, progress) = handler->isAvailable(backup.absolutePath); + isAvailable &= handlerIsAvailable; + availabilityProgress += progress / _backupHandlers.size(); + } + variantBackups.push_back(QVariantMap({ + { "id", backup.id }, + { "name", backup.name }, + { "createdAtMillis", backup.createdAt.toMSecsSinceEpoch() }, + { "isAvailable", isAvailable }, + { "availabilityProgress", availabilityProgress }, + { "isManualBackup", backup.isManualBackup } + })); + } + float recoveryProgress = 0.0f; bool isRecovering = _isRecovering.load(); if (_isRecovering) { @@ -365,7 +396,7 @@ void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise pr }; QVariantMap info { - { "backups", backups }, + { "backups", variantBackups }, { "status", status } }; @@ -404,32 +435,27 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) } void DomainContentBackupManager::load() { - QDir backupDir { _backupDirectory }; - if (backupDir.exists()) { - - auto matchingFiles = backupDir.entryInfoList({ "backup-*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); - - for (const auto& file : matchingFiles) { - QFile backupFile { file.absoluteFilePath() }; - if (!backupFile.open(QIODevice::ReadOnly)) { - qCritical() << "Could not open file:" << file.absoluteFilePath(); - qCritical() << " ERROR:" << backupFile.errorString(); - continue; - } - - QuaZip zip { &backupFile }; - if (!zip.open(QuaZip::mdUnzip)) { - qCritical() << "Could not open backup archive:" << file.absoluteFilePath(); - qCritical() << " ERROR:" << zip.getZipError(); - continue; - } - - for (auto& handler : _backupHandlers) { - handler->loadBackup(zip); - } - - zip.close(); + 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(zip); + } + + zip.close(); } } diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 4ec3d7bcc7..2cfda4b650 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -24,6 +24,14 @@ #include +struct BackupItemInfo { + QString id; + QString name; + QString absolutePath; + QDateTime createdAt; + bool isManualBackup; +}; + class DomainContentBackupManager : public GenericThread { Q_OBJECT public: @@ -43,14 +51,13 @@ public: int 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 getAllBackupInformation(MiniPromise::Promise promise); + void getAllBackupsAndStatus(MiniPromise::Promise promise); void createManualBackup(MiniPromise::Promise promise, const QString& name); void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); void deleteBackup(MiniPromise::Promise promise, const QString& backupName); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index fe145b341b..157eaa483f 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2128,13 +2128,13 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; } else if (url.path() == URI_API_BACKUPS) { - auto deferred = makePromise("getAllBackupInformation"); + 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->getAllBackupInformation(deferred); + _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()); From 27c26bab8659b965229d451efb79edb6e4290615 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 13:57:38 -0800 Subject: [PATCH 094/260] Fix ambiguous int64_t in HTTPConnection --- libraries/embedded-webserver/src/HTTPConnection.cpp | 2 +- libraries/embedded-webserver/src/HTTPConnection.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index 3f6f8d64ee..00879e1380 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -177,7 +177,7 @@ void HTTPConnection::respond(const char* code, std::unique_ptr device disconnect(_socket, &QTcpSocket::readyRead, this, nullptr); } -void HTTPConnection::respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, int64_t contentLength) { +void HTTPConnection::respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, qint64 contentLength) { _socket->write("HTTP/1.1 "); _socket->write(code); diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index ec00864514..60408d4325 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.h +++ b/libraries/embedded-webserver/src/HTTPConnection.h @@ -105,7 +105,7 @@ protected slots: void readContent (); protected: - void respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, int64_t size); + void respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, qint64 size); /// The parent HTTP manager HTTPManager* _parentManager; From 4c1f22f84e62213b3de4e7cb658a0473809a37d3 Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Fri, 16 Feb 2018 14:01:33 -0800 Subject: [PATCH 095/260] Models and Avatar cast shadows (box doesn't, yet). --- interface/src/avatar/MyAvatar.cpp | 5 +++- .../src/RenderableEntityItem.cpp | 1 + libraries/entities/src/EntityItem.cpp | 28 +++++++++++++++++++ libraries/entities/src/EntityItem.h | 5 ++++ libraries/entities/src/ModelEntityItem.cpp | 28 ------------------- libraries/entities/src/ModelEntityItem.h | 5 ---- .../render-utils/src/MeshPartPayload.cpp | 2 +- 7 files changed, 39 insertions(+), 35 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 97b8c71a32..c70dc9df98 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1112,7 +1112,7 @@ void MyAvatar::setEnableDebugDrawIKChains(bool isEnabled) { void MyAvatar::setEnableMeshVisible(bool isEnabled) { _skeletonModel->setVisibleInScene(isEnabled, qApp->getMain3DScene(), render::ItemKey::TAG_BITS_NONE, true); - _skeletonModel->setCanCastShadow(true, qApp->getMain3DScene()); + _skeletonModel->setCanCastShadow(isEnabled, qApp->getMain3DScene()); } void MyAvatar::setEnableInverseKinematics(bool isEnabled) { @@ -2010,8 +2010,11 @@ void MyAvatar::preDisplaySide(RenderArgs* renderArgs) { _attachmentData[i].jointName.compare("RightEye", Qt::CaseInsensitive) == 0 || _attachmentData[i].jointName.compare("HeadTop_End", Qt::CaseInsensitive) == 0 || _attachmentData[i].jointName.compare("Face", Qt::CaseInsensitive) == 0) { + _attachmentModels[i]->setVisibleInScene(shouldDrawHead, qApp->getMain3DScene(), render::ItemKey::TAG_BITS_NONE, true); + + _attachmentModels[i]->setCanCastShadow(shouldDrawHead, qApp->getMain3DScene()); } } } diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index aca2f4d35b..7f2f57d1ac 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -371,6 +371,7 @@ void EntityRenderer::doRenderUpdateSynchronous(const ScenePointer& scene, Transa _moving = entity->isMovingRelativeToParent(); _visible = entity->getVisible(); + _canCastShadow = entity->getCanCastShadow(); _cauterized = entity->getCauterized(); _needsRenderUpdate = false; }); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index ed13a46414..e14d0b6757 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -99,6 +99,7 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_REGISTRATION_POINT; requestedProperties += PROP_ANGULAR_DAMPING; requestedProperties += PROP_VISIBLE; + requestedProperties += PROP_CAN_CAST_SHADOW; requestedProperties += PROP_COLLISIONLESS; requestedProperties += PROP_COLLISION_MASK; requestedProperties += PROP_DYNAMIC; @@ -257,6 +258,7 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_REGISTRATION_POINT, getRegistrationPoint()); APPEND_ENTITY_PROPERTY(PROP_ANGULAR_DAMPING, getAngularDamping()); APPEND_ENTITY_PROPERTY(PROP_VISIBLE, getVisible()); + APPEND_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, getCanCastShadow()); APPEND_ENTITY_PROPERTY(PROP_COLLISIONLESS, getCollisionless()); APPEND_ENTITY_PROPERTY(PROP_COLLISION_MASK, getCollisionMask()); APPEND_ENTITY_PROPERTY(PROP_DYNAMIC, getDynamic()); @@ -807,6 +809,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef READ_ENTITY_PROPERTY(PROP_ANGULAR_DAMPING, float, setAngularDamping); READ_ENTITY_PROPERTY(PROP_VISIBLE, bool, setVisible); + READ_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow); READ_ENTITY_PROPERTY(PROP_COLLISIONLESS, bool, setCollisionless); READ_ENTITY_PROPERTY(PROP_COLLISION_MASK, uint8_t, setCollisionMask); READ_ENTITY_PROPERTY(PROP_DYNAMIC, bool, setDynamic); @@ -900,6 +903,7 @@ void EntityItem::debugDump() const { qCDebug(entities, " edited ago:%f", (double)getEditedAgo()); qCDebug(entities, " position:%f,%f,%f", (double)position.x, (double)position.y, (double)position.z); qCDebug(entities) << " dimensions:" << getScaledDimensions(); + qCDebug(entities) << " can cast shadow" << getCanCastShadow(); } // adjust any internal timestamps to fix clock skew for this server @@ -1242,6 +1246,7 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper COPY_ENTITY_PROPERTY_TO_PROPERTIES(angularDamping, getAngularDamping); COPY_ENTITY_PROPERTY_TO_PROPERTIES(localRenderAlpha, getLocalRenderAlpha); COPY_ENTITY_PROPERTY_TO_PROPERTIES(visible, getVisible); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(canCastShadow, getCanCastShadow); COPY_ENTITY_PROPERTY_TO_PROPERTIES(collisionless, getCollisionless); COPY_ENTITY_PROPERTY_TO_PROPERTIES(collisionMask, getCollisionMask); COPY_ENTITY_PROPERTY_TO_PROPERTIES(dynamic, getDynamic); @@ -1354,6 +1359,7 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(collisionSoundURL, setCollisionSoundURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(localRenderAlpha, setLocalRenderAlpha); SET_ENTITY_PROPERTY_FROM_PROPERTIES(visible, setVisible); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(canCastShadow, setCanCastShadow); SET_ENTITY_PROPERTY_FROM_PROPERTIES(userData, setUserData); // Certifiable Properties @@ -2731,6 +2737,28 @@ void EntityItem::setVisible(bool value) { } } +bool EntityItem::getCanCastShadow() const { + bool result; + withReadLock([&] { + result = _canCastShadow; + }); + return result; +} + +void EntityItem::setCanCastShadow(bool value) { + bool changed = false; + withWriteLock([&] { + if (_canCastShadow != value) { + changed = true; + _canCastShadow = value; + } + }); + + if (changed) { + emit requestRenderUpdate(); + } +} + bool EntityItem::isChildOfMyAvatar() const { QUuid ancestorID = findAncestorOfType(NestableType::Avatar); return !ancestorID.isNull() && (ancestorID == Physics::getSessionUUID() || ancestorID == AVATAR_SELF_ID); diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 5f84bcc311..19a1a9c88e 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -273,6 +273,10 @@ public: bool getVisible() const; void setVisible(bool value); + + bool getCanCastShadow() const; + void setCanCastShadow(bool value); + inline bool isVisible() const { return getVisible(); } inline bool isInvisible() const { return !getVisible(); } @@ -543,6 +547,7 @@ protected: glm::vec3 _registrationPoint { ENTITY_ITEM_DEFAULT_REGISTRATION_POINT }; float _angularDamping { ENTITY_ITEM_DEFAULT_ANGULAR_DAMPING }; bool _visible { ENTITY_ITEM_DEFAULT_VISIBLE }; + bool _canCastShadow{ ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW }; bool _collisionless { ENTITY_ITEM_DEFAULT_COLLISIONLESS }; uint8_t _collisionMask { ENTITY_COLLISION_MASK_DEFAULT }; bool _dynamic { ENTITY_ITEM_DEFAULT_DYNAMIC }; diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index b1edd47a67..a4fe8e6b1e 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -54,7 +54,6 @@ void ModelEntityItem::setTextures(const QString& textures) { EntityItemProperties ModelEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class - COPY_ENTITY_PROPERTY_TO_PROPERTIES(canCastShadow, getCanCastShadow); COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getXColor); COPY_ENTITY_PROPERTY_TO_PROPERTIES(modelURL, getModelURL); COPY_ENTITY_PROPERTY_TO_PROPERTIES(compoundShapeURL, getCompoundShapeURL); @@ -74,7 +73,6 @@ bool ModelEntityItem::setProperties(const EntityItemProperties& properties) { bool somethingChanged = false; somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class - SET_ENTITY_PROPERTY_FROM_PROPERTIES(canCastShadow, setCanCastShadow); SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); SET_ENTITY_PROPERTY_FROM_PROPERTIES(modelURL, setModelURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); @@ -116,7 +114,6 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, const unsigned char* dataAt = data; bool animationPropertiesChanged = false; - READ_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow); READ_ENTITY_PROPERTY(PROP_COLOR, rgbColor, setColor); READ_ENTITY_PROPERTY(PROP_MODEL_URL, QString, setModelURL); READ_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); @@ -153,7 +150,6 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, EntityPropertyFlags ModelEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); - requestedProperties += PROP_CAN_CAST_SHADOW; requestedProperties += PROP_MODEL_URL; requestedProperties += PROP_COMPOUND_SHAPE_URL; requestedProperties += PROP_TEXTURES; @@ -178,7 +174,6 @@ void ModelEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit bool successPropertyFits = true; - APPEND_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, getCanCastShadow()); APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_MODEL_URL, getModelURL()); APPEND_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, getCompoundShapeURL()); @@ -295,7 +290,6 @@ void ModelEntityItem::updateFrameCount() { } void ModelEntityItem::debugDump() const { - qCDebug(entities) << " can cast shadow" << getCanCastShadow(); qCDebug(entities) << "ModelEntityItem id:" << getEntityItemID(); qCDebug(entities) << " edited ago:" << getEditedAgo(); qCDebug(entities) << " position:" << getWorldPosition(); @@ -728,25 +722,3 @@ bool ModelEntityItem::isAnimatingSomething() const { (_animationProperties.getFPS() != 0.0f); }); } - -bool ModelEntityItem::getCanCastShadow() const { - bool result; - withReadLock([&] { - result = _canCastShadow; - }); - return result; -} - -void ModelEntityItem::setCanCastShadow(bool value) { - bool changed = false; - withWriteLock([&] { - if (_canCastShadow != value) { - changed = true; - _canCastShadow = value; - } - }); - - if (changed) { - emit requestRenderUpdate(); - } -} diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 791eebb7d9..c2109ba51f 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -131,9 +131,6 @@ public: QVector getJointTranslations() const; QVector getJointTranslationsSet() const; - bool getCanCastShadow() const; - void setCanCastShadow(bool value); - private: void setAnimationSettings(const QString& value); // only called for old bitstream format ShapeType computeTrueShapeType() const; @@ -174,8 +171,6 @@ protected: ShapeType _shapeType = SHAPE_TYPE_NONE; - bool _canCastShadow{ ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW }; - private: uint64_t _lastAnimated{ 0 }; AnimationPropertyGroup _previousAnimationProperties; diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 84f6c6aca3..178f5782dd 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -411,7 +411,7 @@ void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& render _worldBound.transform(boundTransform); } -void ModelMeshPartPayload::updateKey(bool isVisible, bool isLayered, uint8_t tagBits, bool isGroupCulled) { +void ModelMeshPartPayload::updateKey(bool isVisible, bool isLayered, bool canCastShadow, uint8_t tagBits, bool isGroupCulled) { ItemKey::Builder builder; builder.withTypeShape(); From 936629ec1a41247b8ab47548b65921bd9ec7b081 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 14:05:40 -0800 Subject: [PATCH 096/260] Update DomainContentBackupManager to use emplace_back where available --- domain-server/src/DomainContentBackupManager.cpp | 6 +++--- domain-server/src/DomainContentBackupManager.h | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index db924c4e4f..618af15e60 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -333,8 +333,8 @@ std::vector DomainContentBackupManager::getAllBackups() { availabilityProgress += progress / _backupHandlers.size(); } - backups.push_back( - { fileInfo.fileName(), name, fileInfo.absoluteFilePath(), createdAt, type == MANUAL_BACKUP_PREFIX }); + backups.emplace_back(fileInfo.fileName(), name, fileInfo.absoluteFilePath(), createdAt, + type == MANUAL_BACKUP_PREFIX); } } @@ -343,7 +343,7 @@ std::vector DomainContentBackupManager::getAllBackups() { void DomainContentBackupManager::getAllBackupsAndStatus(MiniPromise::Promise promise) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "getAllBackupInformation", Q_ARG(MiniPromise::Promise, promise)); + QMetaObject::invokeMethod(this, "getAllBackupsAndStatus", Q_ARG(MiniPromise::Promise, promise)); return; } diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 2cfda4b650..43e4cb16da 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -25,6 +25,13 @@ #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; From 41d7d7efbbf4ec675f04a85e5a5f9f8e083f98a2 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 14:33:19 -0800 Subject: [PATCH 097/260] Fix initializer list style --- domain-server/src/DomainContentBackupManager.h | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 43e4cb16da..f1aa4acab2 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -25,12 +25,8 @@ #include struct BackupItemInfo { - BackupItemInfo(QString pId, QString pName, QString pAbsolutePath, QDateTime pCreatedAt, bool pIsManualBackup) - : id(pId) - , name(pName) - , absolutePath(pAbsolutePath) - , createdAt(pCreatedAt) - , isManualBackup(pIsManualBackup){}; + 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; From a2072062f18841f47d2ba31927d9cdaf96c23050 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 14:33:33 -0800 Subject: [PATCH 098/260] Fix recovery filename not being reset when recovery complete --- domain-server/src/DomainContentBackupManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 618af15e60..dc749ad182 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -160,6 +160,7 @@ bool DomainContentBackupManager::process() { if (!isStillRecovering) { _isRecovering = false; + _recoveryFilename = ""; emit recoveryCompleted(); } } From f4cde44e6af1a4863d771ef6fc4ade79e4c04c32 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 15:25:29 -0800 Subject: [PATCH 099/260] Fix indentation of brace initialization --- domain-server/src/DomainContentBackupManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index dc749ad182..a711d2112d 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -438,14 +438,14 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) void DomainContentBackupManager::load() { auto backups = getAllBackups(); for (auto& backup : backups) { - QFile backupFile{ backup.absolutePath }; + QFile backupFile { backup.absolutePath }; if (!backupFile.open(QIODevice::ReadOnly)) { qCritical() << "Could not open file:" << backup.absolutePath; qCritical() << " ERROR:" << backupFile.errorString(); continue; } - QuaZip zip{ &backupFile }; + QuaZip zip { &backupFile }; if (!zip.open(QuaZip::mdUnzip)) { qCritical() << "Could not open backup archive:" << backup.absolutePath; qCritical() << " ERROR:" << zip.getZipError(); From 29ceffd7cce43ba0c79e15a5ba2889df2b50671d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 14 Feb 2018 12:05:23 -0800 Subject: [PATCH 100/260] add sections to content page for backup/restore --- .../resources/web/content/index.shtml | 2 + .../web/content/js/bootstrap-sortable.min.js | 1 + .../resources/web/content/js/content.js | 193 +++++++++++++++--- .../web/content/js/moment-locale.min.js | 1 + .../resources/web/css/bootstrap-sortable.css | 110 ++++++++++ domain-server/resources/web/css/style.css | 52 ++++- domain-server/resources/web/header.html | 1 + .../resources/web/js/base-settings.js | 8 +- .../resources/web/js/domain-server.js | 58 ++++-- domain-server/resources/web/js/shared.js | 6 +- .../resources/web/settings/js/settings.js | 18 +- 11 files changed, 387 insertions(+), 63 deletions(-) create mode 100755 domain-server/resources/web/content/js/bootstrap-sortable.min.js create mode 100644 domain-server/resources/web/content/js/moment-locale.min.js create mode 100755 domain-server/resources/web/css/bootstrap-sortable.css 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..e2b653995f 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -1,37 +1,174 @@ $(document).ready(function(){ - Settings.afterReloadActions = function() {}; + var RESTORE_SETTINGS_UPLOAD_ID = 'restore-settings-button'; + var RESTORE_SETTINGS_FILE_ID = 'restore-settings-file'; - 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', - }); + function setupBackupUpload() { + // construct the HTML needed for the settings backup panel + var html = "
"; + + html += "Upload a Content Backup to replace the content of this domain"; + html += "
Note: Your domain's 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 += "
"; + + $('#' + Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID + ' .panel-body').html(html); + } + + var GENERATE_ARCHIVE_BUTTON_ID = 'generate-archive-button'; + 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 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 domain content and settings backups." + 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 += "
"; + + // put the base HTML in the content archives panel + $('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html(html); + } + + function reloadLatestBackups() { + // make a GET request to get backup information to populate the table + $.get('/api/backups', function(data) { + // split the returned data into manual and automatic manual backups + var splitBackups = _.partition(data.backups, function(value, index) { + return value.isManualBackup; + }); + + manualBackups = splitBackups[0]; + automaticBackups = splitBackups[1]; + + // populate the backups tables with the backups + function createBackupTableRow(backup) { + return "" + backup.name + "" + + moment(backup.createdAtMillis).format('lll') + + "" + + "
" + + ""; } + + var automaticRows = ""; + + if (automaticBackups.length > 0) { + for (var backupIndex in automaticBackups) { + // create a table row for this backup and add it to the rows we'll put in the table body + automaticRows += createBackupTableRow(automaticBackups[backupIndex]); + } + } + + $('#' + AUTOMATIC_ARCHIVES_TBODY_ID).html(automaticRows); + + var manualRows = ""; + + if (manualBackups.length > 0) { + for (var backupIndex in manualBackups) { + // create a table row for this backup and add it to the rows we'll put in the table body + manualRows += createBackupTableRow(manualBackups[backupIndex]); + } + } + + $('#' + MANUAL_ARCHIVES_TBODY_ID).html(manualRows); + + // tell bootstrap sortable to update for the new rows + $.bootstrapSortable({ applyLast: true }); + + }).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 + $('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html( + "
" + + "There was a problem loading your list of automatic and manual content archives. Please reload the page to try again." + + "
" + ); + + }).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); }); + } - ev.preventDefault(); + // handle click on manual archive creation button + $('body').on('click', '#' + GENERATE_ARCHIVE_BUTTON_ID, function(e) { + e.preventDefault(); - showSpinnerAlert("Uploading Entities File"); + // 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; + } + + // post the provided archive name to ask the server to kick off a manual backup + $.ajax({ + type: 'POST', + url: '/api/backup', + 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 + reloadContentArchives(); + }).fail(function(jqXHR, textStatus, errorThrown) { + + }); + + swal.close(); + }); }); + + Settings.extraGroupsAtIndex = Settings.extraContentGroupsAtIndex; + + Settings.afterReloadActions = function() { + setupBackupUpload(); + setupContentArchives(); + + // load the latest backups immediately + reloadLatestBackups(); + }; }); 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..2bcc870ecf 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,37 @@ 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; +} + +.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/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..961a7df3b2 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)); diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index 2f75794786..184b2b954f 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -55,6 +55,34 @@ $(document).ready(function(){ 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' + } + ] + // 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 +93,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.extraContentGroupsAtIndex[spliceIndex]); + } + $.each(data.content_settings, function(index, group){ if (index > 0) { $contentDropdown.append(""); @@ -73,25 +110,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..040d8959e7 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 = { @@ -164,7 +166,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..d4de0d5f4c 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -14,17 +14,9 @@ $(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 @@ -643,7 +635,6 @@ $(document).ready(function(){ autoNetworkingEl.after(form); } - function setupPlacesTable() { // create a dummy table using our view helper var placesTableSetting = { @@ -1097,8 +1088,5 @@ $(document).ready(function(){ html += ""; $('#settings_backup .panel-body').html(html); - - // add an upload button to the footer to kick off the upload form - } }); From dd5a705836d22a9d34d8d56b672d77115e817557 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 14 Feb 2018 13:29:42 -0800 Subject: [PATCH 101/260] move rolling interval backup rules to automatic content archives --- .../resources/describe-settings.json | 130 +++++++++--------- .../resources/web/content/js/content.js | 30 +++- .../resources/web/js/base-settings.js | 4 + .../src/DomainContentBackupManager.cpp | 6 +- domain-server/src/DomainServer.cpp | 9 +- .../src/DomainServerSettingsManager.cpp | 17 +++ .../src/DomainServerSettingsManager.h | 1 + 7 files changed, 123 insertions(+), 74 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 93d703c8b3..427dc62520 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", @@ -1321,73 +1321,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 +1582,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/content/js/content.js b/domain-server/resources/web/content/js/content.js index e2b653995f..0fd5f37a94 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -23,15 +23,17 @@ $(document).ready(function(){ 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 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 domain content and settings backups." + 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 domain content and settings backups. " + html += "Click here to manage automatic content archive intervals."; html += "
    "; html += ""; @@ -120,6 +122,30 @@ $(document).ready(function(){ }); } + // 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'); + + swal({ + title: "Are you sure?", + text: "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.", + type: "warning", + showCancelButton: true, + confirmButtonText: "Leave and Lose Pending Changes", + closeOnConfirm: true + }, + 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(); diff --git a/domain-server/resources/web/js/base-settings.js b/domain-server/resources/web/js/base-settings.js index 961a7df3b2..3476792222 100644 --- a/domain-server/resources/web/js/base-settings.js +++ b/domain-server/resources/web/js/base-settings.js @@ -126,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() { @@ -805,6 +807,8 @@ function badgeForDifferences(changedElement) { } }); + Settings.pendingChanges = totalChanges; + if (totalChanges == 0) { totalChanges = "" } diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index a711d2112d..345faffec4 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -63,9 +63,9 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire } void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { - qDebug() << settings << settings["backups"] << settings["backups"].isArray(); - if (settings["backups"].isArray()) { - const QJsonArray& backupRules = settings["backups"].toArray(); + static const QString BACKUP_RULES_KEY = "backup_rules"; + if (settings[BACKUP_RULES_KEY].isArray()) { + const QJsonArray& backupRules = settings[BACKUP_RULES_KEY].toArray(); qCDebug(domain_server) << "BACKUP RULES:"; for (const QJsonValue& value : backupRules) { diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 157eaa483f..8247e12de5 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -296,8 +296,15 @@ DomainServer::DomainServer(int argc, char* argv[]) : qCDebug(domain_server) << "Created entities data directory"; } maybeHandleReplacementEntityFile(); + + auto contentArchivesGroup = _settingsManager.valueOrDefaultValueForKeyPath(AUTOMATIC_CONTENT_ARCHIVES_GROUP); + auto archivesIntervalObject = QJsonObject(); - _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); + if (contentArchivesGroup.canConvert()) { + archivesIntervalObject = QJsonObject::fromVariantMap(contentArchivesGroup.toMap()); + } + + _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), archivesIntervalObject)); connect(_contentManager.get(), &DomainContentBackupManager::started, _contentManager.get(), [this](){ _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 85d6a046b5..a50cde0807 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -393,6 +393,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList _standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canRezTemporaryCertifiedEntities); packPermissions(); } + if (oldVersion < 2.0) { const QString WIZARD_COMPLETED_ONCE = "wizard.completed_once"; @@ -400,6 +401,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList *wizardCompletedOnce = QVariant(true); } + if (oldVersion < 2.1) { // convert old avatar scale settings into avatar height. @@ -421,6 +423,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.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; diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index abc70751a8..897a15485f 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -37,6 +37,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 From 2d9f2ebf81c0de164948c372e26e8bba286c7bb9 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 14 Feb 2018 13:50:46 -0800 Subject: [PATCH 102/260] fix active with anchor and settings dropdowns on non-settings pages --- domain-server/resources/web/base-settings-scripts.html | 1 - domain-server/resources/web/footer.html | 1 + domain-server/resources/web/js/domain-server.js | 6 +++--- domain-server/resources/web/settings/js/settings.js | 1 - domain-server/resources/web/wizard/index.shtml | 1 - 5 files changed, 4 insertions(+), 6 deletions(-) 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/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/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index 184b2b954f..6b2d4e1316 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'); @@ -55,7 +55,7 @@ $(document).ready(function(){ var $contentDropdown = $('#content-settings-nav-dropdown'); var $settingsDropdown = $('#domain-settings-nav-dropdown'); - // define extra groups to add to setting panels, with their splice index + // define extra groups to add to setting panels, with their splice index Settings.extraContentGroupsAtIndex = { 0: { html_id: Settings.CONTENT_ARCHIVES_PANEL_ID, @@ -99,7 +99,7 @@ $(document).ready(function(){ } for (var endIndex in Settings.extraContentGroupsAtEnd) { - data.content_settings.push(Settings.extraContentGroupsAtIndex[spliceIndex]); + data.content_settings.push(Settings.extraContentGroupsAtEnd[endIndex]); } $.each(data.content_settings, function(index, group){ diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index d4de0d5f4c..1c6510298f 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -17,7 +17,6 @@ $(document).ready(function(){ Settings.extraGroupsAtEnd = Settings.extraDomainGroupsAtEnd; Settings.extraGroupsAtIndex = Settings.extraDomainGroupsAtIndex; - Settings.afterReloadActions = function() { // append the domain selection modal appendDomainIDButtons(); 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 @@ - From b019895fce5051e18188d75a89c1a4ce791d3765 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 14 Feb 2018 15:49:42 -0800 Subject: [PATCH 103/260] handle entity file upload from new content upload --- .../resources/web/content/js/content.js | 58 +++++++++++++++++-- .../resources/web/js/domain-server.js | 2 +- .../resources/web/settings/js/settings.js | 53 ++++++++++------- domain-server/src/DomainServer.cpp | 30 ++++++++-- 4 files changed, 111 insertions(+), 32 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 0fd5f37a94..4e2c27bf54 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -7,17 +7,67 @@ $(document).ready(function(){ // construct the HTML needed for the settings backup panel var html = "
    "; - html += "Upload a Content Backup to replace the content of this domain"; - html += "
    Note: Your domain's 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 += "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 += "
    "; $('#' + 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() { + if ($(this).val()) { + $('#' + RESTORE_SETTINGS_UPLOAD_ID).attr('disabled', false); + } + }); + + // 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(); + + swal({ + title: "Are you sure?", + text: "Your domain content will be replaced by the uploaded Content Archive or entity file", + type: "warning", + showCancelButton: true, + closeOnConfirm: false + }, + function () { + var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); + + var fileFormData = new FormData(); + fileFormData.append('restore-file', files[0]); + + showSpinnerAlert("Restoring Content"); + + $.ajax({ + url: '/content/upload', + type: 'POST', + cache: false, + processData: false, + contentType: false, + data: fileFormData + }).done(function(data, textStatus, jqXHR) { + swal.close(); + showRestartModal(); + }).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." + ); + }); + }); + }); + var GENERATE_ARCHIVE_BUTTON_ID = 'generate-archive-button'; var AUTOMATIC_ARCHIVES_TABLE_ID = 'automatic-archives-table'; var AUTOMATIC_ARCHIVES_TBODY_ID = 'automatic-archives-tbody'; @@ -136,7 +186,7 @@ $(document).ready(function(){ text: "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.", type: "warning", showCancelButton: true, - confirmButtonText: "Leave and Lose Pending Changes", + confirmButtonText: "Proceed without Saving", closeOnConfirm: true }, function () { diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index 6b2d4e1316..d3b20d40bb 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -79,7 +79,7 @@ $(document).ready(function(){ Settings.extraDomainGroupsAtEnd = [ { html_id: 'settings_backup', - label: 'Settings Backup' + label: 'Settings Backup / Restore' } ] diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 1c6510298f..b73337ef2d 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -1033,31 +1033,40 @@ $(document).ready(function(){ $('body').on('click', '#' + RESTORE_SETTINGS_UPLOAD_ID, function(e){ e.preventDefault(); - var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); + swal({ + title: "Are you sure?", + text: "Your domain settings will be replaced by the uploaded settings", + type: "warning", + showCancelButton: true, + closeOnConfirm: false + }, + 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(); + }); }); }); @@ -1079,7 +1088,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 += ""; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8247e12de5..f3765f6868 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2256,12 +2256,32 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url 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 match sure we handle this filetype for a content restore + auto dispositionValue = QString(firstFormData.first.value("Content-Disposition")); + auto formDataFilenameRegex = QRegExp("filename=\"(\\S+)\""); + 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, formData[0].second)); + + // respond with a 200 for success + connection->respond(HTTPConnection::StatusCode200); + } 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); From 2b39419795166233273eb6688621f66266ddd10d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 14 Feb 2018 17:42:49 -0800 Subject: [PATCH 104/260] keeping AYS DRY and hooking up restore/delete for content archives --- .../resources/web/content/js/content.js | 165 +++++++++++++----- .../resources/web/js/domain-server.js | 19 +- domain-server/resources/web/js/shared.js | 11 ++ .../resources/web/settings/js/settings.js | 78 ++++----- 4 files changed, 175 insertions(+), 98 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 4e2c27bf54..34af9262a2 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -7,7 +7,7 @@ $(document).ready(function(){ // 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 += "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 += ""; @@ -33,39 +33,36 @@ $(document).ready(function(){ $('body').on('click', '#' + RESTORE_SETTINGS_UPLOAD_ID, function(e){ e.preventDefault(); - swal({ - title: "Are you sure?", - text: "Your domain content will be replaced by the uploaded Content Archive or entity file", - type: "warning", - showCancelButton: true, - closeOnConfirm: false - }, - function () { - var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); + 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]); + var fileFormData = new FormData(); + fileFormData.append('restore-file', files[0]); - showSpinnerAlert("Restoring Content"); + showSpinnerAlert("Restoring Content"); - $.ajax({ - url: '/content/upload', - type: 'POST', - cache: false, - processData: false, - contentType: false, - data: fileFormData - }).done(function(data, textStatus, jqXHR) { - swal.close(); - showRestartModal(); - }).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." - ); - }); - }); + $.ajax({ + url: '/content/upload', + type: 'POST', + cache: false, + processData: false, + contentType: false, + data: fileFormData + }).done(function(data, textStatus, jqXHR) { + swal.close(); + showRestartModal(); + }).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." + ); + }); + } + ); }); var GENERATE_ARCHIVE_BUTTON_ID = 'generate-archive-button'; @@ -104,6 +101,10 @@ $(document).ready(function(){ $('#' + 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'; + function reloadLatestBackups() { // make a GET request to get backup information to populate the table $.get('/api/backups', function(data) { @@ -117,12 +118,15 @@ $(document).ready(function(){ // populate the backups tables with the backups function createBackupTableRow(backup) { - return "
    " + + ""; + + ""; } var automaticRows = ""; @@ -172,6 +176,79 @@ $(document).ready(function(){ }); } + // 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(); + + // 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("Restoring Content Archive " + backupName); + + // setup an AJAX POST to request content restore + $.post('/api/backups/recover/' + backupID).done(function(data, textStatus, jqXHR) { + swal.close(); + showRestartModal(); + }).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 + reloadContentArchives(); + }); + } + ) + }); + // handle click on automatic content archive settings link $('body').on('click', '#' + AUTO_ARCHIVES_SETTINGS_LINK_ID, function(e) { if (Settings.pendingChanges > 0) { @@ -181,18 +258,14 @@ $(document).ready(function(){ var settingsLink = $(this).attr('href'); - swal({ - title: "Are you sure?", - text: "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.", - type: "warning", - showCancelButton: true, - confirmButtonText: "Proceed without Saving", - closeOnConfirm: true - }, - function () { - // user wants to drop their changes, switch pages - window.location = settingsLink; - }); + 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; + } + ); } }); diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index d3b20d40bb..2c12e2683a 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -39,16 +39,15 @@ $(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; }); diff --git a/domain-server/resources/web/js/shared.js b/domain-server/resources/web/js/shared.js index 040d8959e7..84bba4de56 100644 --- a/domain-server/resources/web/js/shared.js +++ b/domain-server/resources/web/js/shared.js @@ -98,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') { diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index b73337ef2d..e67ea43158 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -94,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; } @@ -1033,41 +1030,38 @@ $(document).ready(function(){ $('body').on('click', '#' + RESTORE_SETTINGS_UPLOAD_ID, function(e){ e.preventDefault(); - swal({ - title: "Are you sure?", - text: "Your domain settings will be replaced by the uploaded settings", - type: "warning", - showCancelButton: true, - closeOnConfirm: false - }, - function() { - 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() { From 41b0bb8c581ae249b2e3dbb4a915f8912e270fed Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 10:06:36 -0800 Subject: [PATCH 105/260] connect download link from content archive tables --- domain-server/resources/web/content/js/content.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 34af9262a2..12a6d4b734 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -126,7 +126,7 @@ $(document).ready(function(){ + ""; + + "
  • Delete
  • "; } var automaticRows = ""; From 910f3425f8bf3fa080b68dae1e0b5786e6564517 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 10:57:07 -0800 Subject: [PATCH 106/260] fix latest backup refreshing with no caching --- domain-server/resources/web/content/js/content.js | 14 +++++++++----- domain-server/src/DomainServer.cpp | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 12a6d4b734..1e5b6ac131 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -107,7 +107,11 @@ $(document).ready(function(){ function reloadLatestBackups() { // make a GET request to get backup information to populate the table - $.get('/api/backups', function(data) { + $.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; @@ -126,7 +130,7 @@ $(document).ready(function(){ + ""; + + "
  • Delete
  • "; } var automaticRows = ""; @@ -243,7 +247,7 @@ $(document).ready(function(){ }).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 - reloadContentArchives(); + reloadLatestBackups(); }); } ) @@ -295,14 +299,14 @@ $(document).ready(function(){ // post the provided archive name to ask the server to kick off a manual backup $.ajax({ type: 'POST', - url: '/api/backup', + 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 - reloadContentArchives(); + reloadLatestBackups(); }).fail(function(jqXHR, textStatus, errorThrown) { }); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index f3765f6868..c23d17ed95 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1941,7 +1941,7 @@ 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"; @@ -2251,7 +2251,7 @@ 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(); From 6f8381d3787038175b3dff25d345675efeed9657 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 10:59:48 -0800 Subject: [PATCH 107/260] use automatic content archives group const --- domain-server/src/DomainServerSettingsManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index a50cde0807..a3f99facea 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -427,7 +427,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList // 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.backup_rules"; + const QString AUTO_CONTENT_ARCHIVES_RULES_KEYPATH = AUTOMATIC_CONTENT_ARCHIVES_GROUP + ".backup_rules"; QVariant* previousBackupsVariant = _configMap.valueForKeyPath(ENTITY_SERVER_BACKUPS_KEYPATH); From 2020ce5907e05338ec8f7e9fbda3a457dd198df0 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 11:20:02 -0800 Subject: [PATCH 108/260] add API to recover from content archive --- .../src/DomainContentBackupManager.cpp | 54 ++++++++++++++----- .../src/DomainContentBackupManager.h | 3 ++ domain-server/src/DomainServer.cpp | 17 +++++- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 345faffec4..379aa640f8 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -256,6 +257,22 @@ void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, cons }); } +bool DomainContentBackupManager::recoverFromBackupZip(QuaZip& zip, const QString& backupName) { + if (!zip.open(QuaZip::Mode::mdUnzip)) { + qWarning() << "Failed to unzip file: " << zip.getZipName(); + return false; + } else { + _isRecovering = true; + + for (auto& handler : _backupHandlers) { + handler->recoverBackup(zip); + } + + qDebug() << "Successfully started recovering from " << zip.getZipName(); + return true; + } +} + void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, const QString& backupName) { if (_isRecovering) { promise->resolve({ @@ -277,19 +294,9 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, QFile backupFile { backupDir.filePath(backupName) }; if (backupFile.open(QIODevice::ReadOnly)) { QuaZip zip { &backupFile }; - if (!zip.open(QuaZip::Mode::mdUnzip)) { - qWarning() << "Failed to unzip file: " << backupName; - success = false; - } else { - _isRecovering = true; - _recoveryFilename = backupName; - for (auto& handler : _backupHandlers) { - handler->recoverBackup(zip); - } - - qDebug() << "Successfully started recovering from " << backupName; - success = true; - } + + success = recoverFromBackupZip(zip, backupName); + backupFile.close(); } else { success = false; @@ -301,7 +308,28 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, }); } +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 }; + + bool success = recoverFromBackupZip(uploadedZip, MANUAL_BACKUP_PREFIX + "uploaded.zip"); + + promise->resolve({ + { "success", success } + }); +} + std::vector DomainContentBackupManager::getAllBackups() { + QDir backupDir { _backupDirectory }; auto matchingFiles = backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + "*.zip", MANUAL_BACKUP_PREFIX + "*.zip" }, diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index f1aa4acab2..d4b1f60b87 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -63,6 +63,7 @@ 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); @@ -85,6 +86,8 @@ protected: std::pair createBackup(const QString& prefix, const QString& name); + bool recoverFromBackupZip(QuaZip& backupZip, const QString& backupName); + private: const QString _backupDirectory; std::vector _backupHandlers; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index c23d17ed95..7fe1d1a0ab 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2273,10 +2273,25 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url || 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, formData[0].second)); + 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); From de75fe8e9f04618b0e3e9febbc6c2c6481d37e18 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 15:34:03 -0800 Subject: [PATCH 109/260] CR fix for typo in comment --- domain-server/src/DomainServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 7fe1d1a0ab..7cd6cd34fe 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2259,7 +2259,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url auto& firstFormData = formData[0]; // check the file extension to see what kind of file this is - // to match sure we handle this filetype for a content restore + // to make sure we handle this filetype for a content restore auto dispositionValue = QString(firstFormData.first.value("Content-Disposition")); auto formDataFilenameRegex = QRegExp("filename=\"(\\S+)\""); auto matchIndex = formDataFilenameRegex.indexIn(dispositionValue); From 40078450dd855cb4dc8ea371ee9bcf5a0d0cab90 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 11:20:02 -0800 Subject: [PATCH 110/260] add API to recover from content archive --- domain-server/src/DomainContentBackupManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 379aa640f8..40a2a55486 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -309,6 +309,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, } 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)); From cb747c9cdfc394d53b12ba91a6041cd96f807d10 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 17:32:53 -0800 Subject: [PATCH 111/260] refresh backups for availability and restore status --- .../resources/web/content/js/content.js | 89 +++++++++++++++---- domain-server/resources/web/css/style.css | 5 ++ .../resources/web/js/domain-server.js | 2 +- 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 1e5b6ac131..69a8c93f82 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -2,10 +2,19 @@ $(document).ready(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'; + + function progressBarHTML(extraClass, label) { + var html = "
    "; + html += "
    "; + html += label + "
    "; + return html; + } function setupBackupUpload() { // construct the HTML needed for the settings backup panel - var html = "
    "; + 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.
    "; @@ -13,7 +22,10 @@ $(document).ready(function(){ html += ""; html += ""; - html += "
    "; + html += "
    "; + html += "Restore in progress"; + html += progressBarHTML('recovery', 'Restoring'); + html += "
    "; $('#' + Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID + ' .panel-body').html(html); } @@ -71,6 +83,7 @@ $(document).ready(function(){ 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 = []; @@ -84,7 +97,9 @@ $(document).ready(function(){ html += ""; html += "
    " + backup.name + "" + return "
    " + backup.name + "" + moment(backup.createdAtMillis).format('lll') + "" + "" - + "
    "; - var backups_table_head = ""; + var backups_table_head = "" + + "" + + ""; html += backups_table_head; html += "
    Archive NameArchive DateActions
    Archive NameArchive DateActions
    "; @@ -105,7 +120,7 @@ $(document).ready(function(){ var BACKUP_DOWNLOAD_LINK_CLASS = 'download-backup'; var BACKUP_DELETE_LINK_CLASS = 'delete-backup'; - function reloadLatestBackups() { + function reloadBackupInformation() { // make a GET request to get backup information to populate the table $.ajax({ url: '/api/backups', @@ -125,7 +140,7 @@ $(document).ready(function(){ 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(data.status.recoveryProgress + "% Complete"); + } + + 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); + } 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 { + // no special status for this row, use an empty status column + $backupRow.find('td.backup-status').html(''); + } + + $backupRow.find('td.' + ACTION_MENU_CLASS + ' .dropdown').toggle(backup.isAvailable); + } + var automaticRows = ""; if (automaticBackups.length > 0) { for (var backupIndex in automaticBackups) { - // create a table row for this backup and add it to the rows we'll put in the table body - automaticRows += createBackupTableRow(automaticBackups[backupIndex]); + updateOrAddTableRow(automaticBackups[backupIndex], AUTOMATIC_ARCHIVES_TBODY_ID) } } - $('#' + AUTOMATIC_ARCHIVES_TBODY_ID).html(automaticRows); - - var manualRows = ""; - if (manualBackups.length > 0) { for (var backupIndex in manualBackups) { - // create a table row for this backup and add it to the rows we'll put in the table body - manualRows += createBackupTableRow(manualBackups[backupIndex]); + updateOrAddTableRow(manualBackups[backupIndex], MANUAL_ARCHIVES_TBODY_ID) } } - $('#' + MANUAL_ARCHIVES_TBODY_ID).html(manualRows); + // check if the restore action on all rows should be enabled or disabled + $('.' + 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); + } // tell bootstrap sortable to update for the new rows $.bootstrapSortable({ applyLast: true }); @@ -247,7 +299,7 @@ $(document).ready(function(){ }).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 - reloadLatestBackups(); + reloadBackupInformation(); }); } ) @@ -306,7 +358,7 @@ $(document).ready(function(){ }).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 - reloadLatestBackups(); + reloadBackupInformation(); }).fail(function(jqXHR, textStatus, errorThrown) { }); @@ -322,6 +374,9 @@ $(document).ready(function(){ setupContentArchives(); // load the latest backups immediately - reloadLatestBackups(); + reloadBackupInformation(); + + // setup a timer to reload them every 5 seconds + setTimeout(reloadBackupInformation(), 5000); }; }); diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 2bcc870ecf..62f442584e 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -466,6 +466,11 @@ 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; diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index 2c12e2683a..ed9559b6e9 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -62,7 +62,7 @@ $(document).ready(function(){ }, 1: { html_id: Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID, - label: 'Upload Content' + label: 'Upload Content Archive' } }; From faacd986b3924ccc092801a58e79b63f8fe9563f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 10:37:15 -0800 Subject: [PATCH 112/260] remove deleted backups from content archives tables --- .../resources/web/content/js/content.js | 46 +++++++++++++------ .../src/DomainServerSettingsManager.cpp | 3 +- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 69a8c93f82..5c2e134102 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -78,6 +78,8 @@ $(document).ready(function(){ }); 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'; @@ -90,10 +92,10 @@ $(document).ready(function(){ function setupContentArchives() { // construct the HTML needed for the content archives panel - var html = "
    "; + 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 domain content and settings backups. " - html += "Click here to manage automatic content archive intervals."; + html += "Click here to manage automatic content archive intervals."; html += "
    "; html += ""; @@ -110,7 +112,11 @@ $(document).ready(function(){ html += ""; html += "
    "; html += backups_table_head; - html += "
    "; + html += "
    "; + + html += ""; // put the base HTML in the content archives panel $('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html(html); @@ -119,7 +125,8 @@ $(document).ready(function(){ 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'; + function reloadBackupInformation() { // make a GET request to get backup information to populate the table $.ajax({ @@ -153,6 +160,10 @@ $(document).ready(function(){ $progressBar.find('.sr-only').html(data.status.recoveryProgress + "% 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 + "']"); @@ -169,7 +180,7 @@ $(document).ready(function(){ $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); + 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')); @@ -179,22 +190,29 @@ $(document).ready(function(){ } $backupRow.find('td.' + ACTION_MENU_CLASS + ' .dropdown').toggle(backup.isAvailable); + + $backupRow.addClass(ACTIVE_BACKUP_ROW_CLASS); } var automaticRows = ""; if (automaticBackups.length > 0) { for (var backupIndex in automaticBackups) { - updateOrAddTableRow(automaticBackups[backupIndex], AUTOMATIC_ARCHIVES_TBODY_ID) + updateOrAddTableRow(automaticBackups[backupIndex], AUTOMATIC_ARCHIVES_TBODY_ID); + } } if (manualBackups.length > 0) { for (var backupIndex in manualBackups) { - updateOrAddTableRow(manualBackups[backupIndex], MANUAL_ARCHIVES_TBODY_ID) + 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 + $('tbody tr:not(.' + ACTIVE_BACKUP_ROW_CLASS + ')').remove(); + // check if the restore action on all rows should be enabled or disabled $('.' + BACKUP_RESTORE_LINK_CLASS).parent().toggleClass('disabled', data.status.isRecovering); @@ -204,12 +222,15 @@ $(document).ready(function(){ // update the progress bars for current restore status if (data.status.isRecovering) { - updateProgressBars($('.recovery.progress-bar'), data.status.recoveryProgress); + 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 @@ -219,11 +240,8 @@ $(document).ready(function(){ // replace the content archives panel with a simple error message // stating that the user should reload the page - $('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html( - "
    " + - "There was a problem loading your list of automatic and manual content archives. Please reload the page to try again." + - "
    " - ); + $('#' + 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 @@ -377,6 +395,6 @@ $(document).ready(function(){ reloadBackupInformation(); // setup a timer to reload them every 5 seconds - setTimeout(reloadBackupInformation(), 5000); + setInterval(reloadBackupInformation, 5000); }; }); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index a3f99facea..cd7155d9da 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -1356,7 +1356,7 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt BLOCKING_INVOKE_METHOD(this, "settingsResponseObjectForType", Q_RETURN_ARG(QJsonObject, responseObject), - Q_ARG(const QString&, typeValue), + Q_ARG(QString, typeValue), Q_ARG(bool, isAuthenticated), Q_ARG(bool, includeDomainSettings), Q_ARG(bool, includeContentSettings), @@ -1374,6 +1374,7 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt // only enumerate the requested settings type (domain setting or content setting) QJsonArray* filteredDescriptionArray = &_descriptionArray; + if (includeDomainSettings && !includeContentSettings) { filteredDescriptionArray = &_domainSettingsDescription; } else if (includeContentSettings && !includeDomainSettings) { From 5dec3aba505a2ab8c3c210f19bb2441b457b1f73 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 10:49:57 -0800 Subject: [PATCH 113/260] fix download link and restore behaviour with pending --- domain-server/resources/web/content/js/content.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 5c2e134102..717f149760 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -126,7 +126,7 @@ $(document).ready(function(){ var BACKUP_DOWNLOAD_LINK_CLASS = 'download-backup'; var BACKUP_DELETE_LINK_CLASS = 'delete-backup'; var ACTIVE_BACKUP_ROW_CLASS = 'active-backup'; - + function reloadBackupInformation() { // make a GET request to get backup information to populate the table $.ajax({ @@ -151,8 +151,8 @@ $(document).ready(function(){ + ""; + + "
  • Download
  • " + + "
  • Delete
  • "; } function updateProgressBars($progressBar, value) { @@ -267,12 +267,11 @@ $(document).ready(function(){ "Restore content", function() { // show a spinner while we send off our request - showSpinnerAlert("Restoring Content Archive " + backupName); + showSpinnerAlert("Starting restore of " + backupName); // setup an AJAX POST to request content restore $.post('/api/backups/recover/' + backupID).done(function(data, textStatus, jqXHR) { swal.close(); - showRestartModal(); }).fail(function(jqXHR, textStatus, errorThrown) { showErrorMessage( "Error", From 494f93304b9eb84c5815e5e9a50b1c0dd57d421e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 10:58:32 -0800 Subject: [PATCH 114/260] take down AssetClient after content manager --- domain-server/src/DomainServer.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 7cd6cd34fe..584cbe3513 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -380,11 +380,6 @@ void DomainServer::parseCommandLine() { DomainServer::~DomainServer() { qInfo() << "Domain Server is shutting down."; - // cleanup the AssetClient thread - DependencyManager::destroy(); - _assetClientThread.quit(); - _assetClientThread.wait(); - // destroy the LimitedNodeList before the DomainServer QCoreApplication is down DependencyManager::destroy(); @@ -392,6 +387,11 @@ DomainServer::~DomainServer() { _contentManager->aboutToFinish(); _contentManager->terminate(); } + + // cleanup the AssetClient thread + DependencyManager::destroy(); + _assetClientThread.quit(); + _assetClientThread.wait(); } void DomainServer::queuedQuit(QString quitMessage, int exitCode) { From 441b55301f8637fa86f6c297266e4cd250f66252 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 11:01:22 -0800 Subject: [PATCH 115/260] cleanup LNL last during DS shutdown --- domain-server/src/DomainServer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 584cbe3513..5b8d253110 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -380,9 +380,6 @@ void DomainServer::parseCommandLine() { DomainServer::~DomainServer() { qInfo() << "Domain Server is shutting down."; - // destroy the LimitedNodeList before the DomainServer QCoreApplication is down - DependencyManager::destroy(); - if (_contentManager) { _contentManager->aboutToFinish(); _contentManager->terminate(); @@ -392,6 +389,9 @@ DomainServer::~DomainServer() { DependencyManager::destroy(); _assetClientThread.quit(); _assetClientThread.wait(); + + // destroy the LimitedNodeList before the DomainServer QCoreApplication is down + DependencyManager::destroy(); } void DomainServer::queuedQuit(QString quitMessage, int exitCode) { From 8e621a95a3c9abb5a0c4d948d60389f3b80d2533 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 11:04:58 -0800 Subject: [PATCH 116/260] fix typo in debug for writing new entities --- domain-server/src/DomainServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 5b8d253110..81dcf65be5 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1761,7 +1761,7 @@ void DomainServer::processOctreeDataPersistMessage(QSharedPointer Date: Fri, 16 Feb 2018 11:11:54 -0800 Subject: [PATCH 117/260] set DomainContentBackupManager object name so it appears on thread --- domain-server/src/DomainContentBackupManager.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 40a2a55486..f6c6e7a7ba 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -57,6 +57,8 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire _persistInterval(persistInterval), _lastCheck(usecTimestampNow()) { + setObjectName("DomainContentBackupManager"); + // Make sure the backup directory exists. QDir(_backupDirectory).mkpath("."); From 1c053730eb997c5d0f1b037c882c9ab9bbae669d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 14:09:00 -0800 Subject: [PATCH 118/260] make DomainServerSettingsManager thread-safe for use in content backup --- .../src/DomainContentBackupManager.cpp | 2 +- domain-server/src/DomainGatekeeper.cpp | 15 +- domain-server/src/DomainMetadata.cpp | 17 +- domain-server/src/DomainServer.cpp | 150 ++++++++---------- .../src/DomainServerSettingsManager.cpp | 95 +++++------ .../src/DomainServerSettingsManager.h | 26 +-- 6 files changed, 153 insertions(+), 152 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index f6c6e7a7ba..0bef6bb891 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -58,7 +58,7 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire _lastCheck(usecTimestampNow()) { setObjectName("DomainContentBackupManager"); - + // Make sure the backup directory exists. QDir(_backupDirectory).mkpath("."); diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 3aab7b4563..e697bbdda1 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.valueOrDefaultValueForKeyPath(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.valueOrDefaultValueForKeyPath(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 81dcf65be5..9cecea5f70 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -75,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; } @@ -112,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()); } @@ -417,8 +417,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 @@ -461,8 +461,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()) { @@ -472,9 +471,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() @@ -499,11 +498,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 { @@ -543,9 +542,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); @@ -647,8 +643,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) { @@ -656,8 +650,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(); } } @@ -687,9 +682,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 @@ -758,10 +753,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.isValid() && 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" @@ -892,31 +887,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); } } @@ -928,10 +918,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(); @@ -1954,13 +1944,12 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url 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; }; @@ -2028,8 +2017,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 @@ -2326,8 +2315,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url 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; } @@ -2360,8 +2349,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; } @@ -2409,7 +2398,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); @@ -2604,10 +2593,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-]+)($|;)"; @@ -2618,7 +2608,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."; } @@ -2628,13 +2618,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()) { @@ -2679,7 +2669,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"; @@ -2698,10 +2688,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(); @@ -2838,13 +2828,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"; @@ -2920,13 +2911,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) { @@ -3114,17 +3104,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; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index cd7155d9da..ad0381a697 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -38,6 +38,9 @@ #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"; @@ -190,6 +193,9 @@ void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointer(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 @@ -487,6 +482,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)) { @@ -576,15 +574,15 @@ bool DomainServerSettingsManager::unpackPermissionsForKeypath(const QString& key mapPointer->clear(); - QVariant* permissions = _configMap.valueForKeyPath(keyPath, true); - if (!permissions->canConvert(QMetaType::QVariantList)) { + QVariant permissions = valueForKeyPath(keyPath); + + 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(); @@ -1068,12 +1066,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 + _settingsLock.unlock(); + int dotIndex = keyPath.indexOf('.'); QString groupKey = keyPath.mid(0, dotIndex); @@ -1112,9 +1120,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"); @@ -1216,16 +1221,9 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection } bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType) { - - if (thread() != QThread::currentThread()) { - bool success; - BLOCKING_INVOKE_METHOD(this, "restoreSettingsFromObject", - Q_RETURN_ARG(bool, success), - Q_ARG(QJsonObject, settingsToRestore), - Q_ARG(SettingsType, settingsType)); - return success; - } + // grab a write lock since we're about to change the settings map + QWriteLocker locker(&_settingsLock); QJsonArray* filteredDescriptionArray = settingsType == DomainSettings ? &_domainSettingsDescription : &_contentSettingsDescription; @@ -1341,6 +1339,10 @@ 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; } @@ -1352,20 +1354,6 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt bool includeDefaults, bool isForBackup) { QJsonObject responseObject; - if (thread() != QThread::currentThread()) { - - BLOCKING_INVOKE_METHOD(this, "settingsResponseObjectForType", - Q_RETURN_ARG(QJsonObject, responseObject), - Q_ARG(QString, typeValue), - Q_ARG(bool, isAuthenticated), - Q_ARG(bool, includeDomainSettings), - Q_ARG(bool, includeContentSettings), - Q_ARG(bool, includeDefaults), - Q_ARG(bool, isForBackup)); - - return responseObject; - } - if (!typeValue.isEmpty() || isAuthenticated) { // convert the string type value to a QJsonValue QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt()); @@ -1414,21 +1402,21 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt 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 (includeDefaults || 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]; @@ -1567,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"; @@ -1664,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; } @@ -1690,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)) { @@ -1726,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 897a15485f..d81547410b 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"; @@ -53,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); } @@ -119,6 +117,8 @@ public: /// 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(); @@ -138,12 +138,13 @@ private: QStringList _argumentList; QJsonArray filteredDescriptionArray(bool isContentSettings); - 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(); @@ -155,10 +156,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); @@ -192,6 +193,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 From 679513599cce5f1b7a31b9312e3198c46bede1d2 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 14:09:14 -0800 Subject: [PATCH 119/260] fix row hiding and paste events for badging --- domain-server/resources/web/content/js/content.js | 4 ++-- domain-server/resources/web/js/base-settings.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 717f149760..525b989259 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -145,7 +145,7 @@ $(document).ready(function(){ // populate the backups tables with the backups function createBackupTableRow(backup) { return "" - + "" + backup.name + "" + + "" + backup.name + "" + moment(backup.createdAtMillis).format('lll') + "" + "