434 lines
12 KiB
Kotlin
434 lines
12 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.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<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 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)
|
|
}
|
|
}
|