KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
DBotThePony 51a43d70be
More stuff
Scrolling callbacks
Improved btree reader
noclip controls
2022-08-04 17:28:37 +07:00

308 lines
8.0 KiB
Kotlin

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<String>()
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)
}
}