package ru.dbotthepony.kstarbound.client import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Scheduler import io.netty.channel.Channel import io.netty.channel.local.LocalAddress 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.glfw.GLFWImage import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL45.* import org.lwjgl.opengl.GLCapabilities import org.lwjgl.stb.STBImage import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryUtil import org.lwjgl.system.MemoryUtil.memAddressSafe import ru.dbotthepony.kommons.collect.forValidRefs import ru.dbotthepony.kommons.util.IStruct4f import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.matrix.Matrix3f import ru.dbotthepony.kommons.matrix.Matrix3fStack import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2f import ru.dbotthepony.kommons.vector.Vector4f import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.world.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.VertexArrayObject import ru.dbotthepony.kstarbound.client.gl.BufferObject import ru.dbotthepony.kstarbound.client.gl.checkForGLError import ru.dbotthepony.kstarbound.client.gl.properties.GLStateFuncTracker import ru.dbotthepony.kstarbound.client.gl.properties.GLGenericTracker import ru.dbotthepony.kstarbound.client.gl.properties.GLObjectTracker import ru.dbotthepony.kstarbound.client.gl.properties.GLStateIntTracker import ru.dbotthepony.kstarbound.client.gl.properties.GLStateSwitchTracker import ru.dbotthepony.kstarbound.client.gl.properties.GLTexturesTracker import ru.dbotthepony.kstarbound.client.gl.shader.FontProgram 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.shader.UberShader import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder import ru.dbotthepony.kstarbound.client.input.UserInput import ru.dbotthepony.kstarbound.server.network.packets.TrackedPositionPacket 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.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.ExecutionSpinner import ru.dbotthepony.kstarbound.util.formatBytesShort import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.AbstractCell import java.io.Closeable import java.io.File import java.lang.ref.PhantomReference import java.lang.ref.ReferenceQueue import java.lang.ref.WeakReference import java.net.SocketAddress import java.nio.ByteBuffer import java.nio.ByteOrder import java.time.Duration import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinWorkerThread import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock import java.util.function.Consumer import java.util.function.IntConsumer import kotlin.collections.ArrayList import kotlin.concurrent.withLock import kotlin.math.absoluteValue import kotlin.math.roundToInt import kotlin.properties.Delegates class StarboundClient private constructor(val clientID: Int) : Closeable { val window: Long val camera = Camera(this) val input = UserInput() val thread: Thread = Thread.currentThread() private val threadCounter = AtomicInteger() // client specific executor which will accept tasks which involve probable // callback to foreground executor to initialize thread-unsafe data // In above case too many threads will introduce big congestion for resources, stalling entire workload; wasting cpu resources val executor = ForkJoinPool(Runtime.getRuntime().availableProcessors().coerceAtMost(4), { object : ForkJoinWorkerThread(it) { init { name = "Starbound Client $clientID executor ${threadCounter.incrementAndGet()}" } override fun onTermination(exception: Throwable?) { super.onTermination(exception) if (exception != null) { LOGGER.error("$this encountered an exception while executing task", exception) } } } }, null, false) val mailbox = MailboxExecutorService(thread) 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 // potentially visible cells 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 viewportBottomLeft = Vector2d() private set var viewportTopRight = Vector2d() private set var fullbright = true var shouldTerminate = false private set var viewportMatrixScreen: Matrix3f private set get() = Matrix3f.unmodifiable(field) var viewportMatrixWorld: Matrix3f private set get() = Matrix3f.unmodifiable(field) var isRenderingGame = true private set var activeConnection: ClientConnection? = null private set fun connectToLocalServer(client: StarboundClient, address: LocalAddress, uuid: UUID) { check(activeConnection == null) { "Already having active connection to server: $activeConnection" } activeConnection = ClientConnection.connectToLocalServer(client, address, uuid) } fun connectToLocalServer(address: Channel, uuid: UUID) { check(activeConnection == null) { "Already having active connection to server: $activeConnection" } activeConnection = ClientConnection.connectToLocalServer(this, address, uuid) } fun connectToRemoteServer(address: SocketAddress, uuid: UUID) { check(activeConnection == null) { "Already having active connection to server: $activeConnection" } activeConnection = ClientConnection.connectToRemoteServer(this, address, uuid) } private val scissorStack = LinkedList() private val onDrawGUI = ArrayList<() -> Unit>() private val onViewportChanged = ArrayList<(width: Int, height: Int) -> Unit>() private val terminateCallbacks = ArrayList<() -> Unit>() private val openglCleanQueue = ReferenceQueue() private var openglCleanQueueHead: CleanRef? = null private class CleanRef(ref: Any, queue: ReferenceQueue, val fn: IntConsumer, val value: Int) : PhantomReference(ref, queue) { var next: CleanRef? = null var prev: CleanRef? = null } var openglObjectsCreated = 0L private set var openglObjectsCleaned = 0L private set init { check(CLIENTS.get() == null) { "Already has OpenGL context existing at ${Thread.currentThread()}!" } CLIENTS.set(this) lock.lock() try { clients++ 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, 5) GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE) GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE) window = GLFW.glfwCreateWindow(800, 600, "KStarbound", MemoryUtil.NULL, MemoryUtil.NULL) require(window != MemoryUtil.NULL) { "Unable to create 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) } } } var 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() } stack = MemoryStack.stackPush() try { val pWidth = stack.mallocInt(1) val pHeight = stack.mallocInt(1) val pChannels = stack.mallocInt(1) val readFromDisk = readInternalBytes("starbound_icon.png") val buff = ByteBuffer.allocateDirect(readFromDisk.size) buff.put(readFromDisk) buff.position(0) val data = STBImage.stbi_load_from_memory(buff, pWidth, pHeight, pChannels, 4) ?: throw IllegalStateException("Unable to decode starbound_icon.png") val img = GLFWImage.malloc() img.set(pWidth[0], pHeight[0], data) GLFW.nglfwSetWindowIcon(window, 1, memAddressSafe(img)) img.free() } finally { stack.close() } // vsync GLFW.glfwSwapInterval(0) GLFW.glfwShowWindow(window) } val maxTextureBlocks = glGetInteger(GL_MAX_TEXTURE_IMAGE_UNITS) val maxUserTextureBlocks = maxTextureBlocks - 1 // available textures blocks for generic use val maxVertexAttribBindPoints = glGetInteger(GL_MAX_VERTEX_ATTRIB_BINDINGS) init { LOGGER.info("OpenGL Version: ${glGetString(GL_VERSION)}") LOGGER.info("OpenGL Vendor: ${glGetString(GL_VENDOR)}") LOGGER.info("OpenGL Renderer: ${glGetString(GL_RENDERER)}") LOGGER.debug("Max supported texture image units: $maxTextureBlocks") LOGGER.debug("Max supported vertex attribute bind points: $maxVertexAttribBindPoints") } val stack = Matrix3fStack() // минимальное время хранения 5 минут и... val named2DTextures0: Cache = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(1)) .scheduler(Scheduler.systemScheduler()) .build() // ...бесконечное хранение пока кто-то все ещё использует текстуру val named2DTextures1: Cache = Caffeine.newBuilder() .weakValues() .weakKeys() .build() private val fontShaderPrograms = ArrayList>() private val uberShaderPrograms = ArrayList>() val lightMapLocation = maxTextureBlocks - 1 fun addShaderProgram(program: GLShaderProgram) { if (program is UberShader) { uberShaderPrograms.add(WeakReference(program)) } if (program is FontProgram) { fontShaderPrograms.add(WeakReference(program)) } } fun registerCleanable(ref: Any, fn: IntConsumer, nativeRef: Int) { openglObjectsCreated++ val ref0 = CleanRef(ref, openglCleanQueue, fn, nativeRef) if (openglCleanQueueHead == null) { openglCleanQueueHead = ref0 } else { ref0.next = openglCleanQueueHead openglCleanQueueHead!!.prev = ref0 openglCleanQueueHead = ref0 } } private fun executeQueuedTasks() { mailbox.executeQueuedTasks() var next = openglCleanQueue.poll() as CleanRef? while (next != null) { openglObjectsCleaned++ next.fn.accept(next.value) checkForGLError("Removing unreachable OpenGL object") val head = openglCleanQueueHead if (next === head) { openglCleanQueueHead = head.next } else { next.prev?.next = next.next next.next?.prev = next.prev } next = openglCleanQueue.poll() as CleanRef? } } 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 GLGenericTracker(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 vao by GLObjectTracker(::glBindVertexArray) var framebuffer by GLObjectTracker(::glBindFramebuffer, GL_FRAMEBUFFER) var program by GLObjectTracker(::glUseProgram) val textures2D = GLTexturesTracker(maxTextureBlocks) var clearColor by GLGenericTracker(RGBAColor.WHITE) { val (r, g, b, a) = it glClearColor(r, g, b, a) } var blendFunc by GLGenericTracker(BlendFunc()) { glBlendFuncSeparate(it.sourceColor.enum, it.destinationColor.enum, it.sourceAlpha.enum, it.destinationAlpha.enum) } val freeType = FreeType() var font: Font by Delegates.notNull() private set private var fontInitialized = false init { Starbound.loadFont().thenAcceptAsync(Consumer { try { font = Font(it.canonicalPath) fontInitialized = true } catch (err: Throwable) { LOGGER.fatal("Unable to load font", err) } }, mailbox) } val programs = GLPrograms() init { glActiveTexture(GL_TEXTURE0) checkForGLError() } val whiteTexture = GLTexture2D(1, 1, GL_RGB8) val missingTexture = GLTexture2D(8, 8, GL_RGB8) init { val buffer = ByteBuffer.allocateDirect(3) buffer.put(0xFF.toByte()) buffer.put(0xFF.toByte()) buffer.put(0xFF.toByte()) buffer.position(0) whiteTexture.upload(GL_RGB, GL_UNSIGNED_BYTE, buffer) } init { val buffer = ByteBuffer.allocateDirect(3 * 8 * 8) for (row in 0 until 4) { for (x in 0 until 4) { buffer.put(0x80.toByte()) buffer.put(0x0.toByte()) buffer.put(0x80.toByte()) } for (x in 0 until 4) { buffer.put(0x0.toByte()) buffer.put(0x0.toByte()) buffer.put(0x0.toByte()) } } for (row in 0 until 4) { for (x in 0 until 4) { buffer.put(0x0.toByte()) buffer.put(0x0.toByte()) buffer.put(0x0.toByte()) } for (x in 0 until 4) { buffer.put(0x80.toByte()) buffer.put(0x0.toByte()) buffer.put(0x80.toByte()) } } buffer.position(0) missingTexture.upload(GL_RGB, GL_UNSIGNED_BYTE, buffer) } 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 newEBO() = BufferObject.EBO() fun newVBO() = BufferObject.VBO() fun newVAO() = VertexArrayObject() inline fun quadWireframe(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) { val builder = programs.position.builder builder.builder.begin(GeometryType.QUADS_AS_LINES_WIREFRAME) lambda.invoke(builder.builder) builder.upload() programs.position.use() programs.position.colorMultiplier = color programs.position.modelMatrix = stack.last() builder.draw(GL_LINES) } inline fun quadColor(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) { val builder = programs.position.builder builder.builder.begin(GeometryType.QUADS) lambda.invoke(builder.builder) builder.upload() programs.position.use() programs.position.colorMultiplier = color programs.position.modelMatrix = stack.last() builder.draw(GL_TRIANGLES) } inline fun lines(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) { val builder = programs.position.builder builder.builder.begin(GeometryType.LINES) lambda.invoke(builder.builder) builder.upload() programs.position.use() programs.position.colorMultiplier = color programs.position.modelMatrix = stack.last() builder.draw(GL_LINES) } 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) 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(): Matrix3f { return Matrix3f.ortho(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat()) } private fun updateViewportMatrixWorld(): Matrix3f { return Matrix3f.orthoDirect(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat()) } 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(), viewportHeight - 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(), viewportHeight - yMousePos.get().toFloat()) } fun screenToWorld(x: Double, y: Double): Vector2d { val relativeX = (-viewportWidth / 2.0 + x) / settings.zoom / PIXELS_IN_STARBOUND_UNIT + camera.pos.x val relativeY = (-viewportHeight / 2.0 + y) / settings.zoom / PIXELS_IN_STARBOUND_UNIT + camera.pos.y return Vector2d(relativeX, relativeY) } fun screenToWorld(x: Int, y: Int): Vector2d { return screenToWorld(x.toDouble(), y.toDouble()) } fun screenToWorld(value: Vector2d): Vector2d { return screenToWorld(value.x, value.y) } val tileRenderers = TileRenderers(this) var world: ClientWorld? = null init { clearColor = RGBAColor.SLATE_GRAY blend = true blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA } val spinner = ExecutionSpinner(mailbox, ::renderFrame, Starbound.TICK_TIME_ADVANCE_NANOS) val settings = ClientSettings() val viewportCells: ICellAccess = object : ICellAccess { override fun getCell(x: Int, y: Int): AbstractCell { return world!!.getCell(x + viewportCellX, y + viewportCellY) } override fun getCellDirect(x: Int, y: Int): AbstractCell { return world!!.getCellDirect(x + viewportCellX, y + viewportCellY) } override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean { return world!!.setCell(x + viewportCellX, y + viewportCellY, cell) } } var viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight) private set var viewportLightingTexture = GLTexture2D(1, 1, GL_RGB8) private set private var viewportLightingMem: ByteBuffer? = null fun updateViewportParams() { viewportRectangle = AABB.rectangle( camera.pos, viewportWidth / settings.zoom / PIXELS_IN_STARBOUND_UNIT, viewportHeight / settings.zoom / PIXELS_IN_STARBOUND_UNIT ) viewportCellX = roundTowardsNegativeInfinity(viewportRectangle.mins.x) - 16 viewportCellY = roundTowardsNegativeInfinity(viewportRectangle.mins.y) - 16 val viewportCellWidth0 = roundTowardsPositiveInfinity(viewportRectangle.width) + 32 val viewportCellHeight0 = roundTowardsPositiveInfinity(viewportRectangle.height) + 32 if ((viewportCellWidth0 - viewportCellWidth).absoluteValue > 1) viewportCellWidth = viewportCellWidth0 if ((viewportCellHeight0 - viewportCellHeight).absoluteValue > 1) viewportCellHeight = viewportCellHeight0 viewportTopRight = screenToWorld(viewportWidth, viewportHeight) viewportBottomLeft = screenToWorld(0, 0) 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) viewportLightingTexture = GLTexture2D(viewportCellWidth.coerceAtMost(4096), viewportCellHeight.coerceAtMost(4096), GL_RGB8) } else { viewportLightingMem = null } } } fun onDrawGUI(lambda: () -> Unit) { onDrawGUI.add(lambda) } private val layers = LayeredRenderer(this) private var dotsIndex = 0 private val dotTime = 7 private var dotCountdown = dotTime private var dotInc = 1 private fun drawPerformanceBasic(onlyMemory: Boolean) { val runtime = Runtime.getRuntime() if (!onlyMemory) font.render("Latency: ${(spinner.averageRenderWait * 1_00000.0).toInt() / 100f}ms", scale = 0.4f) if (!onlyMemory) font.render("Frame: ${(spinner.averageRenderTime * 1_00000.0).toInt() / 100f}ms", y = font.lineHeight * 0.6f, scale = 0.4f) font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = font.lineHeight * 1.2f, scale = 0.4f) if (!onlyMemory) font.render("OGL C: $openglObjectsCreated D: $openglObjectsCleaned A: ${openglObjectsCreated - openglObjectsCleaned}", y = font.lineHeight * 1.8f, scale = 0.4f) } private fun renderLoadingScreen() { executeQueuedTasks() updateViewportParams() clearColor = RGBAColor.BLACK glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) val min = viewportHeight.coerceAtMost(viewportWidth) val size = min * 0.02f val program = programs.positionColor val builder = program.builder uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen } fontShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen } stack.clear(Matrix3f.identity()) program.colorMultiplier = RGBAColor.WHITE builder.builder.begin(GeometryType.QUADS) if (dotCountdown-- <= 0) { dotCountdown = dotTime dotsIndex += dotInc if (dotsIndex < 0) { dotsIndex = 1 dotInc = 1 } else if (dotsIndex >= 3) { dotsIndex = 1 dotInc = -1 } } val a = if (dotsIndex == 0) RGBAColor.SLATE_GRAY else RGBAColor.WHITE val b = if (dotsIndex == 1) RGBAColor.SLATE_GRAY else RGBAColor.WHITE val c = if (dotsIndex == 2) RGBAColor.SLATE_GRAY else RGBAColor.WHITE builder.builder.centeredQuad(viewportWidth * 0.5f, viewportHeight * 0.5f, size, size) { color(b) } builder.builder.centeredQuad(viewportWidth * 0.5f - size * 2f, viewportHeight * 0.5f, size, size) { color(a) } builder.builder.centeredQuad(viewportWidth * 0.5f + size * 2f, viewportHeight * 0.5f, size, size) { color(c) } builder.builder.quad(0f, viewportHeight - 20f, viewportWidth * Starbound.loadingProgress.toFloat(), viewportHeight.toFloat()) { color(RGBAColor.GREEN) } val runtime = Runtime.getRuntime() //if (runtime.maxMemory() <= 4L * 1024L * 1024L * 1024L) { builder.builder.centeredQuad(viewportWidth * 0.5f, viewportHeight * 0.1f, viewportWidth * 0.7f, 2f) { color(RGBAColor.WHITE) } builder.builder.centeredQuad(viewportWidth * 0.5f, viewportHeight * 0.1f + 20f, viewportWidth * 0.7f, 2f) { color(RGBAColor.WHITE) } builder.builder.centeredQuad(viewportWidth * (0.5f - 0.35f), viewportHeight * 0.1f + 10f, 2f, 20f) { color(RGBAColor.WHITE) } builder.builder.centeredQuad(viewportWidth * (0.5f + 0.35f), viewportHeight * 0.1f + 10f, 2f, 20f) { color(RGBAColor.WHITE) } builder.builder.centeredQuad(viewportWidth * (0.5f - 0.35f + 0.7f * (runtime.totalMemory().toDouble() / runtime.maxMemory().toDouble()).toFloat()), viewportHeight * 0.1f + 10f, 2f, 18f) { color(RGBAColor.RED) } val leftEdge = viewportWidth * (0.5f - 0.35f) + 1f builder.builder.quad( leftEdge, viewportHeight * 0.1f + 1f, leftEdge + viewportWidth * 0.7f * ((runtime.totalMemory() - runtime.freeMemory()) / runtime.maxMemory().toDouble()).toFloat(), viewportHeight * 0.1f + 19f ) { color(RGBAColor(29, 140, 160)) } //} if (fontInitialized) { drawPerformanceBasic(true) } program.use() builder.upload() builder.draw() GLFW.glfwSwapBuffers(window) GLFW.glfwPollEvents() } private fun renderWorld(world: ClientWorld) { updateViewportParams() world.think() stack.clear(Matrix3f.identity()) val viewMatrix = viewportMatrixWorld.copy() .translate(viewportWidth / 2f, viewportHeight / 2f) // центр экрана + координаты отрисовки мира .scale(x = settings.zoom * PIXELS_IN_STARBOUND_UNITf, y = settings.zoom * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера .translate(-camera.pos.x.toFloat(), -camera.pos.y.toFloat()) // перемещаем вид к камере uberShaderPrograms.forValidRefs { it.viewMatrix = viewMatrix } fontShaderPrograms.forValidRefs { it.viewMatrix = viewMatrix } viewportLighting.clear() val viewportLightingMem = viewportLightingMem world.lock.withLock { world.addLayers( layers = layers, size = viewportRectangle) } if (viewportLightingMem != null && !fullbright) { val spos = screenToWorld(mouseCoordinates) viewportLighting.addPointLight(roundTowardsPositiveInfinity(spos.x - viewportCellX), roundTowardsPositiveInfinity(spos.y - viewportCellY), 1f, 1f, 1f) viewportLightingMem.position(0) BufferUtils.zeroBuffer(viewportLightingMem) viewportLightingMem.position(0) viewportLighting.calculate(viewportLightingMem, viewportLightingTexture.width, viewportLightingTexture.height) viewportLightingMem.position(0) val old = textureUnpackAlignment textureUnpackAlignment = if (viewportLightingTexture.width.coerceAtMost(4096) % 4 == 0) 4 else 1 viewportLightingTexture.upload( GL_RGB, GL_UNSIGNED_BYTE, viewportLightingMem ) textureUnpackAlignment = old } else { viewportLightingTexture = whiteTexture } viewportLightingTexture.textureMinFilter = GL_LINEAR textures2D[lightMapLocation] = viewportLightingTexture val lightmapUV = if (fullbright) Vector4f.ZERO else Vector4f( ((viewportBottomLeft.x - viewportCellX) / viewportLighting.width).toFloat(), ((viewportBottomLeft.y - viewportCellY) / viewportLighting.height).toFloat(), (1f - (viewportCellX + viewportCellWidth - viewportTopRight.x) / viewportLighting.width).toFloat(), (1f - (viewportCellY + viewportCellHeight - viewportTopRight.y) / viewportLighting.height).toFloat()) uberShaderPrograms.forValidRefs { it.lightmapTexture = lightMapLocation it.lightmapUV = lightmapUV } } private fun renderFrame(): Boolean { if (GLFW.glfwWindowShouldClose(window)) { close() return false } val world = world if (!isRenderingGame) { executeQueuedTasks() GLFW.glfwPollEvents() if (world != null && Starbound.initialized) world.think() activeConnection?.flush() return true } if (!Starbound.initialized || !fontInitialized) { executeQueuedTasks() renderLoadingScreen() return true } input.think() camera.think(Starbound.TICK_TIME_ADVANCE) executeQueuedTasks() layers.clear() uberShaderPrograms.forValidRefs { if (it.flags.contains(UberShader.Flag.NEEDS_SCREEN_SIZE)) { it.screenSize = Vector2f(viewportWidth.toFloat(), viewportHeight.toFloat()) } } clearColor = RGBAColor.SLATE_GRAY glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) if (world != null) { renderWorld(world) } layers.render() val activeConnection = activeConnection activeConnection?.send(TrackedPositionPacket(camera.pos)) uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen } fontShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen } stack.clear(Matrix3f.identity()) for (fn in onDrawGUI) { fn.invoke() } if (world != null) { font.render("Camera: ${camera.pos} ${settings.zoom}", y = 140f, scale = 0.25f) font.render("Cursor: $mouseCoordinates -> ${screenToWorld(mouseCoordinates)}", y = 160f, scale = 0.25f) font.render("World chunk: ${world.geometry.chunkFromCell(camera.pos)}", y = 180f, scale = 0.25f) } drawPerformanceBasic(false) GLFW.glfwSwapBuffers(window) GLFW.glfwPollEvents() executeQueuedTasks() activeConnection?.flush() return true } private fun spin() { try { while (!shouldTerminate && spinner.spin()) { val ply = activeConnection?.character if (ply != null) { camera.pos = ply.position ply.movement.controlMove = if (input.KEY_A_DOWN) Direction.LEFT else if (input.KEY_D_DOWN) Direction.RIGHT else null ply.movement.controlJump = input.KEY_SPACE_DOWN ply.movement.controlRun = !input.KEY_LEFT_SHIFT_DOWN } else { camera.pos += Vector2d( (if (input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / settings.zoom else 0.0) + (if (input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / settings.zoom else 0.0), (if (input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / settings.zoom else 0.0) + (if (input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / settings.zoom else 0.0) ) camera.pos = world?.geometry?.wrap(camera.pos) ?: camera.pos } if (input.KEY_ESCAPE_PRESSED) { GLFW.glfwSetWindowShouldClose(window, true) } } } catch (err: Throwable) { LOGGER.fatal("Exception in client loop", err) } finally { executor.shutdown() lock.lock() try { if (window != MemoryUtil.NULL) { Callbacks.glfwFreeCallbacks(window) GLFW.glfwDestroyWindow(window) } if (--clients == 0) { GLFW.glfwTerminate() GLFW.glfwSetErrorCallback(null)?.free() glfwInitialized = false } shouldTerminate = true for (callback in terminateCallbacks) { callback.invoke() } } catch (err: Throwable) { LOGGER.fatal("Exception while destroying client", err) } finally { lock.unlock() } } } fun onTermination(lambda: () -> Unit) { terminateCallbacks.add(lambda) } override fun close() { shouldTerminate = true } init { input.addScrollCallback { _, x, y -> if (y > 0.0) { settings.zoom *= y.toFloat() * 2f } else if (y < 0.0) { settings.zoom /= -y.toFloat() * 2f } } } companion object { fun create(): CompletableFuture { val future = CompletableFuture() val clientID = COUNTER.getAndIncrement() val thread = Thread(Runnable { val client = try { StarboundClient(clientID) } catch (err: Throwable) { future.completeExceptionally(err) throw err } future.complete(client) client.spin() }, "Starbound Client $clientID") thread.start() return future } private val COUNTER = AtomicInteger() private val LOGGER = LogManager.getLogger(StarboundClient::class.java) private val CLIENTS = ThreadLocal() private val WHITE = ByteBuffer.allocateDirect(4).also { it.put(0xFF.toByte()) it.put(0xFF.toByte()) it.put(0xFF.toByte()) it.put(0xFF.toByte()) it.position(0) } @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 @Volatile private var clients = 0 @JvmStatic fun readInternal(file: String): String { return ClassLoader.getSystemClassLoader().getResourceAsStream(file)!!.bufferedReader() .let { val read = it.readText() it.close() read } } @JvmStatic fun readInternalBytes(file: String): ByteArray { return ClassLoader.getSystemClassLoader().getResourceAsStream(file)!!.readAllBytes() } } }