More networking workings

This commit is contained in:
DBotThePony 2024-02-04 14:26:34 +07:00
parent 3e2872ea5e
commit 666d746936
Signed by: DBot
GPG Key ID: DCC23B5715498507
15 changed files with 219 additions and 221 deletions

View File

@ -42,7 +42,7 @@ fun main() {
val client = StarboundClient.create().get()
val world = ServerWorld(server, 0L, WorldGeometry(Vector2i(3000, 2000), true, false))
world.addChunkSource(LegacyChunkSource(db))
world.startThread()
world.thread.start()
//Starbound.addFilePath(File("./unpacked_assets/"))
Starbound.addPakPath(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak"))

View File

@ -64,7 +64,7 @@ object Starbound : ISBFileLocator {
const val ENGINE_VERSION = "0.0.1"
const val PROTOCOL_VERSION = 1
const val TICK_TIME_ADVANCE = 1.0 / 60.0
const val TICK_TIME_ADVANCE_NANOS = 16_666_666L
const val TICK_TIME_ADVANCE_NANOS = (TICK_TIME_ADVANCE * 1_000_000_000L).toLong()
// compile flags. uuuugh
const val DEDUP_CELL_STATES = true

View File

@ -64,6 +64,7 @@ import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.image.Image
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.util.ExecutionSpinner
import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.LightCalculator
@ -150,8 +151,6 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
var fullbright = true
var preciseWait = false
var shouldTerminate = false
private set
@ -682,43 +681,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA
}
// nanoseconds
var frameRenderTime = 0L
private set
private var nextRender = System.nanoTime()
private val frameRenderTimes = LongArray(60) { 1L }
private var frameRenderIndex = 0
private val renderWaitTimes = LongArray(60) { 1L }
private var renderWaitIndex = 0
private var lastRender = System.nanoTime()
val averageRenderWait: Double get() {
var sum = 0.0
for (value in renderWaitTimes)
sum += value
if (sum == 0.0)
return 0.0
sum /= 1_000_000_000.0
return sum / renderWaitTimes.size
}
val averageRenderTime: Double get() {
var sum = 0.0
for (value in frameRenderTimes)
sum += value
if (sum == 0.0)
return 0.0
sum /= 1_000_000_000.0
return sum / frameRenderTimes.size
}
val spinner = ExecutionSpinner(mailbox, ::renderFrame, Starbound.TICK_TIME_ADVANCE_NANOS, true)
val settings = ClientSettings()
val viewportCells: ICellAccess = object : ICellAccess {
@ -790,8 +753,8 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
private fun drawPerformanceBasic(onlyMemory: Boolean) {
val runtime = Runtime.getRuntime()
if (!onlyMemory) font.render("Latency: ${(averageRenderWait * 1_00000.0).toInt() / 100f}ms", scale = 0.4f)
if (!onlyMemory) font.render("Frame: ${(averageRenderTime * 1_00000.0).toInt() / 100f}ms", y = font.lineHeight * 0.6f, scale = 0.4f)
if (!onlyMemory) font.render("Latency: ${(spinner.averageRenderWait * 1_00000.0).toInt() / 100f}ms", scale = 0.4f)
if (!onlyMemory) font.render("Frame: ${(spinner.averageRenderTime * 1_00000.0).toInt() / 100f}ms", y = font.lineHeight * 0.6f, scale = 0.4f)
font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = font.lineHeight * 1.2f, scale = 0.4f)
if (!onlyMemory) font.render("OGL C: $openglObjectsCreated D: $openglObjectsCleaned A: ${openglObjectsCreated - openglObjectsCleaned}", y = font.lineHeight * 1.8f, scale = 0.4f)
}
@ -937,112 +900,84 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
}
private fun renderFrame(): Boolean {
ensureSameThread()
var diff = nextRender - System.nanoTime()
// try to sleep until next frame as precise as possible
while (diff > 0L) {
executeQueuedTasks()
diff = nextRender - System.nanoTime()
if (preciseWait) {
if (diff >= 1_500_000L) {
LockSupport.parkNanos(1_000_000L)
} else {
Thread.yield()
}
} else {
LockSupport.parkNanos(diff)
}
if (GLFW.glfwWindowShouldClose(window)) {
close()
return false
}
val mark = System.nanoTime()
try {
if (GLFW.glfwWindowShouldClose(window)) {
close()
return false
}
val world = world
if (!isRenderingGame) {
executeQueuedTasks()
GLFW.glfwPollEvents()
if (world != null && Starbound.initialized)
world.think()
return true
}
if (!Starbound.initialized || !fontInitialized) {
renderLoadingScreen()
return true
}
layers.clear()
uberShaderPrograms.forValidRefs {
if (it.flags.contains(UberShader.Flag.NEEDS_SCREEN_SIZE)) {
it.screenSize = Vector2f(viewportWidth.toFloat(), viewportHeight.toFloat())
}
}
clearColor = RGBAColor.SLATE_GRAY
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
if (world != null) {
renderWorld(world)
}
layers.render()
val activeConnection = activeConnection
if (activeConnection != null) {
activeConnection.send(TrackedPositionPacket(camera.pos))
}
uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }
fontShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }
stack.clear(Matrix3f.identity())
for (fn in onDrawGUI) {
fn.invoke()
}
if (world != null) {
font.render("Camera: ${camera.pos} ${settings.zoom}", y = 140f, scale = 0.25f)
font.render("Cursor: $mouseCoordinates -> ${screenToWorld(mouseCoordinates)}", y = 160f, scale = 0.25f)
font.render("World chunk: ${world.chunkFromCell(camera.pos)}", y = 180f, scale = 0.25f)
}
drawPerformanceBasic(false)
GLFW.glfwSwapBuffers(window)
GLFW.glfwPollEvents()
input.think()
camera.think(Starbound.TICK_TIME_ADVANCE)
val world = world
if (!isRenderingGame) {
executeQueuedTasks()
GLFW.glfwPollEvents()
if (world != null && Starbound.initialized)
world.think()
return true
} finally {
frameRenderTime = System.nanoTime() - mark
frameRenderTimes[++frameRenderIndex % frameRenderTimes.size] = frameRenderTime
renderWaitTimes[++renderWaitIndex % renderWaitTimes.size] = System.nanoTime() - lastRender
lastRender = System.nanoTime()
nextRender = mark + Starbound.TICK_TIME_ADVANCE_NANOS
}
if (!Starbound.initialized || !fontInitialized) {
executeQueuedTasks()
renderLoadingScreen()
return true
}
input.think()
camera.think(Starbound.TICK_TIME_ADVANCE)
executeQueuedTasks()
layers.clear()
uberShaderPrograms.forValidRefs {
if (it.flags.contains(UberShader.Flag.NEEDS_SCREEN_SIZE)) {
it.screenSize = Vector2f(viewportWidth.toFloat(), viewportHeight.toFloat())
}
}
clearColor = RGBAColor.SLATE_GRAY
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
if (world != null) {
renderWorld(world)
}
layers.render()
val activeConnection = activeConnection
if (activeConnection != null) {
activeConnection.send(TrackedPositionPacket(camera.pos))
}
uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }
fontShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }
stack.clear(Matrix3f.identity())
for (fn in onDrawGUI) {
fn.invoke()
}
if (world != null) {
font.render("Camera: ${camera.pos} ${settings.zoom}", y = 140f, scale = 0.25f)
font.render("Cursor: $mouseCoordinates -> ${screenToWorld(mouseCoordinates)}", y = 160f, scale = 0.25f)
font.render("World chunk: ${world.chunkFromCell(camera.pos)}", y = 180f, scale = 0.25f)
}
drawPerformanceBasic(false)
GLFW.glfwSwapBuffers(window)
GLFW.glfwPollEvents()
executeQueuedTasks()
return true
}
private fun spin() {
try {
while (!shouldTerminate && renderFrame()) {
while (!shouldTerminate && spinner.spin()) {
val ply = activeConnection?.character
if (ply != null) {
@ -1056,6 +991,8 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
(if (input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / settings.zoom else 0.0) + (if (input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / settings.zoom else 0.0),
(if (input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / settings.zoom else 0.0) + (if (input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / settings.zoom else 0.0)
)
camera.pos = world?.geometry?.wrap(camera.pos) ?: camera.pos
}
if (input.KEY_ESCAPE_PRESSED) {

View File

@ -17,7 +17,6 @@ import ru.dbotthepony.kstarbound.network.packets.HelloListener
import ru.dbotthepony.kstarbound.network.packets.HelloPacket
import java.net.SocketAddress
import java.util.*
import kotlin.properties.Delegates
// client -> server
class ClientConnection(val client: StarboundClient, type: ConnectionType, uuid: UUID) : Connection(ConnectionSide.CLIENT, type, uuid) {

View File

@ -14,7 +14,7 @@ import ru.dbotthepony.kstarbound.world.api.MutableCell
import java.io.DataInputStream
import java.io.DataOutputStream
class InitialChunkDataPacket(val pos: ChunkPos, val data: List<ImmutableCell>) : IClientPacket {
class ChunkCellsPacket(val pos: ChunkPos, val data: List<ImmutableCell>) : IClientPacket {
constructor(stream: DataInputStream) : this(stream.readChunkPos(), stream.readCollection { MutableCell().read(stream).immutable() })
constructor(chunk: Chunk<*, *>) : this(chunk.pos, ArrayList<ImmutableCell>(CHUNK_SIZE * CHUNK_SIZE).also {
for (x in 0 until CHUNK_SIZE) {
@ -30,12 +30,14 @@ class InitialChunkDataPacket(val pos: ChunkPos, val data: List<ImmutableCell>) :
}
override fun play(connection: ClientConnection) {
val chunk = connection.client.world?.chunkMap?.compute(pos.x, pos.y) ?: return
val itr = data.iterator()
connection.client.mailbox.execute {
val chunk = connection.client.world?.chunkMap?.compute(pos.x, pos.y) ?: return@execute
val itr = data.iterator()
for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) {
chunk.setCell(x, y, itr.next())
for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) {
chunk.setCell(x, y, itr.next())
}
}
}
}

View File

@ -17,9 +17,8 @@ class ForgetChunkPacket(val pos: ChunkPos) : IClientPacket {
}
override fun play(connection: ClientConnection) {
val world = connection.client.world ?: return
world.lock.withLock {
connection.client.mailbox.execute {
val world = connection.client.world ?: return@execute
world.chunkMap.remove(pos)
world.forEachRenderRegion(pos) {

View File

@ -22,6 +22,8 @@ data class JoinWorldPacket(val uuid: UUID, val seed: Long, val geometry: WorldGe
}
override fun play(connection: ClientConnection) {
connection.client.world = ClientWorld(connection.client, seed, geometry)
connection.client.mailbox.execute {
connection.client.world = ClientWorld(connection.client, seed, geometry)
}
}
}

View File

@ -0,0 +1,17 @@
package ru.dbotthepony.kstarbound.client.network.packets
import ru.dbotthepony.kstarbound.client.network.ClientConnection
import ru.dbotthepony.kstarbound.network.IClientPacket
import java.io.DataOutputStream
object LeaveWorldPacket : IClientPacket {
override fun write(stream: DataOutputStream) {
}
override fun play(connection: ClientConnection) {
connection.client.mailbox.execute {
connection.client.world = null
}
}
}

View File

@ -17,11 +17,10 @@ class SpawnWorldObjectPacket(val data: JsonObject) : IClientPacket {
}
override fun play(connection: ClientConnection) {
val world = connection.client.world ?: return
val obj = WorldObject.fromJson(data)
world.mailbox.submit {
val chunk = world.chunkMap[world.geometry.chunkFromCell(obj.pos)] ?: return@submit
connection.client.mailbox.execute {
val world = connection.client.world ?: return@execute
val obj = WorldObject.fromJson(data)
val chunk = world.chunkMap[world.geometry.chunkFromCell(obj.pos)] ?: return@execute
chunk.addObject(obj)
}
}

View File

@ -10,7 +10,7 @@ import io.netty.channel.ChannelPromise
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket
import ru.dbotthepony.kstarbound.client.network.packets.InitialChunkDataPacket
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket
import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket
import ru.dbotthepony.kstarbound.network.packets.DisconnectPacket
@ -127,7 +127,7 @@ object PacketRegistry {
init {
add(::DisconnectPacket)
add(::JoinWorldPacket)
add(::InitialChunkDataPacket)
add(::ChunkCellsPacket)
add(::ForgetChunkPacket)
add(::TrackedPositionPacket)
add(::TrackedSizePacket)

View File

@ -43,12 +43,7 @@ abstract class StarboundServer(val root: File) : Closeable {
fun playerInGame(player: ServerConnection) {
val world = worlds.first()
world.mailbox.execute {
player.world = world
world.players.add(player)
player.send(JoinWorldPacket(world))
}
world.acceptPlayer(player)
}
protected abstract fun close0()

View File

@ -6,7 +6,7 @@ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket
import ru.dbotthepony.kstarbound.client.network.packets.InitialChunkDataPacket
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide
@ -20,10 +20,6 @@ import java.util.*
class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type, UUID(0L, 0L)) {
var world: ServerWorld? = null
set(value) {
field = value
needsToRecomputeTrackedChunks = true
}
var trackedPosition: Vector2d = Vector2d.ZERO
set(value) {
@ -57,6 +53,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
private var needsToRecomputeTrackedChunks = true
fun onLeaveWorld() {
tickets.values.forEach { it.cancel() }
tickets.clear()
sentChunks.clear()
}
private fun recomputeTrackedChunks() {
val world = world ?: return
val trackedPositionChunk = world.geometry.chunkFromCell(trackedPosition)
@ -91,9 +93,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
val world = world
if (world == null) {
tickets.values.forEach { it.cancel() }
tickets.clear()
sentChunks.clear()
onLeaveWorld()
return
}
@ -102,19 +102,19 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
}
for (pos in tickets.keys) {
val chunk = world.chunkMap[pos] ?: continue
if (pos !in sentChunks) {
val chunk = world.chunkMap[pos]
send(ChunkCellsPacket(chunk))
if (chunk != null) {
send(InitialChunkDataPacket(chunk))
chunk.objects.forEach {
send(SpawnWorldObjectPacket(it.serialize()))
}
sentChunks.add(pos)
chunk.objects.forEach {
send(SpawnWorldObjectPacket(it.serialize()))
}
sentChunks.add(pos)
}
}
val itr = sentChunks.iterator()

View File

@ -3,19 +3,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.kommons.collect.chainOptionalFutures
import ru.dbotthepony.kommons.core.KOptional
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket
import ru.dbotthepony.kstarbound.server.StarboundServer
import ru.dbotthepony.kstarbound.server.network.ServerConnection
import ru.dbotthepony.kstarbound.util.ExecutionSpinner
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry
import java.util.Collections
import java.util.concurrent.CompletableFuture
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport
import java.util.function.Consumer
import java.util.function.Supplier
import kotlin.concurrent.withLock
class ServerWorld(
@ -27,10 +31,46 @@ class ServerWorld(
server.worlds.add(this)
}
val players = ObjectArraySet<ServerConnection>()
private val internalPlayers = ArrayList<ServerConnection>()
val players: List<ServerConnection> = Collections.unmodifiableList(internalPlayers)
val thread = Thread(::runThread, "Starbound Server World $seed")
var isStopped: Boolean = false
private fun doAcceptPlayer(player: ServerConnection): Boolean {
if (player !in internalPlayers) {
internalPlayers.add(player)
player.world?.removePlayer(player)
player.world = this
player.send(JoinWorldPacket(this))
return true
}
return false
}
fun acceptPlayer(player: ServerConnection): CompletableFuture<Boolean> {
try {
return CompletableFuture.supplyAsync(Supplier { doAcceptPlayer(player) }, mailbox)
} catch (err: RejectedExecutionException) {
return CompletableFuture.completedFuture(false)
}
}
private fun doRemovePlayer(player: ServerConnection): Boolean {
return internalPlayers.remove(player)
}
fun removePlayer(player: ServerConnection): CompletableFuture<Boolean> {
try {
return CompletableFuture.supplyAsync(Supplier { doRemovePlayer(player) }, mailbox)
} catch (err: RejectedExecutionException) {
return CompletableFuture.completedFuture(false)
}
}
val spinner = ExecutionSpinner(mailbox, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS)
val thread = Thread(spinner, "Starbound Server World $seed")
@Volatile
var isClosed: Boolean = false
private set
init {
@ -44,38 +84,30 @@ class ServerWorld(
chunkProviders.add(source)
}
@Volatile
private var nextThink = 0L
override fun close() {
super.close()
isStopped = true
}
if (!isClosed) {
super.close()
isClosed = true
fun startThread() {
nextThink = System.nanoTime()
thread.start()
}
private fun runThread() {
while (!isStopped) {
var diff = System.nanoTime() - nextThink
while (diff < Starbound.TICK_TIME_ADVANCE_NANOS) {
mailbox.executeQueuedTasks()
diff = System.nanoTime() - nextThink
LockSupport.parkNanos(diff)
diff = System.nanoTime() - nextThink
lock.withLock {
internalPlayers.forEach {
it.world = null
}
}
nextThink = System.nanoTime()
LockSupport.unpark(thread)
}
}
try {
think()
} catch (err: Throwable) {
close()
throw err
}
private fun spin(): Boolean {
if (isClosed) return false
try {
think()
return true
} catch (err: Throwable) {
close()
return false
}
}
@ -84,7 +116,7 @@ class ServerWorld(
override fun thinkInner() {
lock.withLock {
players.forEach { it.tick() }
internalPlayers.forEach { it.tick() }
ticketLists.removeIf {
val valid = it.tick()

View File

@ -5,7 +5,7 @@ import ru.dbotthepony.kstarbound.Starbound
import java.util.concurrent.locks.LockSupport
import java.util.function.BooleanSupplier
class ExecutionSpinner(private val executor: MailboxExecutorService, private val spinner: BooleanSupplier, private val timeBetweenFrames: Long) : Runnable {
class ExecutionSpinner(private val executor: MailboxExecutorService, private val spinner: BooleanSupplier, private val timeBetweenFrames: Long, val precise: Boolean = false) : Runnable {
private var lastRender = System.nanoTime()
private var frameRenderTime = 0L
private val frameRenderTimes = LongArray(60) { 1L }
@ -46,16 +46,23 @@ class ExecutionSpinner(private val executor: MailboxExecutorService, private val
fun spin(): Boolean {
var diff = timeUntilNextFrame()
while (diff > 1_000_000L) {
executor.executeQueuedTasks()
diff = timeUntilNextFrame()
if (diff > 1_000_000L) LockSupport.parkNanos(diff - 400_000L)
}
if (precise) {
while (diff > 1_500_000L) {
executor.executeQueuedTasks()
diff = timeUntilNextFrame()
if (diff > 1_500_000L) LockSupport.parkNanos(diff - 400_000L)
}
while (diff > 0L) {
executor.executeQueuedTasks()
diff = timeUntilNextFrame()
if (diff > 0L) Thread.yield()
while (diff > 0L) {
executor.executeQueuedTasks()
diff = timeUntilNextFrame()
}
} else {
while (diff > 0L) {
executor.executeQueuedTasks()
diff = timeUntilNextFrame()
if (diff > 400_000L) LockSupport.parkNanos(diff)
}
}
val mark = System.nanoTime()

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kommons.core.IStruct2d
import ru.dbotthepony.kommons.core.IStruct2f
import ru.dbotthepony.kommons.core.IStruct2i
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.io.readVec2i
import ru.dbotthepony.kstarbound.io.writeVec2i
@ -21,6 +22,14 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Bool
buff.writeBoolean(loopY)
}
fun wrap(pos: IStruct2i): Vector2i {
return Vector2i(x.cell(pos.component1()), y.cell(pos.component2()))
}
fun wrap(pos: IStruct2d): Vector2d {
return Vector2d(x.cell(pos.component1()), y.cell(pos.component2()))
}
fun chunkFromCell(pos: IStruct2i): ChunkPos {
return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2()))
}