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

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

View File

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

View File

@ -6,6 +6,7 @@ import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.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))

View File

@ -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)

View File

@ -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)

View File

@ -17,6 +17,7 @@ import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.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()

View File

@ -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 }

View File

@ -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))
}
}
}

View File

@ -18,6 +18,7 @@ import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.Mesh
import ru.dbotthepony.kstarbound.client.render.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) {

View File

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

View File

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

View File

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

View File

@ -158,7 +158,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
@JsonFactory
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,

View File

@ -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 {

View File

@ -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()

View File

@ -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

View File

@ -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) {

View File

@ -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")

View File

@ -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>?
}

View File

@ -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()

View File

@ -20,6 +20,8 @@ import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket
import ru.dbotthepony.kstarbound.client.network.packets.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")

View File

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

View File

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

View File

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

View File

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

View File

@ -43,8 +43,8 @@ class LegacyTileArrayUpdatePacket(val origin: Vector2i, val data: Object2DArray<
stream.writeVarInt(data.rows)
stream.writeVarInt(data.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)
}
}

View File

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

View File

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

View File

@ -15,10 +15,10 @@ import ru.dbotthepony.kommons.io.writeKOptional
import ru.dbotthepony.kommons.io.writeMap
import ru.dbotthepony.kommons.io.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()
}

View File

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

View File

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

View File

@ -7,9 +7,11 @@ import io.netty.channel.ChannelInitializer
import io.netty.channel.local.LocalAddress
import io.netty.channel.local.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 {

View File

@ -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
}
}
}

View File

@ -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
}

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.server.world
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()) }
}
}
}

View File

@ -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
}
}
}
}

View File

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

View File

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

View File

@ -33,12 +33,6 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
var seed: Long = 0L
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

View File

@ -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