diff --git a/gradle.properties b/gradle.properties index 064bc69d..5fa37229 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m kotlinVersion=1.9.10 kotlinCoroutinesVersion=1.8.0 -kommonsVersion=2.9.21 +kommonsVersion=2.9.23 ffiVersion=2.2.13 lwjglVersion=3.3.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt index 01da1dad..d3c80b5f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt @@ -6,6 +6,7 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ClientConfigParameters import ru.dbotthepony.kstarbound.defs.MovementParameters +import ru.dbotthepony.kstarbound.defs.UniverseServerConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig @@ -57,6 +58,9 @@ object GlobalDefaults { var sky by Delegates.notNull() private set + var universeServer by Delegates.notNull() + private set + private object EmptyTask : ForkJoinTask() { private fun readResolve(): Any = EmptyTask override fun getRawResult() { @@ -104,6 +108,7 @@ object GlobalDefaults { tasks.add(load("/asteroids_worlds.config", ::asteroidWorlds)) tasks.add(load("/world_template.config", ::worldTemplate)) tasks.add(load("/sky.config", ::sky)) + tasks.add(load("/universe_server.config", ::universeServer)) tasks.add(load("/plants/grassDamage.config", ::grassDamage)) tasks.add(load("/plants/treeDamage.config", ::treeDamage)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 41c3cd5d..ab31722c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -1,9 +1,6 @@ package ru.dbotthepony.kstarbound -import kotlinx.coroutines.async -import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.future -import kotlinx.coroutines.runBlocking import org.apache.logging.log4j.LogManager import org.lwjgl.Version 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.Vector2i import ru.dbotthepony.kstarbound.client.StarboundClient -import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters import ru.dbotthepony.kstarbound.io.BTreeDB5 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.ServerWorld -import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.entities.ItemEntity import java.io.BufferedInputStream @@ -91,12 +86,9 @@ fun main() { // println(VersionedJson(meta)) - val server = IntegratedStarboundServer(File("./")) val client = StarboundClient.create().get() //val client2 = StarboundClient.create().get() - val world = ServerWorld(server, WorldGeometry(Vector2i(3000, 2000), true, false)) - world.addChunkSource(LegacyChunkSource.file(db)) - world.thread.start() + //val world = ServerWorld.load(server, LegacyWorldStorage.file(db)).get() //Starbound.addFilePath(File("./unpacked_assets/")) @@ -113,6 +105,10 @@ fun main() { Starbound.initializeGame() 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!!.position = Vector2d(225.0, 680.0) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 2f9709bf..c030d288 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -15,7 +15,11 @@ import ru.dbotthepony.kommons.gson.NothingAdapter import ru.dbotthepony.kommons.gson.Vector2dTypeAdapter import ru.dbotthepony.kommons.gson.Vector2fTypeAdapter 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.Vector4fTypeAdapter import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kstarbound.collect.WeightedList @@ -207,8 +211,12 @@ object Starbound : ISBFileLocator { registerTypeAdapter(Vector2dTypeAdapter) registerTypeAdapter(Vector2fTypeAdapter) registerTypeAdapter(Vector2iTypeAdapter) + registerTypeAdapter(Vector3dTypeAdapter) + registerTypeAdapter(Vector3fTypeAdapter) + registerTypeAdapter(Vector3iTypeAdapter) registerTypeAdapter(Vector4iTypeAdapter) registerTypeAdapter(Vector4dTypeAdapter) + registerTypeAdapter(Vector4fTypeAdapter) registerTypeAdapterFactory(Line2d.Companion) registerTypeAdapterFactory(UniversePos.Companion) registerTypeAdapterFactory(AbstractPerlinNoise.Companion) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt index 5467af1b..45a40c02 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt @@ -17,6 +17,7 @@ import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket +import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket import java.net.SocketAddress import java.util.* @@ -50,6 +51,8 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn } } + private var clientStateNetVersion = 0L + override fun flush() { if (!pendingDisconnect) { val entries = rpc.write() @@ -57,6 +60,13 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn if (entries != null) { channel.write(ClientContextUpdatePacket(entries, KOptional(), KOptional())) } + + val (data, new) = clientStateGroup.write(clientStateNetVersion) + + if (data.isNotEmpty()) + channel.write(WorldClientStateUpdatePacket(data)) + + clientStateNetVersion = new } super.flush() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 73b60229..ff7b36cb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -947,7 +947,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { val activeConnection = activeConnection - if (activeConnection != null && !activeConnection.isLegacy && activeConnection.isConnected) + if (activeConnection != null && !activeConnection.isLegacy && activeConnection.channel.isOpen) activeConnection.send(TrackedPositionPacket(camera.pos)) uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen } 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 index 834ac41a..0c75423f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt @@ -4,6 +4,7 @@ import ru.dbotthepony.kommons.io.readUUID import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kstarbound.client.ClientConnection import ru.dbotthepony.kstarbound.client.world.ClientWorld +import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.world.World 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) { connection.client.mailbox.execute { - connection.client.world = ClientWorld(connection.client, geometry) + connection.client.world = ClientWorld(connection.client, WorldTemplate(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 9c0fbbf2..e851e97f 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.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.Mesh import ru.dbotthepony.kstarbound.client.render.RenderLayer 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.roundTowardsPositiveInfinity import ru.dbotthepony.kstarbound.world.CHUNK_SIZE @@ -36,8 +37,8 @@ import kotlin.concurrent.withLock class ClientWorld( val client: StarboundClient, - geometry: WorldGeometry, -) : World(geometry) { + template: WorldTemplate, +) : World(template) { private fun determineChunkSize(cells: Int): Int { for (i in 64 downTo 1) { if (cells % i == 0) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ChatDefs.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ChatDefs.kt new file mode 100644 index 00000000..a249abe4 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ChatDefs.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt new file mode 100644 index 00000000..30471685 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt @@ -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) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt new file mode 100644 index 00000000..a43a78f2 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt @@ -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, +) + diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt index 5ba95caf..595e4f1e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt @@ -158,7 +158,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { @JsonFactory data class StoreData( val primaryBiome: String, - val primarySurfaceLiquid: Either?, + val primarySurfaceLiquid: Either? = null, val sizeName: String, val hueShift: Double, val skyColoring: SkyColoring, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt index bf5005f2..1872c1ac 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt @@ -216,7 +216,7 @@ class WorldLayout { )) as JsonObject } - fun fromJson(data: JsonObject) { + fun fromJson(data: JsonObject): WorldLayout { val load = Starbound.gson.fromJson(data, SerializedForm::class.java) worldSize = load.worldSize @@ -246,6 +246,8 @@ class WorldLayout { )) } } + + return this } private fun buildRegion(random: RandomGenerator, params: RegionParameters): Region { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt index 3faef8ba..c59274f3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.defs.world +import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.JsonObject import ru.dbotthepony.kommons.gson.set @@ -50,7 +51,7 @@ class WorldTemplate(val geometry: WorldGeometry) { val skyParameters: SkyParameters = SkyParameters(), val seed: Long = 0L, val size: Either, - val regionData: WorldLayout? = null, + val regionData: JsonElement = JsonNull.INSTANCE, //val customTerrainRegions: ) @@ -90,7 +91,7 @@ class WorldTemplate(val geometry: WorldGeometry) { template.worldParameters = load.worldParameters template.skyParameters = load.skyParameters template.seed = load.seed - template.worldLayout = load.regionData + template.worldLayout = load.regionData.let { if (it is JsonObject) WorldLayout().fromJson(it) else null } template.determineName() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/AbstractTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/AbstractTerrainSelector.kt index 084eb41b..6c60d5f7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/AbstractTerrainSelector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/AbstractTerrainSelector.kt @@ -14,6 +14,7 @@ abstract class AbstractTerrainSelector(val name: String, val config: D, fun toJson(): JsonObject { val result = JsonObject() result["name"] = name + result["type"] = type.jsonName result["config"] = Starbound.gson.toJsonTree(config) result["parameters"] = Starbound.gson.toJsonTree(parameters) return result diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorType.kt index f052027a..487c38b7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorType.kt @@ -48,7 +48,7 @@ enum class TerrainSelectorType(val jsonName: String) { } 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") when (type) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt index c9a64dfd..2c04b929 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt @@ -331,6 +331,16 @@ class FactoryAdapter private constructor( if (presentValues.size % 31 != 0) 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 flags = 0 var flagBit = 0 @@ -354,7 +364,7 @@ class FactoryAdapter private constructor( if (readValues[i] != null) continue 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] } else if (!param.isOptional) { if (!presentValues[i]) throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} is missing") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/CollectionAdapterFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/CollectionAdapterFactory.kt index ba730e77..c4d3f037 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/CollectionAdapterFactory.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/CollectionAdapterFactory.kt @@ -8,8 +8,11 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter 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.IntArraySet import it.unimi.dsi.fastutil.longs.LongArrayList +import it.unimi.dsi.fastutil.longs.LongArraySet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import ru.dbotthepony.kommons.gson.consumeNull import java.lang.reflect.ParameterizedType @@ -31,6 +34,11 @@ object CollectionAdapterFactory : TypeAdapterFactory { IntArrayList::class.java -> Adapter(::IntArrayList, gson.getAdapter(Int::class.java)) LongArrayList::class.java -> Adapter(::LongArrayList, gson.getAdapter(Long::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 } as TypeAdapter? } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index 749ed270..6af7aa04 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -6,14 +6,18 @@ import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelInboundHandlerAdapter import io.netty.channel.ChannelOption import io.netty.channel.nio.NioEventLoopGroup +import it.unimi.dsi.fastutil.ints.IntAVLTreeSet import org.apache.logging.log4j.LogManager -import ru.dbotthepony.kommons.util.KOptional -import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket +import ru.dbotthepony.kommons.io.StreamCodec +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.world.entities.PlayerEntity import java.io.Closeable import java.util.* -import kotlin.properties.Delegates abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : ChannelInboundHandlerAdapter(), Closeable { abstract override fun channelRead(ctx: ChannelHandlerContext, msg: Any) @@ -23,6 +27,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : val rpc = JsonRPC() var connectionID: Int = -1 + var nickname: String = "" val hasChannel get() = ::channel.isInitialized lateinit var channel: Channel @@ -31,9 +36,6 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : var isLegacy: Boolean = true protected set - var isConnected: Boolean = false - protected set - private val handshakeValidator = PacketRegistry.HANDSHAKE.Validator(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) open fun setupLegacy() { - if (isConnected) throw IllegalStateException("Already connected") LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using legacy protocol") if (type == ConnectionType.MEMORY) { @@ -56,11 +57,9 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : } isLegacy = true - isConnected = true } open fun setupNative() { - if (isConnected) throw IllegalStateException("Already connected") LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using native protocol") if (type == ConnectionType.MEMORY) { @@ -72,14 +71,12 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : } isLegacy = false - isConnected = true inGame() } protected open fun onChannelClosed() { - isConnected = false - LOGGER.info("Connection to ${channel.remoteAddress()} is closed") + LOGGER.info("$this is terminated") } fun bind(channel: Channel) { @@ -101,12 +98,16 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : abstract fun inGame() fun send(packet: IPacket) { - channel.write(packet) + if (channel.isOpen) { + channel.write(packet) + } } fun sendAndFlush(packet: IPacket) { - channel.write(packet) - channel.flush() + if (channel.isOpen) { + channel.write(packet) + flush() + } } open fun flush() { @@ -119,6 +120,17 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : 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 { private val EMPTY_UUID = UUID(0L, 0L) private val LOGGER = LogManager.getLogger() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index 39e85694..f0de6459 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -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.SpawnWorldObjectPacket 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.PongPacket 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.ProtocolRequestPacket 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.LegacyTileUpdatePacket 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.WorldStopPacket +import ru.dbotthepony.kstarbound.network.packets.serverbound.ChatSendPacket 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.TrackedSizePacket import java.io.BufferedInputStream @@ -320,6 +327,7 @@ class PacketRegistry(val isLegacy: Boolean) { NATIVE.add(::TrackedSizePacket) NATIVE.add(::SpawnWorldObjectPacket) NATIVE.add(::ForgetEntityPacket) + NATIVE.add(::UniverseTimeUpdatePacket) HANDSHAKE.add(::ProtocolRequestPacket) HANDSHAKE.add(::ProtocolResponsePacket) @@ -331,6 +339,8 @@ class PacketRegistry(val isLegacy: Boolean) { // <-- HandshakeChallenge * // --> HandshakeResponse * // <-- ConnectSuccess / ConnectFailure + // <-- UniverseClockUpdatePacket + // <-- WorldStartPacket LEGACY.skip("ProtocolRequest") LEGACY.skip("ProtocolResponse") @@ -340,8 +350,8 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(::ConnectSuccessPacket) // ConnectSuccess LEGACY.skip("ConnectFailure") LEGACY.add(::HandshakeChallengePacket) // HandshakeChallenge - LEGACY.skip("ChatReceive") - LEGACY.skip("UniverseTimeUpdate") + LEGACY.add(::ChatReceivePacket) + LEGACY.add(::UniverseTimeUpdatePacket) LEGACY.skip("CelestialResponse") LEGACY.skip("PlayerWarpResult") LEGACY.skip("PlanetTypeUpdate") @@ -354,7 +364,7 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(::HandshakeResponsePacket) // HandshakeResponse LEGACY.skip("PlayerWarp") LEGACY.skip("FlyShip") - LEGACY.skip("ChatSend") + LEGACY.add(::ChatSendPacket) LEGACY.skip("CelestialRequest") // Packets sent bidirectionally between the universe client and the universe @@ -389,14 +399,14 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.skip("SpawnEntity") LEGACY.skip("ConnectWire") LEGACY.skip("DisconnectAllWires") - LEGACY.skip("WorldClientStateUpdate") + LEGACY.add(::WorldClientStateUpdatePacket) LEGACY.skip("FindUniqueEntity") LEGACY.skip("WorldStartAcknowledge") LEGACY.add(PingPacket::read) // Packets sent bidirectionally between world client and world server - LEGACY.skip("EntityCreate") - LEGACY.skip("EntityUpdateSet") + LEGACY.add(::EntityCreatePacket) + LEGACY.add(EntityUpdateSetPacket::read) LEGACY.skip("EntityDestroy") LEGACY.skip("EntityInteract") LEGACY.skip("EntityInteractResult") @@ -406,7 +416,7 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.skip("EntityMessage") LEGACY.skip("EntityMessageResponse") LEGACY.skip("UpdateWorldProperties") - LEGACY.skip("StepUpdate") + LEGACY.add(::StepUpdatePacket) // Packets sent system server -> system client LEGACY.skip("SystemWorldStart") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt new file mode 100644 index 00000000..8a28297b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt @@ -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") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt new file mode 100644 index 00000000..25268963 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt @@ -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) : 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() + + for (i in 0 until size) { + val k = stream.readSignedVarInt() + val v = stream.readByteArray() + deltas[k] = v + } + + return EntityUpdateSetPacket(forConnection, deltas) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/StepUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/StepUpdatePacket.kt new file mode 100644 index 00000000..f43d92c1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/StepUpdatePacket.kt @@ -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") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ChatReceivePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ChatReceivePacket.kt new file mode 100644 index 00000000..d5aef135 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ChatReceivePacket.kt @@ -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") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/LegacyTileUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/LegacyTileUpdatePacket.kt index 10f5fe61..f858fbc6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/LegacyTileUpdatePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/LegacyTileUpdatePacket.kt @@ -43,8 +43,8 @@ class LegacyTileArrayUpdatePacket(val origin: Vector2i, val data: Object2DArray< stream.writeVarInt(data.rows) 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) } } @@ -64,8 +64,8 @@ class LegacyTileArrayUpdatePacket(val origin: Vector2i, val data: Object2DArray< val data = Object2DArray.nulls(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) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UniverseTimeUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UniverseTimeUpdatePacket.kt new file mode 100644 index 00000000..f8b508a6 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UniverseTimeUpdatePacket.kt @@ -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) { + + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ChatSendPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ChatSendPacket.kt new file mode 100644 index 00000000..814b894e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ChatSendPacket.kt @@ -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) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt index c73ac18e..c718c10f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt @@ -15,10 +15,10 @@ import ru.dbotthepony.kommons.io.writeKOptional import ru.dbotthepony.kommons.io.writeMap import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kommons.util.KOptional -import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation import ru.dbotthepony.kstarbound.defs.player.ShipUpgrades import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket import ru.dbotthepony.kstarbound.server.ServerConnection import java.io.DataInputStream import java.io.DataOutputStream @@ -58,8 +58,13 @@ data class ClientConnectPacket( override fun play(connection: ServerConnection) { 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.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() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/WorldClientStateUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/WorldClientStateUpdatePacket.kt new file mode 100644 index 00000000..fc167eaf --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/WorldClientStateUpdatePacket.kt @@ -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) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ChatHandler.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ChatHandler.kt new file mode 100644 index 00000000..af300c8f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ChatHandler.kt @@ -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: {}", 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() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt index 441dbd09..10ed265c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt @@ -7,9 +7,11 @@ 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 it.unimi.dsi.fastutil.ints.IntAVLTreeSet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.ConnectionType +import ru.dbotthepony.kstarbound.network.IPacket import java.io.Closeable import java.net.SocketAddress import java.util.* @@ -19,12 +21,54 @@ import kotlin.concurrent.withLock class ServerChannels(val server: StarboundServer) : Closeable { private val channels = CopyOnWriteArrayList() - private val connections = CopyOnWriteArrayList() + val connections = CopyOnWriteArrayList() private var localChannel: Channel? = null private val lock = ReentrantLock() private var isClosed = false - val connectionsView: List = 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") fun createLocalChannel(): Channel { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index 3e46f517..8ea3687a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -10,7 +10,6 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.util.KOptional 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.ChunkCellsPacket 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.clientbound.LegacyTileArrayUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket -import ru.dbotthepony.kstarbound.server.world.IChunkSource -import ru.dbotthepony.kstarbound.server.world.LegacyChunkSource +import ru.dbotthepony.kstarbound.server.world.WorldStorage +import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.IChunkListener -import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.WorldObject @@ -42,7 +40,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn var skyVersion = 0L init { - connectionID = server.nextConnectionID.incrementAndGet() + connectionID = server.channels.nextConnectionID() rpc.add("team.fetchTeamStatus") { JsonObject() @@ -84,17 +82,17 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn private val shipChunks = Object2ObjectOpenHashMap>() private val modifiedShipChunks = ObjectOpenHashSet() - var shipChunkSource by Delegates.notNull() + var shipChunkSource by Delegates.notNull() private set override fun setupLegacy() { super.setupLegacy() - shipChunkSource = LegacyChunkSource.memory(shipChunks) + shipChunkSource = LegacyWorldStorage.memory(shipChunks) } override fun setupNative() { super.setupNative() - shipChunkSource = IChunkSource.Void + shipChunkSource = WorldStorage.EMPTY } fun receiveShipChunks(chunks: Map>) { @@ -145,13 +143,34 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn override fun 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) { 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) { + announceDisconnect(reason) + if (channel.isOpen) { // send pending updates flush() @@ -177,7 +196,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn val world = world ?: return val trackedPositionChunk = world.geometry.chunkFromCell(trackedPosition) needsToRecomputeTrackedChunks = false - if (trackedPositionChunk == this.trackedPositionChunk) return + // if (trackedPositionChunk == this.trackedPositionChunk) return val tracked = ObjectOpenHashSet() @@ -250,14 +269,22 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } override fun inGame() { + server.chat.systemMessage("Player '$nickname' connected") + if (!isLegacy) { server.playerInGame(this) } else { LOGGER.info("Initializing ship world for $this") - shipWorld = ServerWorld(server, WorldGeometry(Vector2i(2048, 2048), false, false)) - shipWorld.addChunkSource(shipChunkSource) - shipWorld.thread.start() - shipWorld.acceptPlayer(this) + + ServerWorld.load(server, shipChunkSource).thenAccept { + shipWorld = it + 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 + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 8fe14eaf..564c5a5a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -1,18 +1,22 @@ package ru.dbotthepony.kstarbound.server +import it.unimi.dsi.fastutil.objects.ObjectArraySet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.MailboxExecutorService +import ru.dbotthepony.kstarbound.GlobalDefaults 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.ServerWorld +import ru.dbotthepony.kstarbound.util.Clock import ru.dbotthepony.kstarbound.util.ExecutionSpinner import java.io.Closeable import java.io.File import java.util.Collections +import java.util.UUID +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock sealed class StarboundServer(val root: File) : Closeable { init { @@ -30,8 +34,7 @@ sealed class StarboundServer(val root: File) : Closeable { val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS) val thread = Thread(spinner, "Starbound Server $serverID") val universe = ServerUniverse() - - val nextConnectionID = AtomicInteger() + val chat = ChatHandler(this) val settings = ServerSettings() val channels = ServerChannels(this) @@ -39,7 +42,16 @@ sealed class StarboundServer(val root: File) : Closeable { var isClosed = false private set + var serverUUID: UUID = UUID.randomUUID() + protected set + + val universeClock = Clock() + init { + mailbox.scheduleAtFixedRate(Runnable { + channels.broadcast(UniverseTimeUpdatePacket(universeClock.seconds)) + }, GlobalDefaults.universeServer.clockUpdatePacketInterval, GlobalDefaults.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS) + thread.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e -> LOGGER.fatal("Unexpected exception in server execution loop, shutting down", e) actuallyClose() @@ -49,6 +61,31 @@ sealed class StarboundServer(val root: File) : Closeable { thread.start() } + private val occupiedNicknames = ObjectArraySet() + + 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) { val world = worlds.first() world.acceptPlayer(player) @@ -58,7 +95,7 @@ sealed class StarboundServer(val root: File) : Closeable { private fun spin(): Boolean { if (isClosed) return false - channels.connectionsView.forEach { if (it.isConnected) it.flush() } + channels.connections.forEach { if (it.channel.isOpen) it.flush() } return !isClosed } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSaver.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSaver.kt deleted file mode 100644 index 43913cbe..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSaver.kt +++ /dev/null @@ -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) - fun saveEntities(pos: ChunkPos, data: Collection) -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt deleted file mode 100644 index 1c030b52..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt +++ /dev/null @@ -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>> - fun getEntities(pos: ChunkPos): CompletableFuture>> - - object Void : IChunkSource { - override fun getTiles(pos: ChunkPos): CompletableFuture>> { - return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY))) - } - - override fun getEntities(pos: ChunkPos): CompletableFuture>> { - return CompletableFuture.completedFuture(KOptional.of(emptyList())) - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt similarity index 63% rename from src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt index 22054a5c..81f28209 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.server.world +import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.arrays.Object2DArray 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.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.api.MutableCell @@ -19,22 +21,28 @@ import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.WorldObject import java.io.BufferedInputStream import java.io.ByteArrayInputStream +import java.io.Closeable import java.io.DataInputStream import java.util.concurrent.CompletableFuture +import java.util.function.Function import java.util.function.Supplier import java.util.zip.InflaterInputStream -class LegacyChunkSource(val loader: Loader) : IChunkSource { - fun interface Loader { +class LegacyWorldStorage(val loader: Loader) : WorldStorage() { + fun interface Loader : Closeable { operator fun invoke(at: ByteKey): CompletableFuture> + + override fun close() { + + } } - override fun getTiles(pos: ChunkPos): CompletableFuture>> { + override fun loadCells(pos: ChunkPos): CompletableFuture>> { val chunkX = pos.x val chunkY = pos.y 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 { val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it)))) reader.skipBytes(3) @@ -50,15 +58,15 @@ class LegacyChunkSource(val loader: Loader) : IChunkSource { reader.close() result as Object2DArray } - } + }, Starbound.EXECUTOR) } - override fun getEntities(pos: ChunkPos): CompletableFuture>> { + override fun loadEntities(pos: ChunkPos): CompletableFuture>> { val chunkX = pos.x val chunkY = pos.y 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 { val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it)))) val i = reader.readVarInt() @@ -81,21 +89,51 @@ class LegacyChunkSource(val loader: Loader) : IChunkSource { reader.close() objects } - } + }, Starbound.EXECUTOR) + } + + override fun loadMetadata(): CompletableFuture> { + 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 { private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } 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 loader = Loader { key -> CompletableFuture.supplyAsync(Supplier { file.read(key) }, carrier) } - return LegacyChunkSource(loader) + + val loader = object : Loader { + override fun invoke(at: ByteKey): CompletableFuture> { + return CompletableFuture.supplyAsync(Supplier { file.read(at) }, carrier) + } + + override fun close() { + file.close() + } + } + + return LegacyWorldStorage(loader) } - fun memory(backing: Map>): LegacyChunkSource { - return LegacyChunkSource { key -> CompletableFuture.completedFuture(backing[key] ?: KOptional()) } + fun memory(backing: Map>): LegacyWorldStorage { + return LegacyWorldStorage { key -> CompletableFuture.completedFuture(backing[key] ?: KOptional()) } } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index 33b8fa34..35b79ca7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -1,15 +1,19 @@ package ru.dbotthepony.kstarbound.server.world +import com.google.gson.JsonElement 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.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import org.apache.logging.log4j.LogManager -import ru.dbotthepony.kommons.collect.chainOptionalFutures -import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket 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.server.StarboundServer 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 java.util.Collections import java.util.concurrent.CompletableFuture +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.RejectedExecutionException import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger @@ -31,15 +36,19 @@ import java.util.function.Consumer import java.util.function.Supplier import kotlin.concurrent.withLock -class ServerWorld( +class ServerWorld private constructor( val server: StarboundServer, - geometry: WorldGeometry, -) : World(geometry) { + template: WorldTemplate, + val storage: WorldStorage, +) : World(template) { init { + if (server.isClosed) + throw RuntimeException() + server.worlds.add(this) } - private val internalPlayers = ArrayList() + private val internalPlayers = CopyOnWriteArrayList() val players: List = Collections.unmodifiableList(internalPlayers) private fun doAcceptPlayer(player: ServerConnection): Boolean { @@ -48,13 +57,14 @@ class ServerWorld( player.onLeaveWorld() player.world?.removePlayer(player) player.world = this + player.trackedPosition = playerSpawnPosition if (player.isLegacy) { val (skyData, skyVersion) = sky.networkedGroup.write(isLegacy = true) player.skyVersion = skyVersion player.sendAndFlush(WorldStartPacket( - templateData = WorldTemplate(geometry).toJson(true), + templateData = template.toJson(true), skyData = skyData.toByteArray(), weatherData = ByteArray(0), playerStart = playerSpawnPosition, @@ -62,8 +72,8 @@ class ServerWorld( respawnInWorld = respawnInWorld, dungeonGravity = mapOf(), dungeonBreathable = mapOf(), - protectedDungeonIDs = setOf(), - worldProperties = JsonObject(), + protectedDungeonIDs = protectedDungeonIDs, + worldProperties = properties.deepCopy(), connectionID = player.connectionID, localInterpolationMode = false, )) @@ -93,6 +103,8 @@ class ServerWorld( } fun removePlayer(player: ServerConnection): CompletableFuture { + check(!isClosed.get()) { "$this is invalid" } + try { return CompletableFuture.supplyAsync(Supplier { doRemovePlayer(player) }, mailbox) } catch (err: RejectedExecutionException) { @@ -110,13 +122,6 @@ class ServerWorld( thread.isDaemon = true } - private val chunkProviders = ArrayList() - var saver: IChunkSaver? = null - - fun addChunkSource(source: IChunkSource) { - chunkProviders.add(source) - } - fun pause() { if (!isClosed.get()) spinner.pause() } @@ -150,6 +155,7 @@ class ServerWorld( think() return true } catch (err: Throwable) { + LOGGER.fatal("Exception in world tick loop", err) close() return false } @@ -178,8 +184,8 @@ class ServerWorld( if (chunk != null) { val unloadable = chunk.entities.filter { it.isApplicableForUnloading } - saver?.saveCells(it.pos, chunk.copyCells()) - saver?.saveEntities(it.pos, unloadable) + storage.saveCells(it.pos, chunk.copyCells()) + storage.saveEntities(it.pos, unloadable) unloadable.forEach { it.remove() @@ -194,6 +200,12 @@ class ServerWorld( } } + fun broadcast(packet: IPacket) { + internalPlayers.forEach { + it.send(packet) + } + } + override fun chunkFactory(pos: ChunkPos): ServerChunk { return ServerChunk(this, pos) } @@ -292,6 +304,7 @@ class ServerWorld( get() = this@TicketList.pos final override var isCanceled: Boolean = false + private var loadFuture: CompletableFuture<*>? = null fun init() { if (first) { @@ -304,27 +317,22 @@ class ServerWorld( val existing = chunkMap[pos] - if (chunkProviders.isNotEmpty() && existing == null) { - chainOptionalFutures(chunkProviders) - { if (!isValid) CompletableFuture.completedFuture(KOptional.empty()) else it.getTiles(pos) } - .thenAccept(Consumer { tiles -> - if (!isValid || !tiles.isPresent) return@Consumer + if (existing == null) { + loadFuture = storage.loadCells(pos).thenAccept { tiles -> + if (!tiles.isPresent) return@thenAccept - chainOptionalFutures(chunkProviders) - { if (!isValid) CompletableFuture.completedFuture(KOptional.empty()) else it.getEntities(pos) } - .thenAcceptAsync(Consumer { ents -> - if (!isValid) return@Consumer - val chunk = chunkMap.compute(pos) ?: return@Consumer - chunk.loadCells(tiles.value) + storage.loadEntities(pos).thenAcceptAsync(Consumer { ents -> + val chunk = chunkMap.compute(pos) ?: return@Consumer + chunk.loadCells(tiles.value) - ents.ifPresent { - for (obj in it) { - obj.spawn(this@ServerWorld) - } - } - }, mailbox) - }) - } else if (existing != null) { + ents.ifPresent { + for (obj in it) { + obj.spawn(this@ServerWorld) + } + } + }, mailbox) + } + } else { existing.addListener(this@TicketList) } } @@ -340,6 +348,7 @@ class ServerWorld( if (isCanceled) return isCanceled = true chunk?.entities?.forEach { e -> listener?.onEntityRemoved(e) } + loadFuture?.cancel(false) 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 { 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 { + 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 + } + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/WorldStorage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/WorldStorage.kt new file mode 100644 index 00000000..621c6d19 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/WorldStorage.kt @@ -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>> + abstract fun loadEntities(pos: ChunkPos): CompletableFuture>> + abstract fun loadMetadata(): CompletableFuture> + + open fun saveEntities(pos: ChunkPos, data: Collection): Boolean { + return false + } + + open fun saveCells(pos: ChunkPos, data: Object2DArray): 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>> { + return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, cell))) + } + + override fun loadEntities(pos: ChunkPos): CompletableFuture>> { + return CompletableFuture.completedFuture(KOptional.of(emptyList())) + } + + override fun loadMetadata(): CompletableFuture> { + 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() + + init { + storage.forEach { children.add(it) } + } + + override fun loadCells(pos: ChunkPos): CompletableFuture>> { + return chainOptionalFutures(children) { it.loadCells(pos) } + } + + override fun loadEntities(pos: ChunkPos): CompletableFuture>> { + return chainOptionalFutures(children) { it.loadEntities(pos) } + } + + override fun loadMetadata(): CompletableFuture> { + return chainOptionalFutures(children) { it.loadMetadata() } + } + + override fun saveEntities(pos: ChunkPos, data: Collection): Boolean { + return children.any { it.saveEntities(pos, data) } + } + + override fun saveCells(pos: ChunkPos, data: Object2DArray): 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() } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clock.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clock.kt new file mode 100644 index 00000000..8e6df071 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clock.kt @@ -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 +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt index 654a1552..39c71096 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt @@ -33,12 +33,6 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) { var seed: Long = 0L private set - init { - if (parameters.seed != null) { - init(parameters.seed) - } - } - val scaleD = parameters.scale.toDouble() 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 g3 by lazy { Double2DArray.allocate(parameters.scale * 2 + 2, 3) } + init { + if (parameters.seed != null) { + init(parameters.seed) + } + } + fun init(seed: Long) { isInitialized = true this.seed = seed diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 886eaea9..b46a12ff 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -1,5 +1,7 @@ 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.objects.ObjectArraySet 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.MailboxExecutorService import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.util.ParallelPerform import ru.dbotthepony.kstarbound.world.api.ICellAccess @@ -30,11 +33,12 @@ import java.util.function.Predicate import java.util.random.RandomGenerator import java.util.stream.Stream -abstract class World, ChunkType : Chunk>(val geometry: WorldGeometry) : ICellAccess, Closeable { +abstract class World, ChunkType : Chunk>(val template: WorldTemplate) : ICellAccess, Closeable { val background = TileView.Background(this) val foreground = TileView.Foreground(this) val mailbox = MailboxExecutorService() val sky = Sky() + val geometry: WorldGeometry = template.geometry override fun getCellDirect(x: Int, y: Int): AbstractCell { if (!geometry.x.inBoundsCell(x) || !geometry.y.inBoundsCell(y)) return AbstractCell.NULL @@ -225,6 +229,11 @@ abstract class World, ChunkType : Chunk