Merge branch 'nazara-next' of https://github.com/DigitalPulseSoftware/NazaraEngine into nazara-next
This commit is contained in:
11
src/Nazara/Core/ECS.cpp
Normal file
11
src/Nazara/Core/ECS.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (C) 2020 Jérôme Leclercq
|
||||
// This file is part of the "Nazara Engine - Core module"
|
||||
// For conditions of distribution and use, see copyright notice in Config.hpp
|
||||
|
||||
#include <Nazara/Core/ECS.hpp>
|
||||
#include <Nazara/Core/Debug.hpp>
|
||||
|
||||
namespace Nz
|
||||
{
|
||||
NAZARA_CORE_API ECS* ECS::s_instance = nullptr;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (C) 2021 Jérôme Leclercq
|
||||
// This file is part of the "Nazara Engine - Utility module"
|
||||
// For conditions of distribution and use, see copyright notice in Prerequisites.hpp
|
||||
|
||||
#include <Nazara/Graphics/Components/GraphicsComponent.hpp>
|
||||
#include <Nazara/Graphics/Debug.hpp>
|
||||
|
||||
namespace Nz
|
||||
{
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// For conditions of distribution and use, see copyright notice in Config.hpp
|
||||
|
||||
#include <Nazara/Graphics/Graphics.hpp>
|
||||
#include <Nazara/Core/ECS.hpp>
|
||||
#include <Nazara/Graphics/MaterialPipeline.hpp>
|
||||
#include <Nazara/Graphics/PredefinedShaderStructs.hpp>
|
||||
#include <stdexcept>
|
||||
@@ -18,6 +19,8 @@ namespace Nz
|
||||
Graphics::Graphics(Config config) :
|
||||
ModuleBase("Graphics", this)
|
||||
{
|
||||
ECS::RegisterComponents();
|
||||
|
||||
Renderer* renderer = Renderer::Instance();
|
||||
|
||||
const std::vector<RenderDeviceInfo>& renderDeviceInfo = renderer->QueryRenderDevices();
|
||||
@@ -42,6 +45,7 @@ namespace Nz
|
||||
|
||||
RenderDeviceFeatures enabledFeatures;
|
||||
enabledFeatures.anisotropicFiltering = renderDeviceInfo[bestRenderDeviceIndex].features.anisotropicFiltering;
|
||||
enabledFeatures.nonSolidFaceFilling = renderDeviceInfo[bestRenderDeviceIndex].features.nonSolidFaceFilling;
|
||||
|
||||
m_renderDevice = renderer->InstanciateRenderDevice(bestRenderDeviceIndex, enabledFeatures);
|
||||
if (!m_renderDevice)
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Nz
|
||||
}
|
||||
}
|
||||
|
||||
void Model::Draw(CommandBufferBuilder& commandBuffer, WorldInstance& instance) const
|
||||
void Model::Draw(CommandBufferBuilder& commandBuffer, const WorldInstance& instance) const
|
||||
{
|
||||
commandBuffer.BindShaderBinding(Graphics::WorldBindingSet, instance.GetShaderBinding());
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@ namespace Nz
|
||||
if ((params.type == GL::ContextType::OpenGL && glVersion >= 460) || m_referenceContext->IsExtensionSupported(GL::Extension::TextureFilterAnisotropic))
|
||||
m_deviceInfo.features.anisotropicFiltering = true;
|
||||
|
||||
if (m_referenceContext->glPolygonMode) //< not supported in core OpenGL ES, but supported in OpenGL or with GL_NV_polygon_mode extension
|
||||
m_deviceInfo.features.nonSolidFaceFilling = true;
|
||||
|
||||
// Limits
|
||||
GLint minUboOffsetAlignment;
|
||||
m_referenceContext->glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &minUboOffsetAlignment);
|
||||
|
||||
@@ -500,13 +500,11 @@ namespace Nz::GL
|
||||
m_state.renderStates.frontFace = targetFrontFace;
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: Use glPolyonMode if available (OpenGL)
|
||||
if (m_state.renderStates.faceFilling != renderStates.faceFilling)
|
||||
if (glPolygonMode && m_state.renderStates.faceFilling != renderStates.faceFilling)
|
||||
{
|
||||
glPolygonMode(GL_FRONT_AND_BACK, FaceFilling[states.faceFilling]);
|
||||
glPolygonMode(GL_FRONT_AND_BACK, ToOpenGL(renderStates.faceFilling));
|
||||
m_state.renderStates.faceFilling = renderStates.faceFilling;
|
||||
}*/
|
||||
}
|
||||
|
||||
if (renderStates.stencilTest)
|
||||
{
|
||||
@@ -663,6 +661,12 @@ namespace Nz::GL
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (function == "glPolygonMode")
|
||||
{
|
||||
constexpr std::size_t functionIndex = UnderlyingCast(FunctionIndex::glPolygonMode);
|
||||
|
||||
return loader.Load<PFNGLPOLYGONMODENVPROC, functionIndex>(glPolygonMode, "glPolygonModeNV", false, false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include <Nazara/Physics3D/Collider3D.hpp>
|
||||
#include <Nazara/Core/PrimitiveList.hpp>
|
||||
#include <Nazara/Physics3D/PhysWorld3D.hpp>
|
||||
#include <Nazara/Utility/IndexBuffer.hpp>
|
||||
#include <Nazara/Utility/StaticMesh.hpp>
|
||||
#include <Nazara/Utility/VertexBuffer.hpp>
|
||||
#include <newton/Newton.h>
|
||||
#include <Nazara/Physics3D/Debug.hpp>
|
||||
|
||||
@@ -139,6 +142,44 @@ namespace Nz
|
||||
NewtonCollisionForEachPolygonDo(m_handles.begin()->second, Nz::Matrix4f::Identity(), newtCallback, const_cast<void*>(static_cast<const void*>(&callback))); //< This isn't that bad; pointer will not be used for writing
|
||||
}
|
||||
|
||||
std::shared_ptr<StaticMesh> Collider3D::GenerateMesh() const
|
||||
{
|
||||
std::vector<Nz::Vector3f> colliderVertices;
|
||||
std::vector<Nz::UInt16> colliderIndices;
|
||||
|
||||
// Generate a line list
|
||||
ForEachPolygon([&](const Nz::Vector3f* vertices, std::size_t vertexCount)
|
||||
{
|
||||
Nz::UInt16 firstIndex = colliderVertices.size();
|
||||
for (std::size_t i = 0; i < vertexCount; ++i)
|
||||
colliderVertices.push_back(vertices[i]);
|
||||
|
||||
for (std::size_t i = 1; i < vertexCount; ++i)
|
||||
{
|
||||
colliderIndices.push_back(firstIndex + i - 1);
|
||||
colliderIndices.push_back(firstIndex + i);
|
||||
}
|
||||
|
||||
if (vertexCount > 2)
|
||||
{
|
||||
colliderIndices.push_back(firstIndex + vertexCount - 1);
|
||||
colliderIndices.push_back(firstIndex);
|
||||
}
|
||||
});
|
||||
|
||||
std::shared_ptr<Nz::VertexBuffer> colliderVB = std::make_shared<Nz::VertexBuffer>(Nz::VertexDeclaration::Get(Nz::VertexLayout::XYZ), colliderVertices.size(), Nz::DataStorage::Software, 0);
|
||||
colliderVB->Fill(colliderVertices.data(), 0, colliderVertices.size());
|
||||
|
||||
std::shared_ptr<Nz::IndexBuffer> colliderIB = std::make_shared<Nz::IndexBuffer>(false, colliderIndices.size(), Nz::DataStorage::Software, 0);
|
||||
colliderIB->Fill(colliderIndices.data(), 0, colliderIndices.size());
|
||||
|
||||
std::shared_ptr<Nz::StaticMesh> colliderSubMesh = std::make_shared<Nz::StaticMesh>(std::move(colliderVB), std::move(colliderIB));
|
||||
colliderSubMesh->GenerateAABB();
|
||||
colliderSubMesh->SetPrimitiveMode(Nz::PrimitiveMode::LineList);
|
||||
|
||||
return colliderSubMesh;
|
||||
}
|
||||
|
||||
NewtonCollision* Collider3D::GetHandle(PhysWorld3D* world) const
|
||||
{
|
||||
auto it = m_handles.find(world);
|
||||
|
||||
10
src/Nazara/Physics3D/Components/RigidBody3DComponent.cpp
Normal file
10
src/Nazara/Physics3D/Components/RigidBody3DComponent.cpp
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (C) 2021 Jérôme Leclercq
|
||||
// This file is part of the "Nazara Engine - Physics 3D module"
|
||||
// For conditions of distribution and use, see copyright notice in Prerequisites.hpp
|
||||
|
||||
#include <Nazara/Physics3D/Components/RigidBody3DComponent.hpp>
|
||||
#include <Nazara/Physics3D/Debug.hpp>
|
||||
|
||||
namespace Nz
|
||||
{
|
||||
}
|
||||
@@ -22,9 +22,22 @@ namespace Nz
|
||||
m_materialIds.emplace("default", NewtonMaterialGetDefaultGroupID(m_world));
|
||||
}
|
||||
|
||||
PhysWorld3D::PhysWorld3D(PhysWorld3D&& physWorld) noexcept :
|
||||
m_callbacks(std::move(physWorld.m_callbacks)),
|
||||
m_materialIds(std::move(physWorld.m_materialIds)),
|
||||
m_maxStepCount(std::move(physWorld.m_maxStepCount)),
|
||||
m_world(std::move(physWorld.m_world)),
|
||||
m_gravity(std::move(physWorld.m_gravity)),
|
||||
m_stepSize(std::move(physWorld.m_stepSize)),
|
||||
m_timestepAccumulator(std::move(physWorld.m_timestepAccumulator))
|
||||
{
|
||||
NewtonWorldSetUserData(m_world, this);
|
||||
}
|
||||
|
||||
PhysWorld3D::~PhysWorld3D()
|
||||
{
|
||||
NewtonDestroy(m_world);
|
||||
if (m_world)
|
||||
NewtonDestroy(m_world);
|
||||
}
|
||||
|
||||
int PhysWorld3D::CreateMaterial(std::string name)
|
||||
@@ -159,6 +172,24 @@ namespace Nz
|
||||
}
|
||||
}
|
||||
|
||||
PhysWorld3D& PhysWorld3D::operator=(PhysWorld3D&& physWorld) noexcept
|
||||
{
|
||||
if (m_world)
|
||||
NewtonDestroy(m_world);
|
||||
|
||||
m_callbacks = std::move(physWorld.m_callbacks);
|
||||
m_materialIds = std::move(physWorld.m_materialIds);
|
||||
m_maxStepCount = std::move(physWorld.m_maxStepCount);
|
||||
m_world = std::move(physWorld.m_world);
|
||||
m_gravity = std::move(physWorld.m_gravity);
|
||||
m_stepSize = std::move(physWorld.m_stepSize);
|
||||
m_timestepAccumulator = std::move(physWorld.m_timestepAccumulator);
|
||||
|
||||
NewtonWorldSetUserData(m_world, this);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
int PhysWorld3D::OnAABBOverlap(const NewtonJoint* const contactJoint, float /*timestep*/, int /*threadIndex*/)
|
||||
{
|
||||
RigidBody3D* bodyA = static_cast<RigidBody3D*>(NewtonBodyGetUserData(NewtonJointGetBody0(contactJoint)));
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <Nazara/Physics3D/Physics3D.hpp>
|
||||
#include <Nazara/Core/Core.hpp>
|
||||
#include <Nazara/Core/ECS.hpp>
|
||||
#include <Nazara/Core/Error.hpp>
|
||||
#include <Nazara/Core/Log.hpp>
|
||||
#include <Nazara/Physics3D/Config.hpp>
|
||||
@@ -16,6 +17,7 @@ namespace Nz
|
||||
Physics3D::Physics3D(Config /*config*/) :
|
||||
ModuleBase("Physics3D", this)
|
||||
{
|
||||
ECS::RegisterComponents();
|
||||
}
|
||||
|
||||
unsigned int Physics3D::GetMemoryUsed()
|
||||
|
||||
@@ -61,17 +61,17 @@ namespace Nz
|
||||
SetRotation(object.GetRotation());
|
||||
}
|
||||
|
||||
RigidBody3D::RigidBody3D(RigidBody3D&& object) :
|
||||
RigidBody3D::RigidBody3D(RigidBody3D&& object) noexcept :
|
||||
m_geom(std::move(object.m_geom)),
|
||||
m_matrix(std::move(object.m_matrix)),
|
||||
m_forceAccumulator(std::move(object.m_forceAccumulator)),
|
||||
m_torqueAccumulator(std::move(object.m_torqueAccumulator)),
|
||||
m_body(object.m_body),
|
||||
m_body(std::move(object.m_body)),
|
||||
m_world(object.m_world),
|
||||
m_gravityFactor(object.m_gravityFactor),
|
||||
m_mass(object.m_mass)
|
||||
{
|
||||
object.m_body = nullptr;
|
||||
NewtonBodySetUserData(m_body, this);
|
||||
}
|
||||
|
||||
RigidBody3D::~RigidBody3D()
|
||||
@@ -380,6 +380,24 @@ namespace Nz
|
||||
return operator=(std::move(physObj));
|
||||
}
|
||||
|
||||
RigidBody3D& RigidBody3D::operator=(RigidBody3D&& object) noexcept
|
||||
{
|
||||
if (m_body)
|
||||
NewtonDestroyBody(m_body);
|
||||
|
||||
m_body = std::move(object.m_body);
|
||||
m_forceAccumulator = std::move(object.m_forceAccumulator);
|
||||
m_geom = std::move(object.m_geom);
|
||||
m_gravityFactor = object.m_gravityFactor;
|
||||
m_mass = object.m_mass;
|
||||
m_matrix = std::move(object.m_matrix);
|
||||
m_torqueAccumulator = std::move(object.m_torqueAccumulator);
|
||||
m_world = object.m_world;
|
||||
|
||||
NewtonBodySetUserData(m_body, this);
|
||||
return *this;
|
||||
}
|
||||
|
||||
void RigidBody3D::UpdateBody()
|
||||
{
|
||||
NewtonBodySetMatrix(m_body, m_matrix);
|
||||
@@ -401,25 +419,6 @@ namespace Nz
|
||||
}
|
||||
}
|
||||
|
||||
RigidBody3D& RigidBody3D::operator=(RigidBody3D&& object)
|
||||
{
|
||||
if (m_body)
|
||||
NewtonDestroyBody(m_body);
|
||||
|
||||
m_body = object.m_body;
|
||||
m_forceAccumulator = std::move(object.m_forceAccumulator);
|
||||
m_geom = std::move(object.m_geom);
|
||||
m_gravityFactor = object.m_gravityFactor;
|
||||
m_mass = object.m_mass;
|
||||
m_matrix = std::move(object.m_matrix);
|
||||
m_torqueAccumulator = std::move(object.m_torqueAccumulator);
|
||||
m_world = object.m_world;
|
||||
|
||||
object.m_body = nullptr;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
void RigidBody3D::ForceAndTorqueCallback(const NewtonBody* body, float timeStep, int threadIndex)
|
||||
{
|
||||
NazaraUnused(timeStep);
|
||||
|
||||
45
src/Nazara/Physics3D/Systems/Physics3DSystem.cpp
Normal file
45
src/Nazara/Physics3D/Systems/Physics3DSystem.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (C) 2021 Jérôme Leclercq
|
||||
// This file is part of the "Nazara Engine - Utility module"
|
||||
// For conditions of distribution and use, see copyright notice in Prerequisites.hpp
|
||||
|
||||
#include <Nazara/Physics3D/Systems/Physics3DSystem.hpp>
|
||||
#include <Nazara/Utility/Components/NodeComponent.hpp>
|
||||
#include <Nazara/Physics3D/Debug.hpp>
|
||||
|
||||
namespace Nz
|
||||
{
|
||||
Physics3DSystem::Physics3DSystem(entt::registry& registry)
|
||||
{
|
||||
m_constructConnection = registry.on_construct<RigidBody3DComponent>().connect<OnConstruct>();
|
||||
}
|
||||
|
||||
Physics3DSystem::~Physics3DSystem()
|
||||
{
|
||||
m_constructConnection.release();
|
||||
}
|
||||
|
||||
void Physics3DSystem::Update(entt::registry& registry, float elapsedTime)
|
||||
{
|
||||
m_physWorld.Step(elapsedTime);
|
||||
|
||||
// Replicate rigid body position to their node components
|
||||
auto velView = registry.view<Nz::NodeComponent, const RigidBody3DComponent>();
|
||||
for (auto [entity, nodeComponent, rigidBodyComponent] : velView.each())
|
||||
{
|
||||
nodeComponent.SetPosition(rigidBodyComponent.GetPosition(), CoordSys::Global);
|
||||
nodeComponent.SetRotation(rigidBodyComponent.GetRotation(), CoordSys::Global);
|
||||
}
|
||||
}
|
||||
|
||||
void Physics3DSystem::OnConstruct(entt::registry& registry, entt::entity entity)
|
||||
{
|
||||
// If our entity already has a node component when adding a rigid body, initialize it with
|
||||
Nz::NodeComponent* node = registry.try_get<NodeComponent>(entity);
|
||||
if (node)
|
||||
{
|
||||
RigidBody3DComponent& rigidBody = registry.get<RigidBody3DComponent>(entity);
|
||||
rigidBody.SetPosition(node->GetPosition());
|
||||
rigidBody.SetRotation(node->GetRotation());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (C) 2021 Jérôme Leclercq
|
||||
// This file is part of the "Nazara Engine - Utility module"
|
||||
// For conditions of distribution and use, see copyright notice in Prerequisites.hpp
|
||||
|
||||
#include <Nazara/Utility/Components/NodeComponent.hpp>
|
||||
#include <Nazara/Core/Error.hpp>
|
||||
#include <Nazara/Utility/Debug.hpp>
|
||||
|
||||
namespace Nz
|
||||
{
|
||||
void NodeComponent::SetParent(entt::registry& registry, entt::entity entity, bool keepDerived)
|
||||
{
|
||||
NodeComponent* nodeComponent = registry.try_get<NodeComponent>(entity);
|
||||
NazaraAssert(nodeComponent, "entity doesn't have a NodeComponent");
|
||||
|
||||
Node::SetParent(nodeComponent, keepDerived);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,35 @@ namespace Nz
|
||||
SetParent(node.m_parent, false);
|
||||
}
|
||||
|
||||
Node::Node(Node&& node) noexcept :
|
||||
m_childs(std::move(node.m_childs)),
|
||||
m_initialRotation(node.m_initialRotation),
|
||||
m_rotation(node.m_rotation),
|
||||
m_initialPosition(node.m_initialPosition),
|
||||
m_initialScale(node.m_initialScale),
|
||||
m_position(node.m_position),
|
||||
m_scale(node.m_scale),
|
||||
m_parent(node.m_parent),
|
||||
m_derivedUpdated(false),
|
||||
m_inheritPosition(node.m_inheritPosition),
|
||||
m_inheritRotation(node.m_inheritRotation),
|
||||
m_inheritScale(node.m_inheritScale),
|
||||
m_transformMatrixUpdated(false),
|
||||
OnNodeInvalidation(std::move(node.OnNodeInvalidation)),
|
||||
OnNodeNewParent(std::move(node.OnNodeNewParent)),
|
||||
OnNodeRelease(std::move(node.OnNodeRelease))
|
||||
{
|
||||
if (m_parent)
|
||||
{
|
||||
m_parent->RemoveChild(&node);
|
||||
m_parent->AddChild(this);
|
||||
node.m_parent = nullptr;
|
||||
}
|
||||
|
||||
for (Node* child : m_childs)
|
||||
child->m_parent = this;
|
||||
}
|
||||
|
||||
Node::~Node()
|
||||
{
|
||||
OnNodeRelease(this);
|
||||
@@ -643,6 +672,42 @@ namespace Nz
|
||||
return *this;
|
||||
}
|
||||
|
||||
Node& Node::operator=(Node&& node) noexcept
|
||||
{
|
||||
if (m_parent)
|
||||
SetParent(nullptr);
|
||||
|
||||
m_inheritPosition = node.m_inheritPosition;
|
||||
m_inheritRotation = node.m_inheritRotation;
|
||||
m_inheritScale = node.m_inheritScale;
|
||||
m_initialPosition = node.m_initialPosition;
|
||||
m_initialRotation = node.m_initialRotation;
|
||||
m_initialScale = node.m_initialScale;
|
||||
m_position = node.m_position;
|
||||
m_rotation = node.m_rotation;
|
||||
m_scale = node.m_scale;
|
||||
|
||||
m_childs = std::move(node.m_childs);
|
||||
for (Node* child : m_childs)
|
||||
child->m_parent = this;
|
||||
|
||||
m_parent = node.m_parent;
|
||||
if (m_parent)
|
||||
{
|
||||
m_parent->RemoveChild(&node);
|
||||
m_parent->AddChild(this);
|
||||
node.m_parent = nullptr;
|
||||
}
|
||||
|
||||
OnNodeInvalidation = std::move(node.OnNodeInvalidation);
|
||||
OnNodeNewParent = std::move(node.OnNodeNewParent);
|
||||
OnNodeRelease = std::move(node.OnNodeRelease);
|
||||
|
||||
InvalidateNode();
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Node::AddChild(Node* node) const
|
||||
{
|
||||
#ifdef NAZARA_DEBUG
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <Nazara/Utility/Utility.hpp>
|
||||
#include <Nazara/Core/CallOnExit.hpp>
|
||||
#include <Nazara/Core/Core.hpp>
|
||||
#include <Nazara/Core/ECS.hpp>
|
||||
#include <Nazara/Core/Error.hpp>
|
||||
#include <Nazara/Utility/Animation.hpp>
|
||||
#include <Nazara/Utility/Buffer.hpp>
|
||||
@@ -39,6 +40,8 @@ namespace Nz
|
||||
Utility::Utility(Config /*config*/) :
|
||||
ModuleBase("Utility", this)
|
||||
{
|
||||
ECS::RegisterComponents();
|
||||
|
||||
if (!Buffer::Initialize())
|
||||
throw std::runtime_error("failed to initialize buffers");
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace Nz
|
||||
deviceInfo.name = physDevice.properties.deviceName;
|
||||
|
||||
deviceInfo.features.anisotropicFiltering = physDevice.features.samplerAnisotropy;
|
||||
deviceInfo.features.nonSolidFaceFilling = physDevice.features.fillModeNonSolid;
|
||||
|
||||
deviceInfo.limits.minUniformBufferOffsetAlignment = physDevice.properties.limits.minUniformBufferOffsetAlignment;
|
||||
|
||||
@@ -274,7 +275,7 @@ namespace Nz
|
||||
|
||||
VkValidationFeaturesEXT features = { VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT };
|
||||
|
||||
std::vector<VkValidationFeatureEnableEXT> enabledFeatures = {
|
||||
std::array<VkValidationFeatureEnableEXT, 1> enabledFeatures = {
|
||||
//VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_EXT,
|
||||
//VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_RESERVE_BINDING_SLOT_EXT,
|
||||
VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT
|
||||
|
||||
Reference in New Issue
Block a user