package ru.dbotthepony.kstarbound.client import org.apache.logging.log4j.LogManager import org.lwjgl.glfw.Callbacks import org.lwjgl.glfw.GLFW import org.lwjgl.glfw.GLFWErrorCallback import org.lwjgl.opengl.GL46.* import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryUtil import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.gl.BlendFunc import ru.dbotthepony.kstarbound.client.gl.GLStateTracker import ru.dbotthepony.kstarbound.client.gl.ScissorRect import ru.dbotthepony.kstarbound.client.input.UserInput import ru.dbotthepony.kstarbound.client.render.Camera import ru.dbotthepony.kstarbound.client.render.GPULightRenderer import ru.dbotthepony.kstarbound.client.render.TextAlignY import ru.dbotthepony.kstarbound.util.formatBytesShort import ru.dbotthepony.kvector.matrix.nfloat.Matrix4f import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.Color import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.nfloat.Vector2f import ru.dbotthepony.kvector.vector.nfloat.Vector3f import java.nio.ByteBuffer import java.nio.ByteOrder import java.util.* import java.util.concurrent.locks.LockSupport import kotlin.collections.ArrayList import kotlin.math.roundToInt class StarboundClient : AutoCloseable { val window: Long val camera = Camera(this) val input = UserInput() var gameTerminated = false private set var viewportWidth = 800 private set var viewportHeight = 600 private set /** * Матрица преобразования экранных координат (в пикселях) в нормализованные координаты */ var viewportMatrixScreen = updateViewportMatrixScreen() private set /** * Матрица преобразования мировых координат в нормализованные координаты */ var viewportMatrixWorld = updateViewportMatrixWorld() private set private val startupTextList = ArrayList() private var finishStartupRendering = System.currentTimeMillis() + 4000L fun putDebugLog(text: String, replace: Boolean = false) { if (replace) { if (startupTextList.isEmpty()) { startupTextList.add(text) } else { startupTextList[startupTextList.size - 1] = text } } else { startupTextList.add(text) } finishStartupRendering = System.currentTimeMillis() + 4000L } private fun updateViewportMatrixScreen(): Matrix4f { return Matrix4f.ortho(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 0.1f, 100f).translate(Vector3f(z = 2f)).toMatrix4f() } private fun updateViewportMatrixWorld(): Matrix4f { return Matrix4f.orthoDirect(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 1f, 100f).toMatrix4f() } private val xMousePos = ByteBuffer.allocateDirect(8).also { it.order(ByteOrder.LITTLE_ENDIAN) }.asDoubleBuffer() private val yMousePos = ByteBuffer.allocateDirect(8).also { it.order(ByteOrder.LITTLE_ENDIAN) }.asDoubleBuffer() val mouseCoordinates: Vector2d get() { xMousePos.position(0) yMousePos.position(0) GLFW.glfwGetCursorPos(window, xMousePos, yMousePos) xMousePos.position(0) yMousePos.position(0) return Vector2d(xMousePos.get(), yMousePos.get()) } val mouseCoordinatesF: Vector2f get() { xMousePos.position(0) yMousePos.position(0) GLFW.glfwGetCursorPos(window, xMousePos, yMousePos) xMousePos.position(0) yMousePos.position(0) return Vector2f(xMousePos.get().toFloat(), yMousePos.get().toFloat()) } /** * Преобразует экранные координаты в мировые */ fun screenToWorld(x: Double, y: Double): Vector2d { val relativeX = camera.pos.x - (viewportWidth / 2.0 - x) / PIXELS_IN_STARBOUND_UNIT / settings.zoom val relativeY = (viewportHeight / 2.0 - y) / PIXELS_IN_STARBOUND_UNIT / settings.zoom + camera.pos.y return Vector2d(relativeX, relativeY) } /** * Преобразует экранные координаты в мировые */ fun screenToWorld(value: Vector2d): Vector2d { return screenToWorld(value.x, value.y) } /** * Преобразует экранные координаты в мировые */ fun screenToWorld(x: Float, y: Float): Vector2f { val relativeX = camera.pos.x - (viewportWidth / 2f - x) / PIXELS_IN_STARBOUND_UNITf / settings.zoom val relativeY = (viewportHeight / 2f - y) / PIXELS_IN_STARBOUND_UNITf / settings.zoom + camera.pos.y return Vector2f(relativeX, relativeY) } /** * Преобразует экранные координаты в мировые */ fun screenToWorld(value: Vector2f): Vector2f { return screenToWorld(value.x, value.y) } /** * Преобразует мировые координаты в экранные */ fun worldToScreen(x: Float, y: Float): Vector2f { val relativeX = (x - camera.pos.x) * PIXELS_IN_STARBOUND_UNITf * settings.zoom + viewportWidth / 2f val relativeY = (camera.pos.y - y) * PIXELS_IN_STARBOUND_UNITf * settings.zoom + viewportHeight / 2f return Vector2f(relativeX, relativeY) } var isRenderingGame = true private set init { GLFWErrorCallback.create { error, description -> LOGGER.error("LWJGL error {}: {}", error, description) }.set() check(GLFW.glfwInit()) { "Unable to initialize GLFW" } GLFW.glfwDefaultWindowHints() GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE) GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE) GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 4) GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 6) GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE) window = GLFW.glfwCreateWindow(viewportWidth, viewportHeight, "KStarbound", MemoryUtil.NULL, MemoryUtil.NULL) require(window != MemoryUtil.NULL) { "Unable to create GLFW window" } startupTextList.add("Created GLFW window") input.installCallback(window) GLFW.glfwSetFramebufferSizeCallback(window) { _, w, h -> if (w == 0 || h == 0) { isRenderingGame = false return@glfwSetFramebufferSizeCallback } isRenderingGame = true viewportWidth = w viewportHeight = h viewportMatrixScreen = updateViewportMatrixScreen() viewportMatrixWorld = updateViewportMatrixWorld() glViewport(0, 0, w, h) lightRenderer.resizeFramebuffer(viewportWidth, viewportHeight) for (callback in onViewportChanged) { callback.invoke(w, h) } } val stack = MemoryStack.stackPush() try { val pWidth = stack.mallocInt(1) val pHeight = stack.mallocInt(1) GLFW.glfwGetWindowSize(window, pWidth, pHeight) val vidmode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor())!! GLFW.glfwSetWindowPos( window, (vidmode.width() - pWidth[0]) / 2, (vidmode.height() - pHeight[0]) / 2 ) } finally { stack.close() } GLFW.glfwMakeContextCurrent(window) // vsync GLFW.glfwSwapInterval(0) GLFW.glfwShowWindow(window) putDebugLog("Initialized GLFW window") } val gl = GLStateTracker() val lightRenderer = GPULightRenderer(gl) init { lightRenderer.resizeFramebuffer(viewportWidth, viewportHeight) } var world: ClientWorld? = ClientWorld(this, 0L, 0) fun ensureSameThread() = gl.ensureSameThread() init { putDebugLog("Initialized OpenGL context") gl.clearColor = Color.SLATE_GREY gl.blend = true gl.blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA } var frameRenderTime = 0.0 private set val framesPerSecond get() = if (frameRenderTime == 0.0) 1.0 else 1.0 / frameRenderTime private val frameRenderTimes = DoubleArray(60) { 1.0 } private var frameRenderIndex = 0 val averageFramesPerSecond: Double get() { var sum = 0.0 for (value in frameRenderTimes) { sum += value } if (sum == 0.0) return 0.0 return frameRenderTimes.size / sum } val settings = ClientSettings() private val onViewportChanged = ArrayList<(width: Int, height: Int) -> Unit>() fun onViewportChanged(callback: (width: Int, height: Int) -> Unit) { onViewportChanged.add(callback) } private val onDrawGUI = ArrayList<() -> Unit>() fun onDrawGUI(lambda: () -> Unit) { onDrawGUI.add(lambda) } private val onPreDrawWorld = ArrayList<() -> Unit>() fun onPreDrawWorld(lambda: () -> Unit) { onPreDrawWorld.add(lambda) } private val onPostDrawWorld = ArrayList<() -> Unit>() fun onPostDrawWorld(lambda: () -> Unit) { onPostDrawWorld.add(lambda) } private val onPostDrawWorldOnce = ArrayList<() -> Unit>() fun onPostDrawWorldOnce(lambda: () -> Unit) { onPostDrawWorldOnce.add(lambda) } private val scissorStack = LinkedList() fun pushScissorRect(x: Float, y: Float, width: Float, height: Float) { return pushScissorRect(x.roundToInt(), y.roundToInt(), width.roundToInt(), height.roundToInt()) } @Suppress("NAME_SHADOWING") fun pushScissorRect(x: Int, y: Int, width: Int, height: Int) { var x = x var y = y var width = width var height = height val peek = scissorStack.lastOrNull() if (peek != null) { x = x.coerceAtLeast(peek.x) y = y.coerceAtLeast(peek.y) width = width.coerceAtMost(peek.width) height = height.coerceAtMost(peek.height) if (peek.x == x && peek.y == y && peek.width == width && peek.height == height) { scissorStack.add(peek) return } } val rect = ScissorRect(x, y, width, height) scissorStack.add(rect) gl.scissorRect = rect gl.scissor = true } fun popScissorRect() { scissorStack.removeLast() val peek = scissorStack.lastOrNull() if (peek == null) { gl.scissor = false return } val y = viewportHeight - peek.y - peek.height gl.scissorRect = ScissorRect(peek.x, y, peek.width, peek.height) } val currentScissorRect get() = scissorStack.lastOrNull() fun renderFrame(): Boolean { ensureSameThread() if (GLFW.glfwWindowShouldClose(window)) { close() return false } if (!isRenderingGame) { gl.cleanup() GLFW.glfwPollEvents() LockSupport.parkNanos(1_000_000L) return true } val measure = GLFW.glfwGetTime() if (frameRenderTime != 0.0 && Starbound.initialized) world?.think(frameRenderTime) gl.clearColor = Color.SLATE_GREY glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) gl.matrixStack.clear(viewportMatrixWorld.toMutableMatrix4f()) gl.matrixStack.push() .translateWithMultiplication(viewportWidth / 2f, viewportHeight / 2f, 2f) // центр экрана + координаты отрисовки мира .scale(x = settings.zoom * PIXELS_IN_STARBOUND_UNITf, y = settings.zoom * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера .translateWithMultiplication(-camera.pos.x, -camera.pos.y) // перемещаем вид к камере for (lambda in onPreDrawWorld) { lambda.invoke() } for (i in onPostDrawWorldOnce.size - 1 downTo 0) { onPostDrawWorldOnce[i].invoke() onPostDrawWorldOnce.removeAt(i) } world?.render( AABB.rectangle( camera.pos.toDoubleVector(), viewportWidth / settings.zoom / PIXELS_IN_STARBOUND_UNIT, viewportHeight / settings.zoom / PIXELS_IN_STARBOUND_UNIT)) for (lambda in onPostDrawWorld) { lambda.invoke() } gl.matrixStack.pop() gl.matrixStack.clear(viewportMatrixScreen.toMutableMatrix4f()) val thisTime = System.currentTimeMillis() if (startupTextList.isNotEmpty() && thisTime <= finishStartupRendering) { var alpha = 1f if (finishStartupRendering - thisTime < 1000L) { alpha = (finishStartupRendering - thisTime) / 1000f } gl.matrixStack.push() gl.matrixStack.translateWithMultiplication(y = viewportHeight.toFloat()) var shade = 255 for (i in startupTextList.size - 1 downTo 0) { val size = gl.font.render(startupTextList[i], alignY = TextAlignY.BOTTOM, scale = 0.4f, color = Color.SHADES_OF_GRAY[shade].copy(a = alpha)) gl.matrixStack.translateWithMultiplication(y = -size.height * 1.2f) if (shade > 120) { shade -= 10 } } gl.matrixStack.pop() } for (fn in onDrawGUI) { fn.invoke() } val runtime = Runtime.getRuntime() gl.font.render("FPS: ${(averageFramesPerSecond * 100f).toInt() / 100f}", scale = 0.4f) gl.font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = gl.font.lineHeight * 0.5f, scale = 0.4f) GLFW.glfwSwapBuffers(window) GLFW.glfwPollEvents() input.think() camera.think(GLFW.glfwGetTime() - measure) gl.cleanup() frameRenderTime = GLFW.glfwGetTime() - measure frameRenderTimes[++frameRenderIndex % frameRenderTimes.size] = frameRenderTime return true } private val terminateCallbacks = ArrayList<() -> Unit>() fun onTermination(lambda: () -> Unit) { terminateCallbacks.add(lambda) } override fun close() { if (gameTerminated) return if (window != MemoryUtil.NULL) { Callbacks.glfwFreeCallbacks(window) GLFW.glfwDestroyWindow(window) } GLFW.glfwTerminate() GLFW.glfwSetErrorCallback(null)?.free() gameTerminated = true for (callback in terminateCallbacks) { callback.invoke() } } companion object { private val LOGGER = LogManager.getLogger(StarboundClient::class.java) } }