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.GLStateTracker import ru.dbotthepony.kstarbound.client.input.UserInput import ru.dbotthepony.kstarbound.client.render.Camera import ru.dbotthepony.kstarbound.client.render.TextAlignX 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.nfloat.Vector3f 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 viewportMatrixGUI = updateViewportMatrixA() private set var viewportMatrixGame = updateViewportMatrixB() 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 updateViewportMatrixA(): Matrix4f { return Matrix4f.ortho(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 0.1f, 100f) } private fun updateViewportMatrixB(): Matrix4f { return Matrix4f.orthoDirect(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 1f, 100f) } 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 -> viewportWidth = w viewportHeight = h viewportMatrixGUI = updateViewportMatrixA() viewportMatrixGame = updateViewportMatrixB() glViewport(0, 0, 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() var world: ClientWorld? = ClientWorld(this, 0L) fun ensureSameThread() = gl.ensureSameThread() init { putDebugLog("Initialized OpenGL context") gl.clearColor = Color.SLATE_GREY gl.blend = true glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_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>() 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) } fun renderFrame(): Boolean { ensureSameThread() if (GLFW.glfwWindowShouldClose(window)) { close() return false } val measure = GLFW.glfwGetTime() if (frameRenderTime != 0.0 && Starbound.initialized) world?.think(frameRenderTime) glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) gl.matrixStack.clear(viewportMatrixGame.toMutableMatrix4f()) gl.matrixStack.push() .translateWithMultiplication(viewportWidth / 2f, viewportHeight / 2f, 2f) // центр экрана + координаты отрисовки мира .scale(x = settings.scale * PIXELS_IN_STARBOUND_UNITf, y = settings.scale * 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.scale / PIXELS_IN_STARBOUND_UNIT, viewportHeight / settings.scale / PIXELS_IN_STARBOUND_UNIT)) for (lambda in onPostDrawWorld) { lambda.invoke() } gl.matrixStack.pop() gl.matrixStack.clear(viewportMatrixGUI.toMutableMatrix4f().translate(Vector3f(z = 2f))) 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) } }