Archived
1
0
Fork 0

Move packet encoders and decoders to their own project

This commit is contained in:
Moritz Ruth 2020-09-09 22:56:25 +02:00
parent 8403140a70
commit b021a4b753
94 changed files with 733 additions and 727 deletions

5
.gitignore vendored
View file

@ -1,6 +1,5 @@
/.gradle/
/build/
/blokk-api/build/
/blokk-server/build/
build/
!src/**/build
/data/

View file

@ -1,4 +1,5 @@
# Blokk
## To Do
- Move packets to their own project
- Move packet encoders and decoders to their own project
- Stop Using Spek
- Support packet compression

View file

@ -3,7 +3,7 @@ package space.blokk.chat
/**
* Legacy formatting codes. You should use [ChatComponent][space.blokk.chat.ChatComponent] whenever it's possible,
* but sometimes these codes are required, for example in
* [the name of a player in a server list sample][space.blokk.net.protocols.status.ResponsePacket.Players.SampleEntry.name].
* [the name of a player in a server list sample][space.blokk.net.packets.status.ResponsePacket.Players.SampleEntry.name].
*/
enum class FormattingCode(private val char: Char) {
BLACK('0'),

View file

@ -4,8 +4,8 @@ import io.netty.buffer.ByteBuf
import kotlinx.coroutines.CoroutineScope
import space.blokk.events.EventTarget
import space.blokk.net.events.SessionEvent
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.Protocol
import space.blokk.net.packets.OutgoingPacket
import space.blokk.net.packets.Protocol
import java.net.InetAddress
interface Session : EventTarget<SessionEvent> {

View file

@ -2,7 +2,7 @@ package space.blokk.net.events
import space.blokk.events.Cancellable
import space.blokk.net.Session
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.packets.IncomingPacket
/**
* Emitted when a packet is received. **You should only listen to this event when there is no other option.**

View file

@ -2,7 +2,7 @@ package space.blokk.net.events
import space.blokk.events.Cancellable
import space.blokk.net.Session
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.packets.OutgoingPacket
/**
* Emitted when a packet is going to be sent. **You should only listen to this event when there is no other option.**

View file

@ -2,7 +2,7 @@ package space.blokk.net.events
import space.blokk.events.Cancellable
import space.blokk.net.Session
import space.blokk.net.protocols.status.ResponsePacket
import space.blokk.net.packets.status.ResponsePacket
/**
* Emitted when a client requests general info about the server, most likely to show it in the server list.

View file

@ -0,0 +1,8 @@
package space.blokk.net.packets
abstract class Packet {
override fun toString(): String = this::class.java.simpleName + "(no data)"
}
abstract class IncomingPacket : Packet()
abstract class OutgoingPacket : Packet()

View file

@ -0,0 +1,14 @@
package space.blokk.net.packets
import io.netty.buffer.ByteBuf
import kotlin.reflect.KClass
abstract class PacketCodec<T : Packet>(val id: Int, val dataType: KClass<T>)
abstract class IncomingPacketCodec<T : IncomingPacket>(id: Int, dataType: KClass<T>) : PacketCodec<T>(id, dataType) {
abstract fun decode(msg: ByteBuf): T
}
abstract class OutgoingPacketCodec<T : OutgoingPacket>(id: Int, dataType: KClass<T>) : PacketCodec<T>(id, dataType) {
abstract fun T.encode(dst: ByteBuf)
}

View file

@ -0,0 +1,16 @@
package space.blokk.net.packets
import kotlin.reflect.KClass
abstract class Protocol constructor(val name: String, vararg codecs: PacketCodec<*>) {
val test = emptyList<Packet>()
private val codecsByPacketType = codecs.map { it.dataType to it }.toMap()
val incomingPacketCodecsByID = codecs.filterIsInstance<IncomingPacketCodec<*>>().map { it.id to it }.toMap()
@Suppress("UNCHECKED_CAST")
fun <T : Packet> getCodecByType(type: KClass<T>): PacketCodec<T> = codecsByPacketType[type] as PacketCodec<T>?
?: throw Exception(
"No packet companion found for this packet type. " +
"This can happen if the packet is not part of the current protocol."
)
}

View file

@ -0,0 +1,22 @@
package space.blokk.net.packets.handshaking
import space.blokk.net.packets.IncomingPacket
/**
* This is the first packet a client should send. Per default, the server immediately switches
* the [session's currentProtocol][space.blokk.net.Session.currentProtocol] to
* `STATUS` or `LOGIN`,
* depending on the value of [loginAttempt].
*
* @param protocolVersion The number of the [protocol version](https://wiki.vg/Protocol_version_numbers) used by the client.
* @param serverAddress Hostname or IP address that was used to connect.
* @param serverPort Port that was used to connect.
* @param loginAttempt Whether the server should use the `STATUS` or
* the `LOGIN` protocol from now on.
*/
data class HandshakePacket(
val protocolVersion: Int,
val serverAddress: String,
val serverPort: Int,
val loginAttempt: Boolean
) : IncomingPacket()

View file

@ -0,0 +1,11 @@
package space.blokk.net.packets.login
import space.blokk.chat.ChatComponent
import space.blokk.net.packets.OutgoingPacket
/**
* Sent by the server to cancel an ongoing login process.
*
* @param reason The reason why the login process was cancelled.
*/
data class DisconnectPacket(val reason: ChatComponent) : OutgoingPacket()

View file

@ -1,10 +1,6 @@
package space.blokk.net.protocols.login
package space.blokk.net.packets.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.MinecraftDataTypes.writeVarInt
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
import space.blokk.net.packets.OutgoingPacket
/**
* Sent by the server to request authentication and encryption from the client. The client must respond with
@ -16,16 +12,6 @@ data class EncryptionRequestPacket(
val publicKey: ByteArray,
val verifyToken: ByteArray
) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<EncryptionRequestPacket>(0x01, EncryptionRequestPacket::class)
override fun encode(dst: ByteBuf) {
dst.writeString("") // Required for legacy reasons
dst.writeVarInt(publicKey.size)
dst.writeBytes(publicKey)
dst.writeVarInt(verifyToken.size)
dst.writeBytes(verifyToken)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View file

@ -0,0 +1,31 @@
package space.blokk.net.packets.login
import space.blokk.net.packets.IncomingPacket
/**
* Sent by the client in response to an [EncryptionRequestPacket].
*
* @see <a href="https://wiki.vg/Protocol_Encryption">https://wiki.vg/Protocol_Encryption</a>
*/
data class EncryptionResponsePacket(
val sharedSecret: ByteArray,
val verifyToken: ByteArray
) : IncomingPacket() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as EncryptionResponsePacket
if (!sharedSecret.contentEquals(other.sharedSecret)) return false
if (!verifyToken.contentEquals(other.verifyToken)) return false
return true
}
override fun hashCode(): Int {
var result = sharedSecret.contentHashCode()
result = 31 * result + verifyToken.contentHashCode()
return result
}
}

View file

@ -1,10 +1,6 @@
package space.blokk.net.protocols.login
package space.blokk.net.packets.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.MinecraftDataTypes.writeVarInt
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
import space.blokk.net.packets.OutgoingPacket
/**
* Optionally sent by the server to transfer additional, non-standard information required for the login process.
@ -19,14 +15,6 @@ data class LoginPluginRequestPacket(
val channel: String,
val data: ByteArray
) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<LoginPluginRequestPacket>(0x04, LoginPluginRequestPacket::class)
override fun encode(dst: ByteBuf) {
dst.writeVarInt(messageID)
dst.writeString(channel)
dst.writeBytes(data)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View file

@ -1,9 +1,6 @@
package space.blokk.net.protocols.login
package space.blokk.net.packets.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion
import space.blokk.net.packets.IncomingPacket
/**
* Sent by the client in response to a [LoginPluginRequestPacket].
@ -14,22 +11,6 @@ data class LoginPluginResponsePacket(
val messageID: Int,
val data: ByteArray?
) : IncomingPacket() {
companion object : IncomingPacketCompanion<LoginPluginResponsePacket>(0x02, LoginPluginResponsePacket::class) {
override fun decode(msg: ByteBuf): LoginPluginResponsePacket {
return with(MinecraftDataTypes) {
val messageID = msg.readVarInt()
val successful = msg.readBoolean()
// TODO: Test this
val data =
if (successful) ByteArray(msg.readableBytes()).also { msg.readBytes(it) }
else null
LoginPluginResponsePacket(messageID, data)
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View file

@ -0,0 +1,14 @@
package space.blokk.net.packets.login
import space.blokk.net.packets.IncomingPacket
/**
* Sent by the client to initialize the login process.
*
* @param username The username of the client. This is not validated (yet). Can only be 16 characters long.
*/
data class LoginStartPacket(val username: String) : IncomingPacket() {
init {
if (username.length > 16) throw IllegalArgumentException("username can only be 16 characters long")
}
}

View file

@ -1,28 +1,18 @@
package space.blokk.net.protocols.login
package space.blokk.net.packets.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
import space.blokk.net.packets.OutgoingPacket
import java.util.*
/**
* Sent by the server to indicate that the login process succeeded.
* After this packet is sent, the [session's currentProtocol][space.blokk.net.Session.currentProtocol] will be switched to
* [PLAY][space.blokk.net.protocols.play.PlayProtocol].
* `PLAY`.
*
* @param uuid The UUID of the player.
* @param username The username of the player. Can only be 16 characters long.
*/
data class LoginSuccessPacket(val uuid: UUID, val username: String) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<LoginSuccessPacket>(0x02, LoginSuccessPacket::class)
init {
if (username.length > 16) throw IllegalArgumentException("username can only be 16 characters long")
}
override fun encode(dst: ByteBuf) {
dst.writeString(uuid.toString())
dst.writeString(username)
}
}

View file

@ -0,0 +1,11 @@
package space.blokk.net.packets.login
import space.blokk.net.packets.OutgoingPacket
/**
* Sent by the server to indicate that all following packets are compressed and the client should also compress its
* packets.
*
* @param threshold Maximum size before a packet is compressed. Values lower than 1 will disable compression.
*/
data class SetCompressionPacket(val threshold: Int) : OutgoingPacket()

View file

@ -0,0 +1,18 @@
package space.blokk.net.packets.play
import space.blokk.net.packets.IncomingPacket
import space.blokk.players.ChatMode
import space.blokk.players.DisplayedSkinParts
import space.blokk.players.MainHand
/**
* Sent by the client when the player joins or the settings changed.
*/
data class ClientSettingsPacket(
val locale: String,
val viewDistance: Byte,
val chatMode: ChatMode,
val chatColorsEnabled: Boolean,
val displayedSkinParts: DisplayedSkinParts,
val mainHand: MainHand
) : IncomingPacket()

View file

@ -1,9 +1,6 @@
package space.blokk.net.protocols.play
package space.blokk.net.packets.play
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion
import space.blokk.net.packets.IncomingPacket
/**
* Can be used by client-side mods to communicate with a potentially modded server.
@ -13,19 +10,6 @@ import space.blokk.net.protocols.IncomingPacketCompanion
* @param data Data which was sent.
*/
data class IncomingPluginMessagePacket(val channel: String, val data: ByteArray) : IncomingPacket() {
companion object : IncomingPacketCompanion<IncomingPluginMessagePacket>(0x0B, IncomingPluginMessagePacket::class) {
override fun decode(msg: ByteBuf): IncomingPluginMessagePacket {
return with(MinecraftDataTypes) {
val channel = msg.readString()
// TODO: Test this
val data = ByteArray(msg.readableBytes()).also { msg.readBytes(it) }
IncomingPluginMessagePacket(channel, data)
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View file

@ -0,0 +1,35 @@
package space.blokk.net.packets.play
import space.blokk.net.packets.OutgoingPacket
import space.blokk.players.GameMode
import space.blokk.worlds.WorldDimension
import space.blokk.worlds.WorldType
/**
* Sent by the server after the client logged in.
*
* @param entityID ID of the player entity.
* @param gameMode Game mode of the player.
* @param hardcore Whether the player is in hardcore mode.
* @param worldDimension Dimension of the world the player joins.
* @param worldSeedHash First 8 bytes of the SHA-256 hash of the world's seed.
* @param worldType Type of the world the player joins.
* @param viewDistance Maximum view distance.
* @param reducedDebugInfo Whether the debug screen shows only reduced info.
* @param respawnScreenEnabled Whether the respawn screen is shown when the player dies.
*/
data class JoinGamePacket(
val entityID: Int,
val gameMode: GameMode,
val hardcore: Boolean,
val worldDimension: WorldDimension,
val worldSeedHash: Long,
val worldType: WorldType,
val viewDistance: Byte,
val reducedDebugInfo: Boolean,
val respawnScreenEnabled: Boolean
) : OutgoingPacket() {
init {
if (viewDistance < 2 || viewDistance > 32) throw IllegalArgumentException("viewDistance must be between 2 and 32.")
}
}

View file

@ -0,0 +1,13 @@
package space.blokk.net.packets.play
import io.netty.buffer.ByteBuf
import space.blokk.net.packets.OutgoingPacket
/**
* Can be used by mods or plugins to communicate with a potentially modded client.
* Minecraft itself [also uses this for some things](https://wiki.vg/Plugin_channels#Channels_internal_to_Minecraft).
*
* @param channel Channel in which the message should be send.
* @param data Data which should be send. The ByteBuf needs to be released by the caller.
*/
data class OutgoingPluginMessagePacket(val channel: String, val data: ByteBuf) : OutgoingPacket()

View file

@ -1,8 +1,6 @@
package space.blokk.net.protocols.play
package space.blokk.net.packets.play
import io.netty.buffer.ByteBuf
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
import space.blokk.net.packets.OutgoingPacket
/**
* Tells the client some things about the player.
@ -21,18 +19,4 @@ data class PlayerAbilitiesPacket(
val instantlyBreakBlocks: Boolean,
val flyingSpeed: Float = 0.5f,
val fieldOfView: Float = 0.1f
) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<PlayerAbilitiesPacket>(0x32, PlayerAbilitiesPacket::class)
override fun encode(dst: ByteBuf) {
var flags = 0
if (invulnerable) flags = flags and 0x01
if (flying) flags = flags and 0x02
if (flyingAllowed) flags = flags and 0x04
if (instantlyBreakBlocks) flags = flags and 0x08
dst.writeByte(flags)
dst.writeFloat(flyingSpeed)
dst.writeFloat(fieldOfView)
}
}
) : OutgoingPacket()

View file

@ -0,0 +1,9 @@
package space.blokk.net.packets.play
import space.blokk.net.packets.OutgoingPacket
import space.blokk.worlds.WorldDifficulty
/**
* Sets the difficulty shown in the pause menu.
*/
data class ServerDifficultyPacket(val difficulty: WorldDifficulty, val locked: Boolean) : OutgoingPacket()

View file

@ -0,0 +1,10 @@
package space.blokk.net.packets.status
import space.blokk.net.packets.IncomingPacket
/**
* Sent by the client to determine the RTT of the connection. The server responds with a [PongPacket].
*
* @param payload A random value generated by the client. The [PongPacket] sent in response must use the same payload.
*/
data class PingPacket(val payload: Long) : IncomingPacket()

View file

@ -0,0 +1,10 @@
package space.blokk.net.packets.status
import space.blokk.net.packets.OutgoingPacket
/**
* Sent by the server in response to a [PingPacket].
*
* @param payload A random value generated by the client. This must be the same as in the corresponding [PingPacket].
*/
data class PongPacket(val payload: Long) : OutgoingPacket()

View file

@ -0,0 +1,12 @@
package space.blokk.net.packets.status
import space.blokk.net.packets.IncomingPacket
/**
* Sent by the client to request general information about the server.
*
* The server responds with a [ResponsePacket].
*
* @see [space.blokk.net.events.ServerListInfoRequestEvent]
*/
class RequestPacket : IncomingPacket()

View file

@ -1,13 +1,8 @@
package space.blokk.net.protocols.status
package space.blokk.net.packets.status
import com.squareup.moshi.JsonClass
import io.netty.buffer.ByteBuf
import space.blokk.Blokk
import space.blokk.chat.TextComponent
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
import space.blokk.utils.toJson
import space.blokk.net.packets.OutgoingPacket
/**
* Sent by the server in response to a [RequestPacket].
@ -26,27 +21,11 @@ data class ResponsePacket(
val players: Players,
val favicon: String? = null
) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<ResponsePacket>(0x00, ResponsePacket::class)
init {
if (description.getExtraTypes().find { it != TextComponent::class } != null)
throw Exception("description may only contain instances of TextComponent")
}
override fun encode(dst: ByteBuf) {
with(MinecraftDataTypes) {
dst.writeString(Blokk.json.toJson(mapOf(
"version" to mapOf(
"name" to versionName,
"protocol" to protocolVersion
),
"players" to players,
"description" to description,
"favicon" to favicon?.let { "data:image/png;base64,$it" }
)))
}
}
@JsonClass(generateAdapter = true)
data class Players(val max: Int, val online: Int, val sample: List<SampleEntry>) {
/**

View file

@ -1,29 +0,0 @@
package space.blokk.net.protocols
import io.netty.buffer.ByteBuf
import kotlin.reflect.KClass
abstract class Packet {
override fun toString(): String = this::class.java.simpleName + "(no data)"
}
abstract class IncomingPacket : Packet()
abstract class OutgoingPacket : Packet() {
abstract fun encode(dst: ByteBuf)
}
sealed class PacketCompanion<T : Packet>(val id: Int, val packetType: KClass<T>)
abstract class IncomingPacketCompanion<T : IncomingPacket>(
id: Int,
packetType: KClass<T>
) : PacketCompanion<T>(id, packetType) {
abstract fun decode(msg: ByteBuf): T
}
abstract class OutgoingPacketCompanion<T : OutgoingPacket>(
id: Int,
packetType: KClass<T>
) : PacketCompanion<T>(id, packetType)

View file

@ -1,27 +0,0 @@
package space.blokk.net.protocols
abstract class Protocol internal constructor(val name: String, vararg packets: PacketCompanion<*>) {
val incomingPackets = packets.filterIsInstance<IncomingPacketCompanion<*>>()
val incomingPacketsByID = incomingPackets.mapToIDMap()
val outgoingPackets = packets.filterIsInstance<OutgoingPacketCompanion<*>>()
val outgoingPacketsByID = outgoingPackets.mapToIDMap()
val packetCompanionsByPacketType = packets.map { it.packetType to it }.toMap()
private fun <T : PacketCompanion<*>> Iterable<T>.mapToIDMap() = map { it.id to it }.toMap()
fun validate() {
ensureDistinctIDs(incomingPackets, "serverbound")
ensureDistinctIDs(outgoingPackets, "clientbound")
}
override fun toString() = name
private fun ensureDistinctIDs(packets: Iterable<PacketCompanion<*>>, type: String) {
packets.groupBy { it.id }.forEach { (id, p) ->
if (p.count() > 1) {
val packetsString = p.joinToString(", ", limit = 4) { it.packetType.simpleName.toString() }
throw Error("Multiple $type packets use the same ID (0x${id.toString(16)}): $packetsString")
}
}
}
}

View file

@ -1,10 +0,0 @@
package space.blokk.net.protocols
import space.blokk.net.protocols.handshaking.HandshakingProtocol
import space.blokk.net.protocols.login.LoginProtocol
import space.blokk.net.protocols.status.StatusProtocol
object Protocols {
val all = setOf(HandshakingProtocol, StatusProtocol, LoginProtocol)
fun validate() = all.forEach(Protocol::validate)
}

View file

@ -1,38 +0,0 @@
package space.blokk.net.protocols.handshaking
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion
/**
* This is the first packet a client should send. Per default, the server immediately switches
* the [session's currentProtocol][space.blokk.net.Session.currentProtocol] to
* [STATUS][space.blokk.net.protocols.status.StatusProtocol] or [LOGIN][space.blokk.net.protocols.login.LoginProtocol],
* depending on the value of [loginAttempt].
*
* @param protocolVersion The number of the [protocol version](https://wiki.vg/Protocol_version_numbers) used by the client.
* @param serverAddress Hostname or IP address that was used to connect.
* @param serverPort Port that was used to connect.
* @param loginAttempt Whether the server should use the [STATUS][space.blokk.net.protocols.status.StatusProtocol] or
* the [LOGIN][space.blokk.net.protocols.login.LoginProtocol] protocol from now on.
*/
data class HandshakePacket(
val protocolVersion: Int,
val serverAddress: String,
val serverPort: Int,
val loginAttempt: Boolean
) : IncomingPacket() {
companion object : IncomingPacketCompanion<HandshakePacket>(0x00, HandshakePacket::class) {
override fun decode(msg: ByteBuf): HandshakePacket {
return with(MinecraftDataTypes) {
HandshakePacket(
protocolVersion = msg.readVarInt(),
serverAddress = msg.readString(),
serverPort = msg.readUnsignedShort(),
loginAttempt = msg.readVarInt() == 2
)
}
}
}
}

View file

@ -1,5 +0,0 @@
package space.blokk.net.protocols.handshaking
import space.blokk.net.protocols.Protocol
object HandshakingProtocol : Protocol("HANDSHAKING", HandshakePacket)

View file

@ -1,20 +0,0 @@
package space.blokk.net.protocols.login
import io.netty.buffer.ByteBuf
import space.blokk.chat.ChatComponent
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
/**
* Sent by the server to cancel an ongoing login process.
*
* @param reason The reason why the login process was cancelled.
*/
data class DisconnectPacket(val reason: ChatComponent) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<DisconnectPacket>(0x00, DisconnectPacket::class)
override fun encode(dst: ByteBuf) {
dst.writeString(reason.toJson())
}
}

View file

@ -1,50 +0,0 @@
package space.blokk.net.protocols.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion
/**
* Sent by the client in response to an [EncryptionRequestPacket].
*
* @see <a href="https://wiki.vg/Protocol_Encryption">https://wiki.vg/Protocol_Encryption</a>
*/
data class EncryptionResponsePacket(
val sharedSecret: ByteArray,
val verifyToken: ByteArray
) : IncomingPacket() {
companion object : IncomingPacketCompanion<EncryptionResponsePacket>(0x01, EncryptionResponsePacket::class) {
override fun decode(msg: ByteBuf): EncryptionResponsePacket {
return with(MinecraftDataTypes) {
val sharedSecretLength = msg.readVarInt()
val sharedSecret = ByteArray(sharedSecretLength)
msg.readBytes(sharedSecret)
val verifyTokenLength = msg.readVarInt()
val verifyToken = ByteArray(verifyTokenLength)
msg.readBytes(verifyToken)
EncryptionResponsePacket(sharedSecret, verifyToken)
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as EncryptionResponsePacket
if (!sharedSecret.contentEquals(other.sharedSecret)) return false
if (!verifyToken.contentEquals(other.verifyToken)) return false
return true
}
override fun hashCode(): Int {
var result = sharedSecret.contentHashCode()
result = 31 * result + verifyToken.contentHashCode()
return result
}
}

View file

@ -1,15 +0,0 @@
package space.blokk.net.protocols.login
import space.blokk.net.protocols.Protocol
object LoginProtocol : Protocol(
"LOGIN",
DisconnectPacket,
LoginStartPacket,
EncryptionRequestPacket,
EncryptionResponsePacket,
SetCompressionPacket,
LoginSuccessPacket,
LoginPluginRequestPacket,
LoginPluginResponsePacket
)

View file

@ -1,23 +0,0 @@
package space.blokk.net.protocols.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion
/**
* Sent by the client to initialize the login process.
*
* @param username The username of the client. This is not validated (yet). Can only be 16 characters long.
*/
data class LoginStartPacket(val username: String) : IncomingPacket() {
companion object : IncomingPacketCompanion<LoginStartPacket>(0x00, LoginStartPacket::class) {
override fun decode(msg: ByteBuf): LoginStartPacket {
return with(MinecraftDataTypes) { LoginStartPacket(msg.readString()) }
}
}
init {
if (username.length > 16) throw IllegalArgumentException("username can only be 16 characters long")
}
}

View file

@ -1,20 +0,0 @@
package space.blokk.net.protocols.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeVarInt
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
/**
* Sent by the server to indicate that all following packets are compressed and the client should also compress its
* packets.
*
* @param threshold Maximum size before a packet is compressed. Values lower than 1 will disable compression.
*/
data class SetCompressionPacket(val threshold: Int) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<SetCompressionPacket>(0x03, SetCompressionPacket::class)
override fun encode(dst: ByteBuf) {
dst.writeVarInt(threshold)
}
}

View file

@ -1,67 +0,0 @@
package space.blokk.net.protocols.play
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.PacketDecodingException
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion
import space.blokk.players.ChatMode
import space.blokk.players.DisplayedSkinParts
import space.blokk.players.MainHand
import space.blokk.utils.checkBit
/**
* Sent by the client when the player joins or the settings changed.
*/
data class ClientSettingsPacket(
val locale: String,
val viewDistance: Byte,
val chatMode: ChatMode,
val chatColorsEnabled: Boolean,
val displayedSkinParts: DisplayedSkinParts,
val mainHand: MainHand
) : IncomingPacket() {
companion object : IncomingPacketCompanion<ClientSettingsPacket>(0x05, ClientSettingsPacket::class) {
override fun decode(msg: ByteBuf): ClientSettingsPacket {
return with(MinecraftDataTypes) {
val locale = msg.readString()
val viewDistance = msg.readByte()
val chatMode = when (msg.readVarInt()) {
0 -> ChatMode.ENABLED
1 -> ChatMode.COMMANDS_ONLY
2 -> ChatMode.HIDDEN
else -> throw PacketDecodingException()
}
val chatColorsEnabled = msg.readBoolean()
val skinFlags = msg.readByte()
val displayedSkinParts = DisplayedSkinParts(
cape = skinFlags.checkBit(0),
jacket = skinFlags.checkBit(1),
leftSleeve = skinFlags.checkBit(2),
rightSleeve = skinFlags.checkBit(3),
leftPantsLeg = skinFlags.checkBit(4),
rightPantsLeg = skinFlags.checkBit(5),
hat = skinFlags.checkBit(6)
)
val mainHand = when (msg.readVarInt()) {
0 -> MainHand.LEFT
1 -> MainHand.RIGHT
else -> throw PacketDecodingException()
}
ClientSettingsPacket(
locale,
viewDistance,
chatMode,
chatColorsEnabled,
displayedSkinParts,
mainHand
)
}
}
}
}

View file

@ -1,22 +0,0 @@
package space.blokk.net.protocols.play
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
/**
* Can be used by mods or plugins to communicate with a potentially modded client.
* Minecraft itself [also uses this for some things](https://wiki.vg/Plugin_channels#Channels_internal_to_Minecraft).
*
* @param channel Channel in which the message should be send.
* @param data Data which should be send. The ByteBuf needs to be released by the caller.
*/
data class OutgoingPluginMessagePacket(val channel: String, val data: ByteBuf) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<OutgoingPluginMessagePacket>(0x19, OutgoingPluginMessagePacket::class)
override fun encode(dst: ByteBuf) {
dst.writeString(channel)
dst.writeBytes(data)
}
}

View file

@ -1,13 +0,0 @@
package space.blokk.net.protocols.play
import space.blokk.net.protocols.Protocol
object PlayProtocol : Protocol(
"PLAY",
JoinGamePacket,
ClientSettingsPacket,
IncomingPluginMessagePacket,
OutgoingPluginMessagePacket,
PlayerAbilitiesPacket,
ServerDifficultyPacket
)

View file

@ -1,26 +0,0 @@
package space.blokk.net.protocols.play
import io.netty.buffer.ByteBuf
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
import space.blokk.worlds.WorldDifficulty
/**
* Sets the difficulty shown in the pause menu.
*/
data class ServerDifficultyPacket(val difficulty: WorldDifficulty, val locked: Boolean) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<ServerDifficultyPacket>(0x0E, ServerDifficultyPacket::class)
override fun encode(dst: ByteBuf) {
dst.writeByte(
when (difficulty) {
WorldDifficulty.PEACEFUL -> 0
WorldDifficulty.EASY -> 1
WorldDifficulty.NORMAL -> 2
WorldDifficulty.HARD -> 3
}
)
dst.writeBoolean(locked)
}
}

View file

@ -1,18 +0,0 @@
package space.blokk.net.protocols.status
import io.netty.buffer.ByteBuf
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion
/**
* Sent by the client to determine the RTT of the connection. The server responds with a [PongPacket].
*
* @param payload A random value generated by the client. The [PongPacket] sent in response must use the same payload.
*/
data class PingPacket(val payload: Long) : IncomingPacket() {
companion object : IncomingPacketCompanion<PingPacket>(0x01, PingPacket::class) {
override fun decode(msg: ByteBuf): PingPacket {
return PingPacket(msg.readLong())
}
}
}

View file

@ -1,18 +0,0 @@
package space.blokk.net.protocols.status
import io.netty.buffer.ByteBuf
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
/**
* Sent by the server in response to a [PingPacket].
*
* @param payload A random value generated by the client. This must be the same as in the corresponding [PingPacket].
*/
data class PongPacket(val payload: Long) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<PongPacket>(0x01, PongPacket::class)
override fun encode(dst: ByteBuf) {
dst.writeLong(payload)
}
}

View file

@ -1,18 +0,0 @@
package space.blokk.net.protocols.status
import io.netty.buffer.ByteBuf
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion
/**
* Sent by the client to request general information about the server.
*
* The server responds with a [ResponsePacket].
*
* @see [space.blokk.net.events.ServerListInfoRequestEvent]
*/
class RequestPacket : IncomingPacket() {
companion object : IncomingPacketCompanion<RequestPacket>(0x00, RequestPacket::class) {
override fun decode(msg: ByteBuf): RequestPacket = RequestPacket()
}
}

View file

@ -1,11 +0,0 @@
package space.blokk.net.protocols.status
import space.blokk.net.protocols.Protocol
object StatusProtocol : Protocol(
"STATUS",
RequestPacket,
ResponsePacket,
PingPacket,
PongPacket
)

View file

@ -1,12 +0,0 @@
package space.blokk.net.protocols
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
object ProtocolsTest : Spek({
describe("Protocols") {
it("are valid") {
Protocols.validate()
}
}
})

View file

@ -0,0 +1,25 @@
plugins {
kotlin("jvm")
}
group = rootProject.group
version = rootProject.version
repositories {
mavenCentral()
}
val nettyVersion = properties["version.netty"].toString()
dependencies {
api(project(":blokk-api"))
// Netty
api("io.netty:netty-buffer:${nettyVersion}")
}
tasks {
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
}

View file

@ -0,0 +1,15 @@
package space.blokk.net.packets.handshaking
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.readString
import space.blokk.net.MinecraftDataTypes.readVarInt
import space.blokk.net.packets.IncomingPacketCodec
object HandshakePacketCodec : IncomingPacketCodec<HandshakePacket>(0x00, HandshakePacket::class) {
override fun decode(msg: ByteBuf): HandshakePacket = HandshakePacket(
protocolVersion = msg.readVarInt(),
serverAddress = msg.readString(),
serverPort = msg.readUnsignedShort(),
loginAttempt = msg.readVarInt() == 2
)
}

View file

@ -0,0 +1,5 @@
package space.blokk.net.packets.handshaking
import space.blokk.net.packets.Protocol
object HandshakingProtocol : Protocol("HANDSHAKING", HandshakePacketCodec)

View file

@ -0,0 +1,11 @@
package space.blokk.net.packets.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.packets.OutgoingPacketCodec
object DisconnectPacketCodec : OutgoingPacketCodec<DisconnectPacket>(0x00, DisconnectPacket::class) {
override fun DisconnectPacket.encode(dst: ByteBuf) {
dst.writeString(reason.toJson())
}
}

View file

@ -0,0 +1,17 @@
package space.blokk.net.packets.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.MinecraftDataTypes.writeVarInt
import space.blokk.net.packets.OutgoingPacketCodec
object EncryptionRequestPacketCodec :
OutgoingPacketCodec<EncryptionRequestPacket>(0x01, EncryptionRequestPacket::class) {
override fun EncryptionRequestPacket.encode(dst: ByteBuf) {
dst.writeString("") // Required for legacy reasons
dst.writeVarInt(publicKey.size)
dst.writeBytes(publicKey)
dst.writeVarInt(verifyToken.size)
dst.writeBytes(verifyToken)
}
}

View file

@ -0,0 +1,20 @@
package space.blokk.net.packets.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.readVarInt
import space.blokk.net.packets.IncomingPacketCodec
object EncryptionResponsePacketCodec :
IncomingPacketCodec<EncryptionResponsePacket>(0x01, EncryptionResponsePacket::class) {
override fun decode(msg: ByteBuf): EncryptionResponsePacket {
val sharedSecretLength = msg.readVarInt()
val sharedSecret = ByteArray(sharedSecretLength)
msg.readBytes(sharedSecret)
val verifyTokenLength = msg.readVarInt()
val verifyToken = ByteArray(verifyTokenLength)
msg.readBytes(verifyToken)
return EncryptionResponsePacket(sharedSecret, verifyToken)
}
}

View file

@ -0,0 +1,15 @@
package space.blokk.net.packets.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.MinecraftDataTypes.writeVarInt
import space.blokk.net.packets.OutgoingPacketCodec
object LoginPluginRequestPacketCodec :
OutgoingPacketCodec<LoginPluginRequestPacket>(0x04, LoginPluginRequestPacket::class) {
override fun LoginPluginRequestPacket.encode(dst: ByteBuf) {
dst.writeVarInt(messageID)
dst.writeString(channel)
dst.writeBytes(data)
}
}

View file

@ -0,0 +1,20 @@
package space.blokk.net.packets.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.readVarInt
import space.blokk.net.packets.IncomingPacketCodec
object LoginPluginResponsePacketCodec :
IncomingPacketCodec<LoginPluginResponsePacket>(0x02, LoginPluginResponsePacket::class) {
override fun decode(msg: ByteBuf): LoginPluginResponsePacket {
val messageID = msg.readVarInt()
val successful = msg.readBoolean()
// TODO: Test this
val data =
if (successful) ByteArray(msg.readableBytes()).also { msg.readBytes(it) }
else null
return LoginPluginResponsePacket(messageID, data)
}
}

View file

@ -0,0 +1,15 @@
package space.blokk.net.packets.login
import space.blokk.net.packets.Protocol
object LoginProtocol : Protocol(
"LOGIN",
DisconnectPacketCodec,
EncryptionRequestPacketCodec,
EncryptionResponsePacketCodec,
LoginPluginRequestPacketCodec,
LoginPluginResponsePacketCodec,
LoginStartPacketCodec,
LoginSuccessPacketCodec,
SetCompressionPacketCodec
)

View file

@ -0,0 +1,9 @@
package space.blokk.net.packets.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.readString
import space.blokk.net.packets.IncomingPacketCodec
object LoginStartPacketCodec : IncomingPacketCodec<LoginStartPacket>(0x00, LoginStartPacket::class) {
override fun decode(msg: ByteBuf): LoginStartPacket = LoginStartPacket(msg.readString())
}

View file

@ -0,0 +1,12 @@
package space.blokk.net.packets.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.packets.OutgoingPacketCodec
object LoginSuccessPacketCodec : OutgoingPacketCodec<LoginSuccessPacket>(0x02, LoginSuccessPacket::class) {
override fun LoginSuccessPacket.encode(dst: ByteBuf) {
dst.writeString(uuid.toString())
dst.writeString(username)
}
}

View file

@ -0,0 +1,11 @@
package space.blokk.net.packets.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeVarInt
import space.blokk.net.packets.OutgoingPacketCodec
object SetCompressionPacketCodec : OutgoingPacketCodec<SetCompressionPacket>(0x03, SetCompressionPacket::class) {
override fun SetCompressionPacket.encode(dst: ByteBuf) {
dst.writeVarInt(threshold)
}
}

View file

@ -0,0 +1,53 @@
package space.blokk.net.packets.play
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.readString
import space.blokk.net.MinecraftDataTypes.readVarInt
import space.blokk.net.PacketDecodingException
import space.blokk.net.packets.IncomingPacketCodec
import space.blokk.players.ChatMode
import space.blokk.players.DisplayedSkinParts
import space.blokk.players.MainHand
import space.blokk.utils.checkBit
object ClientSettingsPacketCodec : IncomingPacketCodec<ClientSettingsPacket>(0x05, ClientSettingsPacket::class) {
override fun decode(msg: ByteBuf): ClientSettingsPacket {
val locale = msg.readString()
val viewDistance = msg.readByte()
val chatMode = when (msg.readVarInt()) {
0 -> ChatMode.ENABLED
1 -> ChatMode.COMMANDS_ONLY
2 -> ChatMode.HIDDEN
else -> throw PacketDecodingException()
}
val chatColorsEnabled = msg.readBoolean()
val skinFlags = msg.readByte()
val displayedSkinParts = DisplayedSkinParts(
cape = skinFlags.checkBit(0),
jacket = skinFlags.checkBit(1),
leftSleeve = skinFlags.checkBit(2),
rightSleeve = skinFlags.checkBit(3),
leftPantsLeg = skinFlags.checkBit(4),
rightPantsLeg = skinFlags.checkBit(5),
hat = skinFlags.checkBit(6)
)
val mainHand = when (msg.readVarInt()) {
0 -> MainHand.LEFT
1 -> MainHand.RIGHT
else -> throw PacketDecodingException()
}
return ClientSettingsPacket(
locale,
viewDistance,
chatMode,
chatColorsEnabled,
displayedSkinParts,
mainHand
)
}
}

View file

@ -0,0 +1,17 @@
package space.blokk.net.packets.play
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.readString
import space.blokk.net.packets.IncomingPacketCodec
object IncomingPluginMessagePacketCodec :
IncomingPacketCodec<IncomingPluginMessagePacket>(0x0B, IncomingPluginMessagePacket::class) {
override fun decode(msg: ByteBuf): IncomingPluginMessagePacket {
val channel = msg.readString()
// TODO: Test this
val data = ByteArray(msg.readableBytes()).also { msg.readBytes(it) }
return IncomingPluginMessagePacket(channel, data)
}
}

View file

@ -1,45 +1,15 @@
package space.blokk.net.protocols.play
package space.blokk.net.packets.play
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.MinecraftDataTypes.writeVarInt
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
import space.blokk.net.packets.OutgoingPacketCodec
import space.blokk.players.GameMode
import space.blokk.worlds.WorldDimension
import space.blokk.worlds.WorldType
/**
* Sent by the server after the client logged in.
*
* @param entityID ID of the player entity.
* @param gameMode Game mode of the player.
* @param hardcore Whether the player is in hardcore mode.
* @param worldDimension Dimension of the world the player joins.
* @param worldSeedHash First 8 bytes of the SHA-256 hash of the world's seed.
* @param worldType Type of the world the player joins.
* @param viewDistance Maximum view distance.
* @param reducedDebugInfo Whether the debug screen shows only reduced info.
* @param respawnScreenEnabled Whether the respawn screen is shown when the player dies.
*/
data class JoinGamePacket(
val entityID: Int,
val gameMode: GameMode,
val hardcore: Boolean,
val worldDimension: WorldDimension,
val worldSeedHash: Long,
val worldType: WorldType,
val viewDistance: Byte,
val reducedDebugInfo: Boolean,
val respawnScreenEnabled: Boolean
) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<JoinGamePacket>(0x26, JoinGamePacket::class)
init {
if (viewDistance < 2 || viewDistance > 32) throw IllegalArgumentException("viewDistance must be between 2 and 32.")
}
override fun encode(dst: ByteBuf) {
object JoinGamePacketCodec : OutgoingPacketCodec<JoinGamePacket>(0x26, JoinGamePacket::class) {
override fun JoinGamePacket.encode(dst: ByteBuf) {
dst.writeInt(entityID)
var gameMode = when (gameMode) {
GameMode.SURVIVAL -> 0

View file

@ -0,0 +1,13 @@
package space.blokk.net.packets.play
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.packets.OutgoingPacketCodec
object OutgoingPluginMessagePacketCodec :
OutgoingPacketCodec<OutgoingPluginMessagePacket>(0x19, OutgoingPluginMessagePacket::class) {
override fun OutgoingPluginMessagePacket.encode(dst: ByteBuf) {
dst.writeString(channel)
dst.writeBytes(data)
}
}

View file

@ -0,0 +1,13 @@
package space.blokk.net.packets.play
import space.blokk.net.packets.Protocol
object PlayProtocol : Protocol(
"PLAY",
ClientSettingsPacketCodec,
IncomingPluginMessagePacketCodec,
JoinGamePacketCodec,
OutgoingPluginMessagePacketCodec,
PlayerAbilitiesPacketCodec,
ServerDifficultyPacketCodec
)

View file

@ -0,0 +1,18 @@
package space.blokk.net.packets.play
import io.netty.buffer.ByteBuf
import space.blokk.net.packets.OutgoingPacketCodec
object PlayerAbilitiesPacketCodec : OutgoingPacketCodec<PlayerAbilitiesPacket>(0x32, PlayerAbilitiesPacket::class) {
override fun PlayerAbilitiesPacket.encode(dst: ByteBuf) {
var flags = 0
if (invulnerable) flags = flags and 0x01
if (flying) flags = flags and 0x02
if (flyingAllowed) flags = flags and 0x04
if (instantlyBreakBlocks) flags = flags and 0x08
dst.writeByte(flags)
dst.writeFloat(flyingSpeed)
dst.writeFloat(fieldOfView)
}
}

View file

@ -0,0 +1,20 @@
package space.blokk.net.packets.play
import io.netty.buffer.ByteBuf
import space.blokk.net.packets.OutgoingPacketCodec
import space.blokk.worlds.WorldDifficulty
object ServerDifficultyPacketCodec : OutgoingPacketCodec<ServerDifficultyPacket>(0x0E, ServerDifficultyPacket::class) {
override fun ServerDifficultyPacket.encode(dst: ByteBuf) {
dst.writeByte(
when (difficulty) {
WorldDifficulty.PEACEFUL -> 0
WorldDifficulty.EASY -> 1
WorldDifficulty.NORMAL -> 2
WorldDifficulty.HARD -> 3
}
)
dst.writeBoolean(locked)
}
}

View file

@ -0,0 +1,8 @@
package space.blokk.net.packets.status
import io.netty.buffer.ByteBuf
import space.blokk.net.packets.IncomingPacketCodec
object PingPacketCodec : IncomingPacketCodec<PingPacket>(0x01, PingPacket::class) {
override fun decode(msg: ByteBuf): PingPacket = PingPacket(msg.readLong())
}

View file

@ -0,0 +1,10 @@
package space.blokk.net.packets.status
import io.netty.buffer.ByteBuf
import space.blokk.net.packets.OutgoingPacketCodec
object PongPacketCodec : OutgoingPacketCodec<PongPacket>(0x01, PongPacket::class) {
override fun PongPacket.encode(dst: ByteBuf) {
dst.writeLong(payload)
}
}

View file

@ -0,0 +1,8 @@
package space.blokk.net.packets.status
import io.netty.buffer.ByteBuf
import space.blokk.net.packets.IncomingPacketCodec
object RequestPacketCodec : IncomingPacketCodec<RequestPacket>(0x00, RequestPacket::class) {
override fun decode(msg: ByteBuf): RequestPacket = RequestPacket()
}

View file

@ -0,0 +1,22 @@
package space.blokk.net.packets.status
import io.netty.buffer.ByteBuf
import space.blokk.Blokk
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.packets.OutgoingPacketCodec
import space.blokk.utils.toJson
object ResponsePacketCodec : OutgoingPacketCodec<ResponsePacket>(0x00, ResponsePacket::class) {
override fun ResponsePacket.encode(dst: ByteBuf) {
dst.writeString(
Blokk.json.toJson(mapOf(
"version" to mapOf(
"name" to versionName,
"protocol" to protocolVersion
),
"players" to players,
"description" to description,
"favicon" to favicon?.let { "data:image/png;base64,$it" }
)))
}
}

View file

@ -0,0 +1,11 @@
package space.blokk.net.packets.status
import space.blokk.net.packets.Protocol
object StatusProtocol : Protocol(
"STATUS",
PingPacketCodec,
RequestPacketCodec,
PongPacketCodec,
ResponsePacketCodec
)

View file

@ -1,6 +1,5 @@
plugins {
kotlin("jvm")
kotlin("kapt")
id("com.github.johnrengelman.shadow") version "6.0.0"
}
@ -24,6 +23,7 @@ dependencies {
// Blokk
implementation(project(":blokk-api"))
implementation(project(":blokk-packet-codecs"))
// Logging
implementation("org.slf4j:slf4j-api:${slf4jVersion}")
@ -42,7 +42,6 @@ dependencies {
implementation("com.squareup.okhttp3:okhttp:4.8.1")
implementation("com.sksamuel.hoplite:hoplite-core:1.3.3")
implementation("com.sksamuel.hoplite:hoplite-yaml:1.3.3")
kapt("com.squareup.moshi:moshi-kotlin-codegen:${moshiVersion}")
}
tasks {

View file

@ -5,21 +5,14 @@ import com.sksamuel.hoplite.ConfigLoader
import com.sksamuel.hoplite.ConfigSource
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import space.blokk.chat.ChatComponent
import space.blokk.chat.TextComponent
import space.blokk.config.BlokkConfig
import space.blokk.events.EventBus
import space.blokk.events.EventHandler
import space.blokk.events.Listener
import space.blokk.logging.Logger
import space.blokk.net.BlokkSocketServer
import space.blokk.net.events.ServerListInfoRequestEvent
import space.blokk.server.Server
import space.blokk.server.events.ServerEvent
import space.blokk.utils.EncryptionUtils
import space.blokk.utils.Ticker
import space.blokk.utils.delayTicks
import java.io.File
import java.security.KeyPair
import kotlin.system.exitProcess
@ -105,22 +98,7 @@ class BlokkServer internal constructor() : Server {
@JvmStatic
fun main(args: Array<String>) {
val server = BlokkServer()
Blokk.sessions.registerListener(object : Listener {
@EventHandler
fun onServerListInfoRequest(event: ServerListInfoRequestEvent) {
event.response = event.response.copy(
description = TextComponent(
event.session.address.hostAddress,
bold = true,
underlined = true,
color = ChatComponent.Color.RED
)
)
}
})
server.start()
BlokkServer().start()
}
}
}

View file

@ -23,7 +23,7 @@ class BlokkChannelInitializer(private val blokkSocketServer: BlokkSocketServer)
channel.pipeline()
.addLast("idle", IdleStateHandler(20, 15, 0))
.addLast("framing", FramingCodec())
.addLast("packets", PacketCodec(session))
.addLast("packets", PacketMessageCodec(session))
.addLast("handler", PacketMessageHandler(session))
}

View file

@ -10,13 +10,13 @@ import space.blokk.logging.Logger
import space.blokk.net.events.PacketReceivedEvent
import space.blokk.net.events.PacketSendEvent
import space.blokk.net.events.SessionEvent
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.Protocol
import space.blokk.net.protocols.handshaking.HandshakingProtocol
import space.blokk.net.protocols.login.DisconnectPacket
import space.blokk.net.protocols.login.LoginProtocol
import space.blokk.net.protocols.play.OutgoingPluginMessagePacket
import space.blokk.net.protocols.play.PlayProtocol
import space.blokk.net.packets.OutgoingPacket
import space.blokk.net.packets.Protocol
import space.blokk.net.packets.handshaking.HandshakingProtocol
import space.blokk.net.packets.login.DisconnectPacket
import space.blokk.net.packets.login.LoginProtocol
import space.blokk.net.packets.play.OutgoingPluginMessagePacket
import space.blokk.net.packets.play.PlayProtocol
import space.blokk.server.events.SessionInitializedEvent
import space.blokk.utils.awaitSuspending
import java.net.InetAddress
@ -107,7 +107,7 @@ class BlokkSession(private val channel: Channel) : Session {
logger trace { "Sending packet: $packet" }
eventBus.emit(PacketSendEvent(this@BlokkSession, packet)).ifNotCancelled {
try {
channel.writeAndFlush(PacketMessage(this@BlokkSession, it.packet)).awaitSuspending()
channel.writeAndFlush(OutgoingPacketMessage(this@BlokkSession, it.packet)).awaitSuspending()
} catch (t: Throwable) {
logger.error("Packet send failed:", t)
}

View file

@ -1,22 +1,21 @@
package space.blokk.net
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion
import space.blokk.net.protocols.handshaking.HandshakingProtocol
import space.blokk.net.protocols.handshaking.HandshakingProtocolHandler
import space.blokk.net.protocols.login.LoginProtocol
import space.blokk.net.protocols.login.LoginProtocolHandler
import space.blokk.net.protocols.play.PlayProtocol
import space.blokk.net.protocols.play.PlayProtocolHandler
import space.blokk.net.protocols.status.StatusProtocol
import space.blokk.net.protocols.status.StatusProtocolHandler
open class ProtocolPacketReceivedEventHandler(handlers: Map<out IncomingPacketCompanion<*>, PacketReceivedEventHandler<out IncomingPacket>>) {
private val handlers = handlers.mapKeys { it.key.packetType }
import space.blokk.net.packets.IncomingPacket
import space.blokk.net.packets.handshaking.HandshakingProtocol
import space.blokk.net.packets.handshaking.HandshakingProtocolHandler
import space.blokk.net.packets.login.LoginProtocol
import space.blokk.net.packets.login.LoginProtocolHandler
import space.blokk.net.packets.play.PlayProtocol
import space.blokk.net.packets.play.PlayProtocolHandler
import space.blokk.net.packets.status.StatusProtocol
import space.blokk.net.packets.status.StatusProtocolHandler
import kotlin.reflect.KClass
open class ProtocolPacketReceivedEventHandler(private val handlers: Map<out KClass<out IncomingPacket>, PacketReceivedEventHandler<out IncomingPacket>>) {
suspend fun handle(session: BlokkSession, packet: IncomingPacket) {
@Suppress("UNCHECKED_CAST")
val handler = handlers[packet::class] as PacketReceivedEventHandler<IncomingPacket>?
val handler: PacketReceivedEventHandler<IncomingPacket>? =
handlers[packet::class] as PacketReceivedEventHandler<IncomingPacket>?
handler?.handle(session, packet)
}
}

View file

@ -3,31 +3,32 @@ package space.blokk.net
import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.ByteToMessageCodec
import space.blokk.net.MinecraftDataTypes.readVarInt
import space.blokk.net.MinecraftDataTypes.varIntReadable
import space.blokk.net.MinecraftDataTypes.writeVarInt
class FramingCodec : ByteToMessageCodec<ByteBuf>() {
private var currentLength: Int? = null
override fun encode(ctx: ChannelHandlerContext, msg: ByteBuf, out: ByteBuf) {
with(MinecraftDataTypes) { out.writeVarInt(msg.readableBytes()) }
out.writeVarInt(msg.readableBytes())
msg.readerIndex(0)
out.writeBytes(msg)
}
override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) {
with(MinecraftDataTypes) {
var length = currentLength
var length = currentLength
if (length == null) {
if (msg.varIntReadable()) {
length = msg.readVarInt()
currentLength = length
} else return
}
if (length == null) {
if (msg.varIntReadable()) {
length = msg.readVarInt()
currentLength = length
} else return
}
if (msg.readableBytes() >= length) {
out.add(msg.readBytes(length))
currentLength = null
}
if (msg.readableBytes() >= length) {
out.add(msg.readBytes(length))
currentLength = null
}
}
}

View file

@ -4,14 +4,14 @@ import io.netty.buffer.Unpooled
import space.blokk.BlokkServer
import space.blokk.chat.TextComponent
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.protocols.login.EncryptionRequestPacket
import space.blokk.net.protocols.login.EncryptionResponsePacket
import space.blokk.net.protocols.login.LoginStartPacket
import space.blokk.net.protocols.login.LoginSuccessPacket
import space.blokk.net.protocols.play.JoinGamePacket
import space.blokk.net.protocols.play.PlayProtocol
import space.blokk.net.protocols.play.PlayerAbilitiesPacket
import space.blokk.net.protocols.play.ServerDifficultyPacket
import space.blokk.net.packets.login.EncryptionRequestPacket
import space.blokk.net.packets.login.EncryptionResponsePacket
import space.blokk.net.packets.login.LoginStartPacket
import space.blokk.net.packets.login.LoginSuccessPacket
import space.blokk.net.packets.play.JoinGamePacket
import space.blokk.net.packets.play.PlayProtocol
import space.blokk.net.packets.play.PlayerAbilitiesPacket
import space.blokk.net.packets.play.ServerDifficultyPacket
import space.blokk.players.GameMode
import space.blokk.utils.AuthenticationHelper
import space.blokk.utils.EncryptionUtils

View file

@ -0,0 +1,17 @@
package space.blokk.net
import io.netty.buffer.ByteBuf
import space.blokk.net.packets.IncomingPacket
import space.blokk.net.packets.OutgoingPacket
import space.blokk.net.packets.OutgoingPacketCodec
import space.blokk.net.packets.Packet
abstract class PacketMessage<T : Packet>(val session: BlokkSession, val packet: T)
class OutgoingPacketMessage<T : OutgoingPacket>(session: BlokkSession, packet: T) : PacketMessage<T>(session, packet) {
@Suppress("UNCHECKED_CAST")
val packetCodec = session.currentProtocol.getCodecByType(packet::class) as OutgoingPacketCodec<T>
fun encodePacket(dst: ByteBuf) = with(packetCodec) { packet.encode(dst) }
}
class IncomingPacketMessage<T : IncomingPacket>(session: BlokkSession, packet: T) : PacketMessage<T>(session, packet)

View file

@ -1,15 +0,0 @@
package space.blokk.net
import space.blokk.net.protocols.Packet
import space.blokk.net.protocols.PacketCompanion
data class PacketMessage<T : Packet>(val session: BlokkSession, val packet: T) {
val packetCompanion by lazy {
@Suppress("UNCHECKED_CAST")
session.currentProtocol.packetCompanionsByPacketType[packet::class] as PacketCompanion<T>?
?: throw Exception(
"No packet companion found for this packet type. " +
"This can happen if the packet is not part of the current protocol."
)
}
}

View file

@ -3,10 +3,11 @@ package space.blokk.net
import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.MessageToMessageCodec
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.MinecraftDataTypes.readVarInt
import space.blokk.net.MinecraftDataTypes.writeVarInt
import java.io.IOException
class PacketCodec(private val session: BlokkSession) : MessageToMessageCodec<ByteBuf, PacketMessage<*>>() {
class PacketMessageCodec(private val session: BlokkSession) : MessageToMessageCodec<ByteBuf, PacketMessage<*>>() {
override fun channelActive(ctx: ChannelHandlerContext) {
session.onConnect()
}
@ -16,21 +17,22 @@ class PacketCodec(private val session: BlokkSession) : MessageToMessageCodec<Byt
}
override fun encode(ctx: ChannelHandlerContext, msg: PacketMessage<*>, out: MutableList<Any>) {
if (msg.packet !is OutgoingPacket) throw Error("Only clientbound packets are allowed. This should never happen.")
val buffer = ctx.alloc().buffer()
with(MinecraftDataTypes) { buffer.writeVarInt(msg.packetCompanion.id) }
msg.packet.encode(buffer)
out.add(buffer)
if (msg !is OutgoingPacketMessage<*>) throw UnsupportedOperationException("Only outgoing packets can get encoded")
val dst = ctx.alloc().buffer()
dst.writeVarInt(msg.packetCodec.id)
msg.encodePacket(dst)
out.add(dst)
}
override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) {
val packetID = with(MinecraftDataTypes) { msg.readVarInt() }
val packetID = msg.readVarInt()
val data = msg.readBytes(msg.readableBytes())
val packetCompanion = session.currentProtocol.incomingPacketsByID[packetID]
val codec = session.currentProtocol.incomingPacketCodecsByID[packetID]
?: throw IOException("Client sent an unknown packet (ID: $packetID)")
out.add(PacketMessage(session, packetCompanion.decode(data)))
out.add(IncomingPacketMessage(session, codec.decode(data)))
}
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {

View file

@ -4,11 +4,10 @@ import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import kotlinx.coroutines.launch
import space.blokk.net.events.PacketReceivedEvent
import space.blokk.net.protocols.IncomingPacket
class PacketMessageHandler(private val session: BlokkSession) : SimpleChannelInboundHandler<PacketMessage<*>>() {
override fun channelRead0(ctx: ChannelHandlerContext, msg: PacketMessage<*>) {
if (msg.packet !is IncomingPacket) throw Error("Only serverbound packets are allowed. This should never happen.")
class PacketMessageHandler(private val session: BlokkSession) :
SimpleChannelInboundHandler<IncomingPacketMessage<*>>() {
override fun channelRead0(ctx: ChannelHandlerContext, msg: IncomingPacketMessage<*>) {
session.logger.trace { "Packet received: ${msg.packet}" }
session.scope.launch { session.eventBus.emit(PacketReceivedEvent(session, msg.packet)) }
}

View file

@ -1,12 +1,12 @@
package space.blokk.net.protocols.handshaking
package space.blokk.net.packets.handshaking
import space.blokk.net.PacketReceivedEventHandler
import space.blokk.net.ProtocolPacketReceivedEventHandler
import space.blokk.net.protocols.login.LoginProtocol
import space.blokk.net.protocols.status.StatusProtocol
import space.blokk.net.packets.login.LoginProtocol
import space.blokk.net.packets.status.StatusProtocol
object HandshakingProtocolHandler : ProtocolPacketReceivedEventHandler(mapOf(
HandshakePacket to PacketReceivedEventHandler.of<HandshakePacket> { session, packet ->
HandshakePacket::class to PacketReceivedEventHandler.of<HandshakePacket> { session, packet ->
session.currentProtocol = if (packet.loginAttempt) LoginProtocol else StatusProtocol
}
))

View file

@ -1,4 +1,4 @@
package space.blokk.net.protocols.login
package space.blokk.net.packets.login
import space.blokk.net.BlokkSession
import space.blokk.net.PacketReceivedEventHandler

View file

@ -1,9 +1,11 @@
package space.blokk.net.protocols.login
package space.blokk.net.packets.login
import space.blokk.net.ProtocolPacketReceivedEventHandler
// NOTE: PacketReceivedEventHandler.of<T> MUST have T specified correctly, otherwise the code breaks at runtime
object LoginProtocolHandler : ProtocolPacketReceivedEventHandler(mapOf(
LoginStartPacket to LoginStartPacketHandler,
EncryptionResponsePacket to EncryptionResponsePacketHandler
))
object LoginProtocolHandler : ProtocolPacketReceivedEventHandler(
mapOf(
LoginStartPacket::class to LoginStartPacketHandler,
EncryptionResponsePacket::class to EncryptionResponsePacketHandler
)
)

View file

@ -1,4 +1,4 @@
package space.blokk.net.protocols.login
package space.blokk.net.packets.login
import space.blokk.net.BlokkSession
import space.blokk.net.PacketReceivedEventHandler

View file

@ -1,4 +1,4 @@
package space.blokk.net.protocols.play
package space.blokk.net.packets.play
import space.blokk.net.BlokkSession
import space.blokk.net.PacketReceivedEventHandler

View file

@ -1,4 +1,4 @@
package space.blokk.net.protocols.play
package space.blokk.net.packets.play
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled

View file

@ -1,11 +1,11 @@
package space.blokk.net.protocols.play
package space.blokk.net.packets.play
import space.blokk.net.ProtocolPacketReceivedEventHandler
// NOTE: PacketReceivedEventHandler.of<T> MUST have T specified correctly, otherwise the code breaks at runtime
object PlayProtocolHandler : ProtocolPacketReceivedEventHandler(
mapOf(
ClientSettingsPacket to ClientSettingsPacketHandler,
IncomingPluginMessagePacket to IncomingPluginMessagePacketHandler
ClientSettingsPacket::class to ClientSettingsPacketHandler,
IncomingPluginMessagePacket::class to IncomingPluginMessagePacketHandler
)
)

View file

@ -1,4 +1,4 @@
package space.blokk.net.protocols.status
package space.blokk.net.packets.status
import space.blokk.chat.FormattingCode
import space.blokk.chat.TextComponent
@ -10,7 +10,7 @@ import java.util.*
// NOTE: PacketReceivedEventHandler.of<T> MUST have T specified correctly, otherwise the code breaks at runtime
object StatusProtocolHandler : ProtocolPacketReceivedEventHandler(mapOf(
RequestPacket to PacketReceivedEventHandler.of<RequestPacket> { session, _ ->
RequestPacket::class to PacketReceivedEventHandler.of<RequestPacket> { session, _ ->
session.eventBus.emit(
ServerListInfoRequestEvent(
session,
@ -33,7 +33,7 @@ object StatusProtocolHandler : ProtocolPacketReceivedEventHandler(mapOf(
)
).ifNotCancelled { session.send(it.response) }
},
PingPacket to PacketReceivedEventHandler.of<PingPacket> { session, packet ->
PingPacket::class to PacketReceivedEventHandler.of<PingPacket> { session, packet ->
session.send(PongPacket(packet.payload))
}
))

View file

@ -4,4 +4,5 @@
<appender-ref ref="console"/>
</root>
<logger name="io.netty" level="WARN"/>
<logger name="ch.qos.logback" level="OFF"/>
</configuration>

View file

@ -9,3 +9,4 @@ rootProject.name = "blokk"
include(":blokk-api")
include(":blokk-server")
include(":blokk-packet-codecs")