From b7ec73bf0f464250ea94c1094ab7448e35ac089c Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sun, 28 Jan 2024 16:13:07 +0700 Subject: [PATCH] Server and network test code --- build.gradle.kts | 2 + .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 62 ++-- .../ru/dbotthepony/kstarbound/Starbound.kt | 4 +- .../kstarbound/client/StarboundClient.kt | 7 +- .../client/network/ClientConnection.kt | 90 ++++++ .../kstarbound/client/network/LocalPlayer.kt | 6 + .../network/packets/InitialChunkDataPacket.kt | 42 +++ .../client/network/packets/JoinWorldPacket.kt | 31 ++ .../kstarbound/client/world/ClientWorld.kt | 21 +- .../ru/dbotthepony/kstarbound/network/API.kt | 90 ++++++ .../kstarbound/network/Connection.kt | 111 +++++++ .../kstarbound/network/Datagrams.kt | 86 ++++++ .../kstarbound/network/IConnectionDetails.kt | 11 + .../kstarbound/network/PacketMapper.kt | 89 ++++++ .../dbotthepony/kstarbound/network/Player.kt | 10 + .../network/packets/PacketMapping.kt | 11 + .../network/packets/TehnicalPackets.kt | 88 ++++++ .../dbotthepony/kstarbound/player/Player.kt | 12 - .../server/IntegratedStarboundServer.kt | 14 + .../kstarbound/server/ServerSettings.kt | 15 + .../kstarbound/server/StarboundServer.kt | 83 +++++ .../server/network/ServerChannels.kt | 93 ++++++ .../server/network/ServerConnection.kt | 44 +++ .../kstarbound/server/network/ServerPlayer.kt | 9 + .../kstarbound/server/world/ServerChunk.kt | 7 + .../kstarbound/server/world/ServerWorld.kt | 170 +++++++++++ .../kstarbound/util/{Ext.kt => JsonUtils.kt} | 14 - .../dbotthepony/kstarbound/util/LEStream.kt | 39 --- .../kstarbound/util/MailboxExecutorService.kt | 8 +- .../kstarbound/util/StreamUtils.kt | 288 ++++++++++++++++++ .../ru/dbotthepony/kstarbound/util/Utils.kt | 15 + .../ru/dbotthepony/kstarbound/world/World.kt | 84 +++-- .../kstarbound/world/WorldGeometry.kt | 20 ++ .../kstarbound/world/api/AbstractCell.kt | 16 + .../world/api/AbstractLiquidState.kt | 8 + .../kstarbound/world/api/AbstractTileState.kt | 11 + .../kstarbound/world/entities/Entity.kt | 2 +- 37 files changed, 1542 insertions(+), 171 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/network/ClientConnection.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/network/LocalPlayer.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/InitialChunkDataPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/Datagrams.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/IConnectionDetails.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketMapper.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/Player.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PacketMapping.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/TehnicalPackets.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/player/Player.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/IntegratedStarboundServer.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerSettings.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerChannels.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerConnection.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerPlayer.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt rename src/main/kotlin/ru/dbotthepony/kstarbound/util/{Ext.kt => JsonUtils.kt} (96%) delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/util/LEStream.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/util/StreamUtils.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt diff --git a/build.gradle.kts b/build.gradle.kts index 669d4aa1..4d25ed1a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 128aac68..86f235d5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 12441ba3..c96e8cfc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index d44b07bf..d448a722 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/ClientConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/ClientConnection.kt new file mode 100644 index 00000000..9c0b5fb9 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/ClientConnection.kt @@ -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() { 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() { override fun initChannel(ch: Channel) { connection.bind(ch) } }) + .connect(address) + .syncUninterruptibly() + + connection.sendHello() + + return connection + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/LocalPlayer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/LocalPlayer.kt new file mode 100644 index 00000000..cca97171 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/LocalPlayer.kt @@ -0,0 +1,6 @@ +package ru.dbotthepony.kstarbound.client.network + +import ru.dbotthepony.kstarbound.network.Player + +class LocalPlayer(connection: ClientConnection) : Player(connection, connection.localUUID) { +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/InitialChunkDataPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/InitialChunkDataPacket.kt new file mode 100644 index 00000000..db9dd3c0 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/InitialChunkDataPacket.kt @@ -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) : IClientPacket { + constructor(stream: DataInputStream) : this(stream.readChunkPos(), stream.readCollection { MutableCell().read(stream).immutable() }) + constructor(chunk: Chunk<*, *>) : this(chunk.pos, ArrayList(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()) + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt new file mode 100644 index 00000000..03afc7de --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt @@ -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) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index 412ddca8..b617b300 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -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(seed, size, loopX, loopY) { + geometry: WorldGeometry, +) : World(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) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt new file mode 100644 index 00000000..b8453b0b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt @@ -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): 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 { + 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) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt new file mode 100644 index 00000000..36223c61 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -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()) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Datagrams.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Datagrams.kt new file mode 100644 index 00000000..0a3525a5 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Datagrams.kt @@ -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) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/IConnectionDetails.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/IConnectionDetails.kt new file mode 100644 index 00000000..0ee8dd8e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/IConnectionDetails.kt @@ -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 +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketMapper.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketMapper.kt new file mode 100644 index 00000000..837f3a4a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketMapper.kt @@ -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>() + private val clazz2Type = Reference2ObjectOpenHashMap, Type<*>>() + + private data class Type(val id: Int, val type: KClass, val factory: IPacketReader, val direction: PacketDirection) + + val size: Int + get() = packets.size + + fun add(type: KClass, reader: IPacketReader, 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 add(reader: IPacketReader, 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() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Player.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Player.kt new file mode 100644 index 00000000..99359936 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Player.kt @@ -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(val connection: T, val uuid: UUID) { + var avatar: Avatar? = null + var character: PlayerEntity? = null +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PacketMapping.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PacketMapping.kt new file mode 100644 index 00000000..d1959e93 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PacketMapping.kt @@ -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) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/TehnicalPackets.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/TehnicalPackets.kt new file mode 100644 index 00000000..a8058e17 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/TehnicalPackets.kt @@ -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) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/player/Player.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/player/Player.kt deleted file mode 100644 index 028cc050..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/player/Player.kt +++ /dev/null @@ -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 -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/IntegratedStarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/IntegratedStarboundServer.kt new file mode 100644 index 00000000..e1add2aa --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/IntegratedStarboundServer.kt @@ -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() { + + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerSettings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerSettings.kt new file mode 100644 index 00000000..58b073e7 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerSettings.kt @@ -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 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt new file mode 100644 index 00000000..bd5493e5 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -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 = Collections.synchronizedList(ArrayList()) + + 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() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerChannels.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerChannels.kt new file mode 100644 index 00000000..22cb3c2b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerChannels.kt @@ -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()) + private val connections = Collections.synchronizedList(ArrayList()) + 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() { + 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() { + 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() + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerConnection.kt new file mode 100644 index 00000000..c69f8a23 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerConnection.kt @@ -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() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerPlayer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerPlayer.kt new file mode 100644 index 00000000..26646746 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/ServerPlayer.kt @@ -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(connection, connection.uuid) { + var world: ServerWorld? = null + +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt new file mode 100644 index 00000000..05a6177d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -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(world, pos) { +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt new file mode 100644 index 00000000..e60c9b11 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -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(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() + private val ticketLists = ArrayList() + + interface ITicket { + fun cancel() + val isCanceled: Boolean + val pos: ChunkPos + val id: Int + } + + interface ITimedTicket : ITicket, Comparable { + 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() + private val temporary = ObjectAVLTreeSet() + 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) + } + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Ext.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/JsonUtils.kt similarity index 96% rename from src/main/kotlin/ru/dbotthepony/kstarbound/util/Ext.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/util/JsonUtils.kt index 70dd8d2a..225e245e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Ext.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/JsonUtils.kt @@ -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 MutableIterable>.forEachValid(block: (T) -> Unit) { - val i = iterator() - - for (v in i) { - val get = v.get() - - if (get == null) { - i.remove() - } else { - block.invoke(get) - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/LEStream.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/LEStream.kt deleted file mode 100644 index 5811beb5..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/LEStream.kt +++ /dev/null @@ -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()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt index 31c4bd9c..c92d7f8c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt @@ -37,7 +37,11 @@ private fun > LinkedList.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>() private val timers = LinkedList>() @@ -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) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/StreamUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/StreamUtils.kt new file mode 100644 index 00000000..2e7c2340 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/StreamUtils.kt @@ -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.writeCollection(collection: Collection, writer: S.(V) -> Unit) { + writeVarIntLE(collection.size) + + for (value in collection) { + writer(this, value) + } +} + +fun > 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.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) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt index 2c4aab7d..3a295a5f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt @@ -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 Stream.filterNotNull(): Stream { return filter { it != null } as Stream } + +inline fun MutableIterable>.forEachValid(block: (T) -> Unit) { + val i = iterator() + + for (v in i) { + val get = v.get() + + if (get == null) { + i.remove() + } else { + block.invoke(get) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 8a1eacbd..ed70fe2a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -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, ChunkType : Chunk>( 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, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk() 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, ChunkType : Chunk, ChunkType : Chunk(divideUp(size.x, CHUNK_SIZE), divideUp(size.y, CHUNK_SIZE)) + private val map = Object2DArray.nulls(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, ChunkType : Chunk, ChunkType : Chunk? @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt index 0f0992b2..d4f08b57 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt @@ -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 @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt index 11a7ce83..f75351fb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt @@ -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)