mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-18 20:05:02 -04:00
fully abstract glfw window
This commit is contained in:
parent
b7fa34dc24
commit
8907adb032
@ -16,6 +16,7 @@ package de.bixilon.minosoft.config.key
|
||||
import org.lwjgl.glfw.GLFW.*
|
||||
import java.util.*
|
||||
|
||||
// ToDo: Replace glfwKeyIds
|
||||
enum class KeyCodes(val glfwKeyId: Int) {
|
||||
KEY_UNKNOWN(GLFW_KEY_UNKNOWN),
|
||||
KEY_SPACE(GLFW_KEY_SPACE),
|
||||
|
@ -25,13 +25,13 @@ import de.bixilon.minosoft.gui.rendering.hud.HUDRenderer
|
||||
import de.bixilon.minosoft.gui.rendering.hud.atlas.TextureLike
|
||||
import de.bixilon.minosoft.gui.rendering.hud.atlas.TextureLikeTexture
|
||||
import de.bixilon.minosoft.gui.rendering.input.key.RenderWindowInputHandler
|
||||
import de.bixilon.minosoft.gui.rendering.modding.events.RenderingStateChangeEvent
|
||||
import de.bixilon.minosoft.gui.rendering.modding.events.ResizeWindowEvent
|
||||
import de.bixilon.minosoft.gui.rendering.modding.events.*
|
||||
import de.bixilon.minosoft.gui.rendering.particle.ParticleRenderer
|
||||
import de.bixilon.minosoft.gui.rendering.shader.Shader
|
||||
import de.bixilon.minosoft.gui.rendering.sky.SkyRenderer
|
||||
import de.bixilon.minosoft.gui.rendering.system.base.RenderSystem
|
||||
import de.bixilon.minosoft.gui.rendering.system.opengl.OpenGLRenderSystem
|
||||
import de.bixilon.minosoft.gui.rendering.system.window.BaseWindow
|
||||
import de.bixilon.minosoft.gui.rendering.system.window.GLFWWindow
|
||||
import de.bixilon.minosoft.gui.rendering.textures.Texture
|
||||
import de.bixilon.minosoft.gui.rendering.textures.TextureArray
|
||||
@ -42,6 +42,7 @@ import de.bixilon.minosoft.protocol.network.connection.PlayConnection
|
||||
import de.bixilon.minosoft.protocol.packets.s2c.play.PositionAndRotationS2CP
|
||||
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
|
||||
import de.bixilon.minosoft.util.CountUpAndDownLatch
|
||||
import de.bixilon.minosoft.util.KUtil.decide
|
||||
import de.bixilon.minosoft.util.KUtil.synchronizedMapOf
|
||||
import de.bixilon.minosoft.util.Queue
|
||||
import de.bixilon.minosoft.util.Stopwatch
|
||||
@ -49,17 +50,13 @@ import de.bixilon.minosoft.util.logging.Log
|
||||
import de.bixilon.minosoft.util.logging.LogMessageType
|
||||
import glm_.vec2.Vec2
|
||||
import glm_.vec2.Vec2i
|
||||
import org.lwjgl.glfw.Callbacks
|
||||
import org.lwjgl.glfw.GLFW.*
|
||||
import org.lwjgl.glfw.GLFWWindowFocusCallback
|
||||
import org.lwjgl.glfw.GLFWWindowIconifyCallback
|
||||
import org.lwjgl.opengl.GL11.*
|
||||
|
||||
class RenderWindow(
|
||||
val connection: PlayConnection,
|
||||
val rendering: Rendering,
|
||||
) {
|
||||
val window = GLFWWindow(connection)
|
||||
val window: BaseWindow = GLFWWindow(connection)
|
||||
val renderSystem: RenderSystem = OpenGLRenderSystem(this)
|
||||
var initialized = false
|
||||
private set
|
||||
@ -70,9 +67,6 @@ class RenderWindow(
|
||||
val screenDimensions
|
||||
get() = window.size
|
||||
|
||||
@Deprecated(message = "", replaceWith = ReplaceWith("window.sizef"))
|
||||
val screenDimensionsF: Vec2
|
||||
get() = window.sizef
|
||||
val inputHandler = RenderWindowInputHandler(this)
|
||||
|
||||
private var deltaFrameTime = 0.0
|
||||
@ -144,7 +138,6 @@ class RenderWindow(
|
||||
tintColorCalculator.init(connection.assetsManager)
|
||||
|
||||
|
||||
|
||||
Log.log(LogMessageType.RENDERING_LOADING) { "Creating context (${stopwatch.labTime()})..." }
|
||||
|
||||
renderSystem.init()
|
||||
@ -188,26 +181,14 @@ class RenderWindow(
|
||||
}
|
||||
|
||||
|
||||
Log.log(LogMessageType.RENDERING_LOADING) { "Registering glfw callbacks (${stopwatch.labTime()})..." }
|
||||
Log.log(LogMessageType.RENDERING_LOADING) { "Registering window callbacks (${stopwatch.labTime()})..." }
|
||||
|
||||
glfwSetWindowFocusCallback(window.window, object : GLFWWindowFocusCallback() {
|
||||
override fun invoke(window: Long, focused: Boolean) {
|
||||
setRenderStatus(if (focused) {
|
||||
RenderingStates.RUNNING
|
||||
} else {
|
||||
RenderingStates.SLOW
|
||||
})
|
||||
}
|
||||
connection.registerEvent(CallbackEventInvoker.of<WindowFocusChangeEvent> {
|
||||
setRenderStatus(it.focused.decide(RenderingStates.RUNNING, RenderingStates.SLOW))
|
||||
})
|
||||
|
||||
glfwSetWindowIconifyCallback(window.window, object : GLFWWindowIconifyCallback() {
|
||||
override fun invoke(window: Long, iconified: Boolean) {
|
||||
setRenderStatus(if (iconified) {
|
||||
RenderingStates.PAUSED
|
||||
} else {
|
||||
RenderingStates.RUNNING
|
||||
})
|
||||
}
|
||||
connection.registerEvent(CallbackEventInvoker.of<WindowIconifyChangeEvent> {
|
||||
setRenderStatus(it.iconified.decide(RenderingStates.PAUSED, RenderingStates.RUNNING))
|
||||
})
|
||||
|
||||
|
||||
@ -237,7 +218,7 @@ class RenderWindow(
|
||||
sendDebugMessage("Toggled polygon mode!")
|
||||
}
|
||||
|
||||
inputHandler.registerKeyCallback(KeyBindingsNames.QUIT_RENDERING) { glfwSetWindowShouldClose(window.window, true) }
|
||||
inputHandler.registerKeyCallback(KeyBindingsNames.QUIT_RENDERING) { window.close() }
|
||||
inputHandler.registerKeyCallback(KeyBindingsNames.TAKE_SCREENSHOT) { screenshotTaker.takeScreenshot() }
|
||||
|
||||
inputHandler.registerKeyCallback(KeyBindingsNames.DEBUG_PAUSE_INCOMING_PACKETS) {
|
||||
@ -252,17 +233,22 @@ class RenderWindow(
|
||||
|
||||
fun startLoop() {
|
||||
Log.log(LogMessageType.RENDERING_LOADING) { "Starting loop" }
|
||||
while (!glfwWindowShouldClose(window.window)) {
|
||||
if (connection.wasConnected) {
|
||||
var closed = false
|
||||
connection.registerEvent(CallbackEventInvoker.of<WindowCloseEvent> {
|
||||
closed = true
|
||||
})
|
||||
|
||||
while (true) {
|
||||
if (connection.wasConnected || closed) {
|
||||
break
|
||||
}
|
||||
if (renderingState == RenderingStates.PAUSED) {
|
||||
Thread.sleep(100L)
|
||||
glfwPollEvents()
|
||||
window.pollEvents()
|
||||
continue
|
||||
}
|
||||
renderStats.startFrame()
|
||||
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) // clear the framebuffer
|
||||
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
|
||||
|
||||
|
||||
val currentTickTime = System.currentTimeMillis()
|
||||
@ -272,7 +258,7 @@ class RenderWindow(
|
||||
this.lastTickTimer = currentTickTime
|
||||
}
|
||||
|
||||
val currentFrame = glfwGetTime()
|
||||
val currentFrame = window.time
|
||||
deltaFrameTime = currentFrame - lastFrame
|
||||
lastFrame = currentFrame
|
||||
|
||||
@ -293,8 +279,9 @@ class RenderWindow(
|
||||
renderStats.endDraw()
|
||||
|
||||
|
||||
glfwSwapBuffers(window.window)
|
||||
glfwPollEvents()
|
||||
window.swapBuffers()
|
||||
window.pollEvents()
|
||||
|
||||
inputHandler.draw(deltaFrameTime)
|
||||
|
||||
// handle opengl context tasks, but limit it per frame
|
||||
@ -304,26 +291,17 @@ class RenderWindow(
|
||||
RenderingStates.SLOW -> Thread.sleep(100L)
|
||||
RenderingStates.RUNNING, RenderingStates.PAUSED -> {
|
||||
}
|
||||
RenderingStates.STOPPED -> glfwSetWindowShouldClose(window.window, true)
|
||||
RenderingStates.STOPPED -> window.close()
|
||||
}
|
||||
renderStats.endFrame()
|
||||
|
||||
if (RenderConstants.SHOW_FPS_IN_WINDOW_TITLE) {
|
||||
glfwSetWindowTitle(window.window, "Minosoft | FPS: ${renderStats.fpsLastSecond}")
|
||||
window.title = "Minosoft | FPS: ${renderStats.fpsLastSecond}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun exit() {
|
||||
Log.log(LogMessageType.RENDERING_LOADING) { "Destroying render window..." }
|
||||
// Free the window callbacks and destroy the window
|
||||
Callbacks.glfwFreeCallbacks(window.window)
|
||||
glfwDestroyWindow(window.window)
|
||||
|
||||
// Terminate GLFW and free the error callback
|
||||
glfwTerminate()
|
||||
glfwSetErrorCallback(null)!!.free()
|
||||
|
||||
window.destroy()
|
||||
Log.log(LogMessageType.RENDERING_LOADING) { "Render window destroyed!" }
|
||||
// disconnect
|
||||
connection.disconnect()
|
||||
@ -350,8 +328,9 @@ class RenderWindow(
|
||||
connection.sender.sendFakeChatMessage(RenderConstants.DEBUG_MESSAGES_PREFIX + message)
|
||||
}
|
||||
|
||||
@Deprecated(message = "", replaceWith = ReplaceWith("window.clipboardText"))
|
||||
fun getClipboardText(): String {
|
||||
return glfwGetClipboardString(window.window) ?: ""
|
||||
return window.clipboardText
|
||||
}
|
||||
|
||||
fun assertOnRenderThread() {
|
||||
|
@ -14,6 +14,7 @@
|
||||
package de.bixilon.minosoft.gui.rendering
|
||||
|
||||
import de.bixilon.minosoft.Minosoft
|
||||
import de.bixilon.minosoft.gui.rendering.modding.events.WindowCloseEvent
|
||||
import de.bixilon.minosoft.gui.rendering.sound.AudioPlayer
|
||||
import de.bixilon.minosoft.protocol.network.connection.PlayConnection
|
||||
import de.bixilon.minosoft.util.CountUpAndDownLatch
|
||||
@ -60,12 +61,11 @@ class Rendering(private val connection: PlayConnection) {
|
||||
CONTEXT_MAP[Thread.currentThread()] = renderWindow
|
||||
renderWindow.init(latch)
|
||||
renderWindow.startLoop()
|
||||
renderWindow.exit()
|
||||
} catch (exception: Throwable) {
|
||||
CONTEXT_MAP.remove(Thread.currentThread())
|
||||
exception.printStackTrace()
|
||||
try {
|
||||
renderWindow.exit()
|
||||
connection.fireEvent(WindowCloseEvent(window = renderWindow.window))
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
if (connection.connectionState.connected) {
|
||||
|
@ -27,10 +27,11 @@ import de.bixilon.minosoft.gui.rendering.input.camera.Camera
|
||||
import de.bixilon.minosoft.gui.rendering.modding.events.MouseMoveEvent
|
||||
import de.bixilon.minosoft.gui.rendering.modding.events.RawCharInputEvent
|
||||
import de.bixilon.minosoft.gui.rendering.modding.events.RawKeyInputEvent
|
||||
import de.bixilon.minosoft.gui.rendering.system.window.CursorModes
|
||||
import de.bixilon.minosoft.gui.rendering.system.window.KeyChangeTypes
|
||||
import de.bixilon.minosoft.modding.event.CallbackEventInvoker
|
||||
import de.bixilon.minosoft.protocol.network.connection.PlayConnection
|
||||
import org.lwjgl.glfw.GLFW.*
|
||||
import de.bixilon.minosoft.util.KUtil.decide
|
||||
|
||||
class RenderWindowInputHandler(
|
||||
val renderWindow: RenderWindow,
|
||||
@ -50,12 +51,7 @@ class RenderWindowInputHandler(
|
||||
|
||||
init {
|
||||
registerKeyCallback(KeyBindingsNames.DEBUG_MOUSE_CATCH) {
|
||||
val newCursorMode = if (it) {
|
||||
GLFW_CURSOR_DISABLED
|
||||
} else {
|
||||
GLFW_CURSOR_NORMAL
|
||||
}
|
||||
glfwSetInputMode(renderWindow.window.window, GLFW_CURSOR, newCursorMode)
|
||||
renderWindow.window.cursorMode = it.decide(CursorModes.DISABLED, CursorModes.NORMAL)
|
||||
renderWindow.sendDebugMessage("Toggled mouse catch!")
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Minosoft
|
||||
* Copyright (C) 2021 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.gui.rendering.modding.events
|
||||
|
||||
import de.bixilon.minosoft.gui.rendering.RenderWindow
|
||||
import de.bixilon.minosoft.gui.rendering.Rendering
|
||||
import de.bixilon.minosoft.gui.rendering.system.window.BaseWindow
|
||||
|
||||
class WindowCloseEvent(
|
||||
renderWindow: RenderWindow = Rendering.currentContext!!,
|
||||
val window: BaseWindow,
|
||||
) : RenderEvent(renderWindow)
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Minosoft
|
||||
* Copyright (C) 2021 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.gui.rendering.modding.events
|
||||
|
||||
import de.bixilon.minosoft.gui.rendering.RenderWindow
|
||||
import de.bixilon.minosoft.gui.rendering.Rendering
|
||||
import de.bixilon.minosoft.gui.rendering.system.window.BaseWindow
|
||||
|
||||
class WindowFocusChangeEvent(
|
||||
renderWindow: RenderWindow = Rendering.currentContext!!,
|
||||
val window: BaseWindow,
|
||||
val focused: Boolean,
|
||||
) : RenderEvent(renderWindow)
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Minosoft
|
||||
* Copyright (C) 2021 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.gui.rendering.modding.events
|
||||
|
||||
import de.bixilon.minosoft.gui.rendering.RenderWindow
|
||||
import de.bixilon.minosoft.gui.rendering.Rendering
|
||||
import de.bixilon.minosoft.gui.rendering.system.window.BaseWindow
|
||||
|
||||
class WindowIconifyChangeEvent(
|
||||
renderWindow: RenderWindow = Rendering.currentContext!!,
|
||||
val window: BaseWindow,
|
||||
val iconified: Boolean,
|
||||
) : RenderEvent(renderWindow)
|
@ -32,6 +32,13 @@ interface BaseWindow {
|
||||
|
||||
var cursorMode: CursorModes
|
||||
|
||||
|
||||
var clipboardText: String
|
||||
var title: String
|
||||
val version: String
|
||||
|
||||
val time: Double
|
||||
|
||||
fun init() {
|
||||
resizable = true
|
||||
swapInterval = Minosoft.config.config.game.other.swapInterval
|
||||
@ -44,8 +51,13 @@ interface BaseWindow {
|
||||
maxSize = DEFAULT_MAXIMUM_WINDOW_SIZE
|
||||
}
|
||||
|
||||
fun destroy()
|
||||
|
||||
fun close()
|
||||
|
||||
fun swapBuffers()
|
||||
|
||||
fun pollEvents()
|
||||
|
||||
companion object {
|
||||
val DEFAULT_WINDOW_SIZE: Vec2i
|
||||
|
@ -14,10 +14,7 @@
|
||||
package de.bixilon.minosoft.gui.rendering.system.window
|
||||
|
||||
import de.bixilon.minosoft.config.key.KeyCodes
|
||||
import de.bixilon.minosoft.gui.rendering.modding.events.MouseMoveEvent
|
||||
import de.bixilon.minosoft.gui.rendering.modding.events.RawCharInputEvent
|
||||
import de.bixilon.minosoft.gui.rendering.modding.events.RawKeyInputEvent
|
||||
import de.bixilon.minosoft.gui.rendering.modding.events.ResizeWindowEvent
|
||||
import de.bixilon.minosoft.gui.rendering.modding.events.*
|
||||
import de.bixilon.minosoft.gui.rendering.system.window.BaseWindow.Companion.DEFAULT_MAXIMUM_WINDOW_SIZE
|
||||
import de.bixilon.minosoft.gui.rendering.system.window.BaseWindow.Companion.DEFAULT_MINIMUM_WINDOW_SIZE
|
||||
import de.bixilon.minosoft.gui.rendering.system.window.BaseWindow.Companion.DEFAULT_WINDOW_SIZE
|
||||
@ -27,6 +24,7 @@ import de.bixilon.minosoft.util.logging.LogLevels
|
||||
import de.bixilon.minosoft.util.logging.LogMessageType
|
||||
import glm_.vec2.Vec2d
|
||||
import glm_.vec2.Vec2i
|
||||
import org.lwjgl.glfw.Callbacks.glfwFreeCallbacks
|
||||
import org.lwjgl.glfw.GLFW.*
|
||||
import org.lwjgl.glfw.GLFWErrorCallback
|
||||
import org.lwjgl.system.MemoryUtil
|
||||
@ -34,8 +32,7 @@ import org.lwjgl.system.MemoryUtil
|
||||
class GLFWWindow(
|
||||
private val eventMaster: EventMaster,
|
||||
) : BaseWindow {
|
||||
@Deprecated("Will be private soon")
|
||||
var window = -1L
|
||||
private var window = -1L
|
||||
|
||||
override var cursorMode: CursorModes = CursorModes.NORMAL
|
||||
set(value) {
|
||||
@ -97,6 +94,27 @@ class GLFWWindow(
|
||||
field = value
|
||||
}
|
||||
|
||||
override var clipboardText: String
|
||||
get() = glfwGetClipboardString(window) ?: ""
|
||||
set(value) {
|
||||
glfwSetClipboardString(window, value)
|
||||
}
|
||||
|
||||
override val version: String
|
||||
get() = glfwGetVersionString()
|
||||
|
||||
override val time: Double
|
||||
get() = glfwGetTime()
|
||||
|
||||
override var title: String = "Window"
|
||||
set(value) {
|
||||
if (field == value) {
|
||||
return
|
||||
}
|
||||
glfwSetWindowTitle(window, value)
|
||||
field = value
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
GLFWErrorCallback.createPrint(System.err).set()
|
||||
check(glfwInit()) { "Unable to initialize GLFW" }
|
||||
@ -110,7 +128,7 @@ class GLFWWindow(
|
||||
|
||||
window = glfwCreateWindow(size.x, size.y, "Minosoft", MemoryUtil.NULL, MemoryUtil.NULL)
|
||||
if (window == MemoryUtil.NULL) {
|
||||
close()
|
||||
destroy()
|
||||
throw RuntimeException("Failed to create the GLFW window")
|
||||
}
|
||||
|
||||
@ -130,11 +148,62 @@ class GLFWWindow(
|
||||
|
||||
glfwSetWindowSizeCallback(window, this::onResize)
|
||||
|
||||
glfwSetWindowCloseCallback(window, this::onClose)
|
||||
glfwSetWindowFocusCallback(window, this::onFocusChange)
|
||||
glfwSetWindowIconifyCallback(window, this::onIconify)
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
override fun destroy() {
|
||||
glfwFreeCallbacks(window)
|
||||
glfwDestroyWindow(window)
|
||||
|
||||
glfwTerminate()
|
||||
glfwSetErrorCallback(null)!!.free()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (eventMaster.fireEvent(WindowCloseEvent(window = this))) {
|
||||
return
|
||||
}
|
||||
|
||||
glfwSetWindowShouldClose(window, true)
|
||||
}
|
||||
|
||||
override fun swapBuffers() {
|
||||
glfwSwapBuffers(window)
|
||||
}
|
||||
|
||||
override fun pollEvents() {
|
||||
glfwPollEvents()
|
||||
}
|
||||
|
||||
private fun onFocusChange(window: Long, focused: Boolean) {
|
||||
if (window != this.window) {
|
||||
return
|
||||
}
|
||||
|
||||
eventMaster.fireEvent(WindowFocusChangeEvent(window = this, focused = focused))
|
||||
}
|
||||
|
||||
private fun onIconify(window: Long, iconified: Boolean) {
|
||||
if (window != this.window) {
|
||||
return
|
||||
}
|
||||
|
||||
eventMaster.fireEvent(WindowIconifyChangeEvent(window = this, iconified = iconified))
|
||||
}
|
||||
|
||||
private fun onClose(window: Long) {
|
||||
if (window != this.window) {
|
||||
return
|
||||
}
|
||||
val cancelled = eventMaster.fireEvent(WindowCloseEvent(window = this))
|
||||
|
||||
if (cancelled) {
|
||||
glfwSetWindowShouldClose(window, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onResize(window: Long, width: Int, height: Int) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user