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.input.UserInput import ru.dbotthepony.kstarbound.client.render.Camera import ru.dbotthepony.kstarbound.client.render.GPULightRenderer import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.TextAlignY import ru.dbotthepony.kstarbound.client.render.TileRenderers import ru.dbotthepony.kstarbound.util.JVMTimeSource import ru.dbotthepony.kstarbound.util.PausableTimeSource import ru.dbotthepony.kstarbound.util.formatBytesShort import ru.dbotthepony.kvector.arrays.Matrix4f import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2f import ru.dbotthepony.kvector.vector.Vector3f import java.io.Closeable 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(val starbound: Starbound) : Closeable { val time = PausableTimeSource(JVMTimeSource.INSTANCE) val window: Long val camera = Camera(this) val input = UserInput() val gl: GLStateTracker var gameTerminated = false private set /** * Матрица преобразования экранных координат (в пикселях) в нормализованные координаты */ var viewportMatrixScreen: Matrix4f private set get() = Matrix4f.unmodifiable(field) /** * Матрица преобразования мировых координат в нормализованные координаты */ var viewportMatrixWorld: Matrix4f private set get() = Matrix4f.unmodifiable(field) 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)) } private fun updateViewportMatrixWorld(): Matrix4f { return Matrix4f.orthoDirect(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 1f, 100f) } 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(800, 600, "KStarbound", MemoryUtil.NULL, MemoryUtil.NULL) require(window != MemoryUtil.NULL) { "Unable to create GLFW window" } startupTextList.add("Created GLFW window") input.installCallback(window) GLFW.glfwMakeContextCurrent(window) gl = GLStateTracker(this) GLFW.glfwSetFramebufferSizeCallback(window) { _, w, h -> if (w == 0 || h == 0) { isRenderingGame = false } else { isRenderingGame = true gl.setViewport(0, 0, w, h) viewportMatrixScreen = updateViewportMatrixScreen() viewportMatrixWorld = updateViewportMatrixWorld() lightRenderer.resizeFramebuffer(w, h) 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 ) gl.setViewport(0, 0, pWidth[0], pHeight[0]) viewportMatrixScreen = updateViewportMatrixScreen() viewportMatrixWorld = updateViewportMatrixWorld() } finally { stack.close() } // vsync GLFW.glfwSwapInterval(0) GLFW.glfwShowWindow(window) putDebugLog("Initialized GLFW window") } val viewportWidth by gl::viewportWidth val viewportHeight by gl::viewportHeight val tileRenderers = TileRenderers(this) val lightRenderer = GPULightRenderer(gl) init { lightRenderer.resizeFramebuffer(viewportWidth, viewportHeight) } var world: ClientWorld? = ClientWorld(this, 0L, 0) init { putDebugLog("Initialized OpenGL context") gl.clearColor = RGBAColor.SLATE_GRAY 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 onDrawGUI = ArrayList<() -> Unit>() private val onPreDrawWorld = ArrayList<(LayeredRenderer) -> Unit>() private val onPostDrawWorld = ArrayList<() -> Unit>() private val onPostDrawWorldOnce = ArrayList<(LayeredRenderer) -> Unit>() private val onViewportChanged = ArrayList<(width: Int, height: Int) -> Unit>() fun onViewportChanged(callback: (width: Int, height: Int) -> Unit) { onViewportChanged.add(callback) } fun onDrawGUI(lambda: () -> Unit) { onDrawGUI.add(lambda) } fun onPreDrawWorld(lambda: (LayeredRenderer) -> Unit) { onPreDrawWorld.add(lambda) } fun onPostDrawWorld(lambda: () -> Unit) { onPostDrawWorld.add(lambda) } fun onPostDrawWorldOnce(lambda: (LayeredRenderer) -> Unit) { onPostDrawWorldOnce.add(lambda) } fun renderFrame(): Boolean { gl.ensureSameThread() if (GLFW.glfwWindowShouldClose(window)) { close() return false } if (!isRenderingGame) { gl.cleanup() GLFW.glfwPollEvents() LockSupport.parkNanos(1_000_000L) return true } val measure = JVMTimeSource.INSTANCE.seconds val world = world if (world != null) { val layers = LayeredRenderer() if (frameRenderTime != 0.0 && starbound.initialized) world.think(frameRenderTime) gl.clearColor = RGBAColor.SLATE_GRAY glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) gl.matrixStack.clear(viewportMatrixWorld) gl.matrixStack.push().last() .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(layers) } for (i in onPostDrawWorldOnce.size - 1 downTo 0) { onPostDrawWorldOnce[i].invoke(layers) onPostDrawWorldOnce.removeAt(i) } world.addLayers( layers = layers, size = AABB.rectangle( camera.pos.toDoubleVector(), viewportWidth / settings.zoom / PIXELS_IN_STARBOUND_UNIT, viewportHeight / settings.zoom / PIXELS_IN_STARBOUND_UNIT)) layers.render(gl.matrixStack) for (lambda in onPostDrawWorld) { lambda.invoke() } gl.matrixStack.pop() } gl.matrixStack.clear(viewportMatrixScreen) 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.last().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 = RGBAColor(shade / 255f, shade / 255f, shade / 255f, alpha)) gl.matrixStack.last().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(JVMTimeSource.INSTANCE.seconds - measure) gl.cleanup() frameRenderTime = JVMTimeSource.INSTANCE.seconds - 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) } }