mirror of
https://github.com/NoelFB/blah.git
synced 2025-07-18 19:41:52 +08:00
restructured project to match a more standard cmake setup
This commit is contained in:
25
src/graphics/blend.cpp
Normal file
25
src/graphics/blend.cpp
Normal file
@ -0,0 +1,25 @@
|
||||
#include <blah/graphics/blend.h>
|
||||
|
||||
using namespace Blah;
|
||||
|
||||
const BlendMode BlendMode::Normal = BlendMode(
|
||||
BlendOp::Add,
|
||||
BlendFactor::One,
|
||||
BlendFactor::OneMinusSrcAlpha,
|
||||
BlendOp::Add,
|
||||
BlendFactor::One,
|
||||
BlendFactor::OneMinusSrcAlpha,
|
||||
BlendMask::RGBA,
|
||||
0xffffffff
|
||||
);
|
||||
|
||||
const BlendMode BlendMode::Subtract = BlendMode(
|
||||
BlendOp::ReverseSubtract,
|
||||
BlendFactor::One,
|
||||
BlendFactor::One,
|
||||
BlendOp::Add,
|
||||
BlendFactor::One,
|
||||
BlendFactor::One,
|
||||
BlendMask::RGBA,
|
||||
0xffffffff
|
||||
);
|
35
src/graphics/framebuffer.cpp
Normal file
35
src/graphics/framebuffer.cpp
Normal file
@ -0,0 +1,35 @@
|
||||
#include <blah/graphics/framebuffer.h>
|
||||
#include "../internal/graphics_backend.h"
|
||||
|
||||
using namespace Blah;
|
||||
|
||||
FrameBufferRef FrameBuffer::create(int width, int height)
|
||||
{
|
||||
static const TextureFormat attachment = TextureFormat::RGBA;
|
||||
return create(width, height, &attachment, 1);
|
||||
}
|
||||
|
||||
FrameBufferRef FrameBuffer::create(int width, int height, const TextureFormat* attachments, int attachment_count)
|
||||
{
|
||||
BLAH_ASSERT(width > 0 && height > 0, "FrameBuffer width and height must be larger than 0");
|
||||
BLAH_ASSERT(attachment_count <= BLAH_ATTACHMENTS, "Exceeded maximum attachment count");
|
||||
BLAH_ASSERT(attachment_count > 0, "At least one attachment must be provided");
|
||||
|
||||
int color_count = 0;
|
||||
int depth_count = 0;
|
||||
|
||||
for (int i = 0; i < attachment_count; i++)
|
||||
{
|
||||
BLAH_ASSERT((int)attachments[i] > (int)TextureFormat::None && (int)attachments[i] < (int)TextureFormat::Count, "Invalid texture format");
|
||||
|
||||
if (attachments[i] == TextureFormat::DepthStencil)
|
||||
depth_count++;
|
||||
else
|
||||
color_count++;
|
||||
}
|
||||
|
||||
BLAH_ASSERT(depth_count <= 1, "FrameBuffer can only have 1 Depth/Stencil Texture");
|
||||
BLAH_ASSERT(color_count <= BLAH_ATTACHMENTS - 1, "Exceeded maximum Color attachment count");
|
||||
|
||||
return GraphicsBackend::create_framebuffer(width, height, attachments, attachment_count);
|
||||
}
|
355
src/graphics/material.cpp
Normal file
355
src/graphics/material.cpp
Normal file
@ -0,0 +1,355 @@
|
||||
#include <blah/graphics/material.h>
|
||||
#include <blah/core/log.h>
|
||||
|
||||
using namespace Blah;
|
||||
|
||||
namespace
|
||||
{
|
||||
int calc_uniform_size(const UniformInfo& uniform)
|
||||
{
|
||||
int components = 0;
|
||||
|
||||
switch (uniform.type)
|
||||
{
|
||||
case UniformType::Float: components = 1; break;
|
||||
case UniformType::Float2: components = 2; break;
|
||||
case UniformType::Float3: components = 3; break;
|
||||
case UniformType::Float4: components = 4; break;
|
||||
case UniformType::Mat3x2: components = 6; break;
|
||||
case UniformType::Mat4x4: components = 16; break;
|
||||
default:
|
||||
BLAH_ERROR("Unespected Uniform Type");
|
||||
break;
|
||||
}
|
||||
|
||||
return components * uniform.array_length;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialRef Material::create(const ShaderRef& shader)
|
||||
{
|
||||
BLAH_ASSERT(shader, "The provided shader is invalid");
|
||||
|
||||
if (shader)
|
||||
return MaterialRef(new Material(shader));
|
||||
|
||||
return MaterialRef();
|
||||
}
|
||||
|
||||
Material::Material(const ShaderRef& shader)
|
||||
{
|
||||
BLAH_ASSERT(shader, "Material is being created with an invalid shader");
|
||||
m_shader = shader;
|
||||
|
||||
auto& uniforms = shader->uniforms();
|
||||
int float_size = 0;
|
||||
|
||||
for (auto& uniform : uniforms)
|
||||
{
|
||||
if (uniform.type == UniformType::None)
|
||||
continue;
|
||||
|
||||
if (uniform.type == UniformType::Texture2D)
|
||||
{
|
||||
for (int i = 0; i < uniform.array_length; i ++)
|
||||
m_textures.push_back(TextureRef());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (uniform.type == UniformType::Sampler2D)
|
||||
{
|
||||
for (int i = 0; i < uniform.array_length; i++)
|
||||
m_samplers.push_back(TextureSampler());
|
||||
continue;
|
||||
}
|
||||
|
||||
float_size += calc_uniform_size(uniform);
|
||||
}
|
||||
|
||||
m_data.expand(float_size);
|
||||
}
|
||||
|
||||
const ShaderRef Material::shader() const
|
||||
{
|
||||
return m_shader;
|
||||
}
|
||||
|
||||
void Material::set_texture(const char* name, const TextureRef& texture, int index)
|
||||
{
|
||||
BLAH_ASSERT(m_shader, "Material Shader is invalid");
|
||||
|
||||
int offset = 0;
|
||||
for (auto& uniform : m_shader->uniforms())
|
||||
{
|
||||
if (uniform.type != UniformType::Texture2D)
|
||||
continue;
|
||||
|
||||
if (strcmp(uniform.name, name) == 0)
|
||||
{
|
||||
m_textures[offset + index] = texture;
|
||||
return;
|
||||
}
|
||||
|
||||
offset += uniform.array_length;
|
||||
if (offset + index >= m_textures.size())
|
||||
break;
|
||||
}
|
||||
|
||||
Log::warn("No Texture Uniform '%s' at index [%i] exists", name, index);
|
||||
}
|
||||
|
||||
void Material::set_texture(int slot, const TextureRef& texture, int index)
|
||||
{
|
||||
BLAH_ASSERT(m_shader, "Material Shader is invalid");
|
||||
|
||||
int s = 0;
|
||||
int offset = 0;
|
||||
for (auto& uniform : m_shader->uniforms())
|
||||
{
|
||||
if (uniform.type != UniformType::Texture2D)
|
||||
continue;
|
||||
|
||||
if (s == slot)
|
||||
{
|
||||
if (index > uniform.array_length)
|
||||
break;
|
||||
|
||||
m_textures[offset + index] = texture;
|
||||
break;
|
||||
}
|
||||
|
||||
offset += uniform.array_length;
|
||||
s++;
|
||||
}
|
||||
}
|
||||
|
||||
TextureRef Material::get_texture(const char* name, int index) const
|
||||
{
|
||||
BLAH_ASSERT(m_shader, "Material Shader is invalid");
|
||||
|
||||
int offset = 0;
|
||||
for (auto& uniform : m_shader->uniforms())
|
||||
{
|
||||
if (uniform.type != UniformType::Texture2D)
|
||||
continue;
|
||||
|
||||
if (strcmp(uniform.name, name) == 0)
|
||||
return m_textures[offset + index];
|
||||
|
||||
offset += uniform.array_length;
|
||||
if (offset + index >= m_textures.size())
|
||||
break;
|
||||
}
|
||||
|
||||
Log::warn("No Texture Uniform '%s' at index [%i] exists", name, index);
|
||||
return TextureRef();
|
||||
}
|
||||
|
||||
TextureRef Material::get_texture(int slot, int index) const
|
||||
{
|
||||
BLAH_ASSERT(m_shader, "Material Shader is invalid");
|
||||
|
||||
int s = 0;
|
||||
int offset = 0;
|
||||
for (auto& uniform : m_shader->uniforms())
|
||||
{
|
||||
if (uniform.type != UniformType::Texture2D)
|
||||
continue;
|
||||
|
||||
if (s == slot)
|
||||
{
|
||||
if (index > uniform.array_length)
|
||||
break;
|
||||
|
||||
return m_textures[offset + index];
|
||||
}
|
||||
|
||||
offset += uniform.array_length;
|
||||
if (offset + index >= m_textures.size())
|
||||
break;
|
||||
|
||||
s++;
|
||||
}
|
||||
|
||||
Log::warn("No Texture Uniform ['%i'] at index [%i] exists", slot, index);
|
||||
return TextureRef();
|
||||
}
|
||||
|
||||
void Material::set_sampler(const char* name, const TextureSampler& sampler, int index)
|
||||
{
|
||||
BLAH_ASSERT(m_shader, "Material Shader is invalid");
|
||||
|
||||
int offset = 0;
|
||||
for (auto& uniform : m_shader->uniforms())
|
||||
{
|
||||
if (uniform.type != UniformType::Sampler2D)
|
||||
continue;
|
||||
|
||||
if (strcmp(uniform.name, name) == 0)
|
||||
{
|
||||
m_samplers[offset + index] = sampler;
|
||||
return;
|
||||
}
|
||||
|
||||
offset += uniform.array_length;
|
||||
if (offset + index >= m_samplers.size())
|
||||
break;
|
||||
}
|
||||
|
||||
Log::warn("No Sampler Uniform '%s' at index [%i] exists", name, index);
|
||||
}
|
||||
|
||||
void Material::set_sampler(int slot, const TextureSampler& sampler, int index)
|
||||
{
|
||||
BLAH_ASSERT(m_shader, "Material Shader is invalid");
|
||||
|
||||
int s = 0;
|
||||
int offset = 0;
|
||||
for (auto& uniform : m_shader->uniforms())
|
||||
{
|
||||
if (uniform.type != UniformType::Sampler2D)
|
||||
continue;
|
||||
|
||||
if (s == slot)
|
||||
{
|
||||
if (index > uniform.array_length)
|
||||
break;
|
||||
|
||||
m_samplers[offset + index] = sampler;
|
||||
break;
|
||||
}
|
||||
|
||||
offset += uniform.array_length;
|
||||
s++;
|
||||
}
|
||||
}
|
||||
|
||||
TextureSampler Material::get_sampler(const char* name, int index) const
|
||||
{
|
||||
BLAH_ASSERT(m_shader, "Material Shader is invalid");
|
||||
|
||||
int offset = 0;
|
||||
for (auto& uniform : m_shader->uniforms())
|
||||
{
|
||||
if (uniform.type != UniformType::Sampler2D)
|
||||
continue;
|
||||
|
||||
if (strcmp(uniform.name, name) == 0)
|
||||
return m_samplers[offset + index];
|
||||
|
||||
offset += uniform.array_length;
|
||||
if (offset + index >= m_samplers.size())
|
||||
break;
|
||||
}
|
||||
|
||||
Log::warn("No Sampler Uniform '%s' at index [%i] exists", name, index);
|
||||
return TextureSampler();
|
||||
}
|
||||
|
||||
TextureSampler Material::get_sampler(int slot, int index) const
|
||||
{
|
||||
BLAH_ASSERT(m_shader, "Material Shader is invalid");
|
||||
|
||||
int s = 0;
|
||||
int offset = 0;
|
||||
for (auto& uniform : m_shader->uniforms())
|
||||
{
|
||||
if (uniform.type != UniformType::Sampler2D)
|
||||
continue;
|
||||
|
||||
if (s == slot)
|
||||
{
|
||||
if (index > uniform.array_length)
|
||||
break;
|
||||
|
||||
return m_samplers[offset + index];
|
||||
}
|
||||
|
||||
offset += uniform.array_length;
|
||||
if (offset + index >= m_samplers.size())
|
||||
break;
|
||||
|
||||
s++;
|
||||
}
|
||||
|
||||
Log::warn("No Sampler Uniform ['%i'] at index [%i] exists", slot, index);
|
||||
return TextureSampler();
|
||||
}
|
||||
|
||||
void Material::set_value(const char* name, const float* value, int64_t length)
|
||||
{
|
||||
BLAH_ASSERT(m_shader, "Material Shader is invalid");
|
||||
BLAH_ASSERT(length >= 0, "Length must be >= 0");
|
||||
|
||||
int index = 0;
|
||||
int offset = 0;
|
||||
for (auto& uniform : m_shader->uniforms())
|
||||
{
|
||||
if (uniform.type == UniformType::Texture2D ||
|
||||
uniform.type == UniformType::Sampler2D ||
|
||||
uniform.type == UniformType::None)
|
||||
continue;
|
||||
|
||||
if (strcmp(uniform.name, name) == 0)
|
||||
{
|
||||
auto max = calc_uniform_size(uniform);
|
||||
if (length > max)
|
||||
{
|
||||
Log::warn("Exceeding length of Uniform '%s' (%i / %i)", name, length, max);
|
||||
length = max;
|
||||
}
|
||||
|
||||
memcpy(m_data.begin() + offset, value, sizeof(float) * length);
|
||||
return;
|
||||
}
|
||||
|
||||
offset += calc_uniform_size(uniform);
|
||||
index++;
|
||||
}
|
||||
|
||||
Log::warn("No Uniform '%s' exists", name);
|
||||
}
|
||||
|
||||
const float* Material::get_value(const char* name, int64_t* length) const
|
||||
{
|
||||
BLAH_ASSERT(m_shader, "Material Shader is invalid");
|
||||
|
||||
int index = 0;
|
||||
int offset = 0;
|
||||
for (auto& uniform : m_shader->uniforms())
|
||||
{
|
||||
if (uniform.type == UniformType::Texture2D ||
|
||||
uniform.type == UniformType::Sampler2D ||
|
||||
uniform.type == UniformType::None)
|
||||
continue;
|
||||
|
||||
if (strcmp(uniform.name, name) == 0)
|
||||
{
|
||||
if (length != nullptr)
|
||||
*length = calc_uniform_size(uniform);
|
||||
return m_data.begin() + offset;
|
||||
}
|
||||
|
||||
index++;
|
||||
offset += calc_uniform_size(uniform);
|
||||
}
|
||||
|
||||
*length = 0;
|
||||
return nullptr;
|
||||
Log::warn("No Uniform '%s' exists", name);
|
||||
}
|
||||
|
||||
const Vector<TextureRef>& Material::textures() const
|
||||
{
|
||||
return m_textures;
|
||||
}
|
||||
|
||||
const Vector<TextureSampler>& Material::samplers() const
|
||||
{
|
||||
return m_samplers;
|
||||
}
|
||||
|
||||
const float* Material::data() const
|
||||
{
|
||||
return m_data.begin();
|
||||
}
|
40
src/graphics/mesh.cpp
Normal file
40
src/graphics/mesh.cpp
Normal file
@ -0,0 +1,40 @@
|
||||
#include <blah/graphics/mesh.h>
|
||||
#include "../internal/graphics_backend.h"
|
||||
|
||||
using namespace Blah;
|
||||
|
||||
|
||||
MeshRef Mesh::create()
|
||||
{
|
||||
return GraphicsBackend::create_mesh();
|
||||
}
|
||||
|
||||
VertexFormat::VertexFormat(std::initializer_list<VertexAttribute> attributes, int stride)
|
||||
{
|
||||
for (auto& it : attributes)
|
||||
this->attributes.push_back(it);
|
||||
|
||||
if (stride <= 0)
|
||||
{
|
||||
stride = 0;
|
||||
|
||||
for (auto& it : attributes)
|
||||
{
|
||||
switch (it.type)
|
||||
{
|
||||
case VertexType::Float: stride += 4; break;
|
||||
case VertexType::Float2: stride += 8; break;
|
||||
case VertexType::Float3: stride += 12; break;
|
||||
case VertexType::Float4: stride += 16; break;
|
||||
case VertexType::Byte4: stride += 4; break;
|
||||
case VertexType::UByte4: stride += 4; break;
|
||||
case VertexType::Short2: stride += 4; break;
|
||||
case VertexType::UShort2: stride += 4; break;
|
||||
case VertexType::Short4: stride += 8; break;
|
||||
case VertexType::UShort4: stride += 8; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->stride = stride;
|
||||
}
|
94
src/graphics/renderpass.cpp
Normal file
94
src/graphics/renderpass.cpp
Normal file
@ -0,0 +1,94 @@
|
||||
#include <blah/graphics/renderpass.h>
|
||||
#include <blah/core/log.h>
|
||||
#include "../internal/graphics_backend.h"
|
||||
|
||||
using namespace Blah;
|
||||
|
||||
RenderPass::RenderPass()
|
||||
{
|
||||
blend = BlendMode::Normal;
|
||||
target = App::backbuffer;
|
||||
mesh = MeshRef();
|
||||
material = MaterialRef();
|
||||
has_viewport = false;
|
||||
has_scissor = false;
|
||||
viewport = Rect();
|
||||
scissor = Rect();
|
||||
index_start = 0;
|
||||
index_count = 0;
|
||||
instance_count = 0;
|
||||
depth = Compare::None;
|
||||
cull = Cull::None;
|
||||
}
|
||||
|
||||
void RenderPass::perform()
|
||||
{
|
||||
BLAH_ASSERT(material, "Trying to draw with an invalid Material");
|
||||
BLAH_ASSERT(material->shader(), "Trying to draw with an invalid Shader");
|
||||
BLAH_ASSERT(mesh, "Trying to draw with an invalid Mesh");
|
||||
|
||||
// copy call
|
||||
RenderPass pass = *this;
|
||||
|
||||
// Validate Backbuffer
|
||||
if (!pass.target)
|
||||
{
|
||||
pass.target = App::backbuffer;
|
||||
Log::warn("Trying to draw with an invalid Target; falling back to Back Buffer");
|
||||
}
|
||||
|
||||
// Validate Index Count
|
||||
int64_t index_count = pass.mesh->index_count();
|
||||
if (pass.index_start + pass.index_count > index_count)
|
||||
{
|
||||
Log::warn(
|
||||
"Trying to draw more indices than exist in the index buffer (%i-%i / %i); trimming extra indices",
|
||||
pass.index_start,
|
||||
pass.index_start + pass.index_count,
|
||||
index_count);
|
||||
|
||||
if (pass.index_start > pass.index_count)
|
||||
return;
|
||||
|
||||
pass.index_count = pass.index_count - pass.index_start;
|
||||
}
|
||||
|
||||
// Validate Instance Count
|
||||
int64_t instance_count = pass.mesh->instance_count();
|
||||
if (pass.instance_count > instance_count)
|
||||
{
|
||||
Log::warn(
|
||||
"Trying to draw more instances than exist in the index buffer (%i / %i); trimming extra instances",
|
||||
pass.instance_count,
|
||||
instance_count);
|
||||
|
||||
pass.instance_count = instance_count;
|
||||
}
|
||||
|
||||
// get the total drawable size
|
||||
Vec2 draw_size;
|
||||
if (!pass.target)
|
||||
draw_size = Vec2(App::draw_width(), App::draw_height());
|
||||
else
|
||||
draw_size = Vec2(pass.target->width(), pass.target->height());
|
||||
|
||||
// Validate Viewport
|
||||
if (!pass.has_viewport)
|
||||
{
|
||||
pass.viewport.x = 0;
|
||||
pass.viewport.y = 0;
|
||||
pass.viewport.w = draw_size.x;
|
||||
pass.viewport.h = draw_size.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
pass.viewport = pass.viewport.overlap_rect(Rect(0, 0, draw_size.x, draw_size.y));
|
||||
}
|
||||
|
||||
// Validate Scissor
|
||||
if (pass.has_scissor)
|
||||
pass.scissor = pass.scissor.overlap_rect(Rect(0, 0, draw_size.x, draw_size.y));
|
||||
|
||||
// perform render
|
||||
GraphicsBackend::render(pass);
|
||||
}
|
39
src/graphics/shader.cpp
Normal file
39
src/graphics/shader.cpp
Normal file
@ -0,0 +1,39 @@
|
||||
#include <blah/graphics/shader.h>
|
||||
#include <blah/core/app.h>
|
||||
#include "../internal/graphics_backend.h"
|
||||
|
||||
using namespace Blah;
|
||||
|
||||
ShaderRef Shader::create(const ShaderData& data)
|
||||
{
|
||||
BLAH_ASSERT(data.vertex.length() > 0, "Must provide a Vertex Shader");
|
||||
BLAH_ASSERT(data.fragment.length() > 0, "Must provide a Fragment Shader");
|
||||
BLAH_ASSERT(data.hlsl_attributes.size() > 0 || App::renderer() != Renderer::D3D11, "D3D11 Shaders must have hlsl_attributes assigned");
|
||||
|
||||
auto shader = GraphicsBackend::create_shader(&data);
|
||||
|
||||
// validate the shader
|
||||
if (shader)
|
||||
{
|
||||
auto& uniforms = shader->uniforms();
|
||||
|
||||
// make sure its uniforms are valid
|
||||
for (auto& it : uniforms)
|
||||
if (it.type == UniformType::None)
|
||||
{
|
||||
BLAH_ERROR_FMT("Uniform '%s' has an invalid type!\n\tOnly Float/Float2/Float3/Float4/Mat3x2/Mat4x4/Texture are allowed!", it.name.cstr());
|
||||
return ShaderRef();
|
||||
}
|
||||
|
||||
// make sure uniform names don't overlap
|
||||
for (int i = 0; i < uniforms.size(); i ++)
|
||||
for (int j = i + 1; j < uniforms.size(); j ++)
|
||||
if (uniforms[i].name == uniforms[j].name)
|
||||
{
|
||||
BLAH_ERROR_FMT("Shader Uniform names '%s' overlap! All Names must be unique.", uniforms[0].name.cstr());
|
||||
return ShaderRef();
|
||||
}
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
61
src/graphics/texture.cpp
Normal file
61
src/graphics/texture.cpp
Normal file
@ -0,0 +1,61 @@
|
||||
#include <blah/graphics/texture.h>
|
||||
#include <blah/images/image.h>
|
||||
#include <blah/streams/stream.h>
|
||||
#include <blah/core/log.h>
|
||||
#include "../internal/graphics_backend.h"
|
||||
|
||||
using namespace Blah;
|
||||
|
||||
TextureRef Texture::create(const Image& image)
|
||||
{
|
||||
auto tex = create(image.width, image.height, TextureFormat::RGBA);
|
||||
if (tex)
|
||||
tex->set_data((unsigned char*)image.pixels);
|
||||
return tex;
|
||||
}
|
||||
|
||||
TextureRef Texture::create(int width, int height, unsigned char* rgba)
|
||||
{
|
||||
auto tex = create(width, height, TextureFormat::RGBA);
|
||||
if (tex)
|
||||
tex->set_data(rgba);
|
||||
return tex;
|
||||
}
|
||||
|
||||
TextureRef Texture::create(int width, int height, TextureFormat format)
|
||||
{
|
||||
BLAH_ASSERT(width > 0 && height > 0, "Texture width and height must be larger than 0");
|
||||
BLAH_ASSERT((int)format > (int)TextureFormat::None && (int)format < (int)TextureFormat::Count, "Invalid texture format");
|
||||
|
||||
return GraphicsBackend::create_texture(width, height, format);
|
||||
}
|
||||
|
||||
TextureRef Texture::create(Stream& stream)
|
||||
{
|
||||
Image img = Image(stream);
|
||||
|
||||
if (img.pixels && img.width > 0 && img.height > 0)
|
||||
{
|
||||
auto tex = create(img.width, img.height, TextureFormat::RGBA);
|
||||
if (tex)
|
||||
tex->set_data((unsigned char*)img.pixels);
|
||||
return tex;
|
||||
}
|
||||
|
||||
return TextureRef();
|
||||
}
|
||||
|
||||
TextureRef Texture::create(const char* file)
|
||||
{
|
||||
Image img = Image(file);
|
||||
|
||||
if (img.pixels)
|
||||
{
|
||||
auto tex = create(img.width, img.height, TextureFormat::RGBA);
|
||||
if (tex)
|
||||
tex->set_data((unsigned char*)img.pixels);
|
||||
return tex;
|
||||
}
|
||||
|
||||
return TextureRef();
|
||||
}
|
Reference in New Issue
Block a user