From a02869401034913c5c9b7a7ea4465a6132b36b08 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Thu, 1 Feb 2024 15:58:44 +0700 Subject: [PATCH] Functional chunk tickets, chunk source, player chunk tracking --- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 26 +- .../kstarbound/client/StarboundClient.kt | 341 +++++++++--------- .../client/network/ClientConnection.kt | 6 +- .../network/packets/ForgetChunkPacket.kt | 30 ++ .../network/packets/TrackedPositionPacket.kt | 40 ++ .../kstarbound/client/world/ClientChunk.kt | 4 +- .../kstarbound/client/world/ClientWorld.kt | 78 ++-- .../ru/dbotthepony/kstarbound/network/API.kt | 20 +- .../kstarbound/network/Connection.kt | 10 +- .../kstarbound/network/PacketMapper.kt | 32 ++ .../network/packets/PacketMapping.kt | 6 + .../kstarbound/server/StarboundServer.kt | 11 +- .../kstarbound/server/network/ServerPlayer.kt | 114 +++++- .../kstarbound/server/world/IChunkSaver.kt | 11 + .../kstarbound/server/world/IChunkSource.kt | 24 ++ .../server/world/LegacyChunkSource.kt | 42 +++ .../kstarbound/server/world/ServerChunk.kt | 11 + .../kstarbound/server/world/ServerWorld.kt | 105 +++++- .../kstarbound/util/StreamUtils.kt | 11 + .../ru/dbotthepony/kstarbound/util/Utils.kt | 16 + .../ru/dbotthepony/kstarbound/world/Chunk.kt | 11 + .../dbotthepony/kstarbound/world/ChunkPos.kt | 7 + .../ru/dbotthepony/kstarbound/world/World.kt | 2 + .../kstarbound/world/WorldGeometry.kt | 29 ++ 24 files changed, 747 insertions(+), 240 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetChunkPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/TrackedPositionPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSaver.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 86f235d5..1b12d2ba 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.io.BTreeDB import ru.dbotthepony.kstarbound.io.readVarInt import ru.dbotthepony.kstarbound.json.VersionedJson import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer +import ru.dbotthepony.kstarbound.server.world.LegacyChunkSource import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.world.Direction @@ -49,6 +50,7 @@ fun main() { val server = IntegratedStarboundServer(File("./")) val client = StarboundClient() val world = ServerWorld(server, 0L, WorldGeometry(Vector2i(3000, 2000), true, false)) + world.addChunkSource(LegacyChunkSource(db)) world.startThread() //Starbound.addFilePath(File("./unpacked_assets/")) @@ -80,10 +82,10 @@ fun main() { //for (chunkX in 0 .. 17) { // for (chunkY in 21 .. 21) { for (chunkY in 18 .. 24) { - val data = db.read(byteArrayOf(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())) - val data2 = db.read(byteArrayOf(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())) + //val data = db.read(byteArrayOf(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())) + //val data2 = db.read(byteArrayOf(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())) - if (data != null) { + /*if (data != null) { var reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data), Inflater()))) reader.skipBytes(3) @@ -96,9 +98,9 @@ fun main() { } } } - } + }*/ - if (data2 != null) { + /*if (data2 != null) { val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data2), Inflater()))) val i = reader.readVarInt() @@ -118,7 +120,7 @@ fun main() { //val read = BinaryJsonReader.readElement(reader) //println(read) - } + }*/ } } @@ -137,7 +139,7 @@ fun main() { //item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0)) } - ClientConnection.connectToLocalServer(client, server.channels.createLocalChannel(), UUID(0L, 0L)) + client.connectToLocalServer(client, server.channels.createLocalChannel(), UUID(0L, 0L)) } //ent.position += Vector2d(y = 14.0, x = -10.0) @@ -161,17 +163,17 @@ fun main() { } while (client.renderFrame()) { - /*client.camera.pos += Vector2d( - (if (client.input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0), - (if (client.input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) - )*/ - if (ply != null) { client.camera.pos = ply!!.position ply!!.movement.controlMove = if (client.input.KEY_A_DOWN) Direction.LEFT else if (client.input.KEY_D_DOWN) Direction.RIGHT else null ply!!.movement.controlJump = client.input.KEY_SPACE_DOWN ply!!.movement.controlRun = !client.input.KEY_LEFT_SHIFT_DOWN + } else { + client.camera.pos += Vector2d( + (if (client.input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0), + (if (client.input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + ) } if (client.input.KEY_ESCAPE_PRESSED) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index d448a722..03f9add4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -3,6 +3,8 @@ 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 @@ -43,6 +45,9 @@ 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.client.network.ClientConnection +import ru.dbotthepony.kstarbound.client.network.packets.TrackedPositionPacket +import ru.dbotthepony.kstarbound.client.network.packets.TrackedSizePacket import ru.dbotthepony.kstarbound.client.render.Camera import ru.dbotthepony.kstarbound.client.render.Font import ru.dbotthepony.kstarbound.client.render.LayeredRenderer @@ -61,6 +66,7 @@ import ru.dbotthepony.kvector.api.IStruct4f import ru.dbotthepony.kvector.arrays.Matrix3f import ru.dbotthepony.kvector.arrays.Matrix3fStack import ru.dbotthepony.kvector.util2d.AABB +import ru.dbotthepony.kvector.util2d.AABBi import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2f @@ -71,6 +77,7 @@ 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 @@ -160,11 +167,26 @@ class StarboundClient : Closeable { 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(client: StarboundClient, address: Channel, uuid: UUID) { + check(activeConnection == null) { "Already having active connection to server: $activeConnection" } + activeConnection = ClientConnection.connectToLocalServer(client, address, uuid) + } + + fun connectToRemoteServer(client: StarboundClient, address: SocketAddress, uuid: UUID) { + check(activeConnection == null) { "Already having active connection to server: $activeConnection" } + activeConnection = ClientConnection.connectToRemoteServer(client, address, uuid) + } + private val scissorStack = LinkedList() 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>() @@ -756,26 +778,10 @@ class StarboundClient : Closeable { } } - 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) - } - private val layers = LayeredRenderer(this) private var dotsIndex = 0 private val dotTime = 7 @@ -791,6 +797,146 @@ class StarboundClient : Closeable { 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.forEachValid { it.viewMatrix = viewportMatrixScreen } + fontShaderPrograms.forEachValid { 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.forEachValid { it.viewMatrix = viewMatrix } + fontShaderPrograms.forEachValid { 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.forEachValid { + it.lightmapTexture = lightMapLocation + it.lightmapUV = lightmapUV + } + } + fun renderFrame(): Boolean { ensureSameThread() @@ -833,81 +979,7 @@ class StarboundClient : Closeable { } if (!Starbound.initialized || !fontInitialized) { - 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.forEachValid { it.viewMatrix = viewportMatrixScreen } - fontShaderPrograms.forEachValid { 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() - + renderLoadingScreen() return true } @@ -923,83 +995,16 @@ class StarboundClient : Closeable { glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) if (world != null) { - updateViewportParams() + renderWorld(world) + } - if (Starbound.initialized) - world.think() + layers.render() - stack.clear(Matrix3f.identity()) + val activeConnection = activeConnection - 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.forEachValid { it.viewMatrix = viewMatrix } - fontShaderPrograms.forEachValid { it.viewMatrix = viewMatrix } - - for (lambda in onPreDrawWorld) { - lambda.invoke(layers) - } - - for (i in onPostDrawWorldOnce.size - 1 downTo 0) { - onPostDrawWorldOnce[i].invoke(layers) - onPostDrawWorldOnce.removeAt(i) - } - - 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.forEachValid { - it.lightmapTexture = lightMapLocation - it.lightmapUV = lightmapUV - } - - layers.render() - - for (lambda in onPostDrawWorld) { - lambda.invoke() - } + if (activeConnection != null) { + activeConnection.send(TrackedPositionPacket(camera.pos)) + activeConnection.send(TrackedSizePacket(12, 12)) } uberShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/ClientConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/ClientConnection.kt index 9c0b5fb9..2dc09eed 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/ClientConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/ClientConnection.kt @@ -41,12 +41,12 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType, uuid: try { msg.play(this) } catch (err: Throwable) { - LOGGER.error("Failed to read serverbound packet $msg", err) + LOGGER.error("Failed to read incoming packet $msg", err) disconnect(err.toString()) } } else { - LOGGER.error("Unknown serverbound packet type $msg") - disconnect("Unknown serverbound packet type $msg") + LOGGER.error("Unknown incoming packet type $msg") + disconnect("Unknown incoming packet type $msg") } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetChunkPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetChunkPacket.kt new file mode 100644 index 00000000..e91c9c47 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetChunkPacket.kt @@ -0,0 +1,30 @@ +package ru.dbotthepony.kstarbound.client.network.packets + +import ru.dbotthepony.kstarbound.client.network.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import ru.dbotthepony.kstarbound.util.readChunkPos +import ru.dbotthepony.kstarbound.util.writeVec2i +import ru.dbotthepony.kstarbound.world.ChunkPos +import java.io.DataInputStream +import java.io.DataOutputStream +import kotlin.concurrent.withLock + +class ForgetChunkPacket(val pos: ChunkPos) : IClientPacket { + constructor(stream: DataInputStream) : this(stream.readChunkPos()) + + override fun write(stream: DataOutputStream) { + stream.writeVec2i(pos) + } + + override fun play(connection: ClientConnection) { + val world = connection.client.world ?: return + + world.lock.withLock { + world.chunkMap.remove(pos) + + world.forEachRenderRegion(pos) { + it.notifyChunkForget() + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/TrackedPositionPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/TrackedPositionPacket.kt new file mode 100644 index 00000000..8795198d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/TrackedPositionPacket.kt @@ -0,0 +1,40 @@ +package ru.dbotthepony.kstarbound.client.network.packets + +import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.server.network.ServerConnection +import ru.dbotthepony.kstarbound.util.readVec2d +import ru.dbotthepony.kstarbound.util.writeVec2d +import ru.dbotthepony.kvector.vector.Vector2d +import java.io.DataInputStream +import java.io.DataOutputStream + +data class TrackedPositionPacket(val pos: Vector2d) : IServerPacket { + constructor(stream: DataInputStream) : this(stream.readVec2d()) + + override fun write(stream: DataOutputStream) { + stream.writeVec2d(pos) + } + + override fun play(connection: ServerConnection) { + connection.player.trackedPosition = pos + } +} + +data class TrackedSizePacket(val width: Int, val height: Int) : IServerPacket { + constructor(stream: DataInputStream) : this(stream.readUnsignedByte(), stream.readUnsignedByte()) + + init { + require(width in 0 .. 12) { "Too big chunk width to track: $width" } + require(height in 0 .. 12) { "Too big chunk height to track: $height" } + } + + override fun write(stream: DataOutputStream) { + stream.writeByte(width) + stream.writeByte(height) + } + + override fun play(connection: ServerConnection) { + connection.player.trackedChunksWidth = width + connection.player.trackedChunksHeight = height + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt index cf46a13a..2834a9c2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt @@ -9,7 +9,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk(seed, geometry) { private fun determineChunkSize(cells: Int): Int { - for (i in 32 downTo 1) { + for (i in 64 downTo 1) { if (cells % i == 0) { return i } @@ -67,35 +69,28 @@ class ClientWorld( inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) { val bakedMeshes = ArrayList, RenderLayer.Point>>() private var currentBakeTask: Future? = null - var isDirty = true + private var bakeTaskID = 0 + private var isDirty = true + + fun markDirty() { + isDirty = true + } fun bake() { - if (!isDirty) { - val currentBakeTask = currentBakeTask ?: return - - if (currentBakeTask.isDone) { - bakedMeshes.clear() - - for ((baked, zLevel) in currentBakeTask.get().bakeIntoMeshes()) { - bakedMeshes.add(baked to zLevel) - } - - this.currentBakeTask = null - } - - return - } - + if (!isDirty) return isDirty = false - currentBakeTask = client.executor.submit(Callable { + val bakeTaskID = ++bakeTaskID + + CompletableFuture.supplyAsync(Supplier { val meshes = LayeredRenderer(client) for (x in 0 until renderRegionWidth) { for (y in 0 until renderRegionHeight) { if (!inBounds(x, y)) continue + if (bakeTaskID != this.bakeTaskID) return@Supplier meshes - val tile = view.getTile(x, y) ?: continue + val tile = view.getTile(x, y) val material = tile.material if (!material.value.isMeta) { @@ -115,10 +110,21 @@ class ClientWorld( } meshes - }) + }, client.executor).thenAcceptAsync(Consumer { + if (bakeTaskID != this.bakeTaskID) return@Consumer + + bakedMeshes.clear() + + for ((baked, zLevel) in it.bakeIntoMeshes()) { + bakedMeshes.add(baked to zLevel) + } + + this.currentBakeTask = null + }, client.mailbox) } } + private var renderCalls = 0 private val liquidMesh = ArrayList>() var liquidIsDirty = true @@ -127,7 +133,22 @@ class ClientWorld( val background = Layer(TileView.Background(view), true) val foreground = Layer(TileView.Foreground(view), false) + fun notifyChunkForget() { + background.markDirty() + foreground.markDirty() + + val renderCalls = renderCalls + + client.mailbox.schedule(Runnable { + if (renderCalls == this.renderCalls) { + background.bakedMeshes.clear() + foreground.bakedMeshes.clear() + } + }, 500L, TimeUnit.MILLISECONDS) + } + fun addLayers(layers: LayeredRenderer, renderOrigin: Vector2f) { + renderCalls++ background.bake() foreground.bake() @@ -215,9 +236,14 @@ class ClientWorld( ix /= renderRegionWidth iy /= renderRegionHeight - for (x in ix .. ix + CHUNK_SIZE / renderRegionWidth) { - for (y in iy .. iy + CHUNK_SIZE / renderRegionWidth) { - renderRegions[renderRegionKey(x, y)]?.let(action) + val paddingX = (CHUNK_SIZE / renderRegionWidth).coerceAtLeast(1) + val paddingY = (CHUNK_SIZE / renderRegionHeight).coerceAtLeast(1) + + for (x in ix - paddingX .. ix + paddingX) { + for (y in iy - paddingY .. iy + paddingY) { + lock.withLock { + renderRegions[renderRegionKey(x, y)]?.let(action) + } } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt index b8453b0b..7d59b1ae 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt @@ -7,7 +7,7 @@ import ru.dbotthepony.kstarbound.server.network.ServerConnection import java.io.DataInputStream import java.io.DataOutputStream import kotlin.reflect.KClass -import kotlin.reflect.full.isSuperclassOf +import kotlin.reflect.full.isSubclassOf fun ByteBuf.writeUTF(value: String) { writeBytes(value.toByteArray().also { check(!it.any { it.toInt() == 0 }) { "Provided UTF string contains NUL" } }) @@ -40,30 +40,32 @@ enum class ConnectionState { CLOSED; } -enum class PacketDirection(val allowedOnClient: Boolean, val allowedOnServer: Boolean) { - SERVER_TO_CLIENT(true, false), - CLIENT_TO_SERVER(false, true), +enum class PacketDirection(val acceptOnClient: Boolean, val acceptOnServer: Boolean) { + FROM_SERVER(true, false), + FROM_CLIENT(false, true), BI_DIRECTIONAL(true, true); fun acceptedOn(side: ConnectionSide): Boolean { if (side == ConnectionSide.SERVER) - return allowedOnServer + return acceptOnServer - return allowedOnClient + return acceptOnClient } companion object { fun get(type: KClass): PacketDirection { - return of(type.isSuperclassOf(IClientPacket::class), type.isSuperclassOf(IServerPacket::class)) + return of(type.isSubclassOf(IClientPacket::class), type.isSubclassOf(IServerPacket::class)) } fun of(allowedOnClient: Boolean, allowedOnServer: Boolean): PacketDirection { if (allowedOnServer && allowedOnClient) return BI_DIRECTIONAL else if (allowedOnServer) - return SERVER_TO_CLIENT + return FROM_CLIENT + else if (allowedOnClient) + return FROM_SERVER else - return CLIENT_TO_SERVER + throw IllegalArgumentException("Packet is not allowed on either side") } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index 36223c61..e32c5a34 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -61,7 +61,13 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va fun initializeHandlers() { val channel = channel ?: throw IllegalStateException("No network channel is bound") - if (type == ConnectionType.NETWORK) channel.pipeline().addLast(PacketMapping.Inbound(side)) + + if (type == ConnectionType.NETWORK) { + channel.pipeline().addLast(PacketMapping.Inbound(side)) + } else { + channel.pipeline().addLast(PacketMapping.InboundValidator(side)) + } + channel.pipeline().addLast(this) if (type == ConnectionType.NETWORK) { @@ -69,6 +75,8 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va channel.pipeline().addFirst(DatagramEncoder) channel.pipeline().addFirst(DatagramDecoder()) + } else { + channel.pipeline().addLast(PacketMapping.OutboundValidator(side)) } inGame() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketMapper.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketMapper.kt index 837f3a4a..0b4fb87e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketMapper.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketMapper.kt @@ -66,6 +66,24 @@ class PacketMapper { } } + inner class InboundValidator(val side: ConnectionSide) : ChannelInboundHandlerAdapter() { + override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { + val type = clazz2Type[msg::class] + + if (type == null) { + LOGGER.error("Unknown packet type ${msg::class}!") + } else if (!type.direction.acceptedOn(side)) { + LOGGER.error("Packet ${type.type} can not be accepted on side $side!") + } else { + try { + ctx.fireChannelRead(msg) + } catch (err: Throwable) { + LOGGER.error("Error while reading incoming packet from network", err) + } + } + } + } + inner class Outbound(val side: ConnectionSide) : ChannelOutboundHandlerAdapter() { override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) { val type = clazz2Type[msg::class] @@ -83,6 +101,20 @@ class PacketMapper { } } + inner class OutboundValidator(val side: ConnectionSide) : ChannelOutboundHandlerAdapter() { + override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) { + val type = clazz2Type[msg::class] + + if (type == null) { + LOGGER.error("Unknown outgoing message type ${msg::class}, it will not reach the other side.") + } else if (!type.direction.acceptedOn(side.opposite)) { + LOGGER.error("Packet ${type.type} can not be accepted on side ${side.opposite}, refusing to send it!") + } else { + ctx.write(msg, promise) + } + } + } + companion object { private val LOGGER = LogManager.getLogger() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PacketMapping.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PacketMapping.kt index d1959e93..83d18c62 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PacketMapping.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PacketMapping.kt @@ -1,11 +1,17 @@ package ru.dbotthepony.kstarbound.network.packets +import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket import ru.dbotthepony.kstarbound.client.network.packets.InitialChunkDataPacket import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket +import ru.dbotthepony.kstarbound.client.network.packets.TrackedPositionPacket +import ru.dbotthepony.kstarbound.client.network.packets.TrackedSizePacket import ru.dbotthepony.kstarbound.network.PacketMapper val PacketMapping = PacketMapper().also { it.add(::DisconnectPacket) it.add(::JoinWorldPacket) it.add(::InitialChunkDataPacket) + it.add(::ForgetChunkPacket) + it.add(::TrackedPositionPacket) + it.add(::TrackedSizePacket) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index bd5493e5..2cd4e271 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -44,17 +44,8 @@ abstract class StarboundServer(val root: File) : Closeable { fun playerInGame(player: ServerPlayer) { val world = worlds.first() player.world = world + world.players.add(player) player.connection.send(JoinWorldPacket(world)) - - for (x in 0 until 100) { - for (y in 0 until 40) { - val chunk = world.chunkMap[x, y] - - if (chunk != null) { - player.connection.send(InitialChunkDataPacket(chunk)) - } - } - } } protected abstract fun close0() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerPlayer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerPlayer.kt index 26646746..f80f0b0f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerPlayer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerPlayer.kt @@ -1,9 +1,121 @@ package ru.dbotthepony.kstarbound.server.network +import it.unimi.dsi.fastutil.longs.LongOpenHashSet +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket +import ru.dbotthepony.kstarbound.client.network.packets.InitialChunkDataPacket import ru.dbotthepony.kstarbound.network.Player +import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.world.ServerWorld +import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kvector.vector.Vector2d +import kotlin.concurrent.withLock class ServerPlayer(connection: ServerConnection) : Player(connection, connection.uuid) { - var world: ServerWorld? = null + inline val server: StarboundServer + get() = connection.server + var world: ServerWorld? = null + set(value) { + field = value + needsToRecomputeTrackedChunks = true + } + + var trackedPosition: Vector2d = Vector2d(238.0, 685.0) + set(value) { + if (field != value) { + field = value + needsToRecomputeTrackedChunks = true + } + } + + var trackedPositionChunk: ChunkPos = ChunkPos.ZERO + private set + + var trackedChunksWidth = 1 + set(value) { + if (field != value) { + field = value + needsToRecomputeTrackedChunks = true + } + } + + var trackedChunksHeight = 1 + set(value) { + if (field != value) { + field = value + needsToRecomputeTrackedChunks = true + } + } + + private val tickets = Object2ObjectOpenHashMap() + private val sentChunks = ObjectOpenHashSet() + + private var needsToRecomputeTrackedChunks = true + + private fun recomputeTrackedChunks() { + val world = world ?: return + val trackedPositionChunk = world.geometry.chunkFromCell(trackedPosition) + needsToRecomputeTrackedChunks = false + if (trackedPositionChunk == this.trackedPositionChunk) return + + val tracked = ObjectOpenHashSet() + + for (x in -trackedChunksWidth .. trackedChunksWidth) { + for (y in -trackedChunksHeight .. trackedChunksHeight) { + tracked.add(world.geometry.wrap(trackedPositionChunk + ChunkPos(x, y))) + } + } + + val itr = tickets.entries.iterator() + + for ((pos, ticket) in itr) { + if (pos !in tracked) { + ticket.cancel() + itr.remove() + } + } + + for (pos in tracked) { + if (pos !in tickets) { + tickets[pos] = world.permanentChunkTicket(pos) + } + } + } + + fun tick() { + val world = world + + if (world == null) { + tickets.values.forEach { it.cancel() } + tickets.clear() + sentChunks.clear() + return + } + + if (needsToRecomputeTrackedChunks) { + recomputeTrackedChunks() + } + + for (pos in tickets.keys) { + if (pos !in sentChunks) { + val chunk = world.chunkMap[pos] + + if (chunk != null) { + connection.send(InitialChunkDataPacket(chunk)) + sentChunks.add(pos) + } + } + } + + val itr = sentChunks.iterator() + + for (pos in itr) { + if (pos !in tickets) { + connection.send(ForgetChunkPacket(pos)) + itr.remove() + } + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSaver.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSaver.kt new file mode 100644 index 00000000..e00b4cd5 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSaver.kt @@ -0,0 +1,11 @@ +package ru.dbotthepony.kstarbound.server.world + +import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.api.AbstractCell +import ru.dbotthepony.kstarbound.world.entities.WorldObject +import ru.dbotthepony.kvector.arrays.Object2DArray + +interface IChunkSaver { + fun saveCells(pos: ChunkPos, data: Object2DArray) + fun saveObjects(pos: ChunkPos, data: Collection) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt new file mode 100644 index 00000000..75dc4cd3 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt @@ -0,0 +1,24 @@ +package ru.dbotthepony.kstarbound.server.world + +import ru.dbotthepony.kstarbound.util.KOptional +import ru.dbotthepony.kstarbound.world.CHUNK_SIZE +import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.api.AbstractCell +import ru.dbotthepony.kstarbound.world.entities.WorldObject +import ru.dbotthepony.kvector.arrays.Object2DArray +import java.util.concurrent.CompletableFuture + +interface IChunkSource { + fun getTiles(pos: ChunkPos): CompletableFuture>> + fun getObjects(pos: ChunkPos): CompletableFuture>> + + object Void : IChunkSource { + override fun getTiles(pos: ChunkPos): CompletableFuture>> { + return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY))) + } + + override fun getObjects(pos: ChunkPos): CompletableFuture>> { + return CompletableFuture.completedFuture(KOptional.of(emptyList())) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt new file mode 100644 index 00000000..12904081 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt @@ -0,0 +1,42 @@ +package ru.dbotthepony.kstarbound.server.world + +import ru.dbotthepony.kstarbound.io.BTreeDB +import ru.dbotthepony.kstarbound.util.KOptional +import ru.dbotthepony.kstarbound.world.CHUNK_SIZE +import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.api.AbstractCell +import ru.dbotthepony.kstarbound.world.api.MutableCell +import ru.dbotthepony.kstarbound.world.entities.WorldObject +import ru.dbotthepony.kvector.arrays.Object2DArray +import java.io.BufferedInputStream +import java.io.ByteArrayInputStream +import java.io.DataInputStream +import java.util.concurrent.CompletableFuture +import java.util.zip.Inflater +import java.util.zip.InflaterInputStream + +class LegacyChunkSource(val db: BTreeDB) : IChunkSource { + override fun getTiles(pos: ChunkPos): CompletableFuture>> { + val chunkX = pos.x + val chunkY = pos.y + val key = byteArrayOf(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()) + val data = db.read(key) ?: return CompletableFuture.completedFuture(KOptional.empty()) + + val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data), Inflater()))) + reader.skipBytes(3) + + val result = Object2DArray.nulls(CHUNK_SIZE, CHUNK_SIZE) + + for (y in 0 until CHUNK_SIZE) { + for (x in 0 until CHUNK_SIZE) { + result[x, y] = MutableCell().read(reader) + } + } + + return CompletableFuture.completedFuture(KOptional(result as Object2DArray)) + } + + override fun getObjects(pos: ChunkPos): CompletableFuture>> { + return CompletableFuture.completedFuture(KOptional.of(listOf())) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt index 05a6177d..bb809901 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -1,7 +1,18 @@ package ru.dbotthepony.kstarbound.server.world +import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.api.AbstractCell +import ru.dbotthepony.kstarbound.world.api.ImmutableCell +import ru.dbotthepony.kvector.arrays.Object2DArray class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk(world, pos) { + fun copyCells(): Object2DArray { + if (cells.isInitialized()) { + return Object2DArray(cells.value) + } else { + return Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY) + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index e60c9b11..b54a0ca3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -1,15 +1,23 @@ package ru.dbotthepony.kstarbound.server.world +import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet +import it.unimi.dsi.fastutil.objects.ObjectArraySet import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.server.StarboundServer +import ru.dbotthepony.kstarbound.server.network.ServerPlayer +import ru.dbotthepony.kstarbound.util.KOptional +import ru.dbotthepony.kstarbound.util.composeFutures import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.WorldGeometry +import ru.dbotthepony.kstarbound.world.api.AbstractCell +import ru.dbotthepony.kvector.arrays.Object2DArray import java.io.Closeable import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.LockSupport +import java.util.function.Consumer import kotlin.concurrent.withLock class ServerWorld( @@ -21,6 +29,8 @@ class ServerWorld( server.worlds.add(this) } + val players = ObjectArraySet() + val thread = Thread(::runThread, "Starbound Server World $seed") var isStopped: Boolean = false private set @@ -29,6 +39,13 @@ class ServerWorld( thread.isDaemon = true } + private val chunkProviders = ArrayList() + var saver: IChunkSaver? = null + + fun addChunkSource(source: IChunkSource) { + chunkProviders.add(source) + } + @Volatile private var nextThink = 0L @@ -75,7 +92,27 @@ class ServerWorld( get() = false override fun thinkInner() { - ticketLists.forEach { it.tick() } + lock.withLock { + players.forEach { it.tick() } + + ticketLists.removeIf { + val valid = it.tick() + + if (!valid) { + val removed = ticketMap.remove(it.pos.toLong()) + check(removed == it) { "Expected to remove $it, but removed $removed" } + + val chunk = chunkMap[it.pos] + + if (chunk != null) { + saver?.saveCells(it.pos, chunk.copyCells()) + chunkMap.remove(it.pos) + } + } + + !valid + } + } } override fun chunkFactory(pos: ChunkPos): ServerChunk { @@ -85,6 +122,24 @@ class ServerWorld( private val ticketMap = Long2ObjectOpenHashMap() private val ticketLists = ArrayList() + private fun getTicketList(pos: ChunkPos): TicketList { + return ticketMap.computeIfAbsent(geometry.wrapToLong(pos), Long2ObjectFunction { TicketList(it) }) + } + + fun permanentChunkTicket(pos: ChunkPos): ITicket { + lock.withLock { + return getTicketList(pos).Ticket().init() + } + } + + fun temporaryChunkTicket(pos: ChunkPos, time: Int): ITicket { + require(time > 0) { "Invalid ticket time: $time" } + + lock.withLock { + return getTicketList(pos).TimedTicket(time).init() + } + } + interface ITicket { fun cancel() val isCanceled: Boolean @@ -104,18 +159,17 @@ class ServerWorld( } private inner class TicketList(val pos: ChunkPos) { - init { - lock.withLock { - check(ticketMap.put(pos.toLong(), this) == null) { "Already had ticket list at $pos" } - ticketLists.add(this) - } - } + constructor(pos: Long) : this(ChunkPos(pos)) + private var first = true private val permanent = ArrayList() private val temporary = ObjectAVLTreeSet() private var ticks = 0 private var nextTicketID = AtomicInteger() + val isValid: Boolean + get() = temporary.isNotEmpty() || permanent.isNotEmpty() + fun tick(): Boolean { ticks++ @@ -129,6 +183,34 @@ class ServerWorld( } open inner class Ticket : ITicket { + open fun init(): Ticket { + if (this is TimedTicket) + temporary.add(this) + else + permanent.add(this) + + if (first) { + first = false + + if (geometry.x.inBoundsChunk(pos.x) && geometry.y.inBoundsChunk(pos.y)) { + ticketLists.add(this@TicketList) + + if (chunkProviders.isNotEmpty()) { + val onFinish = Consumer>> { + if (isValid && it.isPresent) { + val chunk = chunkMap.compute(pos) ?: return@Consumer + chunk.loadCells(it.value) + } + } + + composeFutures(chunkProviders) { it.getTiles(pos) }.thenAcceptAsync(onFinish, mailbox) + } + } + } + + return this + } + final override val id: Int = nextTicketID.getAndIncrement() final override val pos: ChunkPos get() = this@TicketList.pos @@ -150,10 +232,17 @@ class ServerWorld( final override var isCanceled: Boolean = false } - inner class TimedTicket(var expiresAt: Int) : Ticket(), ITimedTicket { + inner class TimedTicket(expiresAt: Int) : Ticket(), ITimedTicket { + var expiresAt = expiresAt + ticks + override val timeRemaining: Int get() = (expiresAt - ticks).coerceAtLeast(0) + override fun init(): TimedTicket { + super.init() + return this + } + override fun prolong(ticks: Int) { if (ticks == 0 || isCanceled) return diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/StreamUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/StreamUtils.kt index 2e7c2340..8f6d4b50 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/StreamUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/StreamUtils.kt @@ -3,7 +3,9 @@ package ru.dbotthepony.kstarbound.util import io.netty.buffer.ByteBuf import it.unimi.dsi.fastutil.bytes.ByteArrayList import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kvector.api.IStruct2d import ru.dbotthepony.kvector.api.IStruct2i +import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2i import java.io.DataInput import java.io.DataOutput @@ -80,10 +82,19 @@ fun OutputStream.writeVec2i(value: IStruct2i) { writeInt(value.component2()) } +fun OutputStream.writeVec2d(value: IStruct2d) { + writeDouble(value.component1()) + writeDouble(value.component2()) +} + fun InputStream.readVec2i(): Vector2i { return Vector2i(readInt(), readInt()) } +fun InputStream.readVec2d(): Vector2d { + return Vector2d(readDouble(), readDouble()) +} + fun InputStream.readChunkPos(): ChunkPos { return ChunkPos(readInt(), readInt()) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt index 3a295a5f..cecebdbe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt @@ -9,6 +9,7 @@ import com.google.gson.JsonObject import ru.dbotthepony.kstarbound.Starbound import java.lang.ref.Reference import java.util.* +import java.util.concurrent.CompletableFuture import java.util.function.Consumer import java.util.stream.Stream @@ -107,3 +108,18 @@ inline fun MutableIterable>.forEachValid(block: (T) -> Unit) { } } } + +fun composeFutures(source: Iterable, mapper: (S) -> CompletableFuture>): CompletableFuture> { + val itr = source.iterator() + + if (itr.hasNext()) { + var future = mapper.invoke(itr.next()) + + for (v in itr) + future = future.thenCompose { if (it.isPresent) CompletableFuture.completedFuture(it) else mapper.invoke(v) } + + return future + } + + return CompletableFuture.completedFuture(KOptional.empty()) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index a5a88b69..e5ea3389 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -41,6 +41,17 @@ abstract class Chunk, This : Chunk) { + val ours = cells.value + source.checkSizeEquals(ours) + + for (x in 0 until CHUNK_SIZE) { + for (y in 0 until CHUNK_SIZE) { + ours[x, y] = source[x, y].immutable() + } + } + } + protected val cells = lazy { Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.NULL) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt index 23a9ac28..fd93e886 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt @@ -28,11 +28,16 @@ private fun circulate(value: Int, bounds: Int): Int { */ data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable { constructor(pos: IStruct2i) : this(pos.component1(), pos.component2()) + constructor(pos: Long) : this(pos.toInt(), (pos ushr 32).toInt()) val tileX = x shl CHUNK_SIZE_BITS val tileY = y shl CHUNK_SIZE_BITS val tile = Vector2i(tileX, tileY) + operator fun plus(other: ChunkPos): ChunkPos { + return ChunkPos(x + other.x, y + other.y) + } + fun tile(x: Int, y: Int): Vector2i { return Vector2i(tileX + x, tileY + y) } @@ -109,6 +114,8 @@ data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable { } companion object { + val ZERO = ChunkPos(0, 0) + fun toLong(x: Int, y: Int): Long { return x.toLong() or (y.toLong() shl 32) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index ed70fe2a..ec4af3bd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -65,8 +65,10 @@ abstract class World, ChunkType : Chunk