Moar packets implemented, chat, tiles, broadcast, ...
This commit is contained in:
parent
21f3a66283
commit
94cc53b176
@ -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
|
||||
|
@ -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<SkyGlobalConfig>()
|
||||
private set
|
||||
|
||||
var universeServer by Delegates.notNull<UniverseServerConfig>()
|
||||
private set
|
||||
|
||||
private object EmptyTask : ForkJoinTask<Unit>() {
|
||||
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))
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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 }
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<ClientWorld, ClientChunk>(geometry) {
|
||||
template: WorldTemplate,
|
||||
) : World<ClientWorld, ClientChunk>(template) {
|
||||
private fun determineChunkSize(cells: Int): Int {
|
||||
for (i in 64 downTo 1) {
|
||||
if (cells % i == 0) {
|
||||
|
44
src/main/kotlin/ru/dbotthepony/kstarbound/defs/ChatDefs.kt
Normal file
44
src/main/kotlin/ru/dbotthepony/kstarbound/defs/ChatDefs.kt
Normal 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)
|
25
src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt
Normal file
25
src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
|
@ -158,7 +158,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
|
||||
@JsonFactory
|
||||
data class StoreData(
|
||||
val primaryBiome: String,
|
||||
val primarySurfaceLiquid: Either<Int, String>?,
|
||||
val primarySurfaceLiquid: Either<Int, String>? = null,
|
||||
val sizeName: String,
|
||||
val hueShift: Double,
|
||||
val skyColoring: SkyColoring,
|
||||
|
@ -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 {
|
||||
|
@ -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<WorldGeometry, Vector2i>,
|
||||
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()
|
||||
|
||||
|
@ -14,6 +14,7 @@ abstract class AbstractTerrainSelector<D : Any>(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
|
||||
|
@ -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) {
|
||||
|
@ -331,6 +331,16 @@ class FactoryAdapter<T : Any> 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<T : Any> 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")
|
||||
|
@ -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<T>?
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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<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)
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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<ChannelFuture>()
|
||||
private val connections = CopyOnWriteArrayList<ServerConnection>()
|
||||
val connections = CopyOnWriteArrayList<ServerConnection>()
|
||||
private var localChannel: Channel? = null
|
||||
private val lock = ReentrantLock()
|
||||
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")
|
||||
fun createLocalChannel(): Channel {
|
||||
|
@ -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<ByteKey, KOptional<ByteArray>>()
|
||||
private val modifiedShipChunks = ObjectOpenHashSet<ByteKey>()
|
||||
var shipChunkSource by Delegates.notNull<IChunkSource>()
|
||||
var shipChunkSource by Delegates.notNull<WorldStorage>()
|
||||
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<ByteKey, KOptional<ByteArray>>) {
|
||||
@ -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<ChunkPos>()
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<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) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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>)
|
||||
}
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
@ -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<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 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<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 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<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 {
|
||||
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<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 {
|
||||
return LegacyChunkSource { key -> CompletableFuture.completedFuture(backing[key] ?: KOptional()) }
|
||||
fun memory(backing: Map<ByteKey, KOptional<ByteArray>>): LegacyWorldStorage {
|
||||
return LegacyWorldStorage { key -> CompletableFuture.completedFuture(backing[key] ?: KOptional()) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ServerWorld, ServerChunk>(geometry) {
|
||||
template: WorldTemplate,
|
||||
val storage: WorldStorage,
|
||||
) : World<ServerWorld, ServerChunk>(template) {
|
||||
init {
|
||||
if (server.isClosed)
|
||||
throw RuntimeException()
|
||||
|
||||
server.worlds.add(this)
|
||||
}
|
||||
|
||||
private val internalPlayers = ArrayList<ServerConnection>()
|
||||
private val internalPlayers = CopyOnWriteArrayList<ServerConnection>()
|
||||
val players: List<ServerConnection> = 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<Boolean> {
|
||||
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<IChunkSource>()
|
||||
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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
36
src/main/kotlin/ru/dbotthepony/kstarbound/util/Clock.kt
Normal file
36
src/main/kotlin/ru/dbotthepony/kstarbound/util/Clock.kt
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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<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 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<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
protected set
|
||||
var respawnInWorld = false
|
||||
protected set
|
||||
var adjustPlayerSpawn = false
|
||||
protected set
|
||||
|
||||
val protectedDungeonIDs = IntArraySet()
|
||||
val properties = JsonObject()
|
||||
|
||||
open fun setPlayerSpawn(position: Vector2d, respawnInWorld: Boolean) {
|
||||
playerSpawnPosition = position
|
||||
|
Loading…
Reference in New Issue
Block a user