organise application
This commit is contained in:
@@ -48,7 +48,44 @@ template <typename T> constexpr T clamp(T x, T min, T max)
|
|||||||
return x < min ? min : (x > max ? max : x);
|
return x < min ? min : (x > max ? max : x);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Uint32 timerCallback(Uint32 interval, void* param)
|
class Application final
|
||||||
|
{
|
||||||
|
int argc;
|
||||||
|
const char** argv;
|
||||||
|
|
||||||
|
SDL_Window* window;
|
||||||
|
SDL_Renderer* renderer;
|
||||||
|
SDL_Texture* codepage;
|
||||||
|
SDL_TimerID blinkTimer;
|
||||||
|
|
||||||
|
bool needRepaint;
|
||||||
|
|
||||||
|
bool mouseLeft, mouseRight;
|
||||||
|
int mousePrevX, mousePrevY, mouseX, mouseY;
|
||||||
|
bool mouseInWindow;
|
||||||
|
|
||||||
|
bool drawing, blinkState;
|
||||||
|
|
||||||
|
std::array<Cell, 2000> display;
|
||||||
|
|
||||||
|
int create();
|
||||||
|
int handleEvent(const SDL_Event& event) noexcept;
|
||||||
|
int update();
|
||||||
|
int repaint();
|
||||||
|
|
||||||
|
static Uint32 timerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
|
inline void damage() noexcept { needRepaint = true; }
|
||||||
|
SDL_Texture* loadTexture(const std::string& path);
|
||||||
|
|
||||||
|
public:
|
||||||
|
Application(int argc, const char* argv[]);
|
||||||
|
~Application();
|
||||||
|
|
||||||
|
int run();
|
||||||
|
};
|
||||||
|
|
||||||
|
Uint32 Application::timerCallback(Uint32 interval, void* param)
|
||||||
{
|
{
|
||||||
SDL_Event event =
|
SDL_Event event =
|
||||||
{
|
{
|
||||||
@@ -66,7 +103,54 @@ static Uint32 timerCallback(Uint32 interval, void* param)
|
|||||||
return interval;
|
return interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
|
Application::Application(int aArgc, const char* aArgv[]) :
|
||||||
|
argc(aArgc), argv(aArgv),
|
||||||
|
window(nullptr), renderer(nullptr), codepage(nullptr), blinkTimer(0),
|
||||||
|
needRepaint(true),
|
||||||
|
mouseLeft(false), mouseRight(false),
|
||||||
|
mousePrevX(0), mousePrevY(0), mouseX(0), mouseY(0),
|
||||||
|
mouseInWindow(false),
|
||||||
|
drawing(false), blinkState(true) {}
|
||||||
|
|
||||||
|
Application::~Application()
|
||||||
|
{
|
||||||
|
SDL_RemoveTimer(blinkTimer);
|
||||||
|
SDL_DestroyTexture(codepage);
|
||||||
|
SDL_DestroyRenderer(renderer);
|
||||||
|
SDL_DestroyWindow(window);
|
||||||
|
SDL_Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Texture* Application::loadTexture(const std::string& path)
|
||||||
|
{
|
||||||
|
unsigned pixelWidth, pixelHeight;
|
||||||
|
std::vector<unsigned char> pixels;
|
||||||
|
if (lodepng::decode(pixels, pixelWidth, pixelHeight, path, LCT_RGBA, 8) != 0)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Uint32 format = SDL_PIXELFORMAT_RGBA8888;
|
||||||
|
constexpr int access = SDL_TEXTUREACCESS_STATIC;
|
||||||
|
const int textureWidth = static_cast<int>(pixelWidth), textureHeight = static_cast<int>(pixelHeight);
|
||||||
|
SDL_Texture* texture = SDL_CreateTexture(renderer, format, access, textureWidth, textureHeight);
|
||||||
|
if (texture == nullptr)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SDL_UpdateTexture(texture, nullptr, pixels.data(), 4 * static_cast<int>(pixelWidth)) != 0 ||
|
||||||
|
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND) != 0)
|
||||||
|
{
|
||||||
|
SDL_DestroyTexture(texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int Application::create()
|
||||||
{
|
{
|
||||||
if (SDL_Init(SDL_INIT_VIDEO) < 0)
|
if (SDL_Init(SDL_INIT_VIDEO) < 0)
|
||||||
{
|
{
|
||||||
@@ -76,47 +160,25 @@ int main(int argc, char** argv)
|
|||||||
constexpr int screenWidth = cellWidth * 80;
|
constexpr int screenWidth = cellWidth * 80;
|
||||||
constexpr int screenHeight = cellHeight * 25;
|
constexpr int screenHeight = cellHeight * 25;
|
||||||
|
|
||||||
SDL_Window* window = nullptr;
|
|
||||||
SDL_Renderer* renderer = nullptr;
|
|
||||||
constexpr Uint32 winFlags = SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE;
|
constexpr Uint32 winFlags = SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE;
|
||||||
if (SDL_CreateWindowAndRenderer(screenWidth * 2, screenHeight * 2, winFlags, &window, &renderer) < 0)
|
if (SDL_CreateWindowAndRenderer(screenWidth * 2, screenHeight * 2, winFlags, &window, &renderer) < 0)
|
||||||
{
|
{
|
||||||
SDL_Quit();
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_SetWindowTitle(window, "EnDOOMed");
|
SDL_SetWindowTitle(window, "EnDOOMed");
|
||||||
SDL_RenderSetLogicalSize(renderer, screenWidth, screenHeight);
|
SDL_RenderSetLogicalSize(renderer, screenWidth, screenHeight);
|
||||||
|
|
||||||
SDL_Texture* codepage = nullptr;
|
if ((codepage = Application::loadTexture("codepage.png")) == nullptr)
|
||||||
{
|
{
|
||||||
unsigned codepageWidth, codepageHeight;
|
return -1;
|
||||||
std::vector<unsigned char> codepagePixels;
|
|
||||||
if (lodepng::decode(codepagePixels, codepageWidth, codepageHeight, "codepage.png", LCT_RGBA, 8) != 0 ||
|
|
||||||
(codepage = SDL_CreateTexture(renderer,
|
|
||||||
SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STATIC,
|
|
||||||
static_cast<int>(codepageWidth), static_cast<int>(codepageHeight))) == nullptr ||
|
|
||||||
SDL_UpdateTexture(codepage, nullptr, codepagePixels.data(), 4 * static_cast<int>(codepageWidth)) != 0 ||
|
|
||||||
SDL_SetTextureBlendMode(codepage, SDL_BLENDMODE_BLEND) != 0)
|
|
||||||
{
|
|
||||||
SDL_DestroyTexture(codepage);
|
|
||||||
SDL_DestroyRenderer(renderer);
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
SDL_Quit();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::array<Cell, 2000> display;
|
|
||||||
std::memset(display.data(), 0, sizeof(Cell) * display.size());
|
std::memset(display.data(), 0, sizeof(Cell) * display.size());
|
||||||
|
|
||||||
FILE* file = fopen("ENDOOM.bin", "rb");
|
FILE* file = fopen("ENDOOM.bin", "rb");
|
||||||
if (file == nullptr)
|
if (file == nullptr)
|
||||||
{
|
{
|
||||||
SDL_DestroyTexture(codepage);
|
|
||||||
SDL_DestroyRenderer(renderer);
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
SDL_Quit();
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +196,71 @@ int main(int argc, char** argv)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_AddTimer(400, timerCallback, nullptr);
|
fclose(file);
|
||||||
|
|
||||||
|
blinkTimer = SDL_AddTimer(400, Application::timerCallback, nullptr);
|
||||||
|
if (blinkTimer == 0)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Application::handleEvent(const SDL_Event& event) noexcept
|
||||||
|
{
|
||||||
|
switch (event.type)
|
||||||
|
{
|
||||||
|
case SDL_QUIT:
|
||||||
|
return 1;
|
||||||
|
case SDL_USEREVENT:
|
||||||
|
blinkState = !blinkState;
|
||||||
|
damage();
|
||||||
|
return 0;
|
||||||
|
case SDL_MOUSEBUTTONDOWN:
|
||||||
|
switch (event.button.button)
|
||||||
|
{
|
||||||
|
case SDL_BUTTON_LEFT:
|
||||||
|
mouseLeft = true;
|
||||||
|
break;
|
||||||
|
case SDL_BUTTON_RIGHT:
|
||||||
|
mouseRight = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case SDL_MOUSEBUTTONUP:
|
||||||
|
switch (event.button.button)
|
||||||
|
{
|
||||||
|
case SDL_BUTTON_LEFT:
|
||||||
|
mouseLeft = false;
|
||||||
|
break;
|
||||||
|
case SDL_BUTTON_RIGHT:
|
||||||
|
mouseRight = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case SDL_MOUSEMOTION:
|
||||||
|
mouseX = event.motion.x;
|
||||||
|
mouseY = event.motion.y;
|
||||||
|
return 0;
|
||||||
|
case SDL_WINDOWEVENT:
|
||||||
|
switch (event.window.event)
|
||||||
|
{
|
||||||
|
case SDL_WINDOWEVENT_LEAVE:
|
||||||
|
mouseInWindow = false;
|
||||||
|
break;
|
||||||
|
case SDL_WINDOWEVENT_ENTER:
|
||||||
|
mouseInWindow = true;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
return 0;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int Application::update()
|
||||||
|
{
|
||||||
Cell leftBrush =
|
Cell leftBrush =
|
||||||
{
|
{
|
||||||
.character = 219,
|
.character = 219,
|
||||||
@@ -151,174 +276,143 @@ int main(int argc, char** argv)
|
|||||||
.blinking = false
|
.blinking = false
|
||||||
};
|
};
|
||||||
|
|
||||||
bool mouseLeft = false, mouseRight = false;
|
if (mouseInWindow && (mouseLeft || mouseRight))
|
||||||
int mousePrevX = 0, mousePrevY = 0;
|
|
||||||
int mouseX = 0, mouseY = 0;
|
|
||||||
bool mouseInWindow = false;
|
|
||||||
|
|
||||||
bool running = true, damage = true;
|
|
||||||
bool blinkState = true;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
{
|
||||||
|
if (!drawing)
|
||||||
|
{
|
||||||
|
mousePrevX = mouseX;
|
||||||
|
mousePrevY = mouseY;
|
||||||
|
drawing = true;
|
||||||
|
}
|
||||||
|
int fromX = clamp(mouseX / cellWidth, 0, 79);
|
||||||
|
int fromY = clamp(mouseY / cellHeight, 0, 24);
|
||||||
|
int toX = clamp(mousePrevX / cellWidth, 0, 79);
|
||||||
|
int toY = clamp(mousePrevY / cellHeight, 0, 24);
|
||||||
|
|
||||||
|
auto srcChar = mouseLeft ? leftBrush : rightBrush;
|
||||||
|
if (toX == fromX && toY == fromY)
|
||||||
|
{
|
||||||
|
display[toX + toY * 80] = srcChar;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bool steep = std::abs(toY - fromY) > std::abs(toX - fromX);
|
||||||
|
if (steep)
|
||||||
|
{
|
||||||
|
std::swap(fromY, fromX);
|
||||||
|
std::swap(toY, toX);
|
||||||
|
}
|
||||||
|
if (fromX > toX)
|
||||||
|
{
|
||||||
|
std::swap(toX, fromX);
|
||||||
|
std::swap(toY, fromY);
|
||||||
|
}
|
||||||
|
|
||||||
|
int deltaX = toX - fromX;
|
||||||
|
int deltaY = std::abs(toY - fromY);
|
||||||
|
int error = deltaX / 2;
|
||||||
|
int y = fromY;
|
||||||
|
int yStep = (fromY < toY) ? 1 : -1;
|
||||||
|
for (int x = fromX; x <= toX; ++x)
|
||||||
|
{
|
||||||
|
int idx = steep
|
||||||
|
? y + x * 80
|
||||||
|
: x + y * 80;
|
||||||
|
display[idx] = srcChar;
|
||||||
|
|
||||||
|
error -= deltaY;
|
||||||
|
if (error < 0)
|
||||||
|
{
|
||||||
|
y += yStep;
|
||||||
|
error += deltaX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mousePrevX = mouseX;
|
mousePrevX = mouseX;
|
||||||
mousePrevY = mouseY;
|
mousePrevY = mouseY;
|
||||||
|
|
||||||
|
damage();
|
||||||
|
}
|
||||||
|
else if (drawing && !mouseLeft && !mouseRight)
|
||||||
|
{
|
||||||
|
drawing = false;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Application::repaint()
|
||||||
|
{
|
||||||
|
SDL_SetRenderDrawColor(renderer, 48, 48, 48, 255);
|
||||||
|
SDL_RenderClear(renderer);
|
||||||
|
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
src.w = cellWidth;
|
||||||
|
src.h = cellHeight;
|
||||||
|
dst.w = cellWidth;
|
||||||
|
dst.h = cellHeight;
|
||||||
|
|
||||||
|
int x = 0, y = 0;
|
||||||
|
for (const auto& currentCharacter : display)
|
||||||
|
{
|
||||||
|
auto currentBgColour = colourTable[static_cast<unsigned char>(currentCharacter.bgColour)];
|
||||||
|
auto currentFgColour = colourTable[static_cast<unsigned char>(currentCharacter.fgColour)];
|
||||||
|
|
||||||
|
src.x = (currentCharacter.character % 32) * cellWidth;
|
||||||
|
src.y = (currentCharacter.character / 32) * cellHeight;
|
||||||
|
dst.x = x * cellWidth;
|
||||||
|
dst.y = y * cellHeight;
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(renderer, currentBgColour.r, currentBgColour.g, currentBgColour.b, currentBgColour.a);
|
||||||
|
SDL_RenderFillRect(renderer, &dst);
|
||||||
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, currentFgColour.a);
|
||||||
|
|
||||||
|
if (blinkState || !currentCharacter.blinking)
|
||||||
|
{
|
||||||
|
SDL_SetTextureColorMod(codepage, currentFgColour.r, currentFgColour.g, currentFgColour.b);
|
||||||
|
SDL_RenderCopy(renderer, codepage, &src, &dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++x >= 80)
|
||||||
|
{
|
||||||
|
x = 0;
|
||||||
|
++y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_RenderPresent(renderer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Application::run()
|
||||||
|
{
|
||||||
|
int res = create();
|
||||||
|
while (res == 0)
|
||||||
|
{
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
if (SDL_WaitEvent(&event))
|
if (SDL_WaitEvent(&event))
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
switch (event.type)
|
res = handleEvent(event);
|
||||||
{
|
} while (res == 0 && SDL_PollEvent(&event) > 0);
|
||||||
case SDL_QUIT:
|
|
||||||
running = false;
|
|
||||||
break;
|
|
||||||
case SDL_USEREVENT:
|
|
||||||
blinkState = !blinkState;
|
|
||||||
damage = true;
|
|
||||||
break;
|
|
||||||
case SDL_MOUSEBUTTONDOWN:
|
|
||||||
switch (event.button.button)
|
|
||||||
{
|
|
||||||
case SDL_BUTTON_LEFT:
|
|
||||||
mouseLeft = true;
|
|
||||||
break;
|
|
||||||
case SDL_BUTTON_RIGHT:
|
|
||||||
mouseRight = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case SDL_MOUSEBUTTONUP:
|
|
||||||
switch (event.button.button)
|
|
||||||
{
|
|
||||||
case SDL_BUTTON_LEFT:
|
|
||||||
mouseLeft = false;
|
|
||||||
break;
|
|
||||||
case SDL_BUTTON_RIGHT:
|
|
||||||
mouseRight = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case SDL_MOUSEMOTION:
|
|
||||||
mouseX = event.motion.x;
|
|
||||||
mouseY = event.motion.y;
|
|
||||||
break;
|
|
||||||
case SDL_WINDOWEVENT:
|
|
||||||
switch (event.window.event)
|
|
||||||
{
|
|
||||||
case SDL_WINDOWEVENT_LEAVE:
|
|
||||||
mouseInWindow = false;
|
|
||||||
break;
|
|
||||||
case SDL_WINDOWEVENT_ENTER:
|
|
||||||
mouseInWindow = true;
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (SDL_PollEvent(&event) > 0);
|
|
||||||
}
|
}
|
||||||
|
if (res == 0)
|
||||||
if (mouseInWindow && (mouseLeft || mouseRight))
|
|
||||||
{
|
{
|
||||||
int fromX = clamp(mouseX / cellWidth, 0, 79);
|
res = update();
|
||||||
int fromY = clamp(mouseY / cellHeight, 0, 24);
|
|
||||||
int toX = clamp(mousePrevX / cellWidth, 0, 79);
|
|
||||||
int toY = clamp(mousePrevY / cellHeight, 0, 24);
|
|
||||||
|
|
||||||
auto srcChar = mouseLeft ? leftBrush : rightBrush;
|
|
||||||
if (toX == fromX && toY == fromY)
|
|
||||||
{
|
|
||||||
display[toX + toY * 80] = srcChar;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bool steep = std::abs(toY - fromY) > std::abs(toX - fromX);
|
|
||||||
if (steep)
|
|
||||||
{
|
|
||||||
std::swap(fromY, fromX);
|
|
||||||
std::swap(toY, toX);
|
|
||||||
}
|
|
||||||
if (fromX > toX)
|
|
||||||
{
|
|
||||||
std::swap(toX, fromX);
|
|
||||||
std::swap(toY, fromY);
|
|
||||||
}
|
|
||||||
|
|
||||||
int deltaX = toX - fromX;
|
|
||||||
int deltaY = std::abs(toY - fromY);
|
|
||||||
int error = deltaX / 2;
|
|
||||||
int y = fromY;
|
|
||||||
int yStep = (fromY < toY) ? 1 : -1;
|
|
||||||
for (int x = fromX; x <= toX; ++x)
|
|
||||||
{
|
|
||||||
int idx = steep
|
|
||||||
? y + x * 80
|
|
||||||
: x + y * 80;
|
|
||||||
display[idx] = srcChar;
|
|
||||||
|
|
||||||
error -= deltaY;
|
|
||||||
if (error < 0)
|
|
||||||
{
|
|
||||||
y += yStep;
|
|
||||||
error += deltaX;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
damage = true;
|
|
||||||
}
|
}
|
||||||
|
if (res == 0 && needRepaint)
|
||||||
if (damage)
|
|
||||||
{
|
{
|
||||||
SDL_SetRenderDrawColor(renderer, 48, 48, 48, 255);
|
res = repaint();
|
||||||
SDL_RenderClear(renderer);
|
needRepaint = false;
|
||||||
|
|
||||||
SDL_Rect src, dst;
|
|
||||||
src.w = cellWidth;
|
|
||||||
src.h = cellHeight;
|
|
||||||
dst.w = cellWidth;
|
|
||||||
dst.h = cellHeight;
|
|
||||||
|
|
||||||
int x = 0, y = 0;
|
|
||||||
for (const auto& currentCharacter : display)
|
|
||||||
{
|
|
||||||
auto currentBgColour = colourTable[static_cast<unsigned char>(currentCharacter.bgColour)];
|
|
||||||
auto currentFgColour = colourTable[static_cast<unsigned char>(currentCharacter.fgColour)];
|
|
||||||
|
|
||||||
src.x = (currentCharacter.character % 32) * cellWidth;
|
|
||||||
src.y = (currentCharacter.character / 32) * cellHeight;
|
|
||||||
dst.x = x * cellWidth;
|
|
||||||
dst.y = y * cellHeight;
|
|
||||||
|
|
||||||
SDL_SetRenderDrawColor(renderer, currentBgColour.r, currentBgColour.g, currentBgColour.b, currentBgColour.a);
|
|
||||||
SDL_RenderFillRect(renderer, &dst);
|
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, currentFgColour.a);
|
|
||||||
|
|
||||||
if (blinkState || !currentCharacter.blinking)
|
|
||||||
{
|
|
||||||
SDL_SetTextureColorMod(codepage, currentFgColour.r, currentFgColour.g, currentFgColour.b);
|
|
||||||
SDL_RenderCopy(renderer, codepage, &src, &dst);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (++x >= 80)
|
|
||||||
{
|
|
||||||
x = 0;
|
|
||||||
++y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_RenderPresent(renderer);
|
|
||||||
damage = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (running);
|
return res < 0 ? 1 : 0;
|
||||||
|
};
|
||||||
|
|
||||||
fclose(file);
|
|
||||||
SDL_DestroyTexture(codepage);
|
int main(int argc, char* argv[])
|
||||||
SDL_DestroyRenderer(renderer);
|
{
|
||||||
SDL_DestroyWindow(window);
|
Application app(argc, const_cast<const char**>(argv));
|
||||||
SDL_Quit();
|
return app.run();
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user