1036 lines
29 KiB
Kotlin
1036 lines
29 KiB
Kotlin
package ru.dbotthepony.kstarbound.client
|
|
|
|
import com.github.benmanes.caffeine.cache.Cache
|
|
import com.github.benmanes.caffeine.cache.Caffeine
|
|
import org.apache.logging.log4j.LogManager
|
|
import org.lwjgl.BufferUtils
|
|
import org.lwjgl.glfw.Callbacks
|
|
import org.lwjgl.glfw.GLFW
|
|
import org.lwjgl.glfw.GLFWErrorCallback
|
|
import org.lwjgl.opengl.GL
|
|
import org.lwjgl.opengl.GL46.*
|
|
import org.lwjgl.opengl.GLCapabilities
|
|
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.freetype.FreeType
|
|
import ru.dbotthepony.kstarbound.client.freetype.InvalidArgumentException
|
|
import ru.dbotthepony.kstarbound.client.gl.BlendFunc
|
|
import ru.dbotthepony.kstarbound.client.gl.GLFrameBuffer
|
|
import ru.dbotthepony.kstarbound.client.gl.GLTexture2D
|
|
import ru.dbotthepony.kstarbound.client.gl.ScissorRect
|
|
import ru.dbotthepony.kstarbound.client.gl.VBOType
|
|
import ru.dbotthepony.kstarbound.client.gl.VertexArrayObject
|
|
import ru.dbotthepony.kstarbound.client.gl.VertexBufferObject
|
|
import ru.dbotthepony.kstarbound.client.gl.checkForGLError
|
|
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateFuncTracker
|
|
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateGenericTracker
|
|
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateIntTracker
|
|
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateSwitchTracker
|
|
import ru.dbotthepony.kstarbound.client.gl.properties.TexturesTracker
|
|
import ru.dbotthepony.kstarbound.client.gl.shader.GLPrograms
|
|
import ru.dbotthepony.kstarbound.client.gl.shader.GLShader
|
|
import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram
|
|
import ru.dbotthepony.kstarbound.client.gl.vertex.GLAttributeList
|
|
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
|
|
import ru.dbotthepony.kstarbound.client.gl.vertex.QuadTransformers
|
|
import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder
|
|
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder
|
|
import ru.dbotthepony.kstarbound.client.input.UserInput
|
|
import ru.dbotthepony.kstarbound.client.render.Box2DRenderer
|
|
import ru.dbotthepony.kstarbound.client.render.Camera
|
|
import ru.dbotthepony.kstarbound.client.render.Font
|
|
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.client.world.ClientWorld
|
|
import ru.dbotthepony.kstarbound.defs.image.Image
|
|
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
|
|
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
|
|
import ru.dbotthepony.kstarbound.util.JVMTimeSource
|
|
import ru.dbotthepony.kstarbound.util.formatBytesShort
|
|
import ru.dbotthepony.kstarbound.world.LightCalculator
|
|
import ru.dbotthepony.kstarbound.world.api.ICellAccess
|
|
import ru.dbotthepony.kstarbound.world.api.IChunkCell
|
|
import ru.dbotthepony.kvector.api.IStruct4f
|
|
import ru.dbotthepony.kvector.arrays.Matrix4f
|
|
import ru.dbotthepony.kvector.arrays.Matrix4fStack
|
|
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.Vector2i
|
|
import ru.dbotthepony.kvector.vector.Vector3f
|
|
import java.io.Closeable
|
|
import java.io.File
|
|
import java.lang.ref.Cleaner
|
|
import java.nio.ByteBuffer
|
|
import java.nio.ByteOrder
|
|
import java.time.Duration
|
|
import java.util.*
|
|
import java.util.concurrent.locks.LockSupport
|
|
import java.util.concurrent.locks.ReentrantLock
|
|
import kotlin.collections.ArrayList
|
|
import kotlin.math.roundToInt
|
|
|
|
class StarboundClient : Closeable {
|
|
val window: Long
|
|
val camera = Camera(this)
|
|
val input = UserInput()
|
|
val thread: Thread = Thread.currentThread()
|
|
val capabilities: GLCapabilities
|
|
|
|
var viewportX: Int = 0
|
|
private set
|
|
var viewportY: Int = 0
|
|
private set
|
|
var viewportWidth: Int = 0
|
|
private set
|
|
var viewportHeight: Int = 0
|
|
private set
|
|
|
|
var viewportCellX = 0
|
|
private set
|
|
var viewportCellY = 0
|
|
private set
|
|
var viewportCellWidth = 0
|
|
private set
|
|
var viewportCellHeight = 0
|
|
private set
|
|
var viewportRectangle = AABB.rectangle(Vector2d.ZERO, 0.0, 0.0)
|
|
private set
|
|
|
|
var fullbright = true
|
|
|
|
var gameTerminated = false
|
|
private set
|
|
|
|
/**
|
|
* Матрица преобразования экранных координат (в пикселях) в нормализованные координаты
|
|
*/
|
|
var viewportMatrixScreen: Matrix4f
|
|
private set
|
|
get() = Matrix4f.unmodifiable(field)
|
|
|
|
/**
|
|
* Матрица преобразования мировых координат в нормализованные координаты
|
|
*/
|
|
var viewportMatrixWorld: Matrix4f
|
|
private set
|
|
get() = Matrix4f.unmodifiable(field)
|
|
|
|
var isRenderingGame = true
|
|
private set
|
|
|
|
private val scissorStack = LinkedList<ScissorRect>()
|
|
private val cleanerBacklog = ArrayList<() -> Unit>()
|
|
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>()
|
|
private val terminateCallbacks = ArrayList<() -> Unit>()
|
|
private val startupTextList = ArrayList<String>()
|
|
private var finishStartupRendering = System.currentTimeMillis() + 4000L
|
|
|
|
private val cleaner = Cleaner.create { r ->
|
|
val thread = Thread(r, "OpenGL Cleaner for '${thread.name}'")
|
|
thread.priority = 2
|
|
thread
|
|
}
|
|
|
|
@Volatile
|
|
var objectsCleaned = 0L
|
|
private set
|
|
|
|
@Volatile
|
|
var gcHits = 0L
|
|
private set
|
|
|
|
init {
|
|
check(CLIENTS.get() == null) { "Already has OpenGL context existing at ${Thread.currentThread()}!" }
|
|
CLIENTS.set(this)
|
|
|
|
lock.lock()
|
|
|
|
try {
|
|
if (!glfwInitialized) {
|
|
check(GLFW.glfwInit()) { "Unable to initialize GLFW" }
|
|
glfwInitialized = true
|
|
|
|
GLFWErrorCallback.create { error, description ->
|
|
LOGGER.error("LWJGL error {}: {}", error, description)
|
|
}.set()
|
|
}
|
|
} finally {
|
|
lock.unlock()
|
|
}
|
|
|
|
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)
|
|
|
|
// This line is critical for LWJGL's interoperation with GLFW's
|
|
// OpenGL context, or any context that is managed externally.
|
|
// LWJGL detects the context that is current in the current thread,
|
|
// creates the GLCapabilities instance and makes the OpenGL
|
|
// bindings available for use.
|
|
capabilities = GL.createCapabilities()
|
|
|
|
GLFW.glfwSetFramebufferSizeCallback(window) { _, w, h ->
|
|
if (w == 0 || h == 0) {
|
|
isRenderingGame = false
|
|
} else {
|
|
isRenderingGame = true
|
|
setViewport(0, 0, w, h)
|
|
viewportMatrixScreen = updateViewportMatrixScreen()
|
|
viewportMatrixWorld = updateViewportMatrixWorld()
|
|
|
|
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
|
|
)
|
|
|
|
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 programs = GLPrograms()
|
|
|
|
val flat2DLines by lazy { StreamVertexBuilder(GLAttributeList.VEC2F, GeometryType.LINES) }
|
|
val flat2DTriangles by lazy { StreamVertexBuilder(GLAttributeList.VEC2F, GeometryType.TRIANGLES) }
|
|
val flat2DTexturedQuads by lazy { StreamVertexBuilder(GLAttributeList.VERTEX_TEXTURE, GeometryType.QUADS) }
|
|
val quadWireframe by lazy { StreamVertexBuilder(GLAttributeList.VEC2F, GeometryType.QUADS_AS_LINES_WIREFRAME) }
|
|
|
|
// минимальное время хранения 5 минут и...
|
|
private val named2DTextures0: Cache<String, GLTexture2D> = Caffeine.newBuilder()
|
|
.expireAfterAccess(Duration.ofMinutes(5))
|
|
.build()
|
|
|
|
// ...бесконечное хранение пока кто-то все ещё использует текстуру
|
|
private val named2DTextures1: Cache<String, GLTexture2D> = Caffeine.newBuilder()
|
|
.weakValues()
|
|
.build()
|
|
|
|
private val missingTexture: GLTexture2D by lazy {
|
|
newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips().also {
|
|
it.textureMinFilter = GL_NEAREST
|
|
it.textureMagFilter = GL_NEAREST
|
|
}
|
|
}
|
|
|
|
private val missingTexturePath = "/assetmissing.png"
|
|
|
|
val matrixStack = Matrix4fStack()
|
|
val freeType = FreeType()
|
|
val font = Font()
|
|
val box2dRenderer = Box2DRenderer()
|
|
|
|
fun registerCleanable(ref: Any, fn: (Int) -> Unit, nativeRef: Int): Cleaner.Cleanable {
|
|
val cleanable = cleaner.register(ref) {
|
|
objectsCleaned++
|
|
|
|
if (isSameThread()) {
|
|
fn(nativeRef)
|
|
checkForGLError()
|
|
} else {
|
|
gcHits++
|
|
|
|
synchronized(cleanerBacklog) {
|
|
cleanerBacklog.add {
|
|
fn(nativeRef)
|
|
checkForGLError()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return cleanable
|
|
}
|
|
|
|
fun cleanup() {
|
|
synchronized(cleanerBacklog) {
|
|
for (lambda in cleanerBacklog) {
|
|
lambda.invoke()
|
|
}
|
|
|
|
cleanerBacklog.clear()
|
|
}
|
|
}
|
|
|
|
var blend by GLStateSwitchTracker(GL_BLEND)
|
|
var scissor by GLStateSwitchTracker(GL_SCISSOR_TEST)
|
|
|
|
var cull by GLStateSwitchTracker(GL_CULL_FACE)
|
|
var cullMode by GLStateFuncTracker(::glCullFace, GL_BACK)
|
|
|
|
var textureUnpackAlignment by GLStateIntTracker(::glPixelStorei, GL_UNPACK_ALIGNMENT, 4)
|
|
|
|
var scissorRect by GLStateGenericTracker(ScissorRect(0, 0, 0, 0)) {
|
|
// require(it.x >= 0) { "Invalid X ${it.x}"}
|
|
// require(it.y >= 0) { "Invalid Y ${it.y}"}
|
|
|
|
require(it.width >= 0) { "Invalid width ${it.width}"}
|
|
require(it.height >= 0) { "Invalid height ${it.height}"}
|
|
|
|
glScissor(it.x, it.y, it.width, it.height)
|
|
}
|
|
|
|
var depthTest by GLStateSwitchTracker(GL_DEPTH_TEST)
|
|
|
|
var VBO: VertexBufferObject? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
|
|
if (field !== value) {
|
|
isMe(value?.client)
|
|
require(value?.isArray != false) { "Provided buffer object is not of Array type" }
|
|
glBindBuffer(GL_ARRAY_BUFFER, value?.pointer ?: 0)
|
|
checkForGLError("Setting Vertex Buffer Object")
|
|
field = value
|
|
}
|
|
}
|
|
|
|
var EBO: VertexBufferObject? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
|
|
if (field !== value) {
|
|
isMe(value?.client)
|
|
require(value?.isElementArray != false) { "Provided buffer object is not of Array type" }
|
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, value?.pointer ?: 0)
|
|
checkForGLError("Setting Element Buffer Object")
|
|
field = value
|
|
}
|
|
}
|
|
|
|
var VAO: VertexArrayObject? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
|
|
if (field !== value) {
|
|
isMe(value?.client)
|
|
glBindVertexArray(value?.pointer ?: 0)
|
|
checkForGLError("Setting Vertex Array Object")
|
|
field = value
|
|
}
|
|
}
|
|
|
|
var readFramebuffer: GLFrameBuffer? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
if (field === value) return
|
|
isMe(value?.client)
|
|
field = value
|
|
|
|
if (value == null) {
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0)
|
|
checkForGLError()
|
|
return
|
|
}
|
|
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, value.pointer)
|
|
checkForGLError()
|
|
}
|
|
|
|
var writeFramebuffer: GLFrameBuffer? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
if (field === value) return
|
|
isMe(value?.client)
|
|
field = value
|
|
|
|
if (value == null) {
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
|
|
checkForGLError()
|
|
return
|
|
}
|
|
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, value.pointer)
|
|
checkForGLError()
|
|
}
|
|
|
|
var framebuffer: GLFrameBuffer?
|
|
get() {
|
|
val readFramebuffer = readFramebuffer
|
|
val writeFramebuffer = writeFramebuffer
|
|
|
|
if (readFramebuffer == writeFramebuffer) {
|
|
return writeFramebuffer
|
|
}
|
|
|
|
return null
|
|
}
|
|
set(value) {
|
|
readFramebuffer = value
|
|
writeFramebuffer = value
|
|
}
|
|
|
|
var program: GLShaderProgram? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
|
|
if (value !== field) {
|
|
isMe(value?.client)
|
|
glUseProgram(value?.pointer ?: 0)
|
|
checkForGLError("Setting shader program")
|
|
field = value
|
|
}
|
|
}
|
|
|
|
var activeTexture = 0
|
|
set(value) {
|
|
ensureSameThread()
|
|
|
|
if (field != value) {
|
|
require(value >= 0) { "Invalid texture block $value" }
|
|
require(value < 80) { "Too big texture block index $value, OpenGL 4.6 guarantee only 80!" }
|
|
glActiveTexture(GL_TEXTURE0 + value)
|
|
checkForGLError()
|
|
field = value
|
|
}
|
|
}
|
|
|
|
var texture2D: GLTexture2D? by TexturesTracker(80)
|
|
|
|
var clearColor by GLStateGenericTracker<IStruct4f>(RGBAColor.WHITE) {
|
|
val (r, g, b, a) = it
|
|
glClearColor(r, g, b, a)
|
|
}
|
|
|
|
var blendFunc by GLStateGenericTracker(BlendFunc()) {
|
|
glBlendFuncSeparate(it.sourceColor.enum, it.destinationColor.enum, it.sourceAlpha.enum, it.destinationAlpha.enum)
|
|
}
|
|
|
|
init {
|
|
glActiveTexture(GL_TEXTURE0)
|
|
checkForGLError()
|
|
}
|
|
|
|
fun setViewport(x: Int, y: Int, width: Int, height: Int) {
|
|
ensureSameThread()
|
|
|
|
if (viewportX != x || viewportY != y || viewportWidth != width || viewportHeight != height) {
|
|
glViewport(x, y, width, height)
|
|
checkForGLError("Setting viewport")
|
|
viewportX = x
|
|
viewportY = y
|
|
viewportWidth = width
|
|
viewportHeight = height
|
|
}
|
|
}
|
|
|
|
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)
|
|
scissorRect = rect
|
|
scissor = true
|
|
}
|
|
|
|
fun popScissorRect() {
|
|
scissorStack.removeLast()
|
|
|
|
val peek = scissorStack.lastOrNull()
|
|
|
|
if (peek == null) {
|
|
scissor = false
|
|
return
|
|
}
|
|
|
|
val y = viewportHeight - peek.y - peek.height
|
|
scissorRect = ScissorRect(peek.x, y, peek.width, peek.height)
|
|
}
|
|
|
|
val currentScissorRect get() = scissorStack.lastOrNull()
|
|
|
|
fun ensureSameThread() {
|
|
if (thread !== Thread.currentThread()) {
|
|
throw IllegalAccessException("Trying to access $this outside of $thread!")
|
|
}
|
|
}
|
|
|
|
fun isSameThread() = thread === Thread.currentThread()
|
|
fun newTexture(name: String = "<unknown>") = GLTexture2D(name)
|
|
|
|
fun loadTexture(path: String): GLTexture2D {
|
|
ensureSameThread()
|
|
|
|
return named2DTextures0.get(path) {
|
|
named2DTextures1.get(it) {
|
|
val data = Image.get(it)
|
|
|
|
if (data == null) {
|
|
LOGGER.error("Texture {} is missing! Falling back to {}", it, missingTexturePath)
|
|
missingTexture
|
|
} else {
|
|
newTexture(it).upload(data).also {
|
|
it.textureMinFilter = GL_NEAREST
|
|
it.textureMagFilter = GL_NEAREST
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun bind(obj: VertexBufferObject): VertexBufferObject {
|
|
if (obj.type == VBOType.ARRAY)
|
|
VBO = obj
|
|
else
|
|
EBO = obj
|
|
|
|
return obj
|
|
}
|
|
|
|
fun unbind(obj: VertexBufferObject): VertexBufferObject {
|
|
if (obj.type == VBOType.ARRAY)
|
|
if (obj == VBO)
|
|
VBO = null
|
|
else
|
|
if (obj == EBO)
|
|
EBO = null
|
|
|
|
return obj
|
|
}
|
|
|
|
fun bind(obj: VertexArrayObject): VertexArrayObject {
|
|
VAO = obj
|
|
return obj
|
|
}
|
|
|
|
fun unbind(obj: VertexArrayObject): VertexArrayObject {
|
|
if (obj == VAO)
|
|
VAO = null
|
|
|
|
return obj
|
|
}
|
|
|
|
fun newVBO() = VertexBufferObject.vbo()
|
|
fun newEBO() = VertexBufferObject.ebo()
|
|
fun newVAO() = VertexArrayObject()
|
|
|
|
inline fun quadWireframe(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) {
|
|
val builder = quadWireframe
|
|
|
|
builder.builder.begin()
|
|
lambda.invoke(builder.builder)
|
|
builder.upload()
|
|
|
|
programs.flat.use()
|
|
programs.flat.color = color
|
|
programs.flat.transform = matrixStack.last()
|
|
|
|
builder.draw(GL_LINES)
|
|
}
|
|
|
|
inline fun quadColor(lambda: (VertexBuilder) -> Unit) {
|
|
val builder = programs.flatColor.builder
|
|
|
|
builder.builder.begin()
|
|
lambda.invoke(builder.builder)
|
|
builder.upload()
|
|
|
|
programs.flatColor.use()
|
|
programs.flatColor.transform = matrixStack.last()
|
|
|
|
builder.draw(GL_TRIANGLES)
|
|
}
|
|
|
|
inline fun quadTexture(texture: GLTexture2D, lambda: (VertexBuilder) -> Unit) {
|
|
val builder = programs.textured2d.builder
|
|
|
|
builder.builder.begin()
|
|
lambda.invoke(builder.builder)
|
|
builder.upload()
|
|
|
|
activeTexture = 0
|
|
texture.bind()
|
|
|
|
programs.textured2d.use()
|
|
programs.textured2d.transform = matrixStack.last()
|
|
programs.textured2d.texture = 0
|
|
|
|
builder.draw(GL_TRIANGLES)
|
|
}
|
|
|
|
inline fun quadWireframe(value: AABB, color: RGBAColor = RGBAColor.WHITE, chain: (VertexBuilder) -> Unit = {}) {
|
|
quadWireframe(color) {
|
|
it.quad(value.mins.x.toFloat(), value.mins.y.toFloat(), value.maxs.x.toFloat(), value.maxs.y.toFloat())
|
|
chain(it)
|
|
}
|
|
}
|
|
|
|
fun vertex(file: File) = GLShader(file, GL_VERTEX_SHADER)
|
|
fun fragment(file: File) = GLShader(file, GL_FRAGMENT_SHADER)
|
|
|
|
fun vertex(contents: String) = GLShader(contents, GL_VERTEX_SHADER)
|
|
fun fragment(contents: String) = GLShader(contents, GL_FRAGMENT_SHADER)
|
|
|
|
fun internalVertex(file: String) = GLShader(readInternal(file), GL_VERTEX_SHADER)
|
|
fun internalFragment(file: String) = GLShader(readInternal(file), GL_FRAGMENT_SHADER)
|
|
fun internalGeometry(file: String) = GLShader(readInternal(file), GL_GEOMETRY_SHADER)
|
|
|
|
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 isMe(state: StarboundClient?) {
|
|
if (state != null && state != this) {
|
|
throw InvalidArgumentException("Provided object does not belong to $this state tracker (belongs to $state)")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
val tileRenderers = TileRenderers(this)
|
|
var world: ClientWorld? = ClientWorld(this, 0L, Vector2i(3000, 2000), true)
|
|
|
|
init {
|
|
putDebugLog("Initialized OpenGL context")
|
|
clearColor = RGBAColor.SLATE_GRAY
|
|
|
|
blend = true
|
|
blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA
|
|
}
|
|
|
|
var frameRenderTime = 0.0
|
|
private set
|
|
|
|
private var lastRender = JVMTimeSource.INSTANCE.seconds
|
|
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()
|
|
|
|
val viewportCells: ICellAccess = object : ICellAccess {
|
|
override fun getCell(x: Int, y: Int): IChunkCell? {
|
|
return world?.getCell(x + viewportCellX, y + viewportCellY)
|
|
}
|
|
|
|
override fun getCellDirect(x: Int, y: Int): IChunkCell? {
|
|
return world?.getCellDirect(x + viewportCellX, y + viewportCellY)
|
|
}
|
|
}
|
|
|
|
var viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight)
|
|
private set
|
|
|
|
val viewportLightingTexture = newTexture("Viewport Lighting")
|
|
private var viewportLightingMem: ByteBuffer? = null
|
|
|
|
fun updateViewportParams() {
|
|
viewportRectangle = AABB.rectangle(
|
|
camera.pos.toDoubleVector(),
|
|
viewportWidth / settings.zoom / PIXELS_IN_STARBOUND_UNIT,
|
|
viewportHeight / settings.zoom / PIXELS_IN_STARBOUND_UNIT)
|
|
|
|
viewportCellX = roundTowardsNegativeInfinity(viewportRectangle.mins.x) - 4
|
|
viewportCellY = roundTowardsNegativeInfinity(viewportRectangle.mins.y) - 4
|
|
viewportCellWidth = roundTowardsPositiveInfinity(viewportRectangle.width) + 8
|
|
viewportCellHeight = roundTowardsPositiveInfinity(viewportRectangle.height) + 8
|
|
|
|
if (viewportLighting.width != viewportCellWidth || viewportLighting.height != viewportCellHeight) {
|
|
viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight)
|
|
viewportLighting.multithreaded = true
|
|
|
|
if (viewportCellWidth > 0 && viewportCellHeight > 0) {
|
|
viewportLightingMem = ByteBuffer.allocateDirect(viewportCellWidth.coerceAtMost(4096) * viewportCellHeight.coerceAtMost(4096) * 3)
|
|
} else {
|
|
viewportLightingMem = null
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
ensureSameThread()
|
|
|
|
val diff = JVMTimeSource.INSTANCE.seconds - lastRender
|
|
|
|
if (diff < Starbound.TICK_TIME_ADVANCE)
|
|
LockSupport.parkNanos(((Starbound.TICK_TIME_ADVANCE - diff) * 1_000_000_000.0).toLong())
|
|
|
|
frameRenderTime = JVMTimeSource.INSTANCE.seconds - lastRender
|
|
frameRenderTimes[++frameRenderIndex % frameRenderTimes.size] = frameRenderTime
|
|
lastRender = JVMTimeSource.INSTANCE.seconds
|
|
|
|
if (GLFW.glfwWindowShouldClose(window)) {
|
|
close()
|
|
return false
|
|
}
|
|
|
|
val world = world
|
|
|
|
if (!isRenderingGame) {
|
|
cleanup()
|
|
GLFW.glfwPollEvents()
|
|
|
|
if (world != null) {
|
|
if (Starbound.initialized)
|
|
world.think()
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
if (world != null) {
|
|
updateViewportParams()
|
|
val layers = LayeredRenderer()
|
|
|
|
if (Starbound.initialized)
|
|
world.think()
|
|
|
|
clearColor = RGBAColor.SLATE_GRAY
|
|
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
|
|
matrixStack.clear(viewportMatrixWorld)
|
|
|
|
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)
|
|
}
|
|
|
|
viewportLighting.clear()
|
|
|
|
world.addLayers(
|
|
layers = layers,
|
|
size = viewportRectangle)
|
|
|
|
layers.render(matrixStack)
|
|
|
|
val viewportLightingMem = viewportLightingMem
|
|
|
|
if (viewportLightingMem != null && !fullbright) {
|
|
viewportLightingMem.position(0)
|
|
BufferUtils.zeroBuffer(viewportLightingMem)
|
|
viewportLightingMem.position(0)
|
|
viewportLighting.calculate(viewportLightingMem, viewportLighting.width.coerceAtMost(4096), viewportLighting.height.coerceAtMost(4096))
|
|
viewportLightingMem.position(0)
|
|
|
|
val old = textureUnpackAlignment
|
|
textureUnpackAlignment = if (viewportLighting.width.coerceAtMost(4096) % 4 == 0) 4 else 1
|
|
|
|
viewportLightingTexture.upload(
|
|
GL_RGB,
|
|
viewportLighting.width.coerceAtMost(4096),
|
|
viewportLighting.height.coerceAtMost(4096),
|
|
GL_RGB,
|
|
GL_UNSIGNED_BYTE,
|
|
viewportLightingMem
|
|
)
|
|
|
|
textureUnpackAlignment = old
|
|
|
|
viewportLightingTexture.textureMinFilter = GL_LINEAR
|
|
//viewportLightingTexture.textureMagFilter = GL_NEAREST
|
|
|
|
//viewportLightingTexture.generateMips()
|
|
|
|
blendFunc = BlendFunc.MULTIPLY_BY_SRC
|
|
|
|
quadTexture(viewportLightingTexture) {
|
|
it.quad(
|
|
(viewportCellX).toFloat(),
|
|
(viewportCellY).toFloat(),
|
|
(viewportCellX + viewportCellWidth).toFloat(),
|
|
(viewportCellY + viewportCellHeight).toFloat(),
|
|
QuadTransformers.uv()
|
|
)
|
|
}
|
|
|
|
blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA
|
|
}
|
|
|
|
world.physics.debugDraw()
|
|
|
|
for (lambda in onPostDrawWorld) {
|
|
lambda.invoke()
|
|
}
|
|
|
|
matrixStack.pop()
|
|
}
|
|
|
|
matrixStack.clear(viewportMatrixScreen)
|
|
|
|
val thisTime = System.currentTimeMillis()
|
|
|
|
if (startupTextList.isNotEmpty() && thisTime <= finishStartupRendering) {
|
|
var alpha = 1f
|
|
|
|
if (finishStartupRendering - thisTime < 1000L) {
|
|
alpha = (finishStartupRendering - thisTime) / 1000f
|
|
}
|
|
|
|
matrixStack.push()
|
|
matrixStack.last().translateWithMultiplication(y = viewportHeight.toFloat())
|
|
var shade = 255
|
|
|
|
for (i in startupTextList.size - 1 downTo 0) {
|
|
val size = font.render(startupTextList[i], alignY = TextAlignY.BOTTOM, scale = 0.4f, color = RGBAColor(shade / 255f, shade / 255f, shade / 255f, alpha))
|
|
matrixStack.last().translateWithMultiplication(y = -size.height * 1.2f)
|
|
|
|
if (shade > 120) {
|
|
shade -= 10
|
|
}
|
|
}
|
|
|
|
matrixStack.pop()
|
|
}
|
|
|
|
for (fn in onDrawGUI) {
|
|
fn.invoke()
|
|
}
|
|
|
|
val runtime = Runtime.getRuntime()
|
|
|
|
font.render("FPS: ${(averageFramesPerSecond * 100f).toInt() / 100f}", scale = 0.4f)
|
|
font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = font.lineHeight * 0.5f, scale = 0.4f)
|
|
|
|
GLFW.glfwSwapBuffers(window)
|
|
GLFW.glfwPollEvents()
|
|
input.think()
|
|
|
|
camera.think(Starbound.TICK_TIME_ADVANCE)
|
|
|
|
cleanup()
|
|
|
|
return true
|
|
}
|
|
|
|
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)
|
|
private val CLIENTS = ThreadLocal<StarboundClient>()
|
|
|
|
@JvmStatic
|
|
fun current() = checkNotNull(CLIENTS.get()) { "No client registered to current thread (${Thread.currentThread()})" }
|
|
@JvmStatic
|
|
fun currentOrNull(): StarboundClient? = CLIENTS.get()
|
|
|
|
private val lock = ReentrantLock()
|
|
@Volatile
|
|
private var glfwInitialized = false
|
|
|
|
@JvmStatic
|
|
fun readInternal(file: String): String {
|
|
return ClassLoader.getSystemClassLoader().getResourceAsStream(file)!!.bufferedReader()
|
|
.let {
|
|
val read = it.readText()
|
|
it.close()
|
|
read
|
|
}
|
|
}
|
|
}
|
|
}
|