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 { val result = ArrayList() 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()) } } }