KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt
2024-03-28 13:01:57 +07:00

209 lines
6.1 KiB
Kotlin

package ru.dbotthepony.kstarbound.network
import com.google.common.util.concurrent.ThreadFactoryBuilder
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelOption
import io.netty.channel.nio.NioEventLoopGroup
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.VarIntValueCodec
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.player.Avatar
import ru.dbotthepony.kstarbound.server.ServerChannels
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
import java.io.Closeable
import kotlin.math.roundToInt
import kotlin.properties.Delegates
abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : ChannelInboundHandlerAdapter(), Closeable {
abstract override fun channelRead(ctx: ChannelHandlerContext, msg: Any)
var avatar: Avatar? = null
var character: PlayerEntity? = null
val rpc = JsonRPC()
var entityIDRange: IntRange by Delegates.notNull()
private set
var connectionID: Int = -1
set(value) {
require(value in 1 .. ServerChannels.MAX_PLAYERS) { "Connection ID is out of range: $value" }
field = value
val begin = value * -65536
entityIDRange = begin .. begin + 65535
}
var nickname: String = ""
val hasChannel get() = ::channel.isInitialized
lateinit var channel: Channel
private set
var isLegacy: Boolean = true
protected set
private val handshakeValidator = PacketRegistry.HANDSHAKE.Validator(side)
private val handshakeSerializer = PacketRegistry.HANDSHAKE.Serializer(side)
private val nativeValidator = PacketRegistry.NATIVE.Validator(side)
private val nativeSerializer = PacketRegistry.NATIVE.Serializer(side)
private val legacyValidator = PacketRegistry.LEGACY.Validator(side)
private val legacySerializer = PacketRegistry.LEGACY.Serializer(side)
var isConnected = false
private set
open fun setupLegacy() {
isConnected = true
LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using legacy protocol")
if (type == ConnectionType.MEMORY) {
channel.pipeline().remove(handshakeValidator)
channel.pipeline().addFirst(legacyValidator)
} else {
channel.pipeline().remove(handshakeSerializer)
channel.pipeline().addFirst(legacySerializer)
}
isLegacy = true
}
open fun setupNative() {
isConnected = true
LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using native protocol")
if (type == ConnectionType.MEMORY) {
channel.pipeline().remove(handshakeValidator)
channel.pipeline().addFirst(nativeValidator)
} else {
channel.pipeline().remove(handshakeSerializer)
channel.pipeline().addFirst(nativeSerializer)
}
isLegacy = false
inGame()
}
protected open fun onChannelClosed() {
LOGGER.info("$this is terminated")
}
fun bind(channel: Channel) {
channel.config().setOption(ChannelOption.TCP_NODELAY, true)
this.channel = channel
if (type == ConnectionType.MEMORY)
channel.pipeline().addFirst(handshakeValidator)
else
channel.pipeline().addFirst(handshakeSerializer)
channel.pipeline().addLast(this)
channel.closeFuture().addListener {
onChannelClosed()
}
}
// one connection can be reused for second channel (while first is being closed)
override fun isSharable(): Boolean {
return true
}
override fun ensureNotSharable() {
}
abstract fun inGame()
fun send(packet: IPacket) {
if (channel.isOpen && isConnected) {
channel.write(packet)
}
}
fun sendAndFlush(packet: IPacket) {
if (channel.isOpen && isConnected) {
channel.write(packet)
flush()
}
}
open fun flush() {
channel.flush()
}
abstract fun disconnect(reason: String = "")
override fun close() {
channel.close()
}
val windowXMin = networkedSignedInt()
val windowYMin = networkedSignedInt()
val windowWidth = networkedSignedInt()
val windowHeight = networkedSignedInt()
val playerID = networkedSignedInt()
var playerEntity: PlayerEntity? = null
protected set
// holy shit
val clientSpectatingEntities = BasicNetworkedElement(IntAVLTreeSet(), StreamCodec.Collection(VarIntValueCodec) { IntAVLTreeSet() })
val clientStateGroup = MasterElement(NetworkedGroup(windowXMin, windowYMin, windowWidth, windowHeight, playerID, clientSpectatingEntities))
// in tiles
fun trackingTileRegions(): List<AABBi> {
val result = ArrayList<AABBi>()
val mins = Vector2i(windowXMin.get() - GlobalDefaults.client.windowMonitoringBorder, windowYMin.get() - GlobalDefaults.client.windowMonitoringBorder)
val maxs = Vector2i(windowWidth.get() + GlobalDefaults.client.windowMonitoringBorder, windowHeight.get() + GlobalDefaults.client.windowMonitoringBorder)
val window = AABBi(mins, maxs + mins)
if (window.mins != Vector2i.ZERO && window.maxs != Vector2i.ZERO) {
result.add(window)
}
val playerEntity = playerEntity
if (playerEntity != null) {
// add an extra region the size of the window centered on the player's position to prevent nearby sectors
// being unloaded due to camera panning or centering on other entities
val diff = Vector2d(window.width / 2.0, window.height / 2.0)
val pmins = playerEntity.position - diff
val pmaxs = playerEntity.position + diff
result.add(AABBi(
Vector2i(pmins.x.roundToInt(), pmins.y.roundToInt()),
Vector2i(pmaxs.x.roundToInt(), pmaxs.y.roundToInt()),
))
}
for (entity in clientSpectatingEntities.get()) {
// TODO
}
return result
}
companion object {
private val LOGGER = LogManager.getLogger()
val NIO_POOL by lazy {
NioEventLoopGroup(1, ThreadFactoryBuilder().setDaemon(true).setNameFormat("Starbound Network IO %d").build())
}
}
}