feat: 实现 Window 与 InputModule 消息集成
This commit is contained in:
@@ -227,6 +227,7 @@ add_library(XCEngine STATIC
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Platform/PlatformTypes.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Platform/PlatformTypes.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Platform/Window.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Platform/Window.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Platform/Windows/WindowsWindow.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Platform/Windows/WindowsWindow.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Platform/Window.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Platform/Windows/WindowsWindow.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Platform/Windows/WindowsWindow.cpp
|
||||||
|
|
||||||
# Input
|
# Input
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace Input {
|
namespace Input {
|
||||||
@@ -10,6 +11,7 @@ public:
|
|||||||
virtual void Initialize(void* windowHandle) = 0;
|
virtual void Initialize(void* windowHandle) = 0;
|
||||||
virtual void Shutdown() = 0;
|
virtual void Shutdown() = 0;
|
||||||
virtual void PumpEvents() = 0;
|
virtual void PumpEvents() = 0;
|
||||||
|
virtual void HandleMessage(size_t hwnd, unsigned int msg, size_t wParam, size_t lParam) = 0;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
InputModule() = default;
|
InputModule() = default;
|
||||||
|
|||||||
@@ -16,16 +16,15 @@ public:
|
|||||||
void Initialize(void* windowHandle) override;
|
void Initialize(void* windowHandle) override;
|
||||||
void Shutdown() override;
|
void Shutdown() override;
|
||||||
void PumpEvents() override;
|
void PumpEvents() override;
|
||||||
|
void HandleMessage(size_t hwnd, unsigned int msg, size_t wParam, size_t lParam) override;
|
||||||
void HandleMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void ProcessKeyDown(WPARAM wParam, LPARAM lParam);
|
void ProcessKeyDown(size_t wParam, size_t lParam);
|
||||||
void ProcessKeyUp(WPARAM wParam);
|
void ProcessKeyUp(size_t wParam);
|
||||||
void ProcessMouseMove(WPARAM wParam, LPARAM lParam);
|
void ProcessMouseMove(size_t wParam, size_t lParam);
|
||||||
void ProcessMouseButton(WPARAM wParam, LPARAM lParam, bool pressed, MouseButton button);
|
void ProcessMouseButton(size_t wParam, size_t lParam, bool pressed, MouseButton button);
|
||||||
void ProcessMouseWheel(WPARAM wParam, LPARAM lParam);
|
void ProcessMouseWheel(size_t wParam, size_t lParam);
|
||||||
void ProcessCharInput(WPARAM wParam);
|
void ProcessCharInput(size_t wParam);
|
||||||
|
|
||||||
KeyCode VKCodeToKeyCode(int vkCode);
|
KeyCode VKCodeToKeyCode(int vkCode);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
#include "PlatformTypes.h"
|
#include "PlatformTypes.h"
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
|
namespace Input {
|
||||||
|
class InputModule;
|
||||||
|
}
|
||||||
|
|
||||||
namespace Platform {
|
namespace Platform {
|
||||||
|
|
||||||
class Window {
|
class Window {
|
||||||
@@ -23,6 +27,12 @@ public:
|
|||||||
virtual bool ShouldClose() const = 0;
|
virtual bool ShouldClose() const = 0;
|
||||||
|
|
||||||
virtual void* GetNativeHandle() = 0;
|
virtual void* GetNativeHandle() = 0;
|
||||||
|
|
||||||
|
void SetInputModule(Input::InputModule* module);
|
||||||
|
Input::InputModule* GetInputModule() const { return m_inputModule; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Input::InputModule* m_inputModule = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Platform
|
} // namespace Platform
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ void WindowsInputModule::Shutdown() {
|
|||||||
void WindowsInputModule::PumpEvents() {
|
void WindowsInputModule::PumpEvents() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowsInputModule::HandleMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
void WindowsInputModule::HandleMessage(size_t hwnd, unsigned int message, size_t wParam, size_t lParam) {
|
||||||
if (!m_isInitialized) return;
|
if (!m_isInitialized) return;
|
||||||
|
|
||||||
switch (message) {
|
switch (message) {
|
||||||
@@ -83,17 +83,17 @@ void WindowsInputModule::HandleMessage(HWND hwnd, UINT message, WPARAM wParam, L
|
|||||||
|
|
||||||
case WM_XBUTTONDOWN:
|
case WM_XBUTTONDOWN:
|
||||||
ProcessMouseButton(wParam, lParam, true,
|
ProcessMouseButton(wParam, lParam, true,
|
||||||
(HIWORD(wParam) == 1) ? MouseButton::Button4 : MouseButton::Button5);
|
(HIWORD(static_cast<UINT>(wParam)) == 1) ? MouseButton::Button4 : MouseButton::Button5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WM_XBUTTONUP:
|
case WM_XBUTTONUP:
|
||||||
ProcessMouseButton(wParam, lParam, false,
|
ProcessMouseButton(wParam, lParam, false,
|
||||||
(HIWORD(wParam) == 1) ? MouseButton::Button4 : MouseButton::Button5);
|
(HIWORD(static_cast<UINT>(wParam)) == 1) ? MouseButton::Button4 : MouseButton::Button5);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowsInputModule::ProcessKeyDown(WPARAM wParam, LPARAM lParam) {
|
void WindowsInputModule::ProcessKeyDown(size_t wParam, size_t lParam) {
|
||||||
bool repeat = (lParam & (1 << 30)) != 0;
|
bool repeat = (lParam & (1 << 30)) != 0;
|
||||||
KeyCode keyCode = VKCodeToKeyCode(static_cast<int>(wParam));
|
KeyCode keyCode = VKCodeToKeyCode(static_cast<int>(wParam));
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ void WindowsInputModule::ProcessKeyDown(WPARAM wParam, LPARAM lParam) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowsInputModule::ProcessKeyUp(WPARAM wParam) {
|
void WindowsInputModule::ProcessKeyUp(size_t wParam) {
|
||||||
KeyCode keyCode = VKCodeToKeyCode(static_cast<int>(wParam));
|
KeyCode keyCode = VKCodeToKeyCode(static_cast<int>(wParam));
|
||||||
|
|
||||||
bool alt = (GetKeyState(VK_MENU) & 0x8000) != 0;
|
bool alt = (GetKeyState(VK_MENU) & 0x8000) != 0;
|
||||||
@@ -120,9 +120,9 @@ void WindowsInputModule::ProcessKeyUp(WPARAM wParam) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowsInputModule::ProcessMouseMove(WPARAM wParam, LPARAM lParam) {
|
void WindowsInputModule::ProcessMouseMove(size_t wParam, size_t lParam) {
|
||||||
int x = LOWORD(lParam);
|
int x = LOWORD(static_cast<UINT>(lParam));
|
||||||
int y = HIWORD(lParam);
|
int y = HIWORD(static_cast<UINT>(lParam));
|
||||||
|
|
||||||
int deltaX = x - static_cast<int>(m_lastMousePosition.x);
|
int deltaX = x - static_cast<int>(m_lastMousePosition.x);
|
||||||
int deltaY = y - static_cast<int>(m_lastMousePosition.y);
|
int deltaY = y - static_cast<int>(m_lastMousePosition.y);
|
||||||
@@ -133,22 +133,22 @@ void WindowsInputModule::ProcessMouseMove(WPARAM wParam, LPARAM lParam) {
|
|||||||
InputManager::Get().ProcessMouseMove(x, y, deltaX, deltaY);
|
InputManager::Get().ProcessMouseMove(x, y, deltaX, deltaY);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowsInputModule::ProcessMouseButton(WPARAM wParam, LPARAM lParam, bool pressed, MouseButton button) {
|
void WindowsInputModule::ProcessMouseButton(size_t wParam, size_t lParam, bool pressed, MouseButton button) {
|
||||||
int x = LOWORD(lParam);
|
int x = LOWORD(static_cast<UINT>(lParam));
|
||||||
int y = HIWORD(lParam);
|
int y = HIWORD(static_cast<UINT>(lParam));
|
||||||
|
|
||||||
InputManager::Get().ProcessMouseButton(button, pressed, x, y);
|
InputManager::Get().ProcessMouseButton(button, pressed, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowsInputModule::ProcessMouseWheel(WPARAM wParam, LPARAM lParam) {
|
void WindowsInputModule::ProcessMouseWheel(size_t wParam, size_t lParam) {
|
||||||
int x = LOWORD(lParam);
|
int x = LOWORD(static_cast<UINT>(lParam));
|
||||||
int y = HIWORD(lParam);
|
int y = HIWORD(static_cast<UINT>(lParam));
|
||||||
short delta = static_cast<short>(HIWORD(wParam));
|
short delta = static_cast<short>(HIWORD(static_cast<UINT>(wParam)));
|
||||||
|
|
||||||
InputManager::Get().ProcessMouseWheel(static_cast<float>(delta) / 120.0f, x, y);
|
InputManager::Get().ProcessMouseWheel(static_cast<float>(delta) / 120.0f, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowsInputModule::ProcessCharInput(WPARAM wParam) {
|
void WindowsInputModule::ProcessCharInput(size_t wParam) {
|
||||||
char c = static_cast<char>(wParam);
|
char c = static_cast<char>(wParam);
|
||||||
|
|
||||||
if (c >= 32 && c < 127) {
|
if (c >= 32 && c < 127) {
|
||||||
|
|||||||
11
engine/src/Platform/Window.cpp
Normal file
11
engine/src/Platform/Window.cpp
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#include "Platform/Window.h"
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace Platform {
|
||||||
|
|
||||||
|
void Window::SetInputModule(Input::InputModule* module) {
|
||||||
|
m_inputModule = module;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Platform
|
||||||
|
} // namespace XCEngine
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "Platform/Windows/WindowsWindow.h"
|
#include "Platform/Windows/WindowsWindow.h"
|
||||||
|
#include "Input/Platform/WindowsInputModule.h"
|
||||||
#include <Windows.h>
|
#include <Windows.h>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
@@ -80,6 +81,10 @@ void WindowsWindow::PumpEvents() {
|
|||||||
TranslateMessage(&msg);
|
TranslateMessage(&msg);
|
||||||
DispatchMessage(&msg);
|
DispatchMessage(&msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_inputModule) {
|
||||||
|
m_inputModule->PumpEvents();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowsWindow::SetTitle(const Containers::String& title) {
|
void WindowsWindow::SetTitle(const Containers::String& title) {
|
||||||
@@ -164,6 +169,15 @@ LRESULT CALLBACK WindowsWindow::WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPAR
|
|||||||
window->m_messageCallback(hwnd, msg, wParam, lParam);
|
window->m_messageCallback(hwnd, msg, wParam, lParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window && window->m_inputModule) {
|
||||||
|
window->m_inputModule->HandleMessage(
|
||||||
|
reinterpret_cast<size_t>(hwnd),
|
||||||
|
static_cast<unsigned int>(msg),
|
||||||
|
static_cast<size_t>(wParam),
|
||||||
|
static_cast<size_t>(lParam)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
switch (msg) {
|
switch (msg) {
|
||||||
case WM_CLOSE:
|
case WM_CLOSE:
|
||||||
if (window) window->m_shouldClose = true;
|
if (window) window->m_shouldClose = true;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
set(INPUT_TEST_SOURCES
|
set(INPUT_TEST_SOURCES
|
||||||
test_input_manager.cpp
|
test_input_manager.cpp
|
||||||
|
test_windows_input_module.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(xcengine_input_tests ${INPUT_TEST_SOURCES})
|
add_executable(xcengine_input_tests ${INPUT_TEST_SOURCES})
|
||||||
|
|||||||
194
tests/Input/test_windows_input_module.cpp
Normal file
194
tests/Input/test_windows_input_module.cpp
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <XCEngine/Input/InputManager.h>
|
||||||
|
#include <XCEngine/Input/InputModule.h>
|
||||||
|
#include <XCEngine/Input/Platform/WindowsInputModule.h>
|
||||||
|
#include <XCEngine/Math/Vector2.h>
|
||||||
|
#include <XCEngine/Containers/String.h>
|
||||||
|
|
||||||
|
using namespace XCEngine::Input;
|
||||||
|
using namespace XCEngine::Math;
|
||||||
|
using namespace XCEngine::Containers;
|
||||||
|
using namespace XCEngine::Input::Platform;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
TEST(WindowsInputModule, Construction) {
|
||||||
|
WindowsInputModule module;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WindowsInputModule, InitializeShutdown) {
|
||||||
|
WindowsInputModule module;
|
||||||
|
module.Initialize(nullptr);
|
||||||
|
module.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WindowsInputModule, HandleKeyDown) {
|
||||||
|
InputManager::Get().Initialize(nullptr);
|
||||||
|
|
||||||
|
WindowsInputModule module;
|
||||||
|
module.Initialize(nullptr);
|
||||||
|
|
||||||
|
EXPECT_FALSE(InputManager::Get().IsKeyDown(KeyCode::A));
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x0100, 'A', 0);
|
||||||
|
|
||||||
|
EXPECT_TRUE(InputManager::Get().IsKeyDown(KeyCode::A));
|
||||||
|
|
||||||
|
module.Shutdown();
|
||||||
|
InputManager::Get().Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WindowsInputModule, HandleKeyUp) {
|
||||||
|
InputManager::Get().Initialize(nullptr);
|
||||||
|
|
||||||
|
WindowsInputModule module;
|
||||||
|
module.Initialize(nullptr);
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x0100, 'A', 0);
|
||||||
|
EXPECT_TRUE(InputManager::Get().IsKeyDown(KeyCode::A));
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x0101, 'A', 0);
|
||||||
|
EXPECT_FALSE(InputManager::Get().IsKeyDown(KeyCode::A));
|
||||||
|
|
||||||
|
module.Shutdown();
|
||||||
|
InputManager::Get().Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WindowsInputModule, HandleMouseMove) {
|
||||||
|
InputManager::Get().Initialize(nullptr);
|
||||||
|
|
||||||
|
WindowsInputModule module;
|
||||||
|
module.Initialize(nullptr);
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x0200, 0, 0x00320078);
|
||||||
|
|
||||||
|
Vector2 pos = InputManager::Get().GetMousePosition();
|
||||||
|
EXPECT_EQ(pos.x, 120.0f);
|
||||||
|
EXPECT_EQ(pos.y, 50.0f);
|
||||||
|
|
||||||
|
module.Shutdown();
|
||||||
|
InputManager::Get().Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WindowsInputModule, HandleLeftMouseButtonDown) {
|
||||||
|
InputManager::Get().Initialize(nullptr);
|
||||||
|
|
||||||
|
WindowsInputModule module;
|
||||||
|
module.Initialize(nullptr);
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x0201, 0, 0x00320078);
|
||||||
|
|
||||||
|
EXPECT_TRUE(InputManager::Get().IsMouseButtonDown(MouseButton::Left));
|
||||||
|
|
||||||
|
module.Shutdown();
|
||||||
|
InputManager::Get().Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WindowsInputModule, HandleLeftMouseButtonUp) {
|
||||||
|
InputManager::Get().Initialize(nullptr);
|
||||||
|
|
||||||
|
WindowsInputModule module;
|
||||||
|
module.Initialize(nullptr);
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x0201, 0, 0x00320078);
|
||||||
|
EXPECT_TRUE(InputManager::Get().IsMouseButtonDown(MouseButton::Left));
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x0202, 0, 0x00320078);
|
||||||
|
EXPECT_FALSE(InputManager::Get().IsMouseButtonDown(MouseButton::Left));
|
||||||
|
|
||||||
|
module.Shutdown();
|
||||||
|
InputManager::Get().Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WindowsInputModule, HandleRightMouseButton) {
|
||||||
|
InputManager::Get().Initialize(nullptr);
|
||||||
|
|
||||||
|
WindowsInputModule module;
|
||||||
|
module.Initialize(nullptr);
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x0204, 0, 0x00320078);
|
||||||
|
EXPECT_TRUE(InputManager::Get().IsMouseButtonDown(MouseButton::Right));
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x0205, 0, 0x00320078);
|
||||||
|
EXPECT_FALSE(InputManager::Get().IsMouseButtonDown(MouseButton::Right));
|
||||||
|
|
||||||
|
module.Shutdown();
|
||||||
|
InputManager::Get().Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WindowsInputModule, HandleMiddleMouseButton) {
|
||||||
|
InputManager::Get().Initialize(nullptr);
|
||||||
|
|
||||||
|
WindowsInputModule module;
|
||||||
|
module.Initialize(nullptr);
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x0207, 0, 0x00320078);
|
||||||
|
EXPECT_TRUE(InputManager::Get().IsMouseButtonDown(MouseButton::Middle));
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x0208, 0, 0x00320078);
|
||||||
|
EXPECT_FALSE(InputManager::Get().IsMouseButtonDown(MouseButton::Middle));
|
||||||
|
|
||||||
|
module.Shutdown();
|
||||||
|
InputManager::Get().Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WindowsInputModule, HandleMouseWheel) {
|
||||||
|
InputManager::Get().Initialize(nullptr);
|
||||||
|
|
||||||
|
WindowsInputModule module;
|
||||||
|
module.Initialize(nullptr);
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x020A, 0x00030000, 0x00780078);
|
||||||
|
|
||||||
|
float scrollDelta = InputManager::Get().GetMouseScrollDelta();
|
||||||
|
EXPECT_EQ(scrollDelta, 0.025f);
|
||||||
|
|
||||||
|
module.Shutdown();
|
||||||
|
InputManager::Get().Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WindowsInputModule, HandleTextInput) {
|
||||||
|
InputManager::Get().Initialize(nullptr);
|
||||||
|
|
||||||
|
WindowsInputModule module;
|
||||||
|
module.Initialize(nullptr);
|
||||||
|
|
||||||
|
bool eventFired = false;
|
||||||
|
char capturedChar = 0;
|
||||||
|
|
||||||
|
uint64_t id = InputManager::Get().OnTextInput().Subscribe([&eventFired, &capturedChar](const TextInputEvent& e) {
|
||||||
|
eventFired = true;
|
||||||
|
capturedChar = e.character;
|
||||||
|
});
|
||||||
|
|
||||||
|
module.HandleMessage(0, 0x0102, 'X', 0);
|
||||||
|
|
||||||
|
EXPECT_TRUE(eventFired);
|
||||||
|
EXPECT_EQ(capturedChar, 'X');
|
||||||
|
|
||||||
|
InputManager::Get().OnTextInput().Unsubscribe(id);
|
||||||
|
module.Shutdown();
|
||||||
|
InputManager::Get().Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WindowsInputModule, MultipleModules) {
|
||||||
|
InputManager::Get().Initialize(nullptr);
|
||||||
|
|
||||||
|
WindowsInputModule module1;
|
||||||
|
WindowsInputModule module2;
|
||||||
|
|
||||||
|
module1.Initialize(nullptr);
|
||||||
|
module2.Initialize(nullptr);
|
||||||
|
|
||||||
|
module1.HandleMessage(0, 0x0100, 'A', 0);
|
||||||
|
EXPECT_TRUE(InputManager::Get().IsKeyDown(KeyCode::A));
|
||||||
|
|
||||||
|
module2.HandleMessage(0, 0x0100, 'B', 0);
|
||||||
|
EXPECT_TRUE(InputManager::Get().IsKeyDown(KeyCode::B));
|
||||||
|
|
||||||
|
module1.Shutdown();
|
||||||
|
module2.Shutdown();
|
||||||
|
InputManager::Get().Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user