mirror of
https://github.com/bailwillharr/engine.git
synced 2024-09-21 04:51:18 +00:00
Do a ton of stuff. can't remember what
This commit is contained in:
parent
df02820c7d
commit
d04e4a168a
@ -5,7 +5,10 @@
|
|||||||
namespace engine {
|
namespace engine {
|
||||||
|
|
||||||
class Window;
|
class Window;
|
||||||
|
class Input;
|
||||||
class GFXDevice;
|
class GFXDevice;
|
||||||
|
class ResourceManager;
|
||||||
|
class SceneRoot;
|
||||||
|
|
||||||
class Application {
|
class Application {
|
||||||
|
|
||||||
@ -21,7 +24,9 @@ namespace engine {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<Window> m_win;
|
std::unique_ptr<Window> m_win;
|
||||||
std::unique_ptr<GFXDevice> m_gfx;
|
std::unique_ptr<Input> m_input;
|
||||||
|
std::unique_ptr<ResourceManager> m_res;
|
||||||
|
std::unique_ptr<SceneRoot> m_scene;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ namespace engine {
|
|||||||
~GFXDevice();
|
~GFXDevice();
|
||||||
|
|
||||||
// adds a draw call to the queue
|
// adds a draw call to the queue
|
||||||
// vertexBuffer is required. indexBuffer and uniformData can be NULL
|
// vertexBuffer is required, indexBuffer can be NULL, uniformData is required
|
||||||
void draw(const gfx::Pipeline* pipeline, const gfx::Buffer* vertexBuffer, const gfx::Buffer* indexBuffer, uint32_t count, const void* uniformData);
|
void draw(const gfx::Pipeline* pipeline, const gfx::Buffer* vertexBuffer, const gfx::Buffer* indexBuffer, uint32_t count, const void* uniformData);
|
||||||
|
|
||||||
// Call once per frame. Executes all queued draw calls and renders to the screen.
|
// Call once per frame. Executes all queued draw calls and renders to the screen.
|
||||||
@ -42,4 +42,6 @@ namespace engine {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
extern GFXDevice* gfxdev;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
namespace engine {
|
namespace engine {
|
||||||
class Window;
|
class Window;
|
||||||
class Input;
|
class Input;
|
||||||
|
|
||||||
class ResourceManager;
|
class ResourceManager;
|
||||||
|
|
||||||
class SceneRoot;
|
class SceneRoot;
|
||||||
@ -30,9 +29,9 @@ namespace engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct GameIO {
|
struct GameIO {
|
||||||
engine::Window* const win;
|
Window* win;
|
||||||
engine::Input* const input;
|
Input* input;
|
||||||
ResourceManager* const resMan;
|
ResourceManager* resMan;
|
||||||
};
|
};
|
||||||
|
|
||||||
// This object lives until it is deleted by its parent(s) or finally when the "Scene" is destroyed.
|
// This object lives until it is deleted by its parent(s) or finally when the "Scene" is destroyed.
|
||||||
@ -45,10 +44,11 @@ namespace engine {
|
|||||||
Object& operator=(const Object&) = delete;
|
Object& operator=(const Object&) = delete;
|
||||||
~Object();
|
~Object();
|
||||||
|
|
||||||
engine::Window& win;
|
Window& win;
|
||||||
engine::Input& inp;
|
Input& inp;
|
||||||
ResourceManager& res;
|
ResourceManager& res;
|
||||||
|
|
||||||
|
|
||||||
SceneRoot& root;
|
SceneRoot& root;
|
||||||
|
|
||||||
std::string getName();
|
std::string getName();
|
||||||
|
@ -18,30 +18,23 @@ struct Vertex {
|
|||||||
glm::vec2 uv;
|
glm::vec2 uv;
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace engine {
|
|
||||||
class GFXDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace engine::resources {
|
namespace engine::resources {
|
||||||
|
|
||||||
class ENGINE_API Mesh : public Resource {
|
class ENGINE_API Mesh : public Resource {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Mesh(GFXDevice* gfx, const std::vector<Vertex>& vertices);
|
Mesh(const std::vector<Vertex>& vertices);
|
||||||
Mesh(GFXDevice* gfx, const std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices);
|
Mesh(const std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices);
|
||||||
Mesh(GFXDevice* gfx, const std::filesystem::path& resPath);
|
Mesh(const std::filesystem::path& resPath);
|
||||||
~Mesh() override;
|
~Mesh() override;
|
||||||
|
|
||||||
void drawMesh(const gfx::Pipeline* pipeline);
|
|
||||||
|
|
||||||
std::vector<Vertex> m_vertices;
|
std::vector<Vertex> m_vertices;
|
||||||
std::vector<uint32_t> m_indices;
|
std::vector<uint32_t> m_indices;
|
||||||
|
|
||||||
private:
|
|
||||||
const gfx::Buffer* vb;
|
const gfx::Buffer* vb;
|
||||||
const gfx::Buffer* ib;
|
const gfx::Buffer* ib;
|
||||||
|
|
||||||
GFXDevice* gfx;
|
private:
|
||||||
|
|
||||||
void initMesh();
|
void initMesh();
|
||||||
|
|
||||||
|
@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
#include "resource.hpp"
|
#include "resource.hpp"
|
||||||
|
|
||||||
#include <glad/glad.h>
|
|
||||||
|
|
||||||
#include <glm/mat4x4.hpp>
|
#include <glm/mat4x4.hpp>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
@ -14,7 +14,6 @@ namespace engine {
|
|||||||
public:
|
public:
|
||||||
// create a new empty scene
|
// create a new empty scene
|
||||||
SceneRoot(struct GameIO things);
|
SceneRoot(struct GameIO things);
|
||||||
SceneRoot(const std::filesystem::path& file, struct GameIO things);
|
|
||||||
|
|
||||||
SceneRoot(const SceneRoot&) = delete;
|
SceneRoot(const SceneRoot&) = delete;
|
||||||
SceneRoot& operator=(const SceneRoot&) = delete;
|
SceneRoot& operator=(const SceneRoot&) = delete;
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
#include "engine.hpp"
|
#include "engine.hpp"
|
||||||
|
|
||||||
#include "window.hpp"
|
|
||||||
#include "gfx_device.hpp"
|
|
||||||
#include "resource_manager.hpp"
|
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
#include "window.hpp"
|
||||||
#include <glm/ext.hpp>
|
#include "input.hpp"
|
||||||
|
#include "resource_manager.hpp"
|
||||||
|
#include "sceneroot.hpp"
|
||||||
|
|
||||||
|
#include "gfx_device.hpp"
|
||||||
|
|
||||||
|
#include "resources/mesh.hpp"
|
||||||
|
|
||||||
struct UBO {
|
struct UBO {
|
||||||
glm::mat4 model{};
|
glm::mat4 model{};
|
||||||
@ -17,48 +20,27 @@ struct UBO {
|
|||||||
static engine::gfx::Pipeline* pipeline;
|
static engine::gfx::Pipeline* pipeline;
|
||||||
static engine::gfx::Buffer* vb;
|
static engine::gfx::Buffer* vb;
|
||||||
static engine::gfx::Buffer* ib;
|
static engine::gfx::Buffer* ib;
|
||||||
static engine::gfx::Buffer* ub;
|
|
||||||
|
|
||||||
namespace engine {
|
namespace engine {
|
||||||
|
|
||||||
Application::Application(const char* appName, const char* appVersion)
|
Application::Application(const char* appName, const char* appVersion)
|
||||||
{
|
{
|
||||||
m_win = std::make_unique<Window>(appName, true);
|
m_win = std::make_unique<Window>(appName, true);
|
||||||
m_gfx = std::make_unique<GFXDevice>(appName, appVersion, m_win->getHandle());
|
|
||||||
|
|
||||||
engine::ResourceManager resMan{};
|
gfxdev = new GFXDevice(appName, appVersion, m_win->getHandle());
|
||||||
struct Vertex {
|
|
||||||
glm::vec2 pos;
|
|
||||||
glm::vec3 col;
|
|
||||||
};
|
|
||||||
gfx::VertexFormat vertFormat{
|
|
||||||
.stride = (uint32_t)sizeof(Vertex),
|
|
||||||
};
|
|
||||||
vertFormat.attributeDescriptions.push_back({0, gfx::VertexAttribFormat::VEC2, 0});
|
|
||||||
vertFormat.attributeDescriptions.push_back({1, gfx::VertexAttribFormat::VEC3, offsetof(Vertex, col)});
|
|
||||||
|
|
||||||
pipeline = m_gfx->createPipeline(resMan.getFilePath("shader.vert.spv").string().c_str(), resMan.getFilePath("shader.frag.spv").string().c_str(), vertFormat, sizeof(UBO));
|
|
||||||
|
|
||||||
const std::vector<Vertex> vertices = {
|
|
||||||
{ { 0.5f, -0.5f}, {1.0f, 0.0f, 0.0f} },
|
|
||||||
{ { 0.5f, 0.5f}, {0.0f, 1.0f, 0.0f} },
|
|
||||||
{ {-0.5f, -0.5f}, {0.0f, 0.0f, 1.0f} },
|
|
||||||
{ {-0.5f, 0.5f}, {0.0f, 1.0f, 1.0f} },
|
|
||||||
};
|
|
||||||
vb = m_gfx->createBuffer(gfx::BufferType::VERTEX, sizeof(Vertex) * vertices.size(), vertices.data());
|
|
||||||
const std::vector<uint32_t> indices = {
|
|
||||||
0, 1, 2, 2, 1, 3,
|
|
||||||
};
|
|
||||||
ib = m_gfx->createBuffer(gfx::BufferType::INDEX, sizeof(uint32_t) * indices.size(), indices.data());
|
|
||||||
|
|
||||||
|
m_input = std::make_unique<Input>(*m_win);
|
||||||
|
m_res = std::make_unique<ResourceManager>();
|
||||||
|
GameIO things{};
|
||||||
|
things.win = m_win.get();
|
||||||
|
things.input = m_input.get();
|
||||||
|
things.resMan = m_res.get();
|
||||||
|
m_scene = std::make_unique<SceneRoot>(things);
|
||||||
}
|
}
|
||||||
|
|
||||||
Application::~Application()
|
Application::~Application()
|
||||||
{
|
{
|
||||||
// m_gfx->destroyBuffer(ub);
|
delete gfxdev;
|
||||||
m_gfx->destroyBuffer(ib);
|
|
||||||
m_gfx->destroyBuffer(vb);
|
|
||||||
m_gfx->destroyPipeline(pipeline);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::gameLoop()
|
void Application::gameLoop()
|
||||||
@ -68,6 +50,8 @@ namespace engine {
|
|||||||
uint64_t lastTick = m_win->getNanos();
|
uint64_t lastTick = m_win->getNanos();
|
||||||
constexpr int TICKFREQ = 1; // in hz
|
constexpr int TICKFREQ = 1; // in hz
|
||||||
|
|
||||||
|
auto myMesh = m_res->get<resources::Mesh>("meshes/monke.mesh");
|
||||||
|
|
||||||
// single-threaded game loop
|
// single-threaded game loop
|
||||||
while (m_win->isRunning()) {
|
while (m_win->isRunning()) {
|
||||||
|
|
||||||
@ -88,23 +72,17 @@ namespace engine {
|
|||||||
m_win->setCloseFlag();
|
m_win->setCloseFlag();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* draw */
|
m_scene->updateStuff();
|
||||||
UBO initialUbo{
|
|
||||||
.model = glm::mat4{1.0f},
|
|
||||||
.view = glm::mat4{1.0f},
|
|
||||||
.proj = glm::mat4{1.0f}
|
|
||||||
};
|
|
||||||
initialUbo.model = glm::scale(glm::mat4{ 1.0f }, glm::vec3{ 0.5f, 0.5f, 1.0f });
|
|
||||||
m_gfx->draw(pipeline, vb, ib, 6, &initialUbo);
|
|
||||||
|
|
||||||
m_gfx->renderFrame();
|
/* draw */
|
||||||
|
gfxdev->renderFrame();
|
||||||
|
|
||||||
/* poll events */
|
/* poll events */
|
||||||
m_win->getInputAndEvents();
|
m_win->getInputAndEvents();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_gfx->waitIdle();
|
gfxdev->waitIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,12 @@
|
|||||||
|
|
||||||
namespace engine {
|
namespace engine {
|
||||||
|
|
||||||
static constexpr uint32_t FRAMES_IN_FLIGHT = 2; // This improved FPS by 5x!
|
// EXTERNED GLOBAL VARIABLE
|
||||||
|
GFXDevice* gfxdev = nullptr;
|
||||||
|
|
||||||
|
static constexpr uint32_t FRAMES_IN_FLIGHT = 2; // This improved FPS by 5x! (on Intel IGPU)
|
||||||
|
|
||||||
|
static constexpr size_t UNIFORM_BUFFER_MAX_SIZE = 256; // bytes
|
||||||
|
|
||||||
// structures and enums
|
// structures and enums
|
||||||
|
|
||||||
@ -73,6 +78,7 @@ namespace engine {
|
|||||||
const gfx::Buffer* vertexBuffer = nullptr;
|
const gfx::Buffer* vertexBuffer = nullptr;
|
||||||
const gfx::Buffer* indexBuffer = nullptr; // if this is nullptr, don't use indexed
|
const gfx::Buffer* indexBuffer = nullptr; // if this is nullptr, don't use indexed
|
||||||
uint32_t count = 0;
|
uint32_t count = 0;
|
||||||
|
uint8_t uniformData[UNIFORM_BUFFER_MAX_SIZE];
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class QueueFlags : uint32_t {
|
enum class QueueFlags : uint32_t {
|
||||||
@ -634,6 +640,11 @@ namespace engine {
|
|||||||
|
|
||||||
GFXDevice::GFXDevice(const char* appName, const char* appVersion, SDL_Window* window)
|
GFXDevice::GFXDevice(const char* appName, const char* appVersion, SDL_Window* window)
|
||||||
{
|
{
|
||||||
|
if (gfxdev != nullptr) {
|
||||||
|
throw std::runtime_error("There can only be one graphics device");
|
||||||
|
}
|
||||||
|
gfxdev = this;
|
||||||
|
|
||||||
pimpl = std::make_unique<Impl>();
|
pimpl = std::make_unique<Impl>();
|
||||||
|
|
||||||
VkResult res;
|
VkResult res;
|
||||||
@ -1077,8 +1088,7 @@ namespace engine {
|
|||||||
assert(vertexBuffer->type == gfx::BufferType::VERTEX);
|
assert(vertexBuffer->type == gfx::BufferType::VERTEX);
|
||||||
assert(vertexBuffer != nullptr);
|
assert(vertexBuffer != nullptr);
|
||||||
assert(indexBuffer == nullptr || indexBuffer->type == gfx::BufferType::INDEX);
|
assert(indexBuffer == nullptr || indexBuffer->type == gfx::BufferType::INDEX);
|
||||||
|
assert(uniformData != nullptr);
|
||||||
VkResult res;
|
|
||||||
|
|
||||||
DrawCall call{
|
DrawCall call{
|
||||||
.vertexBuffer = vertexBuffer,
|
.vertexBuffer = vertexBuffer,
|
||||||
@ -1086,18 +1096,9 @@ namespace engine {
|
|||||||
.count = count,
|
.count = count,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (uniformData != nullptr) {
|
size_t uniformDataSize = pipeline->uniformBuffers[pimpl->FRAMECOUNT % FRAMES_IN_FLIGHT]->size;
|
||||||
uint32_t frameIndex = pimpl->FRAMECOUNT % FRAMES_IN_FLIGHT;
|
|
||||||
|
|
||||||
void* dest;
|
memcpy(call.uniformData, uniformData, uniformDataSize);
|
||||||
res = vmaMapMemory(pimpl->allocator, pipeline->uniformBuffers[frameIndex]->allocation, &dest);
|
|
||||||
assert(res == VK_SUCCESS);
|
|
||||||
|
|
||||||
memcpy(dest, uniformData, pipeline->uniformBuffers[frameIndex]->size);
|
|
||||||
|
|
||||||
vmaUnmapMemory(pimpl->allocator, pipeline->uniformBuffers[frameIndex]->allocation);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pimpl->drawQueues[pipeline].push(call);
|
pimpl->drawQueues[pipeline].push(call);
|
||||||
}
|
}
|
||||||
@ -1171,6 +1172,15 @@ namespace engine {
|
|||||||
vkCmdBindDescriptorSets(pimpl->commandBuffers[frameIndex], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline->layout, 0, 1, &pipeline->descriptorSets[frameIndex], 0, nullptr);
|
vkCmdBindDescriptorSets(pimpl->commandBuffers[frameIndex], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline->layout, 0, 1, &pipeline->descriptorSets[frameIndex], 0, nullptr);
|
||||||
while (queue.empty() == false) {
|
while (queue.empty() == false) {
|
||||||
DrawCall call = queue.front();
|
DrawCall call = queue.front();
|
||||||
|
|
||||||
|
void* uniformDest;
|
||||||
|
res = vmaMapMemory(pimpl->allocator, pipeline->uniformBuffers[frameIndex]->allocation, &uniformDest);
|
||||||
|
assert(res == VK_SUCCESS);
|
||||||
|
|
||||||
|
memcpy(uniformDest, call.uniformData, pipeline->uniformBuffers[frameIndex]->size);
|
||||||
|
|
||||||
|
vmaUnmapMemory(pimpl->allocator, pipeline->uniformBuffers[frameIndex]->allocation);
|
||||||
|
|
||||||
vkCmdBindVertexBuffers(pimpl->commandBuffers[frameIndex], 0, 1, &call.vertexBuffer->buffer, offsets);
|
vkCmdBindVertexBuffers(pimpl->commandBuffers[frameIndex], 0, 1, &call.vertexBuffer->buffer, offsets);
|
||||||
|
|
||||||
if (call.indexBuffer == nullptr) {
|
if (call.indexBuffer == nullptr) {
|
||||||
@ -1180,6 +1190,7 @@ namespace engine {
|
|||||||
vkCmdBindIndexBuffer(pimpl->commandBuffers[frameIndex], call.indexBuffer->buffer, 0, VK_INDEX_TYPE_UINT32);
|
vkCmdBindIndexBuffer(pimpl->commandBuffers[frameIndex], call.indexBuffer->buffer, 0, VK_INDEX_TYPE_UINT32);
|
||||||
vkCmdDrawIndexed(pimpl->commandBuffers[frameIndex], call.count, 1, 0, 0, 0);
|
vkCmdDrawIndexed(pimpl->commandBuffers[frameIndex], call.count, 1, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
queue.pop();
|
queue.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,16 +36,11 @@ static void loadMeshFromFile(const std::filesystem::path& path, std::vector<Vert
|
|||||||
|
|
||||||
void Mesh::initMesh()
|
void Mesh::initMesh()
|
||||||
{
|
{
|
||||||
vb = gfx->createBuffer(gfx::BufferType::VERTEX, m_vertices.size() * sizeof(Vertex), m_vertices.data());
|
vb = gfxdev->createBuffer(gfx::BufferType::VERTEX, m_vertices.size() * sizeof(Vertex), m_vertices.data());
|
||||||
ib = gfx->createBuffer(gfx::BufferType::INDEX, m_indices.size() * sizeof(uint32_t), m_indices.data());
|
ib = gfxdev->createBuffer(gfx::BufferType::INDEX, m_indices.size() * sizeof(uint32_t), m_indices.data());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mesh::drawMesh(const gfx::Pipeline* pipeline)
|
Mesh::Mesh(const std::vector<Vertex>& vertices) : Resource("", "mesh")
|
||||||
{
|
|
||||||
gfx->draw(pipeline, vb, ib, m_indices.size(), nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
Mesh::Mesh(GFXDevice* gfx, const std::vector<Vertex>& vertices) : Resource("", "mesh"), gfx(gfx)
|
|
||||||
{
|
{
|
||||||
// constructor for custom meshes without an index array
|
// constructor for custom meshes without an index array
|
||||||
m_vertices = vertices; // COPY over vertices
|
m_vertices = vertices; // COPY over vertices
|
||||||
@ -55,7 +50,7 @@ Mesh::Mesh(GFXDevice* gfx, const std::vector<Vertex>& vertices) : Resource("", "
|
|||||||
initMesh();
|
initMesh();
|
||||||
}
|
}
|
||||||
|
|
||||||
Mesh::Mesh(GFXDevice* gfx, const std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices) : Resource("", "mesh"), gfx(gfx)
|
Mesh::Mesh(const std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices) : Resource("", "mesh")
|
||||||
{
|
{
|
||||||
m_vertices = vertices; // COPY over vertices
|
m_vertices = vertices; // COPY over vertices
|
||||||
m_indices = indices; // COPY over indices;
|
m_indices = indices; // COPY over indices;
|
||||||
@ -63,7 +58,7 @@ Mesh::Mesh(GFXDevice* gfx, const std::vector<Vertex>& vertices, const std::vecto
|
|||||||
}
|
}
|
||||||
|
|
||||||
// To be used with the resource manager
|
// To be used with the resource manager
|
||||||
Mesh::Mesh(GFXDevice* gfx, const std::filesystem::path& resPath) : Resource(resPath, "mesh"), gfx(gfx)
|
Mesh::Mesh(const std::filesystem::path& resPath) : Resource(resPath, "mesh")
|
||||||
{
|
{
|
||||||
loadMeshFromFile(resPath, &m_vertices, &m_indices);
|
loadMeshFromFile(resPath, &m_vertices, &m_indices);
|
||||||
initMesh();
|
initMesh();
|
||||||
@ -71,8 +66,8 @@ Mesh::Mesh(GFXDevice* gfx, const std::filesystem::path& resPath) : Resource(resP
|
|||||||
|
|
||||||
Mesh::~Mesh()
|
Mesh::~Mesh()
|
||||||
{
|
{
|
||||||
gfx->destroyBuffer(ib);
|
gfxdev->destroyBuffer(ib);
|
||||||
gfx->destroyBuffer(vb);
|
gfxdev->destroyBuffer(vb);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,6 @@ namespace engine {
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
SceneRoot::SceneRoot(const std::filesystem::path& file, struct GameIO things) : SceneRoot(things)
|
|
||||||
{
|
|
||||||
// TODO: make this a resource
|
|
||||||
//loadFromSceneFile(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
SceneRoot::~SceneRoot()
|
SceneRoot::~SceneRoot()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user