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.io.readVarInt
import ru.dbotthepony.kstarbound.json.VersionedJson import ru.dbotthepony.kstarbound.json.VersionedJson
import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer
import ru.dbotthepony.kstarbound.server.world.LegacyChunkSource
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
@ -49,6 +50,7 @@ fun main() {
val server = IntegratedStarboundServer(File("./")) val server = IntegratedStarboundServer(File("./"))
val client = StarboundClient() val client = StarboundClient()
val world = ServerWorld(server, 0L, WorldGeometry(Vector2i(3000, 2000), true, false)) val world = ServerWorld(server, 0L, WorldGeometry(Vector2i(3000, 2000), true, false))
world.addChunkSource(LegacyChunkSource(db))
world.startThread() world.startThread()
//Starbound.addFilePath(File("./unpacked_assets/")) //Starbound.addFilePath(File("./unpacked_assets/"))
@ -80,10 +82,10 @@ fun main() {
//for (chunkX in 0 .. 17) { //for (chunkX in 0 .. 17) {
// for (chunkY in 21 .. 21) { // for (chunkY in 21 .. 21) {
for (chunkY in 18 .. 24) { for (chunkY in 18 .. 24) {
val data = db.read(byteArrayOf(1, (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())) //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()))) var reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data), Inflater())))
reader.skipBytes(3) reader.skipBytes(3)
@ -96,9 +98,9 @@ fun main() {
} }
} }
} }
} }*/
if (data2 != null) { /*if (data2 != null) {
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data2), Inflater()))) val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data2), Inflater())))
val i = reader.readVarInt() val i = reader.readVarInt()
@ -118,7 +120,7 @@ fun main() {
//val read = BinaryJsonReader.readElement(reader) //val read = BinaryJsonReader.readElement(reader)
//println(read) //println(read)
} }*/
} }
} }
@ -137,7 +139,7 @@ fun main() {
//item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0)) //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) //ent.position += Vector2d(y = 14.0, x = -10.0)
@ -161,17 +163,17 @@ fun main() {
} }
while (client.renderFrame()) { 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) { if (ply != null) {
client.camera.pos = ply!!.position 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.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.controlJump = client.input.KEY_SPACE_DOWN
ply!!.movement.controlRun = !client.input.KEY_LEFT_SHIFT_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) { 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.Cache
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Scheduler 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.apache.logging.log4j.LogManager
import org.lwjgl.BufferUtils import org.lwjgl.BufferUtils
import org.lwjgl.glfw.Callbacks 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.GeometryType
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder
import ru.dbotthepony.kstarbound.client.input.UserInput 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.Camera
import ru.dbotthepony.kstarbound.client.render.Font import ru.dbotthepony.kstarbound.client.render.Font
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer 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.Matrix3f
import ru.dbotthepony.kvector.arrays.Matrix3fStack import ru.dbotthepony.kvector.arrays.Matrix3fStack
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.util2d.AABBi
import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.RGBAColor
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2f import ru.dbotthepony.kvector.vector.Vector2f
@ -71,6 +77,7 @@ import java.io.File
import java.lang.ref.PhantomReference import java.lang.ref.PhantomReference
import java.lang.ref.ReferenceQueue import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.net.SocketAddress
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.time.Duration import java.time.Duration
@ -160,11 +167,26 @@ class StarboundClient : Closeable {
var isRenderingGame = true var isRenderingGame = true
private set 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 scissorStack = LinkedList<ScissorRect>()
private val onDrawGUI = ArrayList<() -> Unit>() private val onDrawGUI = ArrayList<() -> Unit>()
private val onPreDrawWorld = ArrayList<(LayeredRenderer) -> Unit>()
private val onPostDrawWorld = ArrayList<() -> Unit>()
private val onPostDrawWorldOnce = ArrayList<(LayeredRenderer) -> Unit>()
private val onViewportChanged = ArrayList<(width: Int, height: Int) -> Unit>() private val onViewportChanged = ArrayList<(width: Int, height: Int) -> Unit>()
private val terminateCallbacks = ArrayList<() -> 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) { fun onDrawGUI(lambda: () -> Unit) {
onDrawGUI.add(lambda) 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 val layers = LayeredRenderer(this)
private var dotsIndex = 0 private var dotsIndex = 0
private val dotTime = 7 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) 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 { fun renderFrame(): Boolean {
ensureSameThread() ensureSameThread()
@ -833,81 +979,7 @@ class StarboundClient : Closeable {
} }
if (!Starbound.initialized || !fontInitialized) { if (!Starbound.initialized || !fontInitialized) {
executeQueuedTasks() renderLoadingScreen()
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()
return true return true
} }
@ -923,83 +995,16 @@ class StarboundClient : Closeable {
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
if (world != null) { if (world != null) {
updateViewportParams() renderWorld(world)
}
if (Starbound.initialized) layers.render()
world.think()
stack.clear(Matrix3f.identity()) val activeConnection = activeConnection
val viewMatrix = viewportMatrixWorld.copy() if (activeConnection != null) {
.translate(viewportWidth / 2f, viewportHeight / 2f) // центр экрана + координаты отрисовки мира activeConnection.send(TrackedPositionPacket(camera.pos))
.scale(x = settings.zoom * PIXELS_IN_STARBOUND_UNITf, y = settings.zoom * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера activeConnection.send(TrackedSizePacket(12, 12))
.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()
}
} }
uberShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen } uberShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen }

View File

@ -41,12 +41,12 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType, uuid:
try { try {
msg.play(this) msg.play(this)
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.error("Failed to read serverbound packet $msg", err) LOGGER.error("Failed to read incoming packet $msg", err)
disconnect(err.toString()) disconnect(err.toString())
} }
} else { } else {
LOGGER.error("Unknown serverbound packet type $msg") LOGGER.error("Unknown incoming packet type $msg")
disconnect("Unknown serverbound 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) super.foregroundChanges(x, y, cell)
world.forEachRenderRegion(pos.tile(x, y)) { 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) super.backgroundChanges(x, y, cell)
world.forEachRenderRegion(pos.tile(x, y)) { 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 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.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.longs.LongArraySet import it.unimi.dsi.fastutil.longs.LongArraySet
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ReferenceArraySet import it.unimi.dsi.fastutil.objects.ReferenceArraySet
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf 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.RGBAColor
import ru.dbotthepony.kvector.vector.Vector2f import ru.dbotthepony.kvector.vector.Vector2f
import ru.dbotthepony.kvector.vector.Vector2i 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.Future
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
class ClientWorld( class ClientWorld(
@ -38,7 +40,7 @@ class ClientWorld(
geometry: WorldGeometry, geometry: WorldGeometry,
) : World<ClientWorld, ClientChunk>(seed, geometry) { ) : World<ClientWorld, ClientChunk>(seed, geometry) {
private fun determineChunkSize(cells: Int): Int { private fun determineChunkSize(cells: Int): Int {
for (i in 32 downTo 1) { for (i in 64 downTo 1) {
if (cells % i == 0) { if (cells % i == 0) {
return i return i
} }
@ -67,35 +69,28 @@ class ClientWorld(
inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) { inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) {
val bakedMeshes = ArrayList<Pair<ConfiguredMesh<*>, RenderLayer.Point>>() val bakedMeshes = ArrayList<Pair<ConfiguredMesh<*>, RenderLayer.Point>>()
private var currentBakeTask: Future<LayeredRenderer>? = null private var currentBakeTask: Future<LayeredRenderer>? = null
var isDirty = true private var bakeTaskID = 0
private var isDirty = true
fun markDirty() {
isDirty = true
}
fun bake() { fun bake() {
if (!isDirty) { if (!isDirty) return
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
}
isDirty = false isDirty = false
currentBakeTask = client.executor.submit(Callable { val bakeTaskID = ++bakeTaskID
CompletableFuture.supplyAsync(Supplier {
val meshes = LayeredRenderer(client) val meshes = LayeredRenderer(client)
for (x in 0 until renderRegionWidth) { for (x in 0 until renderRegionWidth) {
for (y in 0 until renderRegionHeight) { for (y in 0 until renderRegionHeight) {
if (!inBounds(x, y)) continue 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 val material = tile.material
if (!material.value.isMeta) { if (!material.value.isMeta) {
@ -115,10 +110,21 @@ class ClientWorld(
} }
meshes 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>>() private val liquidMesh = ArrayList<Pair<Mesh, RGBAColor>>()
var liquidIsDirty = true var liquidIsDirty = true
@ -127,7 +133,22 @@ class ClientWorld(
val background = Layer(TileView.Background(view), true) val background = Layer(TileView.Background(view), true)
val foreground = Layer(TileView.Foreground(view), false) 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) { fun addLayers(layers: LayeredRenderer, renderOrigin: Vector2f) {
renderCalls++
background.bake() background.bake()
foreground.bake() foreground.bake()
@ -215,9 +236,14 @@ class ClientWorld(
ix /= renderRegionWidth ix /= renderRegionWidth
iy /= renderRegionHeight iy /= renderRegionHeight
for (x in ix .. ix + CHUNK_SIZE / renderRegionWidth) { val paddingX = (CHUNK_SIZE / renderRegionWidth).coerceAtLeast(1)
for (y in iy .. iy + CHUNK_SIZE / renderRegionWidth) { val paddingY = (CHUNK_SIZE / renderRegionHeight).coerceAtLeast(1)
renderRegions[renderRegionKey(x, y)]?.let(action)
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.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.full.isSuperclassOf import kotlin.reflect.full.isSubclassOf
fun ByteBuf.writeUTF(value: String) { fun ByteBuf.writeUTF(value: String) {
writeBytes(value.toByteArray().also { check(!it.any { it.toInt() == 0 }) { "Provided UTF string contains NUL" } }) writeBytes(value.toByteArray().also { check(!it.any { it.toInt() == 0 }) { "Provided UTF string contains NUL" } })
@ -40,30 +40,32 @@ enum class ConnectionState {
CLOSED; CLOSED;
} }
enum class PacketDirection(val allowedOnClient: Boolean, val allowedOnServer: Boolean) { enum class PacketDirection(val acceptOnClient: Boolean, val acceptOnServer: Boolean) {
SERVER_TO_CLIENT(true, false), FROM_SERVER(true, false),
CLIENT_TO_SERVER(false, true), FROM_CLIENT(false, true),
BI_DIRECTIONAL(true, true); BI_DIRECTIONAL(true, true);
fun acceptedOn(side: ConnectionSide): Boolean { fun acceptedOn(side: ConnectionSide): Boolean {
if (side == ConnectionSide.SERVER) if (side == ConnectionSide.SERVER)
return allowedOnServer return acceptOnServer
return allowedOnClient return acceptOnClient
} }
companion object { companion object {
fun get(type: KClass<out IPacket>): PacketDirection { 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 { fun of(allowedOnClient: Boolean, allowedOnServer: Boolean): PacketDirection {
if (allowedOnServer && allowedOnClient) if (allowedOnServer && allowedOnClient)
return BI_DIRECTIONAL return BI_DIRECTIONAL
else if (allowedOnServer) else if (allowedOnServer)
return SERVER_TO_CLIENT return FROM_CLIENT
else if (allowedOnClient)
return FROM_SERVER
else 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() { fun initializeHandlers() {
val channel = channel ?: throw IllegalStateException("No network channel is bound") 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) channel.pipeline().addLast(this)
if (type == ConnectionType.NETWORK) { 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(DatagramEncoder)
channel.pipeline().addFirst(DatagramDecoder()) channel.pipeline().addFirst(DatagramDecoder())
} else {
channel.pipeline().addLast(PacketMapping.OutboundValidator(side))
} }
inGame() 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() { inner class Outbound(val side: ConnectionSide) : ChannelOutboundHandlerAdapter() {
override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) { override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) {
val type = clazz2Type[msg::class] 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 { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
} }

View File

@ -1,11 +1,17 @@
package ru.dbotthepony.kstarbound.network.packets 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.InitialChunkDataPacket
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket 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 import ru.dbotthepony.kstarbound.network.PacketMapper
val PacketMapping = PacketMapper().also { val PacketMapping = PacketMapper().also {
it.add(::DisconnectPacket) it.add(::DisconnectPacket)
it.add(::JoinWorldPacket) it.add(::JoinWorldPacket)
it.add(::InitialChunkDataPacket) 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) { fun playerInGame(player: ServerPlayer) {
val world = worlds.first() val world = worlds.first()
player.world = world player.world = world
world.players.add(player)
player.connection.send(JoinWorldPacket(world)) 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() protected abstract fun close0()

View File

@ -1,9 +1,121 @@
package ru.dbotthepony.kstarbound.server.network 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.network.Player
import ru.dbotthepony.kstarbound.server.StarboundServer
import ru.dbotthepony.kstarbound.server.world.ServerWorld 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) { 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 package ru.dbotthepony.kstarbound.server.world
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos 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) { 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 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.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.server.StarboundServer 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.ChunkPos
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry 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.io.Closeable
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import java.util.function.Consumer
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
class ServerWorld( class ServerWorld(
@ -21,6 +29,8 @@ class ServerWorld(
server.worlds.add(this) server.worlds.add(this)
} }
val players = ObjectArraySet<ServerPlayer>()
val thread = Thread(::runThread, "Starbound Server World $seed") val thread = Thread(::runThread, "Starbound Server World $seed")
var isStopped: Boolean = false var isStopped: Boolean = false
private set private set
@ -29,6 +39,13 @@ class ServerWorld(
thread.isDaemon = true thread.isDaemon = true
} }
private val chunkProviders = ArrayList<IChunkSource>()
var saver: IChunkSaver? = null
fun addChunkSource(source: IChunkSource) {
chunkProviders.add(source)
}
@Volatile @Volatile
private var nextThink = 0L private var nextThink = 0L
@ -75,7 +92,27 @@ class ServerWorld(
get() = false get() = false
override fun thinkInner() { 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 { override fun chunkFactory(pos: ChunkPos): ServerChunk {
@ -85,6 +122,24 @@ class ServerWorld(
private val ticketMap = Long2ObjectOpenHashMap<TicketList>() private val ticketMap = Long2ObjectOpenHashMap<TicketList>()
private val ticketLists = ArrayList<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 { interface ITicket {
fun cancel() fun cancel()
val isCanceled: Boolean val isCanceled: Boolean
@ -104,18 +159,17 @@ class ServerWorld(
} }
private inner class TicketList(val pos: ChunkPos) { private inner class TicketList(val pos: ChunkPos) {
init { constructor(pos: Long) : this(ChunkPos(pos))
lock.withLock {
check(ticketMap.put(pos.toLong(), this) == null) { "Already had ticket list at $pos" }
ticketLists.add(this)
}
}
private var first = true
private val permanent = ArrayList<Ticket>() private val permanent = ArrayList<Ticket>()
private val temporary = ObjectAVLTreeSet<TimedTicket>() private val temporary = ObjectAVLTreeSet<TimedTicket>()
private var ticks = 0 private var ticks = 0
private var nextTicketID = AtomicInteger() private var nextTicketID = AtomicInteger()
val isValid: Boolean
get() = temporary.isNotEmpty() || permanent.isNotEmpty()
fun tick(): Boolean { fun tick(): Boolean {
ticks++ ticks++
@ -129,6 +183,34 @@ class ServerWorld(
} }
open inner class Ticket : ITicket { 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 id: Int = nextTicketID.getAndIncrement()
final override val pos: ChunkPos final override val pos: ChunkPos
get() = this@TicketList.pos get() = this@TicketList.pos
@ -150,10 +232,17 @@ class ServerWorld(
final override var isCanceled: Boolean = false 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 override val timeRemaining: Int
get() = (expiresAt - ticks).coerceAtLeast(0) get() = (expiresAt - ticks).coerceAtLeast(0)
override fun init(): TimedTicket {
super.init()
return this
}
override fun prolong(ticks: Int) { override fun prolong(ticks: Int) {
if (ticks == 0 || isCanceled) return if (ticks == 0 || isCanceled) return

View File

@ -3,7 +3,9 @@ package ru.dbotthepony.kstarbound.util
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.bytes.ByteArrayList
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kvector.api.IStruct2d
import ru.dbotthepony.kvector.api.IStruct2i import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import java.io.DataInput import java.io.DataInput
import java.io.DataOutput import java.io.DataOutput
@ -80,10 +82,19 @@ fun OutputStream.writeVec2i(value: IStruct2i) {
writeInt(value.component2()) writeInt(value.component2())
} }
fun OutputStream.writeVec2d(value: IStruct2d) {
writeDouble(value.component1())
writeDouble(value.component2())
}
fun InputStream.readVec2i(): Vector2i { fun InputStream.readVec2i(): Vector2i {
return Vector2i(readInt(), readInt()) return Vector2i(readInt(), readInt())
} }
fun InputStream.readVec2d(): Vector2d {
return Vector2d(readDouble(), readDouble())
}
fun InputStream.readChunkPos(): ChunkPos { fun InputStream.readChunkPos(): ChunkPos {
return ChunkPos(readInt(), readInt()) return ChunkPos(readInt(), readInt())
} }

View File

@ -9,6 +9,7 @@ import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import java.lang.ref.Reference import java.lang.ref.Reference
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.function.Consumer import java.util.function.Consumer
import java.util.stream.Stream 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 var backgroundChangeset = 0
private set 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 { protected val cells = lazy {
Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.NULL) 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> { data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable<ChunkPos> {
constructor(pos: IStruct2i) : this(pos.component1(), pos.component2()) 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 tileX = x shl CHUNK_SIZE_BITS
val tileY = y shl CHUNK_SIZE_BITS val tileY = y shl CHUNK_SIZE_BITS
val tile = Vector2i(tileX, tileY) 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 { fun tile(x: Int, y: Int): Vector2i {
return Vector2i(tileX + x, tileY + y) return Vector2i(tileX + x, tileY + y)
} }
@ -109,6 +114,8 @@ data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable<ChunkPos> {
} }
companion object { companion object {
val ZERO = ChunkPos(0, 0)
fun toLong(x: Int, y: Int): Long { fun toLong(x: Int, y: Int): Long {
return x.toLong() or (y.toLong() shl 32) 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 inner class ChunkMap {
abstract operator fun get(x: Int, y: Int): ChunkType? abstract operator fun get(x: Int, y: Int): ChunkType?
abstract fun compute(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) 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 getCell(x: Int, y: Int): AbstractCell
abstract fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean 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.readVec2i
import ru.dbotthepony.kstarbound.util.writeVec2i 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 ru.dbotthepony.kvector.vector.Vector2i
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream 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(loopX)
buff.writeBoolean(loopY) 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)
}
} }