Functional chunk tickets, chunk source, player chunk tracking

This commit is contained in:
DBotThePony 2024-02-01 15:58:44 +07:00
parent 3561bf7086
commit a028694010
Signed by: DBot
GPG Key ID: DCC23B5715498507
24 changed files with 747 additions and 240 deletions

View File

@ -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) {

View File

@ -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<ScissorRect>()
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 }

View File

@ -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")
}
}

View File

@ -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()
}
}
}
}

View File

@ -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
}
}

View File

@ -9,7 +9,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
super.foregroundChanges(x, y, cell)
world.forEachRenderRegion(pos.tile(x, y)) {
it.foreground.isDirty = true
it.foreground.markDirty()
}
}
@ -17,7 +17,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
super.backgroundChanges(x, y, cell)
world.forEachRenderRegion(pos.tile(x, y)) {
it.background.isDirty = true
it.background.markDirty()
}
}

View File

@ -1,9 +1,9 @@
package ru.dbotthepony.kstarbound.client.world
import com.google.common.base.Supplier
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.longs.LongArraySet
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ReferenceArraySet
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
@ -28,8 +28,10 @@ import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.RGBAColor
import ru.dbotthepony.kvector.vector.Vector2f
import ru.dbotthepony.kvector.vector.Vector2i
import java.util.concurrent.Callable
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import kotlin.concurrent.withLock
class ClientWorld(
@ -38,7 +40,7 @@ class ClientWorld(
geometry: WorldGeometry,
) : World<ClientWorld, ClientChunk>(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<Pair<ConfiguredMesh<*>, RenderLayer.Point>>()
private var currentBakeTask: Future<LayeredRenderer>? = 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<Pair<Mesh, RGBAColor>>()
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)
}
}
}
}

View File

@ -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<out IPacket>): 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")
}
}
}

View File

@ -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()

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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<ServerConnection>(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<ChunkPos, ServerWorld.ITicket>()
private val sentChunks = ObjectOpenHashSet<ChunkPos>()
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<ChunkPos>()
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()
}
}
}
}

View File

@ -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<out AbstractCell>)
fun saveObjects(pos: ChunkPos, data: Collection<WorldObject>)
}

View File

@ -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<KOptional<Object2DArray<out AbstractCell>>>
fun getObjects(pos: ChunkPos): CompletableFuture<KOptional<Collection<WorldObject>>>
object Void : IChunkSource {
override fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> {
return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY)))
}
override fun getObjects(pos: ChunkPos): CompletableFuture<KOptional<Collection<WorldObject>>> {
return CompletableFuture.completedFuture(KOptional.of(emptyList()))
}
}
}

View File

@ -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<KOptional<Object2DArray<out AbstractCell>>> {
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<MutableCell>(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<out AbstractCell>))
}
override fun getObjects(pos: ChunkPos): CompletableFuture<KOptional<Collection<WorldObject>>> {
return CompletableFuture.completedFuture(KOptional.of(listOf()))
}
}

View File

@ -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<ServerWorld, ServerChunk>(world, pos) {
fun copyCells(): Object2DArray<ImmutableCell> {
if (cells.isInitialized()) {
return Object2DArray(cells.value)
} else {
return Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY)
}
}
}

View File

@ -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<ServerPlayer>()
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<IChunkSource>()
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<TicketList>()
private val ticketLists = ArrayList<TicketList>()
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<Ticket>()
private val temporary = ObjectAVLTreeSet<TimedTicket>()
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<KOptional<Object2DArray<out AbstractCell>>> {
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

View File

@ -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())
}

View File

@ -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 <T> MutableIterable<Reference<T>>.forEachValid(block: (T) -> Unit) {
}
}
}
fun <S, T> composeFutures(source: Iterable<S>, mapper: (S) -> CompletableFuture<KOptional<T>>): CompletableFuture<KOptional<T>> {
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())
}

View File

@ -41,6 +41,17 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
var backgroundChangeset = 0
private set
fun loadCells(source: Object2DArray<out AbstractCell>) {
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)
}

View File

@ -28,11 +28,16 @@ private fun circulate(value: Int, bounds: Int): Int {
*/
data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable<ChunkPos> {
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<ChunkPos> {
}
companion object {
val ZERO = ChunkPos(0, 0)
fun toLong(x: Int, y: Int): Long {
return x.toLong() or (y.toLong() shl 32)
}

View File

@ -65,8 +65,10 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
abstract inner class ChunkMap {
abstract operator fun get(x: Int, y: Int): ChunkType?
abstract fun compute(x: Int, y: Int): ChunkType?
fun compute(pos: ChunkPos) = compute(pos.x, pos.y)
abstract fun remove(x: Int, y: Int)
fun remove(pos: ChunkPos) = remove(pos.x, pos.y)
abstract fun getCell(x: Int, y: Int): AbstractCell
abstract fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean

View File

@ -2,6 +2,9 @@ package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.util.readVec2i
import ru.dbotthepony.kstarbound.util.writeVec2i
import ru.dbotthepony.kvector.api.IStruct2d
import ru.dbotthepony.kvector.api.IStruct2f
import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kvector.vector.Vector2i
import java.io.DataInputStream
import java.io.DataOutputStream
@ -17,4 +20,30 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Bool
buff.writeBoolean(loopX)
buff.writeBoolean(loopY)
}
fun chunkFromCell(pos: IStruct2i): ChunkPos {
return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2()))
}
fun chunkFromCell(pos: IStruct2f): ChunkPos {
return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2()))
}
fun chunkFromCell(pos: IStruct2d): ChunkPos {
return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2()))
}
fun wrap(pos: ChunkPos): ChunkPos {
val x = this.x.chunk(pos.x)
val y = this.y.chunk(pos.y)
if (x == pos.x && y == pos.y) return pos
return ChunkPos(x, y)
}
fun wrapToLong(pos: ChunkPos): Long {
val x = this.x.chunk(pos.x)
val y = this.y.chunk(pos.y)
if (x == pos.x && y == pos.y) return pos.toLong()
return ChunkPos.toLong(x, y)
}
}