Server and network test code

This commit is contained in:
DBotThePony 2024-01-28 16:13:07 +07:00
parent f58b0bca80
commit b7ec73bf0f
Signed by: DBot
GPG Key ID: DCC23B5715498507
37 changed files with 1542 additions and 171 deletions

View File

@ -86,6 +86,8 @@ dependencies {
implementation("com.github.ben-manes.caffeine:caffeine:3.1.5")
implementation("org.classdump.luna:luna-all-shaded:0.4.1")
implementation("io.netty:netty-transport:4.1.105.Final")
}
jmh {

View File

@ -5,19 +5,21 @@ import org.apache.logging.log4j.LogManager
import org.lwjgl.Version
import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.client.network.ClientConnection
import ru.dbotthepony.kstarbound.io.BTreeDB
import ru.dbotthepony.kstarbound.player.Avatar
import ru.dbotthepony.kstarbound.world.entities.ItemEntity
import ru.dbotthepony.kstarbound.json.VersionedJson
import ru.dbotthepony.kstarbound.io.readVarInt
import ru.dbotthepony.kstarbound.json.BinaryJsonReader
import ru.dbotthepony.kstarbound.json.VersionedJson
import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.MutableCell
import ru.dbotthepony.kstarbound.world.entities.ItemEntity
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject
import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2i
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.DataInputStream
@ -30,14 +32,6 @@ import java.util.zip.InflaterInputStream
private val LOGGER = LogManager.getLogger()
fun main() {
/*if (true) {
val state = NewLuaState()
provideRootBindings(state)
state.load("print(root.itemType('haha'))")
return
}*/
LOGGER.info("Running LWJGL ${Version.getVersion()}")
//Thread.sleep(6_000L)
@ -50,9 +44,12 @@ fun main() {
println(meta.readInt())
println(meta.readInt())
println(VersionedJson(meta))
// println(VersionedJson(meta))
val server = IntegratedStarboundServer(File("./"))
val client = StarboundClient()
val world = ServerWorld(server, 0L, WorldGeometry(Vector2i(3000, 2000), true, false))
world.startThread()
//Starbound.addFilePath(File("./unpacked_assets/"))
Starbound.addPakPath(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak"))
@ -72,10 +69,10 @@ fun main() {
var ply: PlayerEntity? = null
Starbound.mailboxInitialized.submit {
ply = PlayerEntity(client.world!!)
//ply = PlayerEntity(client.world!!)
ply!!.position = Vector2d(225.0, 680.0)
ply!!.spawn()
//ply!!.position = Vector2d(225.0, 680.0)
//ply!!.spawn()
//for (chunkX in 17 .. 18) {
//for (chunkX in 14 .. 24) {
@ -90,7 +87,7 @@ fun main() {
var reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data), Inflater())))
reader.skipBytes(3)
val chunk = client.world!!.chunkMap.compute(chunkX, chunkY)
val chunk = world.chunkMap.compute(chunkX, chunkY)
if (chunk != null) {
for (y in 0 .. 31) {
@ -110,7 +107,7 @@ fun main() {
if (obj.identifier == "ObjectEntity") {
try {
WorldObject(client.world!!, obj.content.asJsonObject).spawn()
WorldObject(world, obj.content.asJsonObject).spawn()
//println(obj.content)
//println(created)
} catch (err: Throwable) {
@ -130,7 +127,7 @@ fun main() {
val rand = Random()
for (i in 0 until 0) {
val item = ItemEntity(client.world!!, Registries.items.keys.values.random().value)
val item = ItemEntity(world, Registries.items.keys.values.random().value)
item.position = Vector2d(225.0 - i, 785.0)
item.spawn()
@ -140,28 +137,7 @@ fun main() {
//item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0))
}
// println(Starbound.statusEffects["firecharge"])
AssetPathStack.push("/animations/dust4")
val def = Starbound.gson.fromJson(Starbound.locate("/animations/dust4/dust4.animation").reader(), AnimationDefinition::class.java)
AssetPathStack.pop()
val avatar = Avatar(UUID.randomUUID())
/*
val quest = QuestInstance(avatar, descriptor = QuestDescriptor("floran_mission1"))
quest.init()
quest.start()
var last = 0
client.onPostDrawWorld {
if (++last >= 10) {
quest.update(10)
last = 0
}
}*/
println(Registries.treasurePools["motherpoptopTreasure"]!!.value.evaluate(Random(), 2.0))
ClientConnection.connectToLocalServer(client, server.channels.createLocalChannel(), UUID(0L, 0L))
}
//ent.position += Vector2d(y = 14.0, x = -10.0)
@ -171,7 +147,7 @@ fun main() {
client.onDrawGUI {
client.font.render("Camera: ${client.camera.pos} ${client.settings.zoom}", y = 140f, scale = 0.25f)
client.font.render("Cursor: ${client.mouseCoordinates} -> ${client.screenToWorld(client.mouseCoordinates)}", y = 160f, scale = 0.25f)
client.font.render("World chunk: ${client.world!!.chunkFromCell(client.camera.pos)}", y = 180f, scale = 0.25f)
client.font.render("World chunk: ${client.world?.chunkFromCell(client.camera.pos)}", y = 180f, scale = 0.25f)
}
//ent.spawn()

View File

@ -78,7 +78,9 @@ import kotlin.collections.ArrayList
import kotlin.random.Random
object Starbound : ISBFileLocator {
const val TICK_TIME_ADVANCE = 0.01666666666666664
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 DEDUP_CELL_STATES = true

View File

@ -652,7 +652,7 @@ class StarboundClient : Closeable {
}
val tileRenderers = TileRenderers(this)
var world: ClientWorld? = ClientWorld(this, 0L, Vector2i(3000, 2000), true)
var world: ClientWorld? = null
init {
clearColor = RGBAColor.SLATE_GRAY
@ -919,14 +919,15 @@ class StarboundClient : Closeable {
}
}
clearColor = RGBAColor.SLATE_GRAY
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
if (world != null) {
updateViewportParams()
if (Starbound.initialized)
world.think()
clearColor = RGBAColor.SLATE_GRAY
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
stack.clear(Matrix3f.identity())
val viewMatrix = viewportMatrixWorld.copy()

View File

@ -0,0 +1,90 @@
package ru.dbotthepony.kstarbound.client.network
import io.netty.bootstrap.Bootstrap
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInitializer
import io.netty.channel.local.LocalAddress
import io.netty.channel.local.LocalChannel
import io.netty.channel.socket.nio.NioSocketChannel
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.ConnectionType
import ru.dbotthepony.kstarbound.network.IClientPacket
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) {
private fun sendHello() {
helloListener = HelloListener(this, channel!!).sendHello(localUUID)
}
override var player: LocalPlayer by Delegates.notNull()
private set
override fun onHelloReceived(helloPacket: HelloPacket) {
player = LocalPlayer(this)
}
override fun inGame() {
}
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is IClientPacket) {
try {
msg.play(this)
} catch (err: Throwable) {
LOGGER.error("Failed to read serverbound packet $msg", err)
disconnect(err.toString())
}
} else {
LOGGER.error("Unknown serverbound packet type $msg")
disconnect("Unknown serverbound packet type $msg")
}
}
companion object {
private val LOGGER = LogManager.getLogger()
fun connectToLocalServer(client: StarboundClient, address: LocalAddress, uuid: UUID): ClientConnection {
val connection = ClientConnection(client, ConnectionType.MEMORY, uuid)
Bootstrap()
.group(NIO_POOL)
.channel(LocalChannel::class.java)
.handler(object : ChannelInitializer<Channel>() { override fun initChannel(ch: Channel) { connection.bind(ch) } })
.connect(address)
.syncUninterruptibly()
connection.sendHello()
return connection
}
fun connectToLocalServer(client: StarboundClient, address: Channel, uuid: UUID): ClientConnection {
return connectToLocalServer(client, address.localAddress() as LocalAddress, uuid)
}
fun connectToRemoteServer(client: StarboundClient, address: SocketAddress, uuid: UUID): ClientConnection {
val connection = ClientConnection(client, ConnectionType.NETWORK, uuid)
Bootstrap()
.group(NIO_POOL)
.channel(NioSocketChannel::class.java)
.handler(object : ChannelInitializer<Channel>() { override fun initChannel(ch: Channel) { connection.bind(ch) } })
.connect(address)
.syncUninterruptibly()
connection.sendHello()
return connection
}
}
}

View File

@ -0,0 +1,6 @@
package ru.dbotthepony.kstarbound.client.network
import ru.dbotthepony.kstarbound.network.Player
class LocalPlayer(connection: ClientConnection) : Player<ClientConnection>(connection, connection.localUUID) {
}

View File

@ -0,0 +1,42 @@
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.readCollection
import ru.dbotthepony.kstarbound.util.writeCollection
import ru.dbotthepony.kstarbound.util.writeVec2i
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.ImmutableCell
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 {
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) {
for (y in 0 until CHUNK_SIZE) {
it.add(chunk.getCell(x, y).immutable())
}
}
})
override fun write(stream: DataOutputStream) {
stream.writeVec2i(pos)
stream.writeCollection(data) { it.write(stream) }
}
override fun play(connection: ClientConnection) {
val chunk = connection.client.world?.chunkMap?.compute(pos.x, pos.y) ?: return
val itr = data.iterator()
for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) {
chunk.setCell(x, y, itr.next())
}
}
}
}

View File

@ -0,0 +1,31 @@
package ru.dbotthepony.kstarbound.client.network.packets
import io.netty.buffer.ByteBuf
import ru.dbotthepony.kstarbound.client.network.ClientConnection
import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.util.readUUID
import ru.dbotthepony.kstarbound.util.readVec2i
import ru.dbotthepony.kstarbound.util.writeUUID
import ru.dbotthepony.kstarbound.util.writeVec2i
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kvector.vector.Vector2i
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID
data class JoinWorldPacket(val uuid: UUID, val seed: Long, val geometry: WorldGeometry) : IClientPacket {
constructor(buff: DataInputStream) : this(buff.readUUID(), buff.readLong(), WorldGeometry(buff))
constructor(world: World<*, *>) : this(UUID(0L, 0L), world.seed, world.geometry)
override fun write(stream: DataOutputStream) {
stream.writeUUID(uuid)
stream.writeLong(seed)
geometry.write(stream)
}
override fun play(connection: ClientConnection) {
connection.client.world = ClientWorld(connection.client, seed, geometry)
}
}

View File

@ -18,6 +18,7 @@ import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
import ru.dbotthepony.kstarbound.world.api.TileView
@ -34,10 +35,8 @@ import kotlin.concurrent.withLock
class ClientWorld(
val client: StarboundClient,
seed: Long,
size: Vector2i,
loopX: Boolean = false,
loopY: Boolean = false
) : World<ClientWorld, ClientChunk>(seed, size, loopX, loopY) {
geometry: WorldGeometry,
) : World<ClientWorld, ClientChunk>(seed, geometry) {
private fun determineChunkSize(cells: Int): Int {
for (i in 32 downTo 1) {
if (cells % i == 0) {
@ -48,20 +47,20 @@ class ClientWorld(
throw RuntimeException("unreachable code")
}
override val isClient: Boolean
override val isRemote: Boolean
get() = true
val renderRegionWidth = determineChunkSize(size.x)
val renderRegionHeight = determineChunkSize(size.y)
val renderRegionsX = size.x / renderRegionWidth
val renderRegionsY = size.y / renderRegionHeight
val renderRegionWidth = determineChunkSize(geometry.size.x)
val renderRegionHeight = determineChunkSize(geometry.size.y)
val renderRegionsX = geometry.size.x / renderRegionWidth
val renderRegionsY = geometry.size.y / renderRegionHeight
fun isValidRenderRegionX(value: Int): Boolean {
return loopX || value in 0 .. renderRegionsX
return geometry.loopX || value in 0 .. renderRegionsX
}
fun isValidRenderRegionY(value: Int): Boolean {
return loopY || value in 0 .. renderRegionsY
return geometry.loopY || value in 0 .. renderRegionsY
}
inner class RenderRegion(val x: Int, val y: Int) {

View File

@ -0,0 +1,90 @@
package ru.dbotthepony.kstarbound.network
import io.netty.buffer.ByteBuf
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import ru.dbotthepony.kstarbound.client.network.ClientConnection
import ru.dbotthepony.kstarbound.server.network.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
import kotlin.reflect.KClass
import kotlin.reflect.full.isSuperclassOf
fun ByteBuf.writeUTF(value: String) {
writeBytes(value.toByteArray().also { check(!it.any { it.toInt() == 0 }) { "Provided UTF string contains NUL" } })
writeByte(0)
}
fun ByteBuf.readUTF(): String {
val bytes = ByteArrayList()
var read = readByte()
while (read.toInt() != 0) {
bytes.add(read)
read = readByte()
}
return String(bytes.toByteArray())
}
enum class ConnectionSide {
SERVER,
CLIENT;
val opposite: ConnectionSide
get() = if (this == SERVER) CLIENT else SERVER
}
enum class ConnectionState {
FRESH,
WORKING,
CLOSED;
}
enum class PacketDirection(val allowedOnClient: Boolean, val allowedOnServer: Boolean) {
SERVER_TO_CLIENT(true, false),
CLIENT_TO_SERVER(false, true),
BI_DIRECTIONAL(true, true);
fun acceptedOn(side: ConnectionSide): Boolean {
if (side == ConnectionSide.SERVER)
return allowedOnServer
return allowedOnClient
}
companion object {
fun get(type: KClass<out IPacket>): PacketDirection {
return of(type.isSuperclassOf(IClientPacket::class), type.isSuperclassOf(IServerPacket::class))
}
fun of(allowedOnClient: Boolean, allowedOnServer: Boolean): PacketDirection {
if (allowedOnServer && allowedOnClient)
return BI_DIRECTIONAL
else if (allowedOnServer)
return SERVER_TO_CLIENT
else
return CLIENT_TO_SERVER
}
}
}
enum class ConnectionType {
NETWORK,
MEMORY;
}
fun interface IPacketReader<T : IPacket> {
fun read(stream: DataInputStream): T
}
interface IPacket {
fun write(stream: DataOutputStream)
}
interface IServerPacket : IPacket {
fun play(connection: ServerConnection)
}
interface IClientPacket : IPacket {
fun play(connection: ClientConnection)
}

View File

@ -0,0 +1,111 @@
package ru.dbotthepony.kstarbound.network
import com.google.common.util.concurrent.ThreadFactoryBuilder
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.nio.NioEventLoopGroup
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.network.packets.DisconnectPacket
import ru.dbotthepony.kstarbound.network.packets.HelloListener
import ru.dbotthepony.kstarbound.network.packets.HelloPacket
import ru.dbotthepony.kstarbound.network.packets.PacketMapping
import java.util.*
abstract class Connection(val side: ConnectionSide, val type: ConnectionType, val localUUID: UUID) : ChannelInboundHandlerAdapter(), IConnectionDetails {
abstract override fun channelRead(ctx: ChannelHandlerContext, msg: Any)
protected var channel: Channel? = null
protected var otherSide: HelloPacket? = null
protected var helloListener: HelloListener? = null
abstract val player: Player<*>
override val protocolVersion: Int
get() = otherSide?.protocolVersion ?: 0
override val engineVersion: String
get() = otherSide?.engineVersion ?: "0.0.0"
override val username: String
get() = otherSide?.username ?: ""
override val password: String
get() = otherSide?.password ?: ""
override val uuid: UUID
get() = otherSide?.uuid ?: EMPTY_UUID
fun bind(channel: Channel) {
check(this.channel == null) { "Already having channel bound" }
this.channel = channel
if (side == ConnectionSide.SERVER)
helloListener = HelloListener(this, channel)
}
protected abstract fun onHelloReceived(helloPacket: HelloPacket)
protected abstract fun inGame()
fun helloReceived(helloPacket: HelloPacket) {
println("Hello received $side: $helloPacket")
otherSide = helloPacket
if (side == ConnectionSide.SERVER) {
helloListener!!.sendHello(localUUID)
}
onHelloReceived(helloPacket)
initializeHandlers()
}
fun helloFailed() {
channel!!.close()
}
fun initializeHandlers() {
val channel = channel ?: throw IllegalStateException("No network channel is bound")
if (type == ConnectionType.NETWORK) channel.pipeline().addLast(PacketMapping.Inbound(side))
channel.pipeline().addLast(this)
if (type == ConnectionType.NETWORK) {
channel.pipeline().addLast(PacketMapping.Outbound(side))
channel.pipeline().addFirst(DatagramEncoder)
channel.pipeline().addFirst(DatagramDecoder())
}
inGame()
}
fun send(packet: IPacket) {
val channel = channel ?: throw IllegalStateException("No network channel is bound")
channel.write(packet)
channel.flush()
}
fun sendNoFlush(packet: IPacket) {
val channel = channel ?: throw IllegalStateException("No network channel is bound")
channel.write(packet)
}
fun flush() {
val channel = channel ?: throw IllegalStateException("No network channel is bound")
channel.flush()
}
fun disconnect(reason: String) {
if (side == ConnectionSide.CLIENT) {
channel!!.close()
} else {
channel!!.write(DisconnectPacket(reason))
channel!!.flush()
channel!!.close()
}
}
companion object {
private val EMPTY_UUID = UUID(0L, 0L)
private val LOGGER = LogManager.getLogger()
val NIO_POOL by lazy {
NioEventLoopGroup(ThreadFactoryBuilder().setDaemon(true).setNameFormat("Starbound Network IO %d").build())
}
}
}

View File

@ -0,0 +1,86 @@
package ru.dbotthepony.kstarbound.network
import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelOutboundHandlerAdapter
import io.netty.channel.ChannelPromise
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import org.apache.logging.log4j.LogManager
const val MAX_DATAGRAM_SIZE = 0xFFFFFF
private val LOGGER = LogManager.getLogger()
@Sharable
object DatagramEncoder : ChannelOutboundHandlerAdapter() {
override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) {
if (msg is ByteBuf) {
if (msg.readableBytes() >= MAX_DATAGRAM_SIZE) {
LOGGER.error("Outgoing packet is too big: ${msg.readableBytes()} bytes (max: $MAX_DATAGRAM_SIZE bytes)")
} else {
val size = msg.readableBytes()
msg.ensureWritable(size + 3)
for (i in msg.readableBytes() - 1 downTo 0)
msg.setByte(i + 3, msg.getByte(i).toInt())
val old = msg.writerIndex()
msg.writerIndex(0)
msg.writeMedium(size)
msg.writerIndex(old + 3)
ctx.write(msg, promise)
}
} else {
super.write(ctx, msg, promise)
}
}
}
class DatagramDecoder : ChannelInboundHandlerAdapter() {
private val pendingBytes = ByteArrayList()
private var messageSize = 0
private var discarding = false
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is ByteBuf) {
try {
while (msg.readableBytes() > 0) {
if (messageSize == 0) {
messageSize = msg.readUnsignedMedium()
if (messageSize >= MAX_DATAGRAM_SIZE) {
LOGGER.error("Incoming packet is too big: $messageSize bytes (max: $MAX_DATAGRAM_SIZE bytes), it will be discarded.")
discarding = true
}
}
while (messageSize > 0 && msg.readableBytes() > 0) {
if (!discarding) pendingBytes.add(msg.readByte())
messageSize--
}
if (messageSize == 0) {
if (discarding) {
discarding = false
} else {
val alloc = ctx.alloc().buffer(pendingBytes.size)
for (i in pendingBytes.indices)
alloc.writeByte(pendingBytes.getByte(i).toInt())
pendingBytes.clear()
ctx.fireChannelRead(alloc)
}
}
}
} finally {
msg.release()
}
} else {
super.channelRead(ctx, msg)
}
}
}

View File

@ -0,0 +1,11 @@
package ru.dbotthepony.kstarbound.network
import java.util.*
interface IConnectionDetails {
val protocolVersion: Int
val engineVersion: String
val username: String
val password: String
val uuid: UUID
}

View File

@ -0,0 +1,89 @@
package ru.dbotthepony.kstarbound.network
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufInputStream
import io.netty.buffer.ByteBufOutputStream
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelOutboundHandlerAdapter
import io.netty.channel.ChannelPromise
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import java.io.DataInputStream
import java.io.DataOutputStream
import kotlin.reflect.KClass
class PacketMapper {
private val packets = ArrayList<Type<*>>()
private val clazz2Type = Reference2ObjectOpenHashMap<KClass<*>, Type<*>>()
private data class Type<T : IPacket>(val id: Int, val type: KClass<T>, val factory: IPacketReader<T>, val direction: PacketDirection)
val size: Int
get() = packets.size
fun <T : IPacket> add(type: KClass<T>, reader: IPacketReader<T>, direction: PacketDirection = PacketDirection.get(type)): PacketMapper {
if (packets.size >= 255)
throw IndexOutOfBoundsException("Unable to add any more packet types! 255 is the max")
if (type in clazz2Type)
throw IllegalArgumentException("Already has packet handler for type $reader (${type})")
val ptype = Type(packets.size, type, reader, direction)
packets.add(ptype)
clazz2Type[type] = ptype
return this
}
inline fun <reified T : IPacket> add(reader: IPacketReader<T>, direction: PacketDirection = PacketDirection.get(T::class)): PacketMapper {
return add(T::class, reader, direction)
}
inner class Inbound(val side: ConnectionSide) : ChannelInboundHandlerAdapter() {
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is ByteBuf) {
val packetType = msg.readUnsignedByte().toInt()
val type = packets.getOrNull(packetType)
if (type == null) {
LOGGER.error("Unknown packet type $packetType!")
msg.release()
} else if (!type.direction.acceptedOn(side)) {
LOGGER.error("Packet ${type.type} can not be accepted on side $side!")
msg.release()
} else {
try {
ctx.fireChannelRead(type.factory.read(DataInputStream(ByteBufInputStream(msg))))
} catch (err: Throwable) {
LOGGER.error("Error while reading incoming packet from network", err)
} finally {
msg.release()
}
}
} else {
super.channelRead(ctx, msg)
}
}
}
inner class Outbound(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 {
val buff = ctx.alloc().buffer(2048)
buff.writeByte(type.id)
(msg as IPacket).write(DataOutputStream(ByteBufOutputStream(buff)))
ctx.write(buff, promise)
}
}
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -0,0 +1,10 @@
package ru.dbotthepony.kstarbound.network
import ru.dbotthepony.kstarbound.player.Avatar
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
import java.util.UUID
abstract class Player<T : Connection>(val connection: T, val uuid: UUID) {
var avatar: Avatar? = null
var character: PlayerEntity? = null
}

View File

@ -0,0 +1,11 @@
package ru.dbotthepony.kstarbound.network.packets
import ru.dbotthepony.kstarbound.client.network.packets.InitialChunkDataPacket
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket
import ru.dbotthepony.kstarbound.network.PacketMapper
val PacketMapping = PacketMapper().also {
it.add(::DisconnectPacket)
it.add(::JoinWorldPacket)
it.add(::InitialChunkDataPacket)
}

View File

@ -0,0 +1,88 @@
package ru.dbotthepony.kstarbound.network.packets
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufInputStream
import io.netty.buffer.ByteBufOutputStream
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.network.ClientConnection
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IConnectionDetails
import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.readUTF
import ru.dbotthepony.kstarbound.network.writeUTF
import ru.dbotthepony.kstarbound.server.network.ServerConnection
import ru.dbotthepony.kstarbound.util.readUUID
import ru.dbotthepony.kstarbound.util.writeUUID
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID
data class HelloPacket(
override val protocolVersion: Int,
override val engineVersion: String,
override val username: String,
override val password: String,
override val uuid: UUID
) : IPacket, IConnectionDetails {
constructor(buff: DataInputStream) : this(buff.readInt(), buff.readUTF(), buff.readUTF(), buff.readUTF(), buff.readUUID())
override fun write(stream: DataOutputStream) {
stream.writeInt(protocolVersion)
stream.writeUTF(engineVersion)
stream.writeUTF(username)
stream.writeUTF(password)
stream.writeUUID(uuid)
}
}
data class DisconnectPacket(val reason: String) : IServerPacket, IClientPacket {
constructor(buff: DataInputStream) : this(buff.readUTF())
override fun write(stream: DataOutputStream) {
stream.writeUTF(reason)
}
override fun play(connection: ServerConnection) {
}
override fun play(connection: ClientConnection) {
}
}
class HelloListener(val connection: Connection, private val channel: Channel) : ChannelInboundHandlerAdapter() {
init {
channel.pipeline().addFirst(this)
}
fun sendHello(uuid: UUID, username: String = "", password: String = ""): HelloListener {
val buf = channel.config().allocator.buffer()
buf.writeUTF("KSTARBOUND HELLO")
HelloPacket(Starbound.PROTOCOL_VERSION, Starbound.ENGINE_VERSION, username, password, uuid).write(DataOutputStream(ByteBufOutputStream(buf)))
channel.write(buf)
channel.flush()
return this
}
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is ByteBuf) {
if (msg.readUTF() != "KSTARBOUND HELLO") {
connection.helloFailed()
} else {
connection.helloReceived(HelloPacket(DataInputStream(ByteBufInputStream(msg))))
}
msg.release()
} else {
connection.helloFailed()
}
channel.pipeline().remove(this)
}
}

View File

@ -1,12 +0,0 @@
package ru.dbotthepony.kstarbound.player
import ru.dbotthepony.kstarbound.Starbound
/**
* Игрок - как он есть.
*
* [Player] - источник команд для [Avatar]
*/
class Player(val starbound: Starbound) {
var avatar: Avatar? = null
}

View File

@ -0,0 +1,14 @@
package ru.dbotthepony.kstarbound.server
import java.io.Closeable
import java.io.File
class IntegratedStarboundServer(root: File) : StarboundServer(root), Closeable {
init {
channels.createLocalChannel()
}
override fun close0() {
}
}

View File

@ -0,0 +1,15 @@
package ru.dbotthepony.kstarbound.server
import ru.dbotthepony.kstarbound.json.builder.JsonBuilder
import java.util.UUID
@JsonBuilder
class ServerSettings {
var maxPlayers = 8
var listenPort = 21025
fun from(other: ServerSettings) {
maxPlayers = other.maxPlayers
listenPort = other.listenPort
}
}

View File

@ -0,0 +1,83 @@
package ru.dbotthepony.kstarbound.server
import ru.dbotthepony.kstarbound.client.network.packets.InitialChunkDataPacket
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket
import ru.dbotthepony.kstarbound.server.network.ServerChannels
import ru.dbotthepony.kstarbound.server.network.ServerPlayer
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import java.io.Closeable
import java.io.File
import java.util.Collections
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
abstract class StarboundServer(val root: File) : Closeable {
init {
if (!root.exists()) {
check(root.mkdirs()) { "Unable to create ${root.absolutePath}" }
} else if (!root.isDirectory) {
throw IllegalArgumentException("${root.absolutePath} is not a directory")
}
}
val worlds: MutableList<ServerWorld> = Collections.synchronizedList(ArrayList<ServerWorld>())
val thread = Thread(::runThread, "Starbound Server ${threadCounter.incrementAndGet()}")
val mailbox = MailboxExecutorService(thread)
val settings = ServerSettings()
val channels = ServerChannels(this)
val lock = ReentrantLock()
@Volatile
var isClosed = false
private set
init {
thread.isDaemon = true
thread.start()
}
fun playerInGame(player: ServerPlayer) {
val world = worlds.first()
player.world = 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()
private fun runThread() {
while (!isClosed) {
mailbox.executeQueuedTasks()
LockSupport.park()
}
}
final override fun close() {
lock.withLock {
if (isClosed) return
channels.close()
worlds.forEach { it.close() }
close0()
LockSupport.unpark(thread)
}
}
companion object {
private val threadCounter = AtomicInteger()
}
}

View File

@ -0,0 +1,93 @@
package ru.dbotthepony.kstarbound.server.network
import io.netty.bootstrap.ServerBootstrap
import io.netty.channel.Channel
import io.netty.channel.ChannelFuture
import io.netty.channel.ChannelInitializer
import io.netty.channel.local.LocalAddress
import io.netty.channel.local.LocalServerChannel
import io.netty.channel.socket.nio.NioServerSocketChannel
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.ConnectionType
import ru.dbotthepony.kstarbound.server.StarboundServer
import java.io.Closeable
import java.net.SocketAddress
import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class ServerChannels(val server: StarboundServer) : Closeable {
private val channels = Collections.synchronizedList(ArrayList<ChannelFuture>())
private val connections = Collections.synchronizedList(ArrayList<ServerConnection>())
private var localChannel: Channel? = null
private val lock = ReentrantLock()
private var isClosed = false
@Suppress("name_shadowing")
fun createLocalChannel(): Channel {
val localChannel = localChannel
if (localChannel != null) {
return localChannel
}
lock.withLock {
val localChannel = this.localChannel
if (localChannel != null) {
return localChannel
}
val channel = ServerBootstrap().channel(LocalServerChannel::class.java).group(Connection.NIO_POOL).childHandler(object : ChannelInitializer<Channel>() {
override fun initChannel(ch: Channel) {
val connection = ServerConnection(server, ConnectionType.MEMORY)
connections.add(connection)
connection.bind(ch)
}
}).bind(LocalAddress.ANY).syncUninterruptibly()
channels.add(channel)
this.localChannel = channel.channel()
channel.channel().closeFuture().addListener {
channels.remove(channel)
this.localChannel = null
}
return channel.channel()
}
}
fun createChannel(localAddress: SocketAddress): Channel {
lock.withLock {
val channel = ServerBootstrap().channel(NioServerSocketChannel::class.java).group(Connection.NIO_POOL).childHandler(object : ChannelInitializer<Channel>() {
override fun initChannel(ch: Channel) {
val connection = ServerConnection(server, ConnectionType.NETWORK)
connections.add(connection)
connection.bind(ch)
}
}).bind(localAddress).syncUninterruptibly()
channels.add(channel)
this.localChannel = channel.channel()
channel.channel().closeFuture().addListener {
channels.remove(channel)
}
return channel.channel()
}
}
override fun close() {
lock.withLock {
if (isClosed) return
connections.forEach { it.disconnect("Server is stopping") }
channels.forEach { it.channel().close() }
channels.clear()
connections.clear()
}
}
}

View File

@ -0,0 +1,44 @@
package ru.dbotthepony.kstarbound.server.network
import io.netty.channel.ChannelHandlerContext
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.ConnectionType
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.HelloPacket
import ru.dbotthepony.kstarbound.server.StarboundServer
import java.util.*
import kotlin.properties.Delegates
// server -> client
class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type, UUID(0L, 0L)) {
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is IServerPacket) {
try {
msg.play(this)
} catch (err: Throwable) {
LOGGER.error("Failed to read serverbound packet $msg", err)
disconnect(err.toString())
}
} else {
LOGGER.error("Unknown serverbound packet type $msg")
disconnect("Unknown serverbound packet type $msg")
}
}
override var player: ServerPlayer by Delegates.notNull()
private set
override fun inGame() {
server.playerInGame(player)
}
override fun onHelloReceived(helloPacket: HelloPacket) {
player = ServerPlayer(this)
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -0,0 +1,9 @@
package ru.dbotthepony.kstarbound.server.network
import ru.dbotthepony.kstarbound.network.Player
import ru.dbotthepony.kstarbound.server.world.ServerWorld
class ServerPlayer(connection: ServerConnection) : Player<ServerConnection>(connection, connection.uuid) {
var world: ServerWorld? = null
}

View File

@ -0,0 +1,7 @@
package ru.dbotthepony.kstarbound.server.world
import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos
class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) {
}

View File

@ -0,0 +1,170 @@
package ru.dbotthepony.kstarbound.server.world
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.server.StarboundServer
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry
import java.io.Closeable
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport
import kotlin.concurrent.withLock
class ServerWorld(
val server: StarboundServer,
seed: Long,
geometry: WorldGeometry,
) : World<ServerWorld, ServerChunk>(seed, geometry), Closeable {
init {
server.worlds.add(this)
}
val thread = Thread(::runThread, "Starbound Server World $seed")
var isStopped: Boolean = false
private set
init {
thread.isDaemon = true
}
@Volatile
private var nextThink = 0L
override fun close() {
if (!isStopped) {
isStopped = true
lock.withLock { onUnload() }
}
}
fun startThread() {
nextThink = System.nanoTime()
thread.start()
}
private fun onUnload() {
}
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
}
nextThink = System.nanoTime()
try {
think()
} catch (err: Throwable) {
isStopped = true
onUnload()
throw err
}
}
}
override val isRemote: Boolean
get() = false
override fun thinkInner() {
ticketLists.forEach { it.tick() }
}
override fun chunkFactory(pos: ChunkPos): ServerChunk {
return ServerChunk(this, pos)
}
private val ticketMap = Long2ObjectOpenHashMap<TicketList>()
private val ticketLists = ArrayList<TicketList>()
interface ITicket {
fun cancel()
val isCanceled: Boolean
val pos: ChunkPos
val id: Int
}
interface ITimedTicket : ITicket, Comparable<ITimedTicket> {
val timeRemaining: Int
fun prolong(ticks: Int)
override fun compareTo(other: ITimedTicket): Int {
val cmp = timeRemaining.compareTo(other.timeRemaining)
if (cmp != 0) return cmp
return id.compareTo(other.id)
}
}
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)
}
}
private val permanent = ArrayList<Ticket>()
private val temporary = ObjectAVLTreeSet<TimedTicket>()
private var ticks = 0
private var nextTicketID = AtomicInteger()
fun tick(): Boolean {
ticks++
while (temporary.isNotEmpty() && temporary.first().timeRemaining <= 0) {
val ticket = temporary.first()
ticket.isCanceled = true
temporary.remove(ticket)
}
return temporary.isNotEmpty() || permanent.isNotEmpty()
}
open inner class Ticket : ITicket {
final override val id: Int = nextTicketID.getAndIncrement()
final override val pos: ChunkPos
get() = this@TicketList.pos
final override fun cancel() {
if (isCanceled) return
lock.withLock {
if (isCanceled) return
isCanceled = true
if (this is TimedTicket)
temporary.remove(this)
else
permanent.remove(this)
}
}
final override var isCanceled: Boolean = false
}
inner class TimedTicket(var expiresAt: Int) : Ticket(), ITimedTicket {
override val timeRemaining: Int
get() = (expiresAt - ticks).coerceAtLeast(0)
override fun prolong(ticks: Int) {
if (ticks == 0 || isCanceled) return
lock.withLock {
if (isCanceled) return
temporary.remove(this)
expiresAt += ticks
if (timeRemaining > 0) temporary.add(this)
}
}
}
}
}

View File

@ -268,17 +268,3 @@ fun JsonObject.getObject(key: String): JsonObject {
if (!has(key)) throw JsonSyntaxException("Expected object at $key, got nothing")
return this[key] as? JsonObject ?: throw JsonSyntaxException("Expected object at $key, got ${this[key]}")
}
inline fun <T> MutableIterable<Reference<T>>.forEachValid(block: (T) -> Unit) {
val i = iterator()
for (v in i) {
val get = v.get()
if (get == null) {
i.remove()
} else {
block.invoke(get)
}
}
}

View File

@ -1,39 +0,0 @@
package ru.dbotthepony.kstarbound.util
import java.io.InputStream
import java.io.OutputStream
fun OutputStream.writeLEInt(value: Int) {
write(value)
write(value ushr 8)
write(value ushr 16)
write(value ushr 24)
}
fun OutputStream.writeLEShort(value: Int) {
write(value)
write(value ushr 8)
}
fun OutputStream.writeLELong(value: Long) {
writeLEInt(value.toInt())
writeLEInt((value ushr 32).toInt())
}
fun OutputStream.writeLEFloat(value: Float) = writeLEInt(value.toBits())
fun OutputStream.writeLEDouble(value: Double) = writeLELong(value.toBits())
fun InputStream.readLEShort(): Int {
return read() or (read() shl 8)
}
fun InputStream.readLEInt(): Int {
return read() or (read() shl 8) or (read() shl 16) or (read() shl 24)
}
fun InputStream.readLELong(): Long {
return readLEInt().toLong() or (readLEInt().toLong() shl 32)
}
fun InputStream.readLEFloat() = Float.fromBits(readLEInt())
fun InputStream.readLEDouble() = Double.fromBits(readLELong())

View File

@ -37,7 +37,11 @@ private fun <E : Comparable<E>> LinkedList<E>.enqueue(value: E) {
}
}
class MailboxExecutorService(val thread: Thread = Thread.currentThread()) : ScheduledExecutorService {
class MailboxExecutorService(thread: Thread = Thread.currentThread()) : ScheduledExecutorService {
@Volatile
var thread: Thread = thread
private set
private val futureQueue = ConcurrentLinkedQueue<FutureTask<*>>()
private val timers = LinkedList<Timer<*>>()
@ -97,7 +101,7 @@ class MailboxExecutorService(val thread: Thread = Thread.currentThread()) : Sche
}
fun executeQueuedTasks() {
check(isSameThread()) { "Trying to execute queued tasks in thread ${Thread.currentThread()}, while correct thread is $thread" }
thread = Thread.currentThread()
if (isShutdown) {
if (!isTerminated) {

View File

@ -0,0 +1,288 @@
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.IStruct2i
import ru.dbotthepony.kvector.vector.Vector2i
import java.io.DataInput
import java.io.DataOutput
import java.io.InputStream
import java.io.OutputStream
import java.math.BigDecimal
import java.math.BigInteger
import java.util.*
import kotlin.math.absoluteValue
fun OutputStream.writeLEInt(value: Int) {
write(value)
write(value ushr 8)
write(value ushr 16)
write(value ushr 24)
}
fun OutputStream.writeLEShort(value: Int) {
write(value)
write(value ushr 8)
}
fun OutputStream.writeLELong(value: Long) {
writeLEInt(value.toInt())
writeLEInt((value ushr 32).toInt())
}
fun OutputStream.writeLEFloat(value: Float) = writeLEInt(value.toBits())
fun OutputStream.writeLEDouble(value: Double) = writeLELong(value.toBits())
fun InputStream.readLEShort(): Int {
return read() or (read() shl 8)
}
fun InputStream.readLEInt(): Int {
return read() or (read() shl 8) or (read() shl 16) or (read() shl 24)
}
fun InputStream.readLELong(): Long {
return readLEInt().toLong() or (readLEInt().toLong() shl 32)
}
fun InputStream.readLEFloat() = Float.fromBits(readLEInt())
fun InputStream.readLEDouble() = Double.fromBits(readLELong())
fun OutputStream.writeUTF(value: String) {
write(value.toByteArray().also { check(!it.any { it.toInt() == 0 }) { "Provided UTF string contains NUL" } })
write(0)
}
fun InputStream.readUTF(): String {
val bytes = ByteArrayList()
var read = read()
while (read != 0) {
bytes.add(read.toByte())
read = read()
}
return String(bytes.toByteArray())
}
fun InputStream.readUUID(): UUID {
return UUID(readLong(), readLong())
}
fun OutputStream.writeUUID(value: UUID) {
writeLong(value.mostSignificantBits)
writeLong(value.leastSignificantBits)
}
fun OutputStream.writeVec2i(value: IStruct2i) {
writeInt(value.component1())
writeInt(value.component2())
}
fun InputStream.readVec2i(): Vector2i {
return Vector2i(readInt(), readInt())
}
fun InputStream.readChunkPos(): ChunkPos {
return ChunkPos(readInt(), readInt())
}
fun OutputStream.writeBigDecimal(value: BigDecimal) {
writeInt(value.scale())
val bytes = value.unscaledValue().toByteArray()
writeVarIntLE(bytes.size)
write(bytes)
}
fun InputStream.readBigDecimal(): BigDecimal {
val scale = readInt()
val size = readVarIntLE()
require(size >= 0) { "Negative payload size: $size" }
val bytes = ByteArray(size)
read(bytes)
return BigDecimal(BigInteger(bytes), scale)
}
fun <S : OutputStream, V> S.writeCollection(collection: Collection<V>, writer: S.(V) -> Unit) {
writeVarIntLE(collection.size)
for (value in collection) {
writer(this, value)
}
}
fun <S : InputStream, V, C : MutableCollection<V>> S.readCollection(reader: S.() -> V, factory: (Int) -> C): C {
val size = readVarIntLE()
val collection = factory.invoke(size)
for (i in 0 until size) {
collection.add(reader(this))
}
return collection
}
fun <S : InputStream, V> S.readCollection(reader: S.() -> V) = readCollection(reader, ::ArrayList)
fun OutputStream.writeInt(value: Int) {
if (this is DataOutput) {
writeInt(value)
return
}
write(value ushr 24)
write(value ushr 16)
write(value ushr 8)
write(value)
}
fun InputStream.readInt(): Int {
if (this is DataInput) {
return readInt()
}
return (read() shl 24) or (read() shl 16) or (read() shl 8) or read()
}
fun OutputStream.writeLong(value: Long) {
if (this is DataOutput) {
writeLong(value)
return
}
write((value ushr 48).toInt())
write((value ushr 40).toInt())
write((value ushr 32).toInt())
write((value ushr 24).toInt())
write((value ushr 16).toInt())
write((value ushr 8).toInt())
write(value.toInt())
}
fun InputStream.readLong(): Long {
if (this is DataInput) {
return readLong()
}
return (read().toLong() shl 48) or
(read().toLong() shl 40) or
(read().toLong() shl 32) or
(read().toLong() shl 24) or
(read().toLong() shl 16) or
(read().toLong() shl 8) or
read().toLong()
}
fun OutputStream.writeFloat(value: Float) = writeInt(value.toBits())
fun InputStream.readFloat() = Float.fromBits(readInt())
fun OutputStream.writeDouble(value: Double) = writeLong(value.toBits())
fun InputStream.readDouble() = Double.fromBits(readLong())
fun InputStream.readVarIntLE(): Int {
val readFirst = read()
if (readFirst < 0) {
throw NoSuchElementException("Reached end of stream")
}
if (readFirst and 64 == 0) {
return if (readFirst and 128 != 0) -(readFirst and 63) else readFirst and 63
}
var result = 0
var nextBit = readFirst and 64
var read = readFirst and 63
var i = 0
while (nextBit != 0) {
result = result or (read shl i)
read = read()
if (read < 0) {
throw NoSuchElementException("Reached end of stream")
}
nextBit = read and 128
read = read and 127
if (i == 0)
i = 6
else
i += 7
}
result = result or (read shl i)
return if (readFirst and 128 != 0) -result else result
}
fun OutputStream.writeVarIntLE(value: Int) {
write((if (value < 0) 128 else 0) or (if (value in -63 .. 63) 0 else 64) or (value.absoluteValue and 63))
var written = value.absoluteValue ushr 6
while (written != 0) {
write((written and 127) or (if (written >= 128) 128 else 0))
written = written ushr 7
}
}
fun InputStream.readVarLongLE(): Long {
val readFirst = read()
if (readFirst < 0) {
throw NoSuchElementException("Reached end of stream")
}
if (readFirst and 64 == 0) {
return if (readFirst and 128 != 0) -(readFirst and 63).toLong() else (readFirst and 63).toLong()
}
var result = 0L
var nextBit = readFirst and 64
var read = readFirst and 63
var i = 0
while (nextBit != 0) {
result = result or (read shl i).toLong()
read = read()
if (read < 0) {
throw NoSuchElementException("Reached end of stream")
}
nextBit = read and 128
read = read and 127
if (i == 0)
i = 6
else
i += 7
}
result = result or (read shl i).toLong()
return if (readFirst and 128 != 0) -result else result
}
fun OutputStream.writeVarLongLE(value: Long) {
write((if (value < 0L) 128 else 0) or (if (value in -63 .. 63) 0 else 64) or (value.absoluteValue and 63).toInt())
var written = value.absoluteValue ushr 6
while (written != 0L) {
write((written and 127).toInt() or (if (written >= 128) 128 else 0))
written = written ushr 7
}
}
fun InputStream.readBinaryString(): String {
val size = readVarIntLE()
require(size >= 0) { "Negative payload size: $size" }
val bytes = ByteArray(size)
read(bytes)
return bytes.decodeToString()
}
fun OutputStream.writeBinaryString(input: String) {
val bytes = input.encodeToByteArray()
writeVarIntLE(bytes.size)
write(bytes)
}

View File

@ -7,6 +7,7 @@ import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.Starbound
import java.lang.ref.Reference
import java.util.*
import java.util.function.Consumer
import java.util.stream.Stream
@ -92,3 +93,17 @@ fun UUID.toStarboundString(): String {
fun <T> Stream<T?>.filterNotNull(): Stream<T> {
return filter { it != null } as Stream<T>
}
inline fun <T> MutableIterable<Reference<T>>.forEachValid(block: (T) -> Unit) {
val i = iterator()
for (v in i) {
val get = v.get()
if (get == null) {
i.remove()
} else {
block.invoke(get)
}
}
}

View File

@ -23,7 +23,6 @@ import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kvector.arrays.Object2DArray
import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2i
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
@ -33,19 +32,14 @@ import kotlin.concurrent.withLock
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(
val seed: Long,
val size: Vector2i,
val loopX: Boolean,
val loopY: Boolean
val geometry: WorldGeometry,
) : ICellAccess {
val x: CoordinateMapper = if (loopX) CoordinateMapper.Wrapper(size.x) else CoordinateMapper.Clamper(size.x)
val y: CoordinateMapper = if (loopY) CoordinateMapper.Wrapper(size.y) else CoordinateMapper.Clamper(size.y)
// whenever provided cell position is within actual world borders, ignoring wrapping
fun inBounds(x: Int, y: Int) = this.x.inBoundsCell(x) && this.y.inBoundsCell(y)
fun inBounds(value: IStruct2i) = this.x.inBoundsCell(value.component1()) && this.y.inBoundsCell(value.component2())
fun inBounds(x: Int, y: Int) = geometry.x.inBoundsCell(x) && geometry.y.inBoundsCell(y)
fun inBounds(value: IStruct2i) = geometry.x.inBoundsCell(value.component1()) && geometry.y.inBoundsCell(value.component2())
fun chunkFromCell(x: Int, y: Int) = ChunkPos(this.x.chunkFromCell(x), this.y.chunkFromCell(y))
fun chunkFromCell(x: Double, y: Double) = ChunkPos(this.x.chunkFromCell(x.toInt()), this.y.chunkFromCell(y.toInt()))
fun chunkFromCell(x: Int, y: Int) = ChunkPos(geometry.x.chunkFromCell(x), geometry.y.chunkFromCell(y))
fun chunkFromCell(x: Double, y: Double) = ChunkPos(geometry.x.chunkFromCell(x.toInt()), geometry.y.chunkFromCell(y.toInt()))
fun chunkFromCell(value: IStruct2i) = chunkFromCell(value.component1(), value.component2())
fun chunkFromCell(value: IStruct2d) = chunkFromCell(value.component1(), value.component2())
@ -56,7 +50,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
final override fun randomLongFor(x: Int, y: Int) = super.randomLongFor(x, y) xor seed
override fun getCellDirect(x: Int, y: Int): AbstractCell {
if (!this.x.inBoundsCell(x) || !this.y.inBoundsCell(y)) return AbstractCell.NULL
if (!geometry.x.inBoundsCell(x) || !geometry.y.inBoundsCell(y)) return AbstractCell.NULL
return getCell(x, y)
}
@ -87,7 +81,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
for (ent in orphanedEntities) {
val (ex, ey) = ent.position
if (this@World.x.chunkFromCell(ex) == x && this@World.y.chunkFromCell(ey) == y) {
if (geometry.x.chunkFromCell(ex) == x && geometry.y.chunkFromCell(ey) == y) {
orphanedInThisChunk.add(ent)
}
}
@ -105,19 +99,19 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
private val map = Long2ObjectOpenHashMap<ChunkType>()
override fun getCell(x: Int, y: Int): AbstractCell {
if (!this@World.x.isValidCellIndex(x) || !this@World.y.isValidCellIndex(y)) return AbstractCell.NULL
val ix = this@World.x.cell(x)
val iy = this@World.y.cell(y)
return this[this@World.x.chunkFromCell(ix), this@World.y.chunkFromCell(iy)]?.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK) ?: AbstractCell.NULL
if (!geometry.x.isValidCellIndex(x) || !geometry.y.isValidCellIndex(y)) return AbstractCell.NULL
val ix = geometry.x.cell(x)
val iy = geometry.y.cell(y)
return this[geometry.x.chunkFromCell(ix), geometry.y.chunkFromCell(iy)]?.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK) ?: AbstractCell.NULL
}
override fun get(x: Int, y: Int): ChunkType? {
if (!this@World.x.inBoundsChunk(x) || !this@World.y.inBoundsChunk(y)) return null
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
return map[ChunkPos.toLong(x, y)]
}
override fun compute(x: Int, y: Int): ChunkType? {
if (!this@World.x.inBoundsChunk(x) || !this@World.y.inBoundsChunk(y)) return null
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
val index = ChunkPos.toLong(x, y)
@ -129,11 +123,11 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
}
override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
if (!this@World.x.isValidCellIndex(x) || !this@World.y.isValidCellIndex(y)) return false
val ix = this@World.x.cell(x)
val iy = this@World.y.cell(y)
val cx = this@World.x.chunkFromCell(ix)
val cy = this@World.y.chunkFromCell(iy)
if (!geometry.x.isValidCellIndex(x) || !geometry.y.isValidCellIndex(y)) return false
val ix = geometry.x.cell(x)
val iy = geometry.y.cell(y)
val cx = geometry.x.chunkFromCell(ix)
val cy = geometry.y.chunkFromCell(iy)
val index = ChunkPos.toLong(cx, cy)
@ -145,51 +139,51 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
}
override fun remove(x: Int, y: Int) {
map.remove(ChunkPos.toLong(this@World.x.chunk(x), this@World.y.chunk(y)))
map.remove(ChunkPos.toLong(geometry.x.chunk(x), geometry.y.chunk(y)))
}
}
inner class ArrayChunkMap : ChunkMap() {
private val map = Object2DArray.nulls<ChunkType>(divideUp(size.x, CHUNK_SIZE), divideUp(size.y, CHUNK_SIZE))
private val map = Object2DArray.nulls<ChunkType>(divideUp(geometry.size.x, CHUNK_SIZE), divideUp(geometry.size.y, CHUNK_SIZE))
private fun getRaw(x: Int, y: Int): ChunkType {
return map[x, y] ?: lock.withLock { map[x, y] ?: create(x, y).also { map[x, y] = it } }
private fun getRaw(x: Int, y: Int): ChunkType? {
return map[x, y]
}
override fun compute(x: Int, y: Int): ChunkType? {
if (!this@World.x.inBoundsChunk(x) || !this@World.y.inBoundsChunk(y)) return null
return getRaw(x, y)
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
return map[x, y] ?: lock.withLock { map[x, y] ?: create(x, y).also { map[x, y] = it } }
}
override fun getCell(x: Int, y: Int): AbstractCell {
if (!this@World.x.isValidCellIndex(x) || !this@World.y.isValidCellIndex(y)) return AbstractCell.NULL
val ix = this@World.x.cell(x)
val iy = this@World.y.cell(y)
return getRaw(ix ushr CHUNK_SIZE_BITS, iy ushr CHUNK_SIZE_BITS).getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK)
if (!geometry.x.isValidCellIndex(x) || !geometry.y.isValidCellIndex(y)) return AbstractCell.NULL
val ix = geometry.x.cell(x)
val iy = geometry.y.cell(y)
return map[ix ushr CHUNK_SIZE_BITS, iy ushr CHUNK_SIZE_BITS]?.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK) ?: AbstractCell.NULL
}
override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
if (!this@World.x.isValidCellIndex(x) || !this@World.y.isValidCellIndex(y)) return false
val ix = this@World.x.cell(x)
val iy = this@World.y.cell(y)
return getRaw(ix ushr CHUNK_SIZE_BITS, iy ushr CHUNK_SIZE_BITS).setCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK, cell)
if (!geometry.x.isValidCellIndex(x) || !geometry.y.isValidCellIndex(y)) return false
val ix = geometry.x.cell(x)
val iy = geometry.y.cell(y)
return compute(ix ushr CHUNK_SIZE_BITS, iy ushr CHUNK_SIZE_BITS)!!.setCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK, cell)
}
override fun get(x: Int, y: Int): ChunkType? {
if (!this@World.x.inBoundsChunk(x) || !this@World.y.inBoundsChunk(y)) return null
return getRaw(x, y)
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
return map[x, y]
}
override fun remove(x: Int, y: Int) {
map[this@World.x.chunk(x), this@World.y.chunk(y)] = null
map[geometry.x.chunk(x), geometry.y.chunk(y)] = null
}
}
val chunkMap: ChunkMap = if (size.x <= 32000 && size.y <= 32000) ArrayChunkMap() else SparseChunkMap()
val chunkMap: ChunkMap = if (geometry.size.x <= 32000 && geometry.size.y <= 32000) ArrayChunkMap() else SparseChunkMap()
val random: RandomGenerator = RandomGenerator.of("Xoroshiro128PlusPlus")
var gravity = Vector2d(0.0, -80.0)
abstract val isClient: Boolean
abstract val isRemote: Boolean
// used to synchronize read/writes to various world state stuff/memory structure
val lock = ReentrantLock()
@ -204,7 +198,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
for (ent in entities) {
ent.thinkShared()
if (isClient)
if (isRemote)
ent.thinkClient()
else
ent.thinkServer()
@ -216,7 +210,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
for (ent in objects) {
ent.thinkShared()
if (isClient)
if (isRemote)
ent.thinkClient()
else
ent.thinkServer()

View File

@ -0,0 +1,20 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.util.readVec2i
import ru.dbotthepony.kstarbound.util.writeVec2i
import ru.dbotthepony.kvector.vector.Vector2i
import java.io.DataInputStream
import java.io.DataOutputStream
data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Boolean) {
constructor(buff: DataInputStream) : this(buff.readVec2i(), buff.readBoolean(), buff.readBoolean())
val x: CoordinateMapper = if (loopX) CoordinateMapper.Wrapper(size.x) else CoordinateMapper.Clamper(size.x)
val y: CoordinateMapper = if (loopY) CoordinateMapper.Wrapper(size.y) else CoordinateMapper.Clamper(size.y)
fun write(buff: DataOutputStream) {
buff.writeVec2i(size)
buff.writeBoolean(loopX)
buff.writeBoolean(loopY)
}
}

View File

@ -4,6 +4,7 @@ import com.github.benmanes.caffeine.cache.Interner
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.util.HashTableInterner
import java.io.DataInputStream
import java.io.DataOutputStream
sealed class AbstractCell {
abstract val foreground: AbstractTileState
@ -18,6 +19,21 @@ sealed class AbstractCell {
abstract fun immutable(): ImmutableCell
abstract fun mutable(): MutableCell
fun write(stream: DataOutputStream) {
foreground.write(stream)
background.write(stream)
liquid.write(stream)
stream.write(0) // collisionMap
stream.writeShort(dungeonId)
stream.writeByte(biome)
stream.writeByte(envBiome)
stream.writeBoolean(isIndestructible)
stream.write(0) // unknown
}
companion object {
fun skip(stream: DataInputStream) {
AbstractTileState.skip(stream)

View File

@ -6,6 +6,7 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.util.HashTableInterner
import java.io.DataInputStream
import java.io.DataOutputStream
sealed class AbstractLiquidState {
abstract val def: Registry.Entry<LiquidDefinition>?
@ -16,6 +17,13 @@ sealed class AbstractLiquidState {
abstract fun mutable(): MutableLiquidState
abstract fun immutable(): ImmutableLiquidState
fun write(stream: DataOutputStream) {
stream.writeByte(def?.id ?: 0)
stream.writeFloat(level)
stream.writeFloat(pressure)
stream.writeBoolean(isInfinite)
}
companion object {
fun skip(stream: DataInputStream) {
stream.skipNBytes(1 + 4 + 4 + 1)

View File

@ -7,7 +7,9 @@ import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.writeUTF
import java.io.DataInputStream
import java.io.DataOutputStream
sealed class AbstractTileState {
abstract val material: Registry.Entry<TileDefinition>
@ -19,6 +21,15 @@ sealed class AbstractTileState {
abstract fun immutable(): ImmutableTileState
abstract fun mutable(): MutableTileState
fun write(stream: DataOutputStream) {
stream.writeShort(material.id ?: 0)
stream.writeBoolean(modifier != null)
stream.writeShort(modifier?.id ?: 0)
stream.writeByte(color.ordinal)
stream.write((hueShift / 360f * 255).toInt())
stream.write((modifierHueShift / 360f * 255).toInt())
}
companion object {
fun skip(stream: DataInputStream) {
stream.skipNBytes(2 + 1 + 1 + 2 + 1)

View File

@ -48,7 +48,7 @@ abstract class Entity(val world: World<*, *>) {
return
val old = field
field = Vector2d(world.x.cell(value.x), world.y.cell(value.y))
field = Vector2d(world.geometry.x.cell(value.x), world.geometry.y.cell(value.y))
if (isSpawned && !isRemoved) {
val oldChunkPos = world.chunkFromCell(old)