1111 lines
34 KiB
Kotlin
1111 lines
34 KiB
Kotlin
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<ScissorRect>()
|
|
private val onDrawGUI = ArrayList<() -> Unit>()
|
|
private val onViewportChanged = ArrayList<(width: Int, height: Int) -> Unit>()
|
|
private val terminateCallbacks = ArrayList<() -> Unit>()
|
|
|
|
private val openglCleanQueue = ReferenceQueue<Any>()
|
|
private var openglCleanQueueHead: CleanRef? = null
|
|
|
|
private class CleanRef(ref: Any, queue: ReferenceQueue<Any>, val fn: IntConsumer, val value: Int) : PhantomReference<Any>(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<Image, GLTexture2D> = Caffeine.newBuilder()
|
|
.expireAfterAccess(Duration.ofMinutes(1))
|
|
.scheduler(Scheduler.systemScheduler())
|
|
.build()
|
|
|
|
// ...бесконечное хранение пока кто-то все ещё использует текстуру
|
|
val named2DTextures1: Cache<Image, GLTexture2D> = Caffeine.newBuilder()
|
|
.weakValues()
|
|
.weakKeys()
|
|
.build()
|
|
|
|
private val fontShaderPrograms = ArrayList<WeakReference<FontProgram>>()
|
|
private val uberShaderPrograms = ArrayList<WeakReference<UberShader>>()
|
|
|
|
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<VertexArrayObject>(::glBindVertexArray)
|
|
var framebuffer by GLObjectTracker<GLFrameBuffer>(::glBindFramebuffer, GL_FRAMEBUFFER)
|
|
var program by GLObjectTracker<GLShaderProgram>(::glUseProgram)
|
|
|
|
val textures2D = GLTexturesTracker<GLTexture2D>(maxTextureBlocks)
|
|
|
|
var clearColor by GLGenericTracker<IStruct4f>(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<StarboundClient> {
|
|
val future = CompletableFuture<StarboundClient>()
|
|
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<StarboundClient>()
|
|
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()
|
|
}
|
|
}
|
|
}
|