Moar packets implemented, chat, tiles, broadcast, ...

This commit is contained in:
DBotThePony 2024-03-20 00:22:26 +07:00
parent 21f3a66283
commit 94cc53b176
Signed by: DBot
GPG Key ID: DCC23B5715498507
41 changed files with 887 additions and 162 deletions

View File

@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
kotlinVersion=1.9.10 kotlinVersion=1.9.10
kotlinCoroutinesVersion=1.8.0 kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.9.21 kommonsVersion=2.9.23
ffiVersion=2.2.13 ffiVersion=2.2.13
lwjglVersion=3.3.0 lwjglVersion=3.3.0

View File

@ -6,6 +6,7 @@ import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.ClientConfigParameters import ru.dbotthepony.kstarbound.defs.ClientConfigParameters
import ru.dbotthepony.kstarbound.defs.MovementParameters import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.UniverseServerConfig
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig
@ -57,6 +58,9 @@ object GlobalDefaults {
var sky by Delegates.notNull<SkyGlobalConfig>() var sky by Delegates.notNull<SkyGlobalConfig>()
private set private set
var universeServer by Delegates.notNull<UniverseServerConfig>()
private set
private object EmptyTask : ForkJoinTask<Unit>() { private object EmptyTask : ForkJoinTask<Unit>() {
private fun readResolve(): Any = EmptyTask private fun readResolve(): Any = EmptyTask
override fun getRawResult() { override fun getRawResult() {
@ -104,6 +108,7 @@ object GlobalDefaults {
tasks.add(load("/asteroids_worlds.config", ::asteroidWorlds)) tasks.add(load("/asteroids_worlds.config", ::asteroidWorlds))
tasks.add(load("/world_template.config", ::worldTemplate)) tasks.add(load("/world_template.config", ::worldTemplate))
tasks.add(load("/sky.config", ::sky)) tasks.add(load("/sky.config", ::sky))
tasks.add(load("/universe_server.config", ::universeServer))
tasks.add(load("/plants/grassDamage.config", ::grassDamage)) tasks.add(load("/plants/grassDamage.config", ::grassDamage))
tasks.add(load("/plants/treeDamage.config", ::treeDamage)) tasks.add(load("/plants/treeDamage.config", ::treeDamage))

View File

@ -1,9 +1,6 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.future import kotlinx.coroutines.future.future
import kotlinx.coroutines.runBlocking
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.lwjgl.Version import org.lwjgl.Version
import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.io.ByteKey
@ -11,13 +8,11 @@ import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters
import ru.dbotthepony.kstarbound.io.BTreeDB5 import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer
import ru.dbotthepony.kstarbound.server.world.LegacyChunkSource import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerUniverse import ru.dbotthepony.kstarbound.server.world.ServerUniverse
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.entities.ItemEntity import ru.dbotthepony.kstarbound.world.entities.ItemEntity
import java.io.BufferedInputStream import java.io.BufferedInputStream
@ -91,12 +86,9 @@ fun main() {
// println(VersionedJson(meta)) // println(VersionedJson(meta))
val server = IntegratedStarboundServer(File("./"))
val client = StarboundClient.create().get() val client = StarboundClient.create().get()
//val client2 = StarboundClient.create().get() //val client2 = StarboundClient.create().get()
val world = ServerWorld(server, WorldGeometry(Vector2i(3000, 2000), true, false)) //val world = ServerWorld.load(server, LegacyWorldStorage.file(db)).get()
world.addChunkSource(LegacyChunkSource.file(db))
world.thread.start()
//Starbound.addFilePath(File("./unpacked_assets/")) //Starbound.addFilePath(File("./unpacked_assets/"))
@ -113,6 +105,10 @@ fun main() {
Starbound.initializeGame() Starbound.initializeGame()
Starbound.mailboxInitialized.submit { Starbound.mailboxInitialized.submit {
val server = IntegratedStarboundServer(File("./"))
val world = ServerWorld.create(server, WorldGeometry(Vector2i(3000, 2000), true, false), LegacyWorldStorage.file(db))
world.thread.start()
//ply = PlayerEntity(client.world!!) //ply = PlayerEntity(client.world!!)
//ply!!.position = Vector2d(225.0, 680.0) //ply!!.position = Vector2d(225.0, 680.0)

View File

@ -15,7 +15,11 @@ import ru.dbotthepony.kommons.gson.NothingAdapter
import ru.dbotthepony.kommons.gson.Vector2dTypeAdapter import ru.dbotthepony.kommons.gson.Vector2dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector2fTypeAdapter import ru.dbotthepony.kommons.gson.Vector2fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector2iTypeAdapter import ru.dbotthepony.kommons.gson.Vector2iTypeAdapter
import ru.dbotthepony.kommons.gson.Vector3dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector3fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector3iTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4dTypeAdapter import ru.dbotthepony.kommons.gson.Vector4dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter
import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.collect.WeightedList
@ -207,8 +211,12 @@ object Starbound : ISBFileLocator {
registerTypeAdapter(Vector2dTypeAdapter) registerTypeAdapter(Vector2dTypeAdapter)
registerTypeAdapter(Vector2fTypeAdapter) registerTypeAdapter(Vector2fTypeAdapter)
registerTypeAdapter(Vector2iTypeAdapter) registerTypeAdapter(Vector2iTypeAdapter)
registerTypeAdapter(Vector3dTypeAdapter)
registerTypeAdapter(Vector3fTypeAdapter)
registerTypeAdapter(Vector3iTypeAdapter)
registerTypeAdapter(Vector4iTypeAdapter) registerTypeAdapter(Vector4iTypeAdapter)
registerTypeAdapter(Vector4dTypeAdapter) registerTypeAdapter(Vector4dTypeAdapter)
registerTypeAdapter(Vector4fTypeAdapter)
registerTypeAdapterFactory(Line2d.Companion) registerTypeAdapterFactory(Line2d.Companion)
registerTypeAdapterFactory(UniversePos.Companion) registerTypeAdapterFactory(UniversePos.Companion)
registerTypeAdapterFactory(AbstractPerlinNoise.Companion) registerTypeAdapterFactory(AbstractPerlinNoise.Companion)

View File

@ -17,6 +17,7 @@ import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket
import java.net.SocketAddress import java.net.SocketAddress
import java.util.* import java.util.*
@ -50,6 +51,8 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn
} }
} }
private var clientStateNetVersion = 0L
override fun flush() { override fun flush() {
if (!pendingDisconnect) { if (!pendingDisconnect) {
val entries = rpc.write() val entries = rpc.write()
@ -57,6 +60,13 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn
if (entries != null) { if (entries != null) {
channel.write(ClientContextUpdatePacket(entries, KOptional(), KOptional())) channel.write(ClientContextUpdatePacket(entries, KOptional(), KOptional()))
} }
val (data, new) = clientStateGroup.write(clientStateNetVersion)
if (data.isNotEmpty())
channel.write(WorldClientStateUpdatePacket(data))
clientStateNetVersion = new
} }
super.flush() super.flush()

View File

@ -947,7 +947,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
val activeConnection = activeConnection val activeConnection = activeConnection
if (activeConnection != null && !activeConnection.isLegacy && activeConnection.isConnected) if (activeConnection != null && !activeConnection.isLegacy && activeConnection.channel.isOpen)
activeConnection.send(TrackedPositionPacket(camera.pos)) activeConnection.send(TrackedPositionPacket(camera.pos))
uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen } uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }

View File

@ -4,6 +4,7 @@ import ru.dbotthepony.kommons.io.readUUID
import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kstarbound.client.ClientConnection import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.client.world.ClientWorld import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.WorldGeometry
@ -22,7 +23,7 @@ data class JoinWorldPacket(val uuid: UUID, val geometry: WorldGeometry) : IClien
override fun play(connection: ClientConnection) { override fun play(connection: ClientConnection) {
connection.client.mailbox.execute { connection.client.mailbox.execute {
connection.client.world = ClientWorld(connection.client, geometry) connection.client.world = ClientWorld(connection.client, WorldTemplate(geometry))
} }
} }
} }

View File

@ -18,6 +18,7 @@ import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.Mesh import ru.dbotthepony.kstarbound.client.render.Mesh
import ru.dbotthepony.kstarbound.client.render.RenderLayer import ru.dbotthepony.kstarbound.client.render.RenderLayer
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
@ -36,8 +37,8 @@ import kotlin.concurrent.withLock
class ClientWorld( class ClientWorld(
val client: StarboundClient, val client: StarboundClient,
geometry: WorldGeometry, template: WorldTemplate,
) : World<ClientWorld, ClientChunk>(geometry) { ) : World<ClientWorld, ClientChunk>(template) {
private fun determineChunkSize(cells: Int): Int { private fun determineChunkSize(cells: Int): Int {
for (i in 64 downTo 1) { for (i in 64 downTo 1) {
if (cells % i == 0) { if (cells % i == 0) {

View File

@ -0,0 +1,44 @@
package ru.dbotthepony.kstarbound.defs
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.writeBinaryString
import java.io.DataInputStream
import java.io.DataOutputStream
enum class ChatSendMode {
BROADCAST, // Global
LOCAL, // Planet (world)
PARTY; // Party members only
}
data class MessageContext(val mode: Mode, val channelName: String = "") {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(Mode.entries[stream.readUnsignedByte()], stream.readBinaryString())
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(mode.ordinal)
stream.writeBinaryString(channelName)
}
enum class Mode {
LOCAL,
PARTY,
BROADCAST,
WHISPER,
COMMAND_RESULT,
RADIO_MESSAGE,
WORLD;
}
companion object {
val LOCAL = MessageContext(Mode.LOCAL)
val PARTY = MessageContext(Mode.PARTY)
val BROADCAST = MessageContext(Mode.BROADCAST)
val WHISPER = MessageContext(Mode.WHISPER)
val COMMAND_RESULT = MessageContext(Mode.COMMAND_RESULT)
val RADIO_MESSAGE = MessageContext(Mode.RADIO_MESSAGE)
val WORLD = MessageContext(Mode.WORLD)
}
}
// sender 0 is server
data class ChatMessage(val context: MessageContext, val sender: Int = 0, val senderNick: String = "Server", val portrait: String = "", val text: String)

View File

@ -0,0 +1,25 @@
package ru.dbotthepony.kstarbound.defs
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class EntityType(val jsonName: String) : IStringSerializable {
PLAYER("PlayerEntity"),
MONSTER("MonsterEntity"),
OBJECT("ObjectEntity"),
ITEM_DROP("ItemDropEntity"),
PROJECTILE("ProjectileEntity"),
PLANT("PlantEntity"),
PLANT_DROP("PlantDropEntity"), // wat
NPC("NpcEntity"),
STAGEHAND("StagehandEntity"),
VEHICLE("VehicleEntity");
override fun match(name: String): Boolean {
return name == jsonName
}
override fun write(out: JsonWriter) {
out.name(jsonName)
}
}

View File

@ -0,0 +1,10 @@
package ru.dbotthepony.kstarbound.defs
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class UniverseServerConfig(
// in milliseconds
val clockUpdatePacketInterval: Long = 500L,
)

View File

@ -158,7 +158,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
@JsonFactory @JsonFactory
data class StoreData( data class StoreData(
val primaryBiome: String, val primaryBiome: String,
val primarySurfaceLiquid: Either<Int, String>?, val primarySurfaceLiquid: Either<Int, String>? = null,
val sizeName: String, val sizeName: String,
val hueShift: Double, val hueShift: Double,
val skyColoring: SkyColoring, val skyColoring: SkyColoring,

View File

@ -216,7 +216,7 @@ class WorldLayout {
)) as JsonObject )) as JsonObject
} }
fun fromJson(data: JsonObject) { fun fromJson(data: JsonObject): WorldLayout {
val load = Starbound.gson.fromJson(data, SerializedForm::class.java) val load = Starbound.gson.fromJson(data, SerializedForm::class.java)
worldSize = load.worldSize worldSize = load.worldSize
@ -246,6 +246,8 @@ class WorldLayout {
)) ))
} }
} }
return this
} }
private fun buildRegion(random: RandomGenerator, params: RegionParameters): Region { private fun buildRegion(random: RandomGenerator, params: RegionParameters): Region {

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.defs.world package ru.dbotthepony.kstarbound.defs.world
import com.google.gson.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
@ -50,7 +51,7 @@ class WorldTemplate(val geometry: WorldGeometry) {
val skyParameters: SkyParameters = SkyParameters(), val skyParameters: SkyParameters = SkyParameters(),
val seed: Long = 0L, val seed: Long = 0L,
val size: Either<WorldGeometry, Vector2i>, val size: Either<WorldGeometry, Vector2i>,
val regionData: WorldLayout? = null, val regionData: JsonElement = JsonNull.INSTANCE,
//val customTerrainRegions: //val customTerrainRegions:
) )
@ -90,7 +91,7 @@ class WorldTemplate(val geometry: WorldGeometry) {
template.worldParameters = load.worldParameters template.worldParameters = load.worldParameters
template.skyParameters = load.skyParameters template.skyParameters = load.skyParameters
template.seed = load.seed template.seed = load.seed
template.worldLayout = load.regionData template.worldLayout = load.regionData.let { if (it is JsonObject) WorldLayout().fromJson(it) else null }
template.determineName() template.determineName()

View File

@ -14,6 +14,7 @@ abstract class AbstractTerrainSelector<D : Any>(val name: String, val config: D,
fun toJson(): JsonObject { fun toJson(): JsonObject {
val result = JsonObject() val result = JsonObject()
result["name"] = name result["name"] = name
result["type"] = type.jsonName
result["config"] = Starbound.gson.toJsonTree(config) result["config"] = Starbound.gson.toJsonTree(config)
result["parameters"] = Starbound.gson.toJsonTree(parameters) result["parameters"] = Starbound.gson.toJsonTree(parameters)
return result return result

View File

@ -48,7 +48,7 @@ enum class TerrainSelectorType(val jsonName: String) {
} }
fun createFactory(json: JsonObject): TerrainSelectorFactory<*, *> { fun createFactory(json: JsonObject): TerrainSelectorFactory<*, *> {
val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' element of terrain json") val name = json["name"]?.asString ?: ""
val type = json["type"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'type' element of terrain json") val type = json["type"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'type' element of terrain json")
when (type) { when (type) {

View File

@ -331,6 +331,16 @@ class FactoryAdapter<T : Any> private constructor(
if (presentValues.size % 31 != 0) argumentFlagCount++ if (presentValues.size % 31 != 0) argumentFlagCount++
readValues = readValues.copyOf(readValues.size + argumentFlagCount) readValues = readValues.copyOf(readValues.size + argumentFlagCount)
for ((i, field) in types.withIndex()) {
val param = regularFactory.parameters[i]
if (readValues[i] == null && param.isOptional && !param.type.isMarkedNullable) {
// while this makes whole shit way more lenient, at least it avoids silly errors
// caused by quirks in original engine serialization process
presentValues[i] = false
}
}
var flagIndex = readValues.size - argumentFlagCount var flagIndex = readValues.size - argumentFlagCount
var flags = 0 var flags = 0
var flagBit = 0 var flagBit = 0
@ -354,7 +364,7 @@ class FactoryAdapter<T : Any> private constructor(
if (readValues[i] != null) continue if (readValues[i] != null) continue
val param = regularFactory.parameters[i] val param = regularFactory.parameters[i]
if (param.isOptional && (!presentValues[i] || readValues[i] == null && i in syntheticPrimitives)) { if (param.isOptional && (!presentValues[i] || i in syntheticPrimitives)) {
readValues[i] = syntheticPrimitives[i] readValues[i] = syntheticPrimitives[i]
} else if (!param.isOptional) { } else if (!param.isOptional) {
if (!presentValues[i]) throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} is missing") if (!presentValues[i]) throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} is missing")

View File

@ -8,8 +8,11 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.doubles.DoubleArrayList import it.unimi.dsi.fastutil.doubles.DoubleArrayList
import it.unimi.dsi.fastutil.doubles.DoubleArraySet
import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.LongArrayList import it.unimi.dsi.fastutil.longs.LongArrayList
import it.unimi.dsi.fastutil.longs.LongArraySet
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
@ -31,6 +34,11 @@ object CollectionAdapterFactory : TypeAdapterFactory {
IntArrayList::class.java -> Adapter(::IntArrayList, gson.getAdapter(Int::class.java)) IntArrayList::class.java -> Adapter(::IntArrayList, gson.getAdapter(Int::class.java))
LongArrayList::class.java -> Adapter(::LongArrayList, gson.getAdapter(Long::class.java)) LongArrayList::class.java -> Adapter(::LongArrayList, gson.getAdapter(Long::class.java))
DoubleArrayList::class.java -> Adapter(::DoubleArrayList, gson.getAdapter(Double::class.java)) DoubleArrayList::class.java -> Adapter(::DoubleArrayList, gson.getAdapter(Double::class.java))
IntArraySet::class.java -> Adapter(::IntArraySet, gson.getAdapter(Int::class.java))
LongArraySet::class.java -> Adapter(::LongArraySet, gson.getAdapter(Long::class.java))
DoubleArraySet::class.java -> Adapter(::DoubleArraySet, gson.getAdapter(Double::class.java))
else -> null else -> null
} as TypeAdapter<T>? } as TypeAdapter<T>?
} }

View File

@ -6,14 +6,18 @@ import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelOption import io.netty.channel.ChannelOption
import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.nio.NioEventLoopGroup
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket import ru.dbotthepony.kommons.io.VarIntValueCodec
import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.GroupElement
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.player.Avatar import ru.dbotthepony.kstarbound.player.Avatar
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
import java.io.Closeable import java.io.Closeable
import java.util.* import java.util.*
import kotlin.properties.Delegates
abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : ChannelInboundHandlerAdapter(), Closeable { abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : ChannelInboundHandlerAdapter(), Closeable {
abstract override fun channelRead(ctx: ChannelHandlerContext, msg: Any) abstract override fun channelRead(ctx: ChannelHandlerContext, msg: Any)
@ -23,6 +27,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
val rpc = JsonRPC() val rpc = JsonRPC()
var connectionID: Int = -1 var connectionID: Int = -1
var nickname: String = ""
val hasChannel get() = ::channel.isInitialized val hasChannel get() = ::channel.isInitialized
lateinit var channel: Channel lateinit var channel: Channel
@ -31,9 +36,6 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
var isLegacy: Boolean = true var isLegacy: Boolean = true
protected set protected set
var isConnected: Boolean = false
protected set
private val handshakeValidator = PacketRegistry.HANDSHAKE.Validator(side) private val handshakeValidator = PacketRegistry.HANDSHAKE.Validator(side)
private val handshakeSerializer = PacketRegistry.HANDSHAKE.Serializer(side) private val handshakeSerializer = PacketRegistry.HANDSHAKE.Serializer(side)
@ -44,7 +46,6 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
private val legacySerializer = PacketRegistry.LEGACY.Serializer(side) private val legacySerializer = PacketRegistry.LEGACY.Serializer(side)
open fun setupLegacy() { open fun setupLegacy() {
if (isConnected) throw IllegalStateException("Already connected")
LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using legacy protocol") LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using legacy protocol")
if (type == ConnectionType.MEMORY) { if (type == ConnectionType.MEMORY) {
@ -56,11 +57,9 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
} }
isLegacy = true isLegacy = true
isConnected = true
} }
open fun setupNative() { open fun setupNative() {
if (isConnected) throw IllegalStateException("Already connected")
LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using native protocol") LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using native protocol")
if (type == ConnectionType.MEMORY) { if (type == ConnectionType.MEMORY) {
@ -72,14 +71,12 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
} }
isLegacy = false isLegacy = false
isConnected = true
inGame() inGame()
} }
protected open fun onChannelClosed() { protected open fun onChannelClosed() {
isConnected = false LOGGER.info("$this is terminated")
LOGGER.info("Connection to ${channel.remoteAddress()} is closed")
} }
fun bind(channel: Channel) { fun bind(channel: Channel) {
@ -101,12 +98,16 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
abstract fun inGame() abstract fun inGame()
fun send(packet: IPacket) { fun send(packet: IPacket) {
channel.write(packet) if (channel.isOpen) {
channel.write(packet)
}
} }
fun sendAndFlush(packet: IPacket) { fun sendAndFlush(packet: IPacket) {
channel.write(packet) if (channel.isOpen) {
channel.flush() channel.write(packet)
flush()
}
} }
open fun flush() { open fun flush() {
@ -119,6 +120,17 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
channel.close() channel.close()
} }
val windowXMin = networkedSignedInt()
val windowYMin = networkedSignedInt()
val windowWidth = networkedSignedInt()
val windowHeight = networkedSignedInt()
val playerID = networkedSignedInt()
// holy shit
val clientPresenceEntities = BasicNetworkedElement(IntAVLTreeSet(), StreamCodec.Collection(VarIntValueCodec) { IntAVLTreeSet() })
val clientStateGroup = MasterElement(GroupElement(windowXMin, windowYMin, windowWidth, windowHeight, playerID, clientPresenceEntities))
companion object { companion object {
private val EMPTY_UUID = UUID(0L, 0L) private val EMPTY_UUID = UUID(0L, 0L)
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()

View File

@ -20,6 +20,8 @@ import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket
import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket
import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket
import ru.dbotthepony.kstarbound.network.packets.PingPacket import ru.dbotthepony.kstarbound.network.packets.PingPacket
import ru.dbotthepony.kstarbound.network.packets.PongPacket import ru.dbotthepony.kstarbound.network.packets.PongPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientConnectPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientConnectPacket
@ -28,12 +30,17 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.HandshakeChallengeP
import ru.dbotthepony.kstarbound.network.packets.serverbound.HandshakeResponsePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.HandshakeResponsePacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolResponsePacket import ru.dbotthepony.kstarbound.network.packets.ProtocolResponsePacket
import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ChatReceivePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ChatSendPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket
import ru.dbotthepony.kstarbound.server.network.packets.TrackedPositionPacket import ru.dbotthepony.kstarbound.server.network.packets.TrackedPositionPacket
import ru.dbotthepony.kstarbound.server.network.packets.TrackedSizePacket import ru.dbotthepony.kstarbound.server.network.packets.TrackedSizePacket
import java.io.BufferedInputStream import java.io.BufferedInputStream
@ -320,6 +327,7 @@ class PacketRegistry(val isLegacy: Boolean) {
NATIVE.add(::TrackedSizePacket) NATIVE.add(::TrackedSizePacket)
NATIVE.add(::SpawnWorldObjectPacket) NATIVE.add(::SpawnWorldObjectPacket)
NATIVE.add(::ForgetEntityPacket) NATIVE.add(::ForgetEntityPacket)
NATIVE.add(::UniverseTimeUpdatePacket)
HANDSHAKE.add(::ProtocolRequestPacket) HANDSHAKE.add(::ProtocolRequestPacket)
HANDSHAKE.add(::ProtocolResponsePacket) HANDSHAKE.add(::ProtocolResponsePacket)
@ -331,6 +339,8 @@ class PacketRegistry(val isLegacy: Boolean) {
// <-- HandshakeChallenge * // <-- HandshakeChallenge *
// --> HandshakeResponse * // --> HandshakeResponse *
// <-- ConnectSuccess / ConnectFailure // <-- ConnectSuccess / ConnectFailure
// <-- UniverseClockUpdatePacket
// <-- WorldStartPacket
LEGACY.skip("ProtocolRequest") LEGACY.skip("ProtocolRequest")
LEGACY.skip("ProtocolResponse") LEGACY.skip("ProtocolResponse")
@ -340,8 +350,8 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(::ConnectSuccessPacket) // ConnectSuccess LEGACY.add(::ConnectSuccessPacket) // ConnectSuccess
LEGACY.skip("ConnectFailure") LEGACY.skip("ConnectFailure")
LEGACY.add(::HandshakeChallengePacket) // HandshakeChallenge LEGACY.add(::HandshakeChallengePacket) // HandshakeChallenge
LEGACY.skip("ChatReceive") LEGACY.add(::ChatReceivePacket)
LEGACY.skip("UniverseTimeUpdate") LEGACY.add(::UniverseTimeUpdatePacket)
LEGACY.skip("CelestialResponse") LEGACY.skip("CelestialResponse")
LEGACY.skip("PlayerWarpResult") LEGACY.skip("PlayerWarpResult")
LEGACY.skip("PlanetTypeUpdate") LEGACY.skip("PlanetTypeUpdate")
@ -354,7 +364,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(::HandshakeResponsePacket) // HandshakeResponse LEGACY.add(::HandshakeResponsePacket) // HandshakeResponse
LEGACY.skip("PlayerWarp") LEGACY.skip("PlayerWarp")
LEGACY.skip("FlyShip") LEGACY.skip("FlyShip")
LEGACY.skip("ChatSend") LEGACY.add(::ChatSendPacket)
LEGACY.skip("CelestialRequest") LEGACY.skip("CelestialRequest")
// Packets sent bidirectionally between the universe client and the universe // Packets sent bidirectionally between the universe client and the universe
@ -389,14 +399,14 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.skip("SpawnEntity") LEGACY.skip("SpawnEntity")
LEGACY.skip("ConnectWire") LEGACY.skip("ConnectWire")
LEGACY.skip("DisconnectAllWires") LEGACY.skip("DisconnectAllWires")
LEGACY.skip("WorldClientStateUpdate") LEGACY.add(::WorldClientStateUpdatePacket)
LEGACY.skip("FindUniqueEntity") LEGACY.skip("FindUniqueEntity")
LEGACY.skip("WorldStartAcknowledge") LEGACY.skip("WorldStartAcknowledge")
LEGACY.add(PingPacket::read) LEGACY.add(PingPacket::read)
// Packets sent bidirectionally between world client and world server // Packets sent bidirectionally between world client and world server
LEGACY.skip("EntityCreate") LEGACY.add(::EntityCreatePacket)
LEGACY.skip("EntityUpdateSet") LEGACY.add(EntityUpdateSetPacket::read)
LEGACY.skip("EntityDestroy") LEGACY.skip("EntityDestroy")
LEGACY.skip("EntityInteract") LEGACY.skip("EntityInteract")
LEGACY.skip("EntityInteractResult") LEGACY.skip("EntityInteractResult")
@ -406,7 +416,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.skip("EntityMessage") LEGACY.skip("EntityMessage")
LEGACY.skip("EntityMessageResponse") LEGACY.skip("EntityMessageResponse")
LEGACY.skip("UpdateWorldProperties") LEGACY.skip("UpdateWorldProperties")
LEGACY.skip("StepUpdate") LEGACY.add(::StepUpdatePacket)
// Packets sent system server -> system client // Packets sent system server -> system client
LEGACY.skip("SystemWorldStart") LEGACY.skip("SystemWorldStart")

View File

@ -0,0 +1,37 @@
package ru.dbotthepony.kstarbound.network.packets
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
class EntityCreatePacket(val entityType: EntityType, val storeData: ByteArray, val firstNetState: ByteArray, val entityID: Int) : IServerPacket, IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
EntityType.entries[stream.readUnsignedByte()],
stream.readByteArray(),
stream.readByteArray(),
stream.readSignedVarInt()
)
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(entityType.ordinal)
stream.writeByteArray(storeData)
stream.writeByteArray(firstNetState)
stream.writeSignedVarInt(entityID)
}
override fun play(connection: ServerConnection) {
}
override fun play(connection: ClientConnection) {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,53 @@
package ru.dbotthepony.kstarbound.network.packets
import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
class EntityUpdateSetPacket(val forConnection: Int, val deltas: Int2ObjectMap<ByteArray>) : IServerPacket, IClientPacket {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeVarInt(forConnection)
stream.writeVarInt(deltas.size)
for ((k, v) in deltas.entries) {
stream.writeSignedVarInt(k)
stream.writeByteArray(v)
}
}
override fun play(connection: ServerConnection) {
}
override fun play(connection: ClientConnection) {
TODO("Not yet implemented")
}
companion object {
fun read(stream: DataInputStream, isLegacy: Boolean): EntityUpdateSetPacket {
val forConnection = stream.readVarInt()
val size = stream.readVarInt()
val deltas = Int2ObjectAVLTreeMap<ByteArray>()
for (i in 0 until size) {
val k = stream.readSignedVarInt()
val v = stream.readByteArray()
deltas[k] = v
}
return EntityUpdateSetPacket(forConnection, deltas)
}
}
}

View File

@ -0,0 +1,26 @@
package ru.dbotthepony.kstarbound.network.packets
import ru.dbotthepony.kommons.io.readVarLong
import ru.dbotthepony.kommons.io.writeVarLong
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
class StepUpdatePacket(val remoteStep: Long) : IServerPacket, IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVarLong())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeVarLong(remoteStep)
}
override fun play(connection: ServerConnection) {
}
override fun play(connection: ClientConnection) {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,32 @@
package ru.dbotthepony.kstarbound.network.packets.clientbound
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.defs.ChatMessage
import ru.dbotthepony.kstarbound.defs.MessageContext
import ru.dbotthepony.kstarbound.network.IClientPacket
import java.io.DataInputStream
import java.io.DataOutputStream
class ChatReceivePacket(val data: ChatMessage) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(ChatMessage(
MessageContext(stream, isLegacy),
stream.readUnsignedShort(), // bugger, written as short again
stream.readBinaryString(),
stream.readBinaryString(),
stream.readBinaryString(),
))
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
data.context.write(stream, isLegacy)
stream.writeShort(data.sender)
stream.writeBinaryString(data.senderNick)
stream.writeBinaryString(data.portrait)
stream.writeBinaryString(data.text)
}
override fun play(connection: ClientConnection) {
TODO("Not yet implemented")
}
}

View File

@ -43,8 +43,8 @@ class LegacyTileArrayUpdatePacket(val origin: Vector2i, val data: Object2DArray<
stream.writeVarInt(data.rows) stream.writeVarInt(data.rows)
stream.writeVarInt(data.columns) stream.writeVarInt(data.columns)
for (y in data.columnIndices) { for (x in data.rowIndices) {
for (x in data.rowIndices) { for (y in data.columnIndices) {
data[y, x].write(stream) data[y, x].write(stream)
} }
} }
@ -64,8 +64,8 @@ class LegacyTileArrayUpdatePacket(val origin: Vector2i, val data: Object2DArray<
val data = Object2DArray.nulls<LegacyNetworkCellState>(columns, rows) val data = Object2DArray.nulls<LegacyNetworkCellState>(columns, rows)
for (y in data.columnIndices) { for (x in data.rowIndices) {
for (x in data.rowIndices) { for (y in data.columnIndices) {
data[y, x] = LegacyNetworkCellState.read(stream) data[y, x] = LegacyNetworkCellState.read(stream)
} }
} }

View File

@ -0,0 +1,24 @@
package ru.dbotthepony.kstarbound.network.packets.clientbound
import ru.dbotthepony.kommons.io.readSignedVarLong
import ru.dbotthepony.kommons.io.writeSignedVarLong
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.network.IClientPacket
import java.io.DataInputStream
import java.io.DataOutputStream
import kotlin.math.roundToLong
class UniverseTimeUpdatePacket(val time: Double) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(if (isLegacy) stream.readSignedVarLong() * 0.05 else stream.readDouble())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
if (isLegacy)
stream.writeSignedVarLong((time / 0.05).roundToLong())
else
stream.writeDouble(time)
}
override fun play(connection: ClientConnection) {
}
}

View File

@ -0,0 +1,22 @@
package ru.dbotthepony.kstarbound.network.packets.serverbound
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kstarbound.defs.ChatSendMode
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
class ChatSendPacket(val text: String, val mode: ChatSendMode) : IServerPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBinaryString(), ChatSendMode.entries[stream.readUnsignedByte()])
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBinaryString(text)
stream.writeByte(mode.ordinal)
}
override fun play(connection: ServerConnection) {
connection.server.chat.handle(connection, this)
}
}

View File

@ -15,10 +15,10 @@ import ru.dbotthepony.kommons.io.writeKOptional
import ru.dbotthepony.kommons.io.writeMap import ru.dbotthepony.kommons.io.writeMap
import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation
import ru.dbotthepony.kstarbound.defs.player.ShipUpgrades import ru.dbotthepony.kstarbound.defs.player.ShipUpgrades
import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
@ -58,8 +58,13 @@ data class ClientConnectPacket(
override fun play(connection: ServerConnection) { override fun play(connection: ServerConnection) {
LOGGER.info("Client connection request received from ${connection.channel.remoteAddress()}, Player $playerName/$playerUuid (account '$account')") LOGGER.info("Client connection request received from ${connection.channel.remoteAddress()}, Player $playerName/$playerUuid (account '$account')")
connection.nickname = connection.server.reserveNickname(playerName, "Player_${connection.connectionID}")
connection.receiveShipChunks(shipChunks) connection.receiveShipChunks(shipChunks)
connection.sendAndFlush(ConnectSuccessPacket(connection.connectionID, UUID(4L, 4L), CelestialBaseInformation())) connection.send(ConnectSuccessPacket(connection.connectionID, connection.server.serverUUID, connection.server.universe.baseInformation))
connection.send(UniverseTimeUpdatePacket(connection.server.universeClock.seconds))
connection.channel.flush()
connection.inGame() connection.inGame()
} }

View File

@ -0,0 +1,23 @@
package ru.dbotthepony.kstarbound.network.packets.serverbound
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
// general information about client, such as window size and zoom,
// sent as NetworkedElement deltas
class WorldClientStateUpdatePacket(val deltas: ByteArrayList) : IServerPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(ByteArrayList.wrap(stream.readByteArray()))
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByteArray(deltas.elements(), 0, deltas.size)
}
override fun play(connection: ServerConnection) {
connection.clientStateGroup.read(deltas.elements(), 0, deltas.size)
}
}

View File

@ -0,0 +1,69 @@
package ru.dbotthepony.kstarbound.server
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.ChatMessage
import ru.dbotthepony.kstarbound.defs.ChatSendMode
import ru.dbotthepony.kstarbound.defs.MessageContext
import ru.dbotthepony.kstarbound.network.packets.clientbound.ChatReceivePacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ChatSendPacket
class ChatHandler(val server: StarboundServer) {
fun systemMessage(string: String) {
LOGGER.info("Chat: <Server> {}", string)
server.channels.broadcast(ChatReceivePacket(
ChatMessage(
MessageContext.BROADCAST,
text = string
)
))
}
fun handle(source: ServerConnection, packet: ChatSendPacket) {
when (packet.mode) {
ChatSendMode.BROADCAST -> {
LOGGER.info("Chat: <{}> {}", source.nickname, packet.text)
server.channels.broadcast(ChatReceivePacket(ChatMessage(MessageContext.BROADCAST, sender = source.connectionID, senderNick = source.nickname, text = packet.text)))
}
ChatSendMode.LOCAL -> {
val world = source.world
if (world == null) {
LOGGER.warn("{} tried to say something, but they are in limbo: {}", source.nickname, packet.text)
source.sendAndFlush(ChatReceivePacket(
ChatMessage(
MessageContext.COMMAND_RESULT,
text = "You appear to be in limbo, nobody can hear you! Use Global chat."
)
))
} else {
LOGGER.info("Local chat: <{}> {}", source.nickname, packet.text)
world.broadcast(ChatReceivePacket(
ChatMessage(
MessageContext.LOCAL,
sender = source.connectionID,
senderNick = source.nickname,
text = packet.text
)
))
}
}
ChatSendMode.PARTY -> {
source.sendAndFlush(ChatReceivePacket(
ChatMessage(
MessageContext.COMMAND_RESULT,
text = "Party chat not implemented."
)
))
}
}
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -7,9 +7,11 @@ import io.netty.channel.ChannelInitializer
import io.netty.channel.local.LocalAddress import io.netty.channel.local.LocalAddress
import io.netty.channel.local.LocalServerChannel import io.netty.channel.local.LocalServerChannel
import io.netty.channel.socket.nio.NioServerSocketChannel import io.netty.channel.socket.nio.NioServerSocketChannel
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionType import ru.dbotthepony.kstarbound.network.ConnectionType
import ru.dbotthepony.kstarbound.network.IPacket
import java.io.Closeable import java.io.Closeable
import java.net.SocketAddress import java.net.SocketAddress
import java.util.* import java.util.*
@ -19,12 +21,54 @@ import kotlin.concurrent.withLock
class ServerChannels(val server: StarboundServer) : Closeable { class ServerChannels(val server: StarboundServer) : Closeable {
private val channels = CopyOnWriteArrayList<ChannelFuture>() private val channels = CopyOnWriteArrayList<ChannelFuture>()
private val connections = CopyOnWriteArrayList<ServerConnection>() val connections = CopyOnWriteArrayList<ServerConnection>()
private var localChannel: Channel? = null private var localChannel: Channel? = null
private val lock = ReentrantLock() private val lock = ReentrantLock()
private var isClosed = false private var isClosed = false
val connectionsView: List<ServerConnection> = Collections.unmodifiableList(connections) private var nextConnectionID = 0
private val occupiedConnectionIDs = IntAVLTreeSet()
private val connectionIDLock = Any()
private fun cycleConnectionID(): Int {
val v = ++nextConnectionID and 32767
if (v == 0) {
nextConnectionID++
return 1
}
return v
}
fun nextConnectionID(): Int {
synchronized(connectionIDLock) {
var i = 0
while (i++ <= 32767) { // 32767 is the maximum
val get = cycleConnectionID()
if (!occupiedConnectionIDs.contains(get)) {
occupiedConnectionIDs.add(get)
return get
}
}
}
throw IllegalStateException("No more free connection IDs, how did we end up here?")
}
fun freeConnectionID(id: Int): Boolean {
return synchronized(connectionIDLock) {
occupiedConnectionIDs.remove(id)
}
}
fun broadcast(packet: IPacket) {
connections.forEach {
it.send(packet)
}
}
@Suppress("name_shadowing") @Suppress("name_shadowing")
fun createLocalChannel(): Channel { fun createLocalChannel(): Channel {

View File

@ -10,7 +10,6 @@ import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket
@ -22,12 +21,11 @@ import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket
import ru.dbotthepony.kstarbound.server.world.IChunkSource import ru.dbotthepony.kstarbound.server.world.WorldStorage
import ru.dbotthepony.kstarbound.server.world.LegacyChunkSource import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.WorldObject
@ -42,7 +40,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
var skyVersion = 0L var skyVersion = 0L
init { init {
connectionID = server.nextConnectionID.incrementAndGet() connectionID = server.channels.nextConnectionID()
rpc.add("team.fetchTeamStatus") { rpc.add("team.fetchTeamStatus") {
JsonObject() JsonObject()
@ -84,17 +82,17 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
private val shipChunks = Object2ObjectOpenHashMap<ByteKey, KOptional<ByteArray>>() private val shipChunks = Object2ObjectOpenHashMap<ByteKey, KOptional<ByteArray>>()
private val modifiedShipChunks = ObjectOpenHashSet<ByteKey>() private val modifiedShipChunks = ObjectOpenHashSet<ByteKey>()
var shipChunkSource by Delegates.notNull<IChunkSource>() var shipChunkSource by Delegates.notNull<WorldStorage>()
private set private set
override fun setupLegacy() { override fun setupLegacy() {
super.setupLegacy() super.setupLegacy()
shipChunkSource = LegacyChunkSource.memory(shipChunks) shipChunkSource = LegacyWorldStorage.memory(shipChunks)
} }
override fun setupNative() { override fun setupNative() {
super.setupNative() super.setupNative()
shipChunkSource = IChunkSource.Void shipChunkSource = WorldStorage.EMPTY
} }
fun receiveShipChunks(chunks: Map<ByteKey, KOptional<ByteArray>>) { fun receiveShipChunks(chunks: Map<ByteKey, KOptional<ByteArray>>) {
@ -145,13 +143,34 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
override fun onChannelClosed() { override fun onChannelClosed() {
super.onChannelClosed() super.onChannelClosed()
server.channels.freeConnectionID(connectionID)
server.channels.connections.remove(this)
server.freeNickname(nickname)
announceDisconnect("Connection to remote host is lost.")
if (::shipWorld.isInitialized) { if (::shipWorld.isInitialized) {
shipWorld.close() shipWorld.close()
} }
} }
private var announcedDisconnect = false
private fun announceDisconnect(reason: String) {
if (!announcedDisconnect && nickname.isNotBlank()) {
if (reason.isBlank()) {
server.chat.systemMessage("Player '$nickname' disconnected")
} else {
server.chat.systemMessage("Player '$nickname' disconnected ($reason)")
}
announcedDisconnect = true
}
}
override fun disconnect(reason: String) { override fun disconnect(reason: String) {
announceDisconnect(reason)
if (channel.isOpen) { if (channel.isOpen) {
// send pending updates // send pending updates
flush() flush()
@ -177,7 +196,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
val world = world ?: return val world = world ?: return
val trackedPositionChunk = world.geometry.chunkFromCell(trackedPosition) val trackedPositionChunk = world.geometry.chunkFromCell(trackedPosition)
needsToRecomputeTrackedChunks = false needsToRecomputeTrackedChunks = false
if (trackedPositionChunk == this.trackedPositionChunk) return // if (trackedPositionChunk == this.trackedPositionChunk) return
val tracked = ObjectOpenHashSet<ChunkPos>() val tracked = ObjectOpenHashSet<ChunkPos>()
@ -250,14 +269,22 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
override fun inGame() { override fun inGame() {
server.chat.systemMessage("Player '$nickname' connected")
if (!isLegacy) { if (!isLegacy) {
server.playerInGame(this) server.playerInGame(this)
} else { } else {
LOGGER.info("Initializing ship world for $this") LOGGER.info("Initializing ship world for $this")
shipWorld = ServerWorld(server, WorldGeometry(Vector2i(2048, 2048), false, false))
shipWorld.addChunkSource(shipChunkSource) ServerWorld.load(server, shipChunkSource).thenAccept {
shipWorld.thread.start() shipWorld = it
shipWorld.acceptPlayer(this) shipWorld.thread.start()
shipWorld.acceptPlayer(this)
}.exceptionally {
LOGGER.error("Error while initializing shipworld for $this", it)
disconnect("Error while initializing shipworld for player: $it")
null
}
} }
} }

View File

@ -1,18 +1,22 @@
package ru.dbotthepony.kstarbound.server package ru.dbotthepony.kstarbound.server
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket
import ru.dbotthepony.kstarbound.server.world.ServerUniverse import ru.dbotthepony.kstarbound.server.world.ServerUniverse
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.Clock
import ru.dbotthepony.kstarbound.util.ExecutionSpinner import ru.dbotthepony.kstarbound.util.ExecutionSpinner
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.util.Collections import java.util.Collections
import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
sealed class StarboundServer(val root: File) : Closeable { sealed class StarboundServer(val root: File) : Closeable {
init { init {
@ -30,8 +34,7 @@ sealed class StarboundServer(val root: File) : Closeable {
val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS) val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS)
val thread = Thread(spinner, "Starbound Server $serverID") val thread = Thread(spinner, "Starbound Server $serverID")
val universe = ServerUniverse() val universe = ServerUniverse()
val chat = ChatHandler(this)
val nextConnectionID = AtomicInteger()
val settings = ServerSettings() val settings = ServerSettings()
val channels = ServerChannels(this) val channels = ServerChannels(this)
@ -39,7 +42,16 @@ sealed class StarboundServer(val root: File) : Closeable {
var isClosed = false var isClosed = false
private set private set
var serverUUID: UUID = UUID.randomUUID()
protected set
val universeClock = Clock()
init { init {
mailbox.scheduleAtFixedRate(Runnable {
channels.broadcast(UniverseTimeUpdatePacket(universeClock.seconds))
}, GlobalDefaults.universeServer.clockUpdatePacketInterval, GlobalDefaults.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS)
thread.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e -> thread.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e ->
LOGGER.fatal("Unexpected exception in server execution loop, shutting down", e) LOGGER.fatal("Unexpected exception in server execution loop, shutting down", e)
actuallyClose() actuallyClose()
@ -49,6 +61,31 @@ sealed class StarboundServer(val root: File) : Closeable {
thread.start() thread.start()
} }
private val occupiedNicknames = ObjectArraySet<String>()
fun reserveNickname(name: String, alternative: String): String {
synchronized(occupiedNicknames) {
var name = name
if (name.lowercase() == "server" || name.isBlank()) {
name = alternative
}
while (name in occupiedNicknames) {
name += "_"
}
occupiedNicknames.add(name)
return name
}
}
fun freeNickname(name: String): Boolean {
return synchronized(occupiedNicknames) {
occupiedNicknames.remove(name)
}
}
fun playerInGame(player: ServerConnection) { fun playerInGame(player: ServerConnection) {
val world = worlds.first() val world = worlds.first()
world.acceptPlayer(player) world.acceptPlayer(player)
@ -58,7 +95,7 @@ sealed class StarboundServer(val root: File) : Closeable {
private fun spin(): Boolean { private fun spin(): Boolean {
if (isClosed) return false if (isClosed) return false
channels.connectionsView.forEach { if (it.isConnected) it.flush() } channels.connections.forEach { if (it.channel.isOpen) it.flush() }
return !isClosed return !isClosed
} }

View File

@ -1,11 +0,0 @@
package ru.dbotthepony.kstarbound.server.world
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
interface IChunkSaver {
fun saveCells(pos: ChunkPos, data: Object2DArray<out AbstractCell>)
fun saveEntities(pos: ChunkPos, data: Collection<AbstractEntity>)
}

View File

@ -1,25 +0,0 @@
package ru.dbotthepony.kstarbound.server.world
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject
import java.util.concurrent.CompletableFuture
interface IChunkSource {
fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>>
fun getEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>>
object Void : IChunkSource {
override fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> {
return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY)))
}
override fun getEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>> {
return CompletableFuture.completedFuture(KOptional.of(emptyList()))
}
}
}

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.server.world package ru.dbotthepony.kstarbound.server.world
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.io.ByteKey
@ -12,6 +13,7 @@ import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.json.VersionedJson import ru.dbotthepony.kstarbound.json.VersionedJson
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.MutableCell import ru.dbotthepony.kstarbound.world.api.MutableCell
@ -19,22 +21,28 @@ import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.WorldObject
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.Closeable
import java.io.DataInputStream import java.io.DataInputStream
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.function.Function
import java.util.function.Supplier import java.util.function.Supplier
import java.util.zip.InflaterInputStream import java.util.zip.InflaterInputStream
class LegacyChunkSource(val loader: Loader) : IChunkSource { class LegacyWorldStorage(val loader: Loader) : WorldStorage() {
fun interface Loader { fun interface Loader : Closeable {
operator fun invoke(at: ByteKey): CompletableFuture<KOptional<ByteArray>> operator fun invoke(at: ByteKey): CompletableFuture<KOptional<ByteArray>>
override fun close() {
}
} }
override fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> { override fun loadCells(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> {
val chunkX = pos.x val chunkX = pos.x
val chunkY = pos.y val chunkY = pos.y
val key = ByteKey(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()) val key = ByteKey(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())
return loader(key).thenApplyAsync { return loader(key).thenApplyAsync(Function {
it.map { it.map {
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it)))) val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it))))
reader.skipBytes(3) reader.skipBytes(3)
@ -50,15 +58,15 @@ class LegacyChunkSource(val loader: Loader) : IChunkSource {
reader.close() reader.close()
result as Object2DArray<out AbstractCell> result as Object2DArray<out AbstractCell>
} }
} }, Starbound.EXECUTOR)
} }
override fun getEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>> { override fun loadEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>> {
val chunkX = pos.x val chunkX = pos.x
val chunkY = pos.y val chunkY = pos.y
val key = ByteKey(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()) val key = ByteKey(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())
return loader(key).thenApplyAsync { return loader(key).thenApplyAsync(Function {
it.map { it.map {
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it)))) val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it))))
val i = reader.readVarInt() val i = reader.readVarInt()
@ -81,21 +89,51 @@ class LegacyChunkSource(val loader: Loader) : IChunkSource {
reader.close() reader.close()
objects objects
} }
} }, Starbound.EXECUTOR)
}
override fun loadMetadata(): CompletableFuture<KOptional<Metadata>> {
return loader(metadataKey).thenApplyAsync(Function {
it.flatMap {
val stream = DataInputStream(BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(it))))
val width = stream.readInt()
val height = stream.readInt()
val json = VersionedJson(stream)
KOptional(Metadata(WorldGeometry(Vector2i(width, height), true, false), json))
}
}, Starbound.EXECUTOR)
}
override fun close() {
loader.close()
} }
companion object { companion object {
private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
private val metadataKey = ByteKey(0, 0, 0, 0, 0)
fun file(file: BTreeDB5): LegacyChunkSource { fun file(file: BTreeDB5): LegacyWorldStorage {
val carrier = CarriedExecutor(Starbound.IO_EXECUTOR) val carrier = CarriedExecutor(Starbound.IO_EXECUTOR)
val loader = Loader { key -> CompletableFuture.supplyAsync(Supplier { file.read(key) }, carrier) }
return LegacyChunkSource(loader) val loader = object : Loader {
override fun invoke(at: ByteKey): CompletableFuture<KOptional<ByteArray>> {
return CompletableFuture.supplyAsync(Supplier { file.read(at) }, carrier)
}
override fun close() {
file.close()
}
}
return LegacyWorldStorage(loader)
} }
fun memory(backing: Map<ByteKey, KOptional<ByteArray>>): LegacyChunkSource { fun memory(backing: Map<ByteKey, KOptional<ByteArray>>): LegacyWorldStorage {
return LegacyChunkSource { key -> CompletableFuture.completedFuture(backing[key] ?: KOptional()) } return LegacyWorldStorage { key -> CompletableFuture.completedFuture(backing[key] ?: KOptional()) }
} }
} }
} }

View File

@ -1,15 +1,19 @@
package ru.dbotthepony.kstarbound.server.world package ru.dbotthepony.kstarbound.server.world
import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.collect.chainOptionalFutures import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket
import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.StarboundServer
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
@ -22,6 +26,7 @@ import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import java.util.Collections import java.util.Collections
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -31,15 +36,19 @@ import java.util.function.Consumer
import java.util.function.Supplier import java.util.function.Supplier
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
class ServerWorld( class ServerWorld private constructor(
val server: StarboundServer, val server: StarboundServer,
geometry: WorldGeometry, template: WorldTemplate,
) : World<ServerWorld, ServerChunk>(geometry) { val storage: WorldStorage,
) : World<ServerWorld, ServerChunk>(template) {
init { init {
if (server.isClosed)
throw RuntimeException()
server.worlds.add(this) server.worlds.add(this)
} }
private val internalPlayers = ArrayList<ServerConnection>() private val internalPlayers = CopyOnWriteArrayList<ServerConnection>()
val players: List<ServerConnection> = Collections.unmodifiableList(internalPlayers) val players: List<ServerConnection> = Collections.unmodifiableList(internalPlayers)
private fun doAcceptPlayer(player: ServerConnection): Boolean { private fun doAcceptPlayer(player: ServerConnection): Boolean {
@ -48,13 +57,14 @@ class ServerWorld(
player.onLeaveWorld() player.onLeaveWorld()
player.world?.removePlayer(player) player.world?.removePlayer(player)
player.world = this player.world = this
player.trackedPosition = playerSpawnPosition
if (player.isLegacy) { if (player.isLegacy) {
val (skyData, skyVersion) = sky.networkedGroup.write(isLegacy = true) val (skyData, skyVersion) = sky.networkedGroup.write(isLegacy = true)
player.skyVersion = skyVersion player.skyVersion = skyVersion
player.sendAndFlush(WorldStartPacket( player.sendAndFlush(WorldStartPacket(
templateData = WorldTemplate(geometry).toJson(true), templateData = template.toJson(true),
skyData = skyData.toByteArray(), skyData = skyData.toByteArray(),
weatherData = ByteArray(0), weatherData = ByteArray(0),
playerStart = playerSpawnPosition, playerStart = playerSpawnPosition,
@ -62,8 +72,8 @@ class ServerWorld(
respawnInWorld = respawnInWorld, respawnInWorld = respawnInWorld,
dungeonGravity = mapOf(), dungeonGravity = mapOf(),
dungeonBreathable = mapOf(), dungeonBreathable = mapOf(),
protectedDungeonIDs = setOf(), protectedDungeonIDs = protectedDungeonIDs,
worldProperties = JsonObject(), worldProperties = properties.deepCopy(),
connectionID = player.connectionID, connectionID = player.connectionID,
localInterpolationMode = false, localInterpolationMode = false,
)) ))
@ -93,6 +103,8 @@ class ServerWorld(
} }
fun removePlayer(player: ServerConnection): CompletableFuture<Boolean> { fun removePlayer(player: ServerConnection): CompletableFuture<Boolean> {
check(!isClosed.get()) { "$this is invalid" }
try { try {
return CompletableFuture.supplyAsync(Supplier { doRemovePlayer(player) }, mailbox) return CompletableFuture.supplyAsync(Supplier { doRemovePlayer(player) }, mailbox)
} catch (err: RejectedExecutionException) { } catch (err: RejectedExecutionException) {
@ -110,13 +122,6 @@ class ServerWorld(
thread.isDaemon = true thread.isDaemon = true
} }
private val chunkProviders = ArrayList<IChunkSource>()
var saver: IChunkSaver? = null
fun addChunkSource(source: IChunkSource) {
chunkProviders.add(source)
}
fun pause() { fun pause() {
if (!isClosed.get()) spinner.pause() if (!isClosed.get()) spinner.pause()
} }
@ -150,6 +155,7 @@ class ServerWorld(
think() think()
return true return true
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.fatal("Exception in world tick loop", err)
close() close()
return false return false
} }
@ -178,8 +184,8 @@ class ServerWorld(
if (chunk != null) { if (chunk != null) {
val unloadable = chunk.entities.filter { it.isApplicableForUnloading } val unloadable = chunk.entities.filter { it.isApplicableForUnloading }
saver?.saveCells(it.pos, chunk.copyCells()) storage.saveCells(it.pos, chunk.copyCells())
saver?.saveEntities(it.pos, unloadable) storage.saveEntities(it.pos, unloadable)
unloadable.forEach { unloadable.forEach {
it.remove() it.remove()
@ -194,6 +200,12 @@ class ServerWorld(
} }
} }
fun broadcast(packet: IPacket) {
internalPlayers.forEach {
it.send(packet)
}
}
override fun chunkFactory(pos: ChunkPos): ServerChunk { override fun chunkFactory(pos: ChunkPos): ServerChunk {
return ServerChunk(this, pos) return ServerChunk(this, pos)
} }
@ -292,6 +304,7 @@ class ServerWorld(
get() = this@TicketList.pos get() = this@TicketList.pos
final override var isCanceled: Boolean = false final override var isCanceled: Boolean = false
private var loadFuture: CompletableFuture<*>? = null
fun init() { fun init() {
if (first) { if (first) {
@ -304,27 +317,22 @@ class ServerWorld(
val existing = chunkMap[pos] val existing = chunkMap[pos]
if (chunkProviders.isNotEmpty() && existing == null) { if (existing == null) {
chainOptionalFutures(chunkProviders) loadFuture = storage.loadCells(pos).thenAccept { tiles ->
{ if (!isValid) CompletableFuture.completedFuture(KOptional.empty()) else it.getTiles(pos) } if (!tiles.isPresent) return@thenAccept
.thenAccept(Consumer { tiles ->
if (!isValid || !tiles.isPresent) return@Consumer
chainOptionalFutures(chunkProviders) storage.loadEntities(pos).thenAcceptAsync(Consumer { ents ->
{ if (!isValid) CompletableFuture.completedFuture(KOptional.empty()) else it.getEntities(pos) } val chunk = chunkMap.compute(pos) ?: return@Consumer
.thenAcceptAsync(Consumer { ents -> chunk.loadCells(tiles.value)
if (!isValid) return@Consumer
val chunk = chunkMap.compute(pos) ?: return@Consumer
chunk.loadCells(tiles.value)
ents.ifPresent { ents.ifPresent {
for (obj in it) { for (obj in it) {
obj.spawn(this@ServerWorld) obj.spawn(this@ServerWorld)
} }
} }
}, mailbox) }, mailbox)
}) }
} else if (existing != null) { } else {
existing.addListener(this@TicketList) existing.addListener(this@TicketList)
} }
} }
@ -340,6 +348,7 @@ class ServerWorld(
if (isCanceled) return if (isCanceled) return
isCanceled = true isCanceled = true
chunk?.entities?.forEach { e -> listener?.onEntityRemoved(e) } chunk?.entities?.forEach { e -> listener?.onEntityRemoved(e) }
loadFuture?.cancel(false)
onCancel() onCancel()
} }
} }
@ -399,7 +408,40 @@ class ServerWorld(
} }
} }
@JsonFactory
data class MetadataJson(
val playerStart: Vector2d,
val respawnInWorld: Boolean,
val adjustPlayerStart: Boolean,
val worldTemplate: JsonObject,
val centralStructure: JsonElement,
val protectedDungeonIds: IntArraySet,
val worldProperties: JsonObject,
val spawningEnabled: Boolean
)
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
fun create(server: StarboundServer, template: WorldTemplate, storage: WorldStorage): ServerWorld {
return ServerWorld(server, template, storage)
}
fun create(server: StarboundServer, geometry: WorldGeometry, storage: WorldStorage): ServerWorld {
return create(server, WorldTemplate(geometry), storage)
}
fun load(server: StarboundServer, storage: WorldStorage): CompletableFuture<ServerWorld> {
return storage.loadMetadata().thenApply {
val meta = it.map { Starbound.gson.fromJson(it.data.content, MetadataJson::class.java) }.orThrow { NoSuchElementException("No world metadata is present") }
val world = create(server, WorldTemplate.fromJson(meta.worldTemplate), storage)
world.playerSpawnPosition = meta.playerStart
world.respawnInWorld = meta.respawnInWorld
world.adjustPlayerSpawn = meta.adjustPlayerStart
world.protectedDungeonIDs.addAll(meta.protectedDungeonIds)
world
}
}
} }
} }

View File

@ -0,0 +1,93 @@
package ru.dbotthepony.kstarbound.server.world
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.collect.chainOptionalFutures
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.json.VersionedJson
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import java.io.Closeable
import java.util.concurrent.CompletableFuture
abstract class WorldStorage : Closeable {
data class Metadata(val geometry: WorldGeometry, val data: VersionedJson)
abstract fun loadCells(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>>
abstract fun loadEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>>
abstract fun loadMetadata(): CompletableFuture<KOptional<Metadata>>
open fun saveEntities(pos: ChunkPos, data: Collection<AbstractEntity>): Boolean {
return false
}
open fun saveCells(pos: ChunkPos, data: Object2DArray<out AbstractCell>): Boolean {
return false
}
open fun saveMetadata(data: Metadata): Boolean {
return false
}
override fun close() {
}
private class Fixed(private val cell: ImmutableCell) : WorldStorage() {
override fun loadCells(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> {
return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, cell)))
}
override fun loadEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>> {
return CompletableFuture.completedFuture(KOptional.of(emptyList()))
}
override fun loadMetadata(): CompletableFuture<KOptional<Metadata>> {
return CompletableFuture.completedFuture(KOptional())
}
}
companion object {
val NULL: WorldStorage = Fixed(AbstractCell.NULL)
val EMPTY: WorldStorage = Fixed(AbstractCell.EMPTY)
}
class Dispatch(vararg storage: WorldStorage) : WorldStorage() {
private val children = ArrayList<WorldStorage>()
init {
storage.forEach { children.add(it) }
}
override fun loadCells(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> {
return chainOptionalFutures(children) { it.loadCells(pos) }
}
override fun loadEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>> {
return chainOptionalFutures(children) { it.loadEntities(pos) }
}
override fun loadMetadata(): CompletableFuture<KOptional<Metadata>> {
return chainOptionalFutures(children) { it.loadMetadata() }
}
override fun saveEntities(pos: ChunkPos, data: Collection<AbstractEntity>): Boolean {
return children.any { it.saveEntities(pos, data) }
}
override fun saveCells(pos: ChunkPos, data: Object2DArray<out AbstractCell>): Boolean {
return children.any { it.saveCells(pos, data) }
}
override fun saveMetadata(data: Metadata): Boolean {
return children.any { it.saveMetadata(data) }
}
override fun close() {
children.forEach { it.close() }
}
}
}

View File

@ -0,0 +1,36 @@
package ru.dbotthepony.kstarbound.util
import ru.dbotthepony.kommons.util.ITimeSource
class Clock : ITimeSource {
var origin = System.nanoTime()
private set
var baseline = 0L
private set
var isPaused = false
private set
fun set(nanos: Long) {
origin = System.nanoTime()
baseline = nanos
}
fun pause() {
if (!isPaused) {
baseline += System.nanoTime() - origin
isPaused = true
}
}
fun unpause() {
if (isPaused) {
origin = System.nanoTime()
isPaused = false
}
}
override val nanos: Long
get() = if (isPaused) baseline else (System.nanoTime() - origin) + baseline
}

View File

@ -33,12 +33,6 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
var seed: Long = 0L var seed: Long = 0L
private set private set
init {
if (parameters.seed != null) {
init(parameters.seed)
}
}
val scaleD = parameters.scale.toDouble() val scaleD = parameters.scale.toDouble()
protected data class Setup(val b0: Int, val b1: Int, val r0: Double, val r1: Double) protected data class Setup(val b0: Int, val b1: Int, val r0: Double, val r1: Double)
@ -48,6 +42,12 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
protected val g2 by lazy { Double2DArray.allocate(parameters.scale * 2 + 2, 2) } protected val g2 by lazy { Double2DArray.allocate(parameters.scale * 2 + 2, 2) }
protected val g3 by lazy { Double2DArray.allocate(parameters.scale * 2 + 2, 3) } protected val g3 by lazy { Double2DArray.allocate(parameters.scale * 2 + 2, 3) }
init {
if (parameters.seed != null) {
init(parameters.seed)
}
}
fun init(seed: Long) { fun init(seed: Long) {
isInitialized = true isInitialized = true
this.seed = seed this.seed = seed

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
@ -10,6 +12,7 @@ import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.util.ParallelPerform import ru.dbotthepony.kstarbound.util.ParallelPerform
import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ICellAccess
@ -30,11 +33,12 @@ import java.util.function.Predicate
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import java.util.stream.Stream import java.util.stream.Stream
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val geometry: WorldGeometry) : ICellAccess, Closeable { abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess, Closeable {
val background = TileView.Background(this) val background = TileView.Background(this)
val foreground = TileView.Foreground(this) val foreground = TileView.Foreground(this)
val mailbox = MailboxExecutorService() val mailbox = MailboxExecutorService()
val sky = Sky() val sky = Sky()
val geometry: WorldGeometry = template.geometry
override fun getCellDirect(x: Int, y: Int): AbstractCell { override fun getCellDirect(x: Int, y: Int): AbstractCell {
if (!geometry.x.inBoundsCell(x) || !geometry.y.inBoundsCell(y)) return AbstractCell.NULL if (!geometry.x.inBoundsCell(x) || !geometry.y.inBoundsCell(y)) return AbstractCell.NULL
@ -225,6 +229,11 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
protected set protected set
var respawnInWorld = false var respawnInWorld = false
protected set protected set
var adjustPlayerSpawn = false
protected set
val protectedDungeonIDs = IntArraySet()
val properties = JsonObject()
open fun setPlayerSpawn(position: Vector2d, respawnInWorld: Boolean) { open fun setPlayerSpawn(position: Vector2d, respawnInWorld: Boolean) {
playerSpawnPosition = position playerSpawnPosition = position