Trying again omfg
This commit is contained in:
parent
9ff0d9edb4
commit
2cb665cf19
@ -4,17 +4,41 @@
|
|||||||
|
|
||||||
#include <GLLoad.h>
|
#include <GLLoad.h>
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
#include <cctype>
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
|
||||||
#include <glm/gtc/type_ptr.hpp>
|
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <map>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
|
||||||
namespace NB {
|
namespace NB {
|
||||||
|
|
||||||
|
class ShaderPreprocessorError : public std::runtime_error {
|
||||||
|
public:
|
||||||
|
enum Codes : unsigned char {
|
||||||
|
NONE,
|
||||||
|
FILE_NOT_FOUND,
|
||||||
|
BUILTIN_NOT_FOUND,
|
||||||
|
CUSTOM,
|
||||||
|
UNDEFINED
|
||||||
|
};
|
||||||
|
|
||||||
|
const Codes code;
|
||||||
|
|
||||||
|
ShaderPreprocessorError(const std::string&, const std::string& file="", int line=-1);
|
||||||
|
ShaderPreprocessorError(Codes, const std::string& arg="", const std::string& file="", int line=-1);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
static std::string errorCodeParser(Codes, const std::string& arg="");
|
||||||
|
};
|
||||||
|
|
||||||
class ShaderError : public std::runtime_error {
|
class ShaderError : public std::runtime_error {
|
||||||
public:
|
public:
|
||||||
ShaderError(const std::string&, const char* shad=nullptr, const std::string& file="", int line=-1);
|
ShaderError(const std::string&, const char* shad=nullptr, const std::string& file="", int line=-1);
|
||||||
@ -23,9 +47,28 @@ protected:
|
|||||||
std::string formatString(const std::string&, const char* shad=nullptr, const std::string& file="", int line=-1);
|
std::string formatString(const std::string&, const char* shad=nullptr, const std::string& file="", int line=-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class ShaderPreprocessor {
|
||||||
|
public:
|
||||||
|
typedef ShaderPreprocessorError::Codes Codes;
|
||||||
|
|
||||||
|
std::vector<std::string> AcceptedExtensions = {"frag", "vert", "tess", "geom", "comp", "shad", "glsl"};
|
||||||
|
std::vector<std::string> Directories;
|
||||||
|
std::map<std::string, std::string> BuiltIns;
|
||||||
|
|
||||||
|
ShaderPreprocessor();
|
||||||
|
|
||||||
|
std::stringstream load(const std::string&) const;
|
||||||
|
std::stringstream include(const std::string&, const std::string& base="", bool relativeFirst=false) const;
|
||||||
|
std::stringstream loadFromRelative(const std::string&, const std::string& base="") const;
|
||||||
|
std::stringstream loadFromDirectory(const std::string&) const;
|
||||||
|
std::stringstream loadFromBuiltIn(const std::string&) const;
|
||||||
|
virtual std::stringstream process(std::stringstream&) const;
|
||||||
|
virtual std::stringstream process(std::string) const;
|
||||||
|
};
|
||||||
|
|
||||||
class Shader {
|
class Shader {
|
||||||
public:
|
public:
|
||||||
Shader(const char* vertexPath, const char* fragmentPath);
|
Shader(const std::stringstream&, const std::stringstream&);
|
||||||
Shader();
|
Shader();
|
||||||
Shader(const Shader&);
|
Shader(const Shader&);
|
||||||
Shader& operator=(const Shader&);
|
Shader& operator=(const Shader&);
|
||||||
@ -38,6 +81,7 @@ public:
|
|||||||
unsigned int id;
|
unsigned int id;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
9
main.cpp
9
main.cpp
@ -27,7 +27,12 @@ int main() {
|
|||||||
|
|
||||||
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
|
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
|
||||||
|
|
||||||
NB::Shader myShader("../shaders/vert.vs", "../shaders/frag.fs");
|
NB::ShaderPreprocessor myPre;
|
||||||
|
myPre.Directories.push_back("../shaders/");
|
||||||
|
|
||||||
|
std::cout << myPre.process(std::string("vert.vs")).rdbuf();
|
||||||
|
|
||||||
|
/* NB::Shader myShader(myPre.load("vert.vs"), myPre.load("frag.fs"));
|
||||||
myShader.use();
|
myShader.use();
|
||||||
|
|
||||||
NB::DrawBuffer myCube(myShader, {{3, GL_FLOAT}, {3, GL_FLOAT}}, GL_TRIANGLES);
|
NB::DrawBuffer myCube(myShader, {{3, GL_FLOAT}, {3, GL_FLOAT}}, GL_TRIANGLES);
|
||||||
@ -78,7 +83,7 @@ int main() {
|
|||||||
// Events and buffer swap
|
// Events and buffer swap
|
||||||
glfwPollEvents();
|
glfwPollEvents();
|
||||||
glfwSwapBuffers(window);
|
glfwSwapBuffers(window);
|
||||||
}
|
} */
|
||||||
|
|
||||||
glfwTerminate();
|
glfwTerminate();
|
||||||
|
|
||||||
|
|||||||
389
shader.cpp
389
shader.cpp
@ -2,6 +2,36 @@
|
|||||||
|
|
||||||
namespace NB{
|
namespace NB{
|
||||||
|
|
||||||
|
// ShaderPreprocessorError class
|
||||||
|
ShaderPreprocessorError::ShaderPreprocessorError(
|
||||||
|
const std::string& msg,
|
||||||
|
const std::string& file,
|
||||||
|
int line
|
||||||
|
) : code(Codes::UNDEFINED), std::runtime_error(formatDebugString(msg, file, line)) {}
|
||||||
|
|
||||||
|
ShaderPreprocessorError::ShaderPreprocessorError(
|
||||||
|
Codes err_code,
|
||||||
|
const std::string& arg,
|
||||||
|
const std::string& file,
|
||||||
|
int line
|
||||||
|
) : code(err_code), std::runtime_error(formatDebugString(errorCodeParser(err_code, arg), file, line)) {}
|
||||||
|
|
||||||
|
std::string ShaderPreprocessorError::errorCodeParser(Codes err_code, const std::string& arg) {
|
||||||
|
switch(err_code) {
|
||||||
|
case Codes::FILE_NOT_FOUND:
|
||||||
|
return "File '" + arg + "' not found.";
|
||||||
|
case Codes::BUILTIN_NOT_FOUND:
|
||||||
|
return "Built-in '" + arg + "' not found.";
|
||||||
|
case Codes::CUSTOM:
|
||||||
|
case Codes::UNDEFINED:
|
||||||
|
return arg;
|
||||||
|
case Codes::NONE:
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShaderError class
|
||||||
ShaderError::ShaderError(
|
ShaderError::ShaderError(
|
||||||
const std::string& msg,
|
const std::string& msg,
|
||||||
const char* shad,
|
const char* shad,
|
||||||
@ -30,26 +60,349 @@ std::string ShaderError::formatString(
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
Shader::Shader(const char* vertexPath, const char* fragmentPath) {
|
// ShaderPreprocessor class
|
||||||
std::string vertexCode, fragmentCode;
|
ShaderPreprocessor::ShaderPreprocessor() {}
|
||||||
std::ifstream vShaderFile, fShaderFile;
|
|
||||||
|
|
||||||
vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
|
std::stringstream ShaderPreprocessor::loadFromRelative(const std::string& path, const std::string& base) const {
|
||||||
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
|
std::string fullpath = base + path;
|
||||||
|
std::ifstream filestream;
|
||||||
try {
|
std::stringstream ret;
|
||||||
vShaderFile.open(vertexPath);
|
filestream.open(fullpath);
|
||||||
fShaderFile.open(fragmentPath);
|
if (filestream.is_open()) {
|
||||||
std::stringstream vShaderStream, fShaderStream;
|
ret << filestream.rdbuf();
|
||||||
vShaderStream << vShaderFile.rdbuf();
|
return ret;
|
||||||
fShaderStream << fShaderFile.rdbuf();
|
|
||||||
vShaderFile.close();
|
|
||||||
fShaderFile.close();
|
|
||||||
vertexCode = vShaderStream.str();
|
|
||||||
fragmentCode = fShaderStream.str();
|
|
||||||
} catch (std::ifstream::failure e) {
|
|
||||||
throw ShaderError("Could not read Shader files.");
|
|
||||||
}
|
}
|
||||||
|
filestream.close();
|
||||||
|
for (const std::string& ext : AcceptedExtensions) {
|
||||||
|
filestream.open(fullpath + ext);
|
||||||
|
if (filestream.is_open()) {
|
||||||
|
ret << filestream.rdbuf();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
filestream.close();
|
||||||
|
}
|
||||||
|
throw ShaderPreprocessorError(Codes::FILE_NOT_FOUND, base + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream ShaderPreprocessor::loadFromDirectory(const std::string& name) const {
|
||||||
|
std::string fullpath;
|
||||||
|
std::ifstream filestream;
|
||||||
|
std::stringstream ret;
|
||||||
|
for (const std::string& path : Directories) {
|
||||||
|
fullpath = path + name;
|
||||||
|
filestream.open(fullpath);
|
||||||
|
if (filestream.is_open()) {
|
||||||
|
ret << filestream.rdbuf();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
filestream.close();
|
||||||
|
for (const std::string& ext : AcceptedExtensions) {
|
||||||
|
filestream.open(fullpath + ext);
|
||||||
|
if (filestream.is_open()) {
|
||||||
|
ret << filestream.rdbuf();
|
||||||
|
}
|
||||||
|
filestream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw ShaderPreprocessorError(Codes::FILE_NOT_FOUND, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream ShaderPreprocessor::loadFromBuiltIn(const std::string& name) const {
|
||||||
|
std::stringstream ret;
|
||||||
|
decltype(BuiltIns)::const_iterator builtin_it = BuiltIns.find(name);
|
||||||
|
if (builtin_it != BuiltIns.end()) {
|
||||||
|
ret << builtin_it->second;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
throw ShaderPreprocessorError(Codes::BUILTIN_NOT_FOUND, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream ShaderPreprocessor::load(const std::string& path) const {
|
||||||
|
try {
|
||||||
|
return loadFromRelative(path);
|
||||||
|
} catch (ShaderPreprocessorError e) {
|
||||||
|
if (e.code == Codes::FILE_NOT_FOUND) {
|
||||||
|
return loadFromDirectory(path);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream ShaderPreprocessor::include(const std::string& name, const std::string& base, bool relativeFirst) const {
|
||||||
|
if (relativeFirst) {
|
||||||
|
try {
|
||||||
|
return loadFromRelative(name, base);
|
||||||
|
} catch (ShaderPreprocessorError e) {
|
||||||
|
if (e.code == Codes::FILE_NOT_FOUND) {
|
||||||
|
try {
|
||||||
|
return loadFromDirectory(name);
|
||||||
|
} catch (ShaderPreprocessorError f) {
|
||||||
|
if (f.code == Codes::FILE_NOT_FOUND) {
|
||||||
|
return loadFromBuiltIn(name);
|
||||||
|
} else {
|
||||||
|
throw f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return loadFromDirectory(name);
|
||||||
|
} catch (ShaderPreprocessorError e) {
|
||||||
|
if (e.code == Codes::FILE_NOT_FOUND) {
|
||||||
|
try {
|
||||||
|
return loadFromBuiltIn(name);
|
||||||
|
} catch (ShaderPreprocessorError f) {
|
||||||
|
if (f.code == Codes::BUILTIN_NOT_FOUND) {
|
||||||
|
return loadFromRelative(name, base);
|
||||||
|
} else {
|
||||||
|
throw f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream ShaderPreprocessor::process(std::stringstream& code) const {
|
||||||
|
/* std::stringstream ret;
|
||||||
|
std::string line;
|
||||||
|
enum State : unsigned char {
|
||||||
|
Start,
|
||||||
|
Token,
|
||||||
|
PreProcToken,
|
||||||
|
Number,
|
||||||
|
WhiteSpace,
|
||||||
|
FSlash,
|
||||||
|
Operator,
|
||||||
|
BlockComment,
|
||||||
|
LineComment
|
||||||
|
};
|
||||||
|
std::pair<std::string, State> const NewState = {"", Start};
|
||||||
|
std::vector<std::pair<std::string, State>> tokens({NewState});
|
||||||
|
State currState;
|
||||||
|
char n;
|
||||||
|
for (char c; code.good() && !code.eof(); code.get(c)) {
|
||||||
|
currState = tokens.back().second;
|
||||||
|
if (std::isblank(c) && currState != BlockComment && currState != LineComment) {
|
||||||
|
if (c == '\n') {
|
||||||
|
|
||||||
|
}
|
||||||
|
switch (currState) {
|
||||||
|
case WhiteSpace:
|
||||||
|
tokens.back().first += c;
|
||||||
|
continue;
|
||||||
|
break;
|
||||||
|
case Start:
|
||||||
|
tokens.back() = {"" + c, WhiteSpace};
|
||||||
|
continue;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
tokens.push_back({"" + c, WhiteSpace});
|
||||||
|
continue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (currState) {
|
||||||
|
case Start:
|
||||||
|
if (c == '_' || std::isalpha(c)) {
|
||||||
|
tokens.back() = {"" + c, Token};
|
||||||
|
continue;
|
||||||
|
} else if (c == '#') {
|
||||||
|
tokens.back() = {"#", PreProcToken};
|
||||||
|
continue;
|
||||||
|
} else if (std::isdigit(c)) {
|
||||||
|
tokens.back() = {"" + c, Number};
|
||||||
|
continue;
|
||||||
|
} else if (c == '.') {
|
||||||
|
code.get(n);
|
||||||
|
if (std::isdigit(n)) {
|
||||||
|
tokens.back() = {"" + c + n, Number};
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
code.unget();
|
||||||
|
tokens.back() = {".", Operator};
|
||||||
|
tokens.push_back(NewState);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tokens.back() = {"" + c, Operator};
|
||||||
|
tokens.push_back(NewState);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case FSlash:
|
||||||
|
if (c == '*') {
|
||||||
|
tokens.back() = {"/*", BlockComment};
|
||||||
|
} else if (c == '/') {
|
||||||
|
tokens.back() = {"//", LineComment};
|
||||||
|
} else {
|
||||||
|
tokens.back().second = Operator;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
break;
|
||||||
|
case BlockComment:
|
||||||
|
tokens.back().first += c;
|
||||||
|
code.get(n);
|
||||||
|
tokens.back().first += n;
|
||||||
|
if (c == '*' && n == '/') {
|
||||||
|
tokens.push_back(NewState);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LineComment:
|
||||||
|
tokens.back().first += c;
|
||||||
|
if (c == '\n') {
|
||||||
|
tokens.push_back(NewState);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret; */
|
||||||
|
std::stringstream ret;
|
||||||
|
const std::vector<std::string> PreprocessorKeywords = {"#include"};
|
||||||
|
|
||||||
|
enum State : unsigned char {
|
||||||
|
Symbol,
|
||||||
|
Directive,
|
||||||
|
BlockCommentStart,
|
||||||
|
BlockCommentEnd,
|
||||||
|
LinecommentStart,
|
||||||
|
Whitespace,
|
||||||
|
Pound,
|
||||||
|
SingleQuote,
|
||||||
|
DoubleQuote,
|
||||||
|
OpenAngle,
|
||||||
|
CloseAngle,
|
||||||
|
FSlash,
|
||||||
|
Semicolon,
|
||||||
|
Star,
|
||||||
|
Newline,
|
||||||
|
Operator
|
||||||
|
};
|
||||||
|
std::vector<std::pair<State, std::string>> stack = {};
|
||||||
|
for (char c; code.good() && !code.eof(); code.get(c)) {
|
||||||
|
State currstate = stack.back().first;
|
||||||
|
bool editstack = false;
|
||||||
|
switch (c) {
|
||||||
|
case '#':
|
||||||
|
stack.push_back({Pound, "#"});
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
stack.push_back({DoubleQuote, "\""});
|
||||||
|
break;
|
||||||
|
case '\'':
|
||||||
|
stack.push_back({SingleQuote, "'"});
|
||||||
|
break;
|
||||||
|
case '<':
|
||||||
|
stack.push_back({OpenAngle, "<"});
|
||||||
|
break;
|
||||||
|
case '>':
|
||||||
|
stack.push_back({CloseAngle, ">"});
|
||||||
|
break;
|
||||||
|
case '/':
|
||||||
|
stack.push_back({FSlash, "/"});
|
||||||
|
break;
|
||||||
|
case '*':
|
||||||
|
stack.push_back({Star, "*"});
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
stack.push_back({Newline, "\n"});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (isblank(c)) {
|
||||||
|
stack.push_back({Whitespace, std::string(1, c)});
|
||||||
|
} else if (isalnum(c)) {
|
||||||
|
if (currstate == Symbol) {
|
||||||
|
stack.back().second += c;
|
||||||
|
} else {
|
||||||
|
stack.push_back({Symbol, std::string(1, c)});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stack.push_back({Operator, std::string(1, c)});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto endIt = stack.end();
|
||||||
|
State secondlast = endIt[-2].first;
|
||||||
|
switch (endIt[-1].first) {
|
||||||
|
case FSlash:
|
||||||
|
if (secondlast == FSlash) {
|
||||||
|
stack.erase(endIt-2, endIt);
|
||||||
|
stack.push_back({LinecommentStart, "//"});
|
||||||
|
} else if (secondlast == Star) {
|
||||||
|
stack.erase(endIt-2, endIt);
|
||||||
|
stack.push_back({BlockCommentEnd, "*/"});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Star:
|
||||||
|
if (secondlast == FSlash) {
|
||||||
|
stack.erase(endIt-2, endIt);
|
||||||
|
stack.push_back({BlockCommentStart, "/*"});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Newline:
|
||||||
|
for (auto i : stack) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream ShaderPreprocessor::process(std::stringstream& code) const {
|
||||||
|
std::stringstream ret;
|
||||||
|
std::string line;
|
||||||
|
const std::vector<std::string> preprocKeys = {
|
||||||
|
"#include",
|
||||||
|
"//",
|
||||||
|
"/*",
|
||||||
|
"*/"
|
||||||
|
};
|
||||||
|
const size_t keysListSize = preprocKeys.size();
|
||||||
|
enum Context : uint8_t {
|
||||||
|
Code,
|
||||||
|
LineComment,
|
||||||
|
BlockComment
|
||||||
|
};
|
||||||
|
Context currcontext = Code;
|
||||||
|
bool blockcomment = false;
|
||||||
|
std::vector<size_t> keysIdx(keysListSize);
|
||||||
|
// Use linked list to count char len of each line
|
||||||
|
while(std::getline(code, line)) {
|
||||||
|
if (!blockcomment) {
|
||||||
|
for (int i = 0; i < keysListSize; ++i) {
|
||||||
|
keysIdx[i] = line.find(preprocKeys[i]);
|
||||||
|
}
|
||||||
|
} else if (keysIdx[2] == std::string::npos) {
|
||||||
|
ret << line;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (keysIdx[0] != std::string::npos) {
|
||||||
|
if (keysIdx[1] != std::string::npos) {
|
||||||
|
if ( (keysIdx[0] > keysIdx[1]) ) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream ShaderPreprocessor::process(std::string name) const {
|
||||||
|
std::stringstream code = load(name);
|
||||||
|
return process(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shader class
|
||||||
|
Shader::Shader(const std::stringstream& vertexShader, const std::stringstream& fragmentShader) {
|
||||||
|
std::string vertexCode, fragmentCode;
|
||||||
|
vertexCode = vertexShader.str();
|
||||||
|
fragmentCode = fragmentShader.str();
|
||||||
|
|
||||||
const char* vShaderCode = vertexCode.c_str();
|
const char* vShaderCode = vertexCode.c_str();
|
||||||
const char* fShaderCode = fragmentCode.c_str();
|
const char* fShaderCode = fragmentCode.c_str();
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
layout (location=0) in vec3 vPos;
|
layout (location=0) in vec3 vPos;
|
||||||
layout (location=1) in vec3 vColor;
|
layout (location=1) in vec3 vColor;
|
||||||
|
|
||||||
|
#include "frag.fs"
|
||||||
|
|
||||||
uniform mat4 look;
|
uniform mat4 look;
|
||||||
|
|
||||||
out vec3 vertColor;
|
out vec3 vertColor;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user