Functional chunk tickets, chunk source, player chunk tracking
This commit is contained in:
parent
3561bf7086
commit
a028694010
@ -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) {
|
||||
|
@ -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 }
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>)
|
||||
}
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
@ -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()))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user