Make packets public and formatted, fix state machine change state incorrectly

This commit is contained in:
2026-09-05 13:22:00 +08:00
parent 15aefbb869
commit 799edce45f
96 files changed
+1718 -873

No files matched your search

@@ -0,0 +1,146 @@
///*
// * Copyright © 2026 RTAkland
// * Author: RTAkland
// * Date: 2026/9/4
// */
//
//
//package cn.rtast.libmc.protocol
//
//import cn.rtast.libmc.protocol.packet.configuration.ServerboundAckFinishConfigurationPacket
//import cn.rtast.libmc.protocol.packet.configuration.ServerboundPongPacket
//import cn.rtast.libmc.protocol.packet.configuration.ServerboundSelectKnownPacksPacket
//import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket
//import cn.rtast.libmc.protocol.packet.login.ServerboundLoginAcknowledgedPacket
//import cn.rtast.libmc.protocol.packet.login.ServerboundLoginStartPacket
//import cn.rtast.libmc.protocol.packet.play.ServerboundKeepAlivePlayPacket
//import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
//import cn.rtast.libmc.protocol.protocol.state.ProtocolState
//import cn.rtast.libmc.protocol.util.generateOfflineUuid
//import cn.rtast.libmc.common.*
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.coroutineScope
//import kotlinx.coroutines.currentCoroutineContext
//import kotlinx.coroutines.isActive
//import kotlinx.coroutines.launch
//import kotlin.uuid.Uuid
//
//
//public class MinecraftChatClient(
// private val host: String,
// private val port: Int,
// private val username: String,
// private val uuid: Uuid = generateOfflineUuid(username),
// private val context: LibMCContext = LibMCContext(),
//) {
// private var state = ProtocolState.HANDSHAKE
//
// public suspend fun start(): Unit = coroutineScope {
// val socket = Socket(host, port, context)
// val input = socket.openReadChannel()
// val output = socket.openWriteChannel()
//
// executeInitHandshake(output)
//
// val readerJob = launch(Dispatchers.Default) {
// handleIncomingPackets(input, output)
// }
//
// readerJob.join()
// }
//
// private fun executeInitHandshake(output: WriteChannel) {
// val handshakePacket = ServerboundHandshakePacket(776, host, port.toUShort(), HandshakeIntent.LOGIN)
// output.sendPacket(handshakePacket, ServerboundHandshakePacket)
// state = ProtocolState.LOGIN
//
// val loginStartPacket = ServerboundLoginStartPacket(username, uuid)
// output.sendPacket(loginStartPacket, ServerboundLoginStartPacket)
// }
//
// private suspend fun handleIncomingPackets(input: ReadChannel, output: WriteChannel) {
// try {
// while (currentCoroutineContext().isActive) {
// val packetLength = input.readVarInt()
// if (packetLength <= 0) continue
//
// val packetBytes = ByteArray(packetLength)
// input.readFully(packetBytes, 0, packetLength)
//
// val buffer = BytesBuffer(packetBytes)
// val packetId = buffer.readVarInt()
// println("received -> State: $state | ID: 0x${packetId.toString(16).uppercase()} | Length: $packetLength")
// try {
// when (state) {
// ProtocolState.LOGIN -> handleLoginPackets(packetId, output)
// ProtocolState.CONFIGURATION -> handleConfigurationPackets(packetId, buffer, output)
// ProtocolState.PLAY -> handlePlayPackets(packetId, buffer, output)
// else -> {}
// }
// } catch (e: Exception) {
// println("parsing 0x${packetId.toString(16).uppercase()} Payload failed: ${e.message}")
// }
// }
// } catch (e: Exception) {
// e.printStackTrace()
// println("disconnecting: ${e.message}")
// }
// }
//
// private fun handleLoginPackets(packetId: Int, output: WriteChannel) {
// when (packetId) {
// 0x02 -> {
// output.sendPacket(ServerboundLoginAcknowledgedPacket(), ServerboundLoginAcknowledgedPacket)
// state = ProtocolState.CONFIGURATION
// println("[3/4] sent LoginAcknowledgedPacket -> switching to CONFIGURATION state")
//
// output.sendPacket(
// ServerboundSelectKnownPacksPacket(knownPacks = emptyList()),
// ServerboundSelectKnownPacksPacket
// )
// }
//
// 0x00 -> {
// println("login denied (ClientboundDisconnectLoginPacket)")
// }
// }
// }
//
// private fun handleConfigurationPackets(packetId: Int, packetBuffer: BytesBuffer, output: WriteChannel) {
// when (packetId) {
// 0x0E -> {
// println("received ClientboundSelectKnownPacksPacket")
// }
//
// 0x03 -> {
// output.sendPacket(ServerboundAckFinishConfigurationPacket, ServerboundAckFinishConfigurationPacket)
// state = ProtocolState.PLAY
// }
//
// 0x05 -> {
// output.sendPacket(ServerboundPongPacket(0), ServerboundPongPacket)
// }
//
// 0x01 -> println("configuration state disconnected")
// }
// }
//
// private fun handlePlayPackets(packetId: Int, packetBuffer: BytesBuffer, output: WriteChannel) {
// try {
// when (packetId) {
// 0x2B -> println("[PLAY] Joined world")
//
// 0x2c -> {
// val keepAliveId = packetBuffer.readLong()
// output.sendPacket(ServerboundKeepAlivePlayPacket(id = keepAliveId), ServerboundKeepAlivePlayPacket)
// println("[PLAY] reply keep alive packet $keepAliveId")
// }
//
// 0x1D -> println("[PLAY] disconnected (ClientboundDisconnectPlayPacket)")
// else -> {}
// }
// } catch (e: Exception) {
// println("parsing 0x${packetId.toString(16).uppercase()} failed, skipped: ${e.message}")
// }
// }
//}
@@ -0,0 +1,19 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.chat
public enum class ChatFilterType(public val id: Int) {
PASS_THROUGH(0),
FULLY_FILTERED(1),
PARTIALLY_FILTERED(2);
public companion object {
public fun fromId(id: Int): ChatFilterType =
entries.firstOrNull { it.id == id } ?: PASS_THROUGH
}
}
@@ -0,0 +1,48 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.chat
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.writeVarInt
public data class PreviousMessageEntry(
val messageId: Int,
val signature: ByteArray?,
) {
public companion object Codec : PacketCodec<PreviousMessageEntry> {
override fun encode(buffer: BytesBuffer, value: PreviousMessageEntry) {
buffer.writeVarInt(value.messageId)
if (value.messageId == 0) {
val sig = requireNotNull(value.signature) { "signature must be present when messageId is 0" }
require(sig.size == 256)
buffer.writeBytes(sig)
}
}
override fun decode(buffer: BytesBuffer): PreviousMessageEntry = throw UnsupportedOperationException() // TODO
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as PreviousMessageEntry
if (messageId != other.messageId) return false
if (!signature.contentEquals(other.signature)) return false
return true
}
override fun hashCode(): Int {
var result = messageId
result = 31 * result + (signature?.contentHashCode() ?: 0)
return result
}
}
@@ -0,0 +1,22 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.client
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
import kotlin.concurrent.Volatile
internal class ClientStateMachine {
@Volatile
var currentState: ProtocolState = ProtocolState.HANDSHAKE
private set
fun transitionTo(newState: ProtocolState) {
println("Changing State $currentState to $newState")
currentState = newState
}
}
@@ -0,0 +1,133 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.client
import cn.rtast.libmc.common.LibMCContext
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.packet.UnknownPacket
import cn.rtast.libmc.protocol.network.NetworkChannel
import cn.rtast.libmc.protocol.packet.configuration.*
import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket
import cn.rtast.libmc.protocol.packet.login.ClientboundDisconnectLoginPacket
import cn.rtast.libmc.protocol.packet.login.ClientboundLoginSuccessPacket
import cn.rtast.libmc.protocol.packet.login.ServerboundLoginAcknowledgedPacket
import cn.rtast.libmc.protocol.packet.login.ServerboundLoginStartPacket
import cn.rtast.libmc.protocol.packet.play.*
import cn.rtast.libmc.protocol.protocol.GameProtocols
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
import cn.rtast.libmc.protocol.util.generateOfflineUuid
import kotlinx.coroutines.*
import kotlin.uuid.Uuid
public class MinecraftClient(
private val host: String,
private val port: Int = 25565,
private val username: String,
private val uuid: Uuid = generateOfflineUuid(username),
private val context: LibMCContext = LibMCContext(),
) {
private val stateMachine = ClientStateMachine()
private val networkChannel = NetworkChannel(host, port, context, stateMachine)
private val listeners = mutableListOf<(MinecraftPacket) -> Unit>()
private var listenJob: Job? = null
public suspend fun connect(protocolVersion: Int = 776) {
networkChannel.connect()
startListening()
networkChannel.sendPacket(
ServerboundHandshakePacket(
protocolVersion, host,
port.toUShort(),
HandshakeIntent.LOGIN
)
)
stateMachine.transitionTo(ProtocolState.LOGIN)
networkChannel.sendPacket(ServerboundLoginStartPacket(username, uuid))
listenJob?.join()
}
private fun startListening() {
listenJob = CoroutineScope(Dispatchers.IO).launch {
try {
while (isActive) {
val packet = networkChannel.readNextPacket()
handleIncomingPackets(packet)
listeners.forEach { it.invoke(packet) }
}
} catch (e: Exception) {
e.printStackTrace()
if (isActive) {
println("Network read loop exception: ${e.message}")
close()
}
}
}
}
private fun handleIncomingPackets(packet: MinecraftPacket) {
when (packet) {
is ClientboundLoginSuccessPacket -> {
networkChannel.sendPacket(ServerboundLoginAcknowledgedPacket)
stateMachine.transitionTo(ProtocolState.CONFIGURATION)
}
is ClientboundDisconnectLoginPacket -> {
println("Login denied: ${packet.reason}")
close()
}
is ClientboundSelectKnownPacksPacket -> {
networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList())) // TODO empty resource packs list
}
is ClientboundPingPacket -> networkChannel.sendPacket(ServerboundPongPacket(packet.id))
is ClientboundKeepAliveConfigurationPacket -> {
networkChannel.sendPacket(ServerboundKeepAliveConfigurationPacket(packet.id))
}
is ClientboundFinishConfigurationPacket -> {
networkChannel.sendPacket(ServerboundAckFinishConfigurationPacket)
stateMachine.transitionTo(ProtocolState.PLAY)
}
is ClientboundDisconnectConfigurationPacket -> {
println("Configuration disconnected: ${packet.reason}")
close()
}
is ClientboundLoginPlayPacket -> {
println("Successfully joined world! Entity ID: ${packet.entityId}")
}
is ClientboundKeepAlivePlayPacket -> {
networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(id = packet.id))
}
is ClientboundStartConfigurationPacket -> {
networkChannel.sendPacket(ServerboundConfigurationAcknowledgedPacket)
stateMachine.transitionTo(ProtocolState.CONFIGURATION)
}
is ClientboundDisconnectPlayPacket -> {
println("Disconnected from play session: ${packet.reason}")
close()
}
// else -> println((packet as? UnknownPacket)?.data?.contentToString() ?: packet)
}
}
public fun onPacket(listener: (MinecraftPacket) -> Unit) {
listeners.add(listener)
}
public fun close() {
listenJob?.cancel()
networkChannel.close()
}
}
@@ -0,0 +1,9 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.codec
@@ -0,0 +1,56 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.network
import cn.rtast.libmc.common.*
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.protocol.client.ClientStateMachine
import cn.rtast.libmc.protocol.protocol.GameProtocols
internal class NetworkChannel(
private val host: String,
private val port: Int,
private val context: LibMCContext,
private val stateMachine: ClientStateMachine,
) {
private var socket: Socket? = null
private var readChannel: ReadChannel? = null
private var writeChannel: WriteChannel? = null
fun connect() {
val sk = Socket(host, port, context)
this.socket = sk
this.readChannel = sk.openReadChannel()
this.writeChannel = sk.openWriteChannel()
}
fun readNextPacket(): MinecraftPacket {
val channel = requireNotNull(readChannel) { "ReadChannel not connected" }
val length = channel.readVarInt()
val buf = channel.readBytes(length).wrap()
val currentState = stateMachine.currentState
val packetId = buf.readVarInt()
return GameProtocols.clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, buf)
}
fun sendPacket(packet: MinecraftPacket) {
val channel = requireNotNull(writeChannel) { "WriteChannel not connected" }
val bodyBuffer = BytesBuffer()
GameProtocols.serverboundGameProtocols.getRegistry(stateMachine.currentState).encodePacket(bodyBuffer, packet)
val frameBuffer = BytesBuffer().apply {
writeVarInt(bodyBuffer.size)
writeBuffer(bodyBuffer)
}
channel.writeFully(frameBuffer.toByteArray())
channel.flush()
}
fun close() {
socket?.close()
}
}
@@ -0,0 +1,27 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.protocol.protocol.game.Identifier
import cn.rtast.libmc.protocol.protocol.game.readIdentifier
import cn.rtast.libmc.protocol.protocol.game.writeIdentifier
public data class ClientboundCookieRequestPacket(val key: Identifier) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundCookieRequestPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundCookieRequestPacket) {
buffer.writeIdentifier(value.key)
}
override fun decode(buffer: BytesBuffer): ClientboundCookieRequestPacket {
return ClientboundCookieRequestPacket(key = buffer.readIdentifier())
}
}
}
@@ -0,0 +1,48 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.protocol.protocol.game.Identifier
import cn.rtast.libmc.protocol.protocol.game.readIdentifier
import cn.rtast.libmc.protocol.protocol.game.writeIdentifier
public data class ClientboundCustomPayloadPacket(val channel: Identifier, val data: ByteArray) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundCustomPayloadPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundCustomPayloadPacket) {
buffer.writeIdentifier(value.channel)
buffer.writeBytes(value.data)
}
override fun decode(buffer: BytesBuffer): ClientboundCustomPayloadPacket {
val channel = buffer.readIdentifier()
val data = buffer.readBytes(buffer.remaining.toInt())
return ClientboundCustomPayloadPacket(channel, data)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ClientboundCustomPayloadPacket
if (channel != other.channel) return false
if (!data.contentEquals(other.data)) return false
return true
}
override fun hashCode(): Int {
var result = channel.hashCode()
result = 31 * result + data.contentHashCode()
return result
}
}
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.protocol.util.readMinimalTextNbt
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.BytesBuffer
public data class ClientboundDisconnectConfigurationPacket(val reason: String) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundDisconnectConfigurationPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundDisconnectConfigurationPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundDisconnectConfigurationPacket {
val reasonText = buffer.readMinimalTextNbt()
return ClientboundDisconnectConfigurationPacket(reason = reasonText)
}
}
}
@@ -0,0 +1,20 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public data object ClientboundFinishConfigurationPacket : MinecraftPacket,
PacketCodec<ClientboundFinishConfigurationPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundFinishConfigurationPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundFinishConfigurationPacket {
return ClientboundFinishConfigurationPacket
}
}
@@ -0,0 +1,24 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public data class ClientboundKeepAliveConfigurationPacket(val id: Long) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundKeepAliveConfigurationPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundKeepAliveConfigurationPacket) {
buffer.writeLong(value.id)
}
override fun decode(buffer: BytesBuffer): ClientboundKeepAliveConfigurationPacket {
return ClientboundKeepAliveConfigurationPacket(buffer.readLong())
}
}
}
@@ -0,0 +1,22 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public data class ClientboundPingPacket(val id: Int) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundPingPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundPingPacket) {
buffer.writeInt(value.id)
}
override fun decode(buffer: BytesBuffer): ClientboundPingPacket = ClientboundPingPacket(buffer.readInt())
}
}
@@ -0,0 +1,29 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.readVarInt
import cn.rtast.libmc.common.writeVarInt
public data class ClientboundSelectKnownPacksPacket(val knownPacks: List<KnownPacks>) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundSelectKnownPacksPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundSelectKnownPacksPacket) {
buffer.writeVarInt(value.knownPacks.size)
value.knownPacks.forEach { KnownPacks.encode(buffer, it) }
}
override fun decode(buffer: BytesBuffer): ClientboundSelectKnownPacksPacket {
val packsCount = buffer.readVarInt()
val packs = List(packsCount) { KnownPacks.decode(buffer) }
return ClientboundSelectKnownPacksPacket(packs)
}
}
}
@@ -0,0 +1,33 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.readMcString
import cn.rtast.libmc.common.writeMcString
import kotlinx.serialization.Serializable
@Serializable
public data class KnownPacks(val namespace: String, val id: String, val version: String) {
public companion object Codec : PacketCodec<KnownPacks> {
override fun encode(buffer: BytesBuffer, value: KnownPacks) {
buffer.writeMcString(value.namespace)
buffer.writeMcString(value.id)
buffer.writeMcString(value.version)
}
override fun decode(buffer: BytesBuffer): KnownPacks {
val namespace = buffer.readMcString()
val id = buffer.readMcString()
val version = buffer.readMcString()
return KnownPacks(namespace, id, version)
}
}
}
@@ -0,0 +1,20 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public data object ServerboundAckFinishConfigurationPacket : MinecraftPacket,
PacketCodec<ServerboundAckFinishConfigurationPacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundAckFinishConfigurationPacket) {}
override fun decode(buffer: BytesBuffer): ServerboundAckFinishConfigurationPacket =
ServerboundAckFinishConfigurationPacket
}
@@ -0,0 +1,60 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.readVarInt
import cn.rtast.libmc.common.writeVarInt
import cn.rtast.libmc.protocol.protocol.game.Identifier
import cn.rtast.libmc.protocol.protocol.game.readIdentifier
import cn.rtast.libmc.protocol.protocol.game.writeIdentifier
public data class ServerboundCookieResponsePacket(val key: Identifier, val payload: ByteArray?) : MinecraftPacket {
public companion object Codec : PacketCodec<ServerboundCookieResponsePacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundCookieResponsePacket) {
buffer.writeIdentifier(value.key)
if (value.payload != null) {
buffer.writeBoolean(true)
buffer.writeVarInt(value.payload.size)
buffer.writeBytes(value.payload)
} else {
buffer.writeBoolean(false)
}
}
override fun decode(buffer: BytesBuffer): ServerboundCookieResponsePacket {
val key = buffer.readIdentifier()
val hasPayload = buffer.readBoolean()
val payload = if (hasPayload) {
val length = buffer.readVarInt()
buffer.readBytes(length)
} else null
return ServerboundCookieResponsePacket(key, payload)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ServerboundCookieResponsePacket
if (key != other.key) return false
if (!payload.contentEquals(other.payload)) return false
return true
}
override fun hashCode(): Int {
var result = key.hashCode()
result = 31 * result + (payload?.contentHashCode() ?: 0)
return result
}
}
@@ -0,0 +1,24 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public data class ServerboundKeepAliveConfigurationPacket(val id: Long) : MinecraftPacket {
public companion object Codec : PacketCodec<ServerboundKeepAliveConfigurationPacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundKeepAliveConfigurationPacket) {
buffer.writeLong(value.id)
}
override fun decode(buffer: BytesBuffer): ServerboundKeepAliveConfigurationPacket {
return ServerboundKeepAliveConfigurationPacket(buffer.readLong())
}
}
}
@@ -0,0 +1,22 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public data class ServerboundPongPacket(val id: Int) : MinecraftPacket {
public companion object Codec : PacketCodec<ServerboundPongPacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundPongPacket) {
buffer.writeInt(value.id)
}
override fun decode(buffer: BytesBuffer): ServerboundPongPacket = ServerboundPongPacket(buffer.readInt())
}
}
@@ -0,0 +1,29 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.configuration
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.readVarInt
import cn.rtast.libmc.common.writeVarInt
public data class ServerboundSelectKnownPacksPacket(val knownPacks: List<KnownPacks>) : MinecraftPacket {
public companion object Codec : PacketCodec<ServerboundSelectKnownPacksPacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundSelectKnownPacksPacket) {
buffer.writeVarInt(value.knownPacks.size)
value.knownPacks.forEach { KnownPacks.encode(buffer, it) }
}
override fun decode(buffer: BytesBuffer): ServerboundSelectKnownPacksPacket {
val packsCount = buffer.readVarInt()
val packs = List(packsCount) { KnownPacks.decode(buffer) }
return ServerboundSelectKnownPacksPacket(packs)
}
}
}
@@ -0,0 +1,33 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.handshake
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.writeMcString
import cn.rtast.libmc.common.writeVarInt
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
public data class ServerboundHandshakePacket(
val protocolVersion: Int,
val serverAddress: String,
val serverPort: UShort,
val intent: HandshakeIntent,
) : MinecraftPacket {
public companion object Codec : PacketCodec<ServerboundHandshakePacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundHandshakePacket) {
buffer.writeVarInt(value.protocolVersion)
buffer.writeMcString(value.serverAddress)
buffer.writeShort(value.serverPort.toShort())
buffer.writeVarInt(value.intent.intentID)
}
override fun decode(buffer: BytesBuffer): ServerboundHandshakePacket = throw UnsupportedOperationException()
}
}
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.login
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.readMcString
public data class ClientboundDisconnectLoginPacket(val reason: String) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundDisconnectLoginPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundDisconnectLoginPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundDisconnectLoginPacket {
val reasonJson = buffer.readMcString()
return ClientboundDisconnectLoginPacket(reason = reasonJson)
}
}
}
@@ -0,0 +1,26 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.login
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.readUuid
import cn.rtast.libmc.protocol.profile.GameProfile
import kotlin.uuid.Uuid
public data class ClientboundLoginSuccessPacket(val gameProfile: GameProfile, val sessionId: Uuid) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundLoginSuccessPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundLoginSuccessPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundLoginSuccessPacket {
val gameProfile = GameProfile.decode(buffer)
val sessionId = buffer.readUuid()
return ClientboundLoginSuccessPacket(gameProfile, sessionId)
}
}
}
@@ -0,0 +1,19 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.login
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public data object ServerboundLoginAcknowledgedPacket : MinecraftPacket,
PacketCodec<ServerboundLoginAcknowledgedPacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundLoginAcknowledgedPacket) {}
override fun decode(buffer: BytesBuffer): ServerboundLoginAcknowledgedPacket = throw UnsupportedOperationException()
}
@@ -0,0 +1,26 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.login
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.writeMcString
import cn.rtast.libmc.common.writeUuid
import kotlin.uuid.Uuid
public data class ServerboundLoginStartPacket(val username: String, val playerUuid: Uuid) : MinecraftPacket {
public companion object Codec : PacketCodec<ServerboundLoginStartPacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundLoginStartPacket) {
buffer.writeMcString(value.username)
buffer.writeUuid(value.playerUuid)
}
override fun decode(buffer: BytesBuffer): ServerboundLoginStartPacket = throw UnsupportedOperationException()
}
}
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.play
import cn.rtast.libmc.protocol.util.readMinimalTextNbt
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.BytesBuffer
public data class ClientboundDisconnectPlayPacket(val reason: String) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundDisconnectPlayPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundDisconnectPlayPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundDisconnectPlayPacket {
val reasonText = buffer.readMinimalTextNbt()
return ClientboundDisconnectPlayPacket(reason = reasonText)
}
}
}
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.play
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public data class ClientboundKeepAlivePlayPacket(val id: Long) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundKeepAlivePlayPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundKeepAlivePlayPacket) {
buffer.writeLong(value.id)
}
override fun decode(buffer: BytesBuffer): ClientboundKeepAlivePlayPacket =
ClientboundKeepAlivePlayPacket(buffer.readLong())
}
}
@@ -0,0 +1,83 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.play
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.readVarInt
import cn.rtast.libmc.protocol.protocol.game.BlockPos
import cn.rtast.libmc.protocol.protocol.game.GameMode
import cn.rtast.libmc.protocol.protocol.game.Identifier
import cn.rtast.libmc.protocol.protocol.game.readBlockPos
import cn.rtast.libmc.protocol.protocol.game.readIdentifier
public data class ClientboundLoginPlayPacket(
val entityId: Int,
val isHardcore: Boolean,
val dimensionNames: List<Identifier>,
val maxPlayers: Int,
val viewDistance: Int,
val simulationDistance: Int,
val isReducedDebugInfo: Boolean,
val enableRespawnScreen: Boolean,
val doLimitedCrafting: Boolean,
val dimensionType: Int,
val dimensionName: Identifier,
val hashedSeed: Long,
val gameMode: GameMode,
val previousGameMode: GameMode,
val isDebug: Boolean,
val isFlat: Boolean,
val hasDeathLocation: Boolean,
val deathDimensionName: Identifier?,
val deathLocation: BlockPos?,
val portalCooldown: Int,
val seaLevel: Int,
val isOnlineMode: Boolean,
val enforceSecureChat: Boolean,
) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundLoginPlayPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundLoginPlayPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundLoginPlayPacket {
val entityId = buffer.readInt()
val isHardcore = buffer.readBoolean()
val dimensionNamesCount = buffer.readVarInt()
val dimensionNames = List(dimensionNamesCount) { buffer.readIdentifier() }
val maxPlayers = buffer.readVarInt()
val viewDistance = buffer.readVarInt()
val simulationDistance = buffer.readVarInt()
val isReducedDebugInfo = buffer.readBoolean()
val enableRespawnScreen = buffer.readBoolean()
val doLimitedCrafting = buffer.readBoolean()
val dimensionType = buffer.readVarInt()
val dimensionName = buffer.readIdentifier()
val hashedSeed = buffer.readLong()
val gameMode = GameMode.fromID(buffer.readByte().toUByte())
val previousGameMode = GameMode.fromID(buffer.readByte())
val isDebug = buffer.readBoolean()
val isFlat = buffer.readBoolean()
val hasDeathLocation = buffer.readBoolean()
val deathDimensionName = if (hasDeathLocation) buffer.readIdentifier() else null
val deathLocation = if (hasDeathLocation) buffer.readBlockPos() else null
val portalCooldown = buffer.readVarInt()
val seaLevel = buffer.readVarInt()
val isOnlineMode = buffer.readBoolean()
val isEnforcesSecureChat = buffer.readBoolean()
return ClientboundLoginPlayPacket(
entityId, isHardcore, dimensionNames, maxPlayers,
viewDistance, simulationDistance, isReducedDebugInfo,
enableRespawnScreen, doLimitedCrafting, dimensionType,
dimensionName, hashedSeed, gameMode, previousGameMode,
isDebug, isFlat, hasDeathLocation, deathDimensionName,
deathLocation, portalCooldown, seaLevel, isOnlineMode,
isEnforcesSecureChat
)
}
}
}
@@ -0,0 +1,24 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.packet.play
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public data class ClientboundPingPlayPacket(val id: Int) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundPingPlayPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundPingPlayPacket) {
buffer.writeInt(value.id)
}
override fun decode(buffer: BytesBuffer): ClientboundPingPlayPacket {
return ClientboundPingPlayPacket(id = buffer.readInt())
}
}
}
@@ -0,0 +1,114 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.play
import cn.rtast.libmc.common.*
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.protocol.chat.ChatFilterType
import cn.rtast.libmc.protocol.chat.PreviousMessageEntry
import cn.rtast.libmc.protocol.util.writeMinimalTextNbt
import kotlin.uuid.Uuid
public data class ClientboundPlayerChatMessagePacket(
val globalIndex: Int,
val sender: Uuid,
val index: Int,
val messageSignature: ByteArray?,
val message: String,
val timestamp: Long,
val salt: Long,
val previousMessages: List<PreviousMessageEntry>,
val unsignedContent: String?,
val filterType: ChatFilterType,
val filterMaskBits: LongArray?,
val chatType: Int,
val senderName: String,
val targetName: String?,
) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundPlayerChatMessagePacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundPlayerChatMessagePacket) {
buffer.writeVarInt(value.globalIndex)
buffer.writeUuid(value.sender)
buffer.writeVarInt(value.index)
val hasSignature = value.messageSignature != null
buffer.writeBoolean(hasSignature)
if (hasSignature) buffer.writeBytes(requireNotNull(value.messageSignature))
buffer.writeMcString(value.message)
buffer.writeLong(value.timestamp)
buffer.writeLong(value.salt)
require(value.previousMessages.size == 20)
buffer.writeVarInt(value.previousMessages.size)
value.previousMessages.forEach { entry -> PreviousMessageEntry.encode(buffer, entry) }
val hasUnsignedContent = value.unsignedContent != null
buffer.writeBoolean(hasUnsignedContent)
value.unsignedContent?.let { buffer.writeMinimalTextNbt(it) }
buffer.writeVarInt(value.filterType.id)
if (value.filterType == ChatFilterType.PARTIALLY_FILTERED) {
val mask = requireNotNull(value.filterMaskBits)
buffer.writeVarInt(mask.size)
mask.forEach { buffer.writeLong(it) }
}
buffer.writeVarInt(value.chatType)
buffer.writeMinimalTextNbt(value.senderName)
val hasTargetName = value.targetName != null
buffer.writeBoolean(hasTargetName)
value.targetName?.let { buffer.writeMinimalTextNbt(it) }
}
override fun decode(buffer: BytesBuffer): ClientboundPlayerChatMessagePacket =
throw UnsupportedOperationException() // TODO
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ClientboundPlayerChatMessagePacket
if (globalIndex != other.globalIndex) return false
if (index != other.index) return false
if (timestamp != other.timestamp) return false
if (salt != other.salt) return false
if (chatType != other.chatType) return false
if (sender != other.sender) return false
if (!messageSignature.contentEquals(other.messageSignature)) return false
if (message != other.message) return false
if (previousMessages != other.previousMessages) return false
if (unsignedContent != other.unsignedContent) return false
if (filterType != other.filterType) return false
if (!filterMaskBits.contentEquals(other.filterMaskBits)) return false
if (senderName != other.senderName) return false
if (targetName != other.targetName) return false
return true
}
override fun hashCode(): Int {
var result = globalIndex
result = 31 * result + index
result = 31 * result + timestamp.hashCode()
result = 31 * result + salt.hashCode()
result = 31 * result + chatType
result = 31 * result + sender.hashCode()
result = 31 * result + (messageSignature?.contentHashCode() ?: 0)
result = 31 * result + message.hashCode()
result = 31 * result + previousMessages.hashCode()
result = 31 * result + unsignedContent.hashCode()
result = 31 * result + filterType.hashCode()
result = 31 * result + (filterMaskBits?.contentHashCode() ?: 0)
result = 31 * result + senderName.hashCode()
result = 31 * result + targetName.hashCode()
return result
}
}
@@ -0,0 +1,20 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.packet.play
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public data object ClientboundStartConfigurationPacket : MinecraftPacket,
PacketCodec<ClientboundStartConfigurationPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundStartConfigurationPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundStartConfigurationPacket {
return ClientboundStartConfigurationPacket
}
}
@@ -0,0 +1,34 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.play
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.writeMcString
import cn.rtast.libmc.common.writeVarInt
import kotlin.time.Clock
public data class ServerboundChatMessagePacket(
val message: String,
val timestamp: Long = Clock.System.now().toEpochMilliseconds(),
val salt: Long = 0L,
) : MinecraftPacket {
public companion object Codec : PacketCodec<ServerboundChatMessagePacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundChatMessagePacket) {
buffer.writeMcString(value.message)
buffer.writeLong(value.timestamp)
buffer.writeLong(value.salt)
buffer.writeBoolean(false) // has signature
buffer.writeVarInt(0) // message count
buffer.writeBytes(byteArrayOf(0, 0, 0))
}
override fun decode(buffer: BytesBuffer): ServerboundChatMessagePacket = throw UnsupportedOperationException()
}
}
@@ -0,0 +1,20 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.packet.play
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public object ServerboundConfigurationAcknowledgedPacket : MinecraftPacket,
PacketCodec<ServerboundConfigurationAcknowledgedPacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundConfigurationAcknowledgedPacket) {}
override fun decode(buffer: BytesBuffer): ServerboundConfigurationAcknowledgedPacket {
return ServerboundConfigurationAcknowledgedPacket
}
}
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.packet.play
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public data class ServerboundKeepAlivePlayPacket(val id: Long) : MinecraftPacket {
public companion object Codec : PacketCodec<ServerboundKeepAlivePlayPacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundKeepAlivePlayPacket) {
buffer.writeLong(value.id)
}
override fun decode(buffer: BytesBuffer): ServerboundKeepAlivePlayPacket =
ServerboundKeepAlivePlayPacket(buffer.readLong())
}
}
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.packet.play
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
public data class ServerboundPongPlayPacket(val id: Int) : MinecraftPacket {
public companion object Codec : PacketCodec<ServerboundPongPlayPacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundPongPlayPacket) {
buffer.writeInt(value.id)
}
override fun decode(buffer: BytesBuffer): ServerboundPongPlayPacket {
return ServerboundPongPlayPacket(id = buffer.readInt())
}
}
}
@@ -0,0 +1,60 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.profile
import cn.rtast.libmc.common.*
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
public data class GameProfile(
val uuid: Uuid,
val username: String,
val properties: List<Property>,
) {
@Serializable
public data class Property(
val name: String,
val value: String,
val signature: String?,
) {
public companion object Codec : PacketCodec<Property> {
override fun encode(buffer: BytesBuffer, value: Property) {
buffer.writeMcString(value.name)
buffer.writeMcString(value.value)
buffer.writeBoolean(value.signature != null)
value.signature?.let { buffer.writeMcString(it) }
}
override fun decode(buffer: BytesBuffer): Property {
val name = buffer.readMcString()
val value = buffer.readMcString()
val hasSignature = buffer.readBoolean()
val signature = if (hasSignature) buffer.readMcString() else null
return Property(name, value, signature)
}
}
}
public companion object Codec : PacketCodec<GameProfile> {
override fun encode(buffer: BytesBuffer, value: GameProfile) {
buffer.writeUuid(value.uuid)
buffer.writeMcString(value.username)
buffer.writeVarInt(value.properties.size) // prefixed array
value.properties.forEach { prop -> Property.encode(buffer, prop) }
}
override fun decode(buffer: BytesBuffer): GameProfile {
val uuid = buffer.readUuid()
val username = buffer.readMcString()
val propertyCount = buffer.readVarInt()
val properties = List(propertyCount) { Property.decode(buffer) }
return GameProfile(uuid, username, properties)
}
}
}
@@ -0,0 +1,64 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.protocol
import cn.rtast.libmc.protocol.packet.configuration.*
import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket
import cn.rtast.libmc.protocol.packet.login.ClientboundDisconnectLoginPacket
import cn.rtast.libmc.protocol.packet.login.ClientboundLoginSuccessPacket
import cn.rtast.libmc.protocol.packet.login.ServerboundLoginAcknowledgedPacket
import cn.rtast.libmc.protocol.packet.login.ServerboundLoginStartPacket
import cn.rtast.libmc.protocol.packet.play.*
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
import cn.rtast.libmc.protocol.protocol.state.ProtocolStateRegistry
internal object GameProtocols {
val clientboundGameProtocols = ProtocolStateRegistry().apply {
register(ProtocolState.CONFIGURATION) {
register(0x00, ClientboundCookieRequestPacket)
register(0x01, ClientboundCustomPayloadPacket)
register(0x02, ClientboundDisconnectConfigurationPacket)
register(0x03, ClientboundFinishConfigurationPacket)
register(0x04, ClientboundKeepAliveConfigurationPacket)
register(0x05, ClientboundPingPacket)
register(0x0E, ClientboundSelectKnownPacksPacket)
}
register(ProtocolState.LOGIN) {
register(0x00, ClientboundDisconnectLoginPacket)
register(0x02, ClientboundLoginSuccessPacket)
}
register(ProtocolState.PLAY) {
register(0x2C, ClientboundKeepAlivePlayPacket)
register(0x2E, ClientboundLoginPlayPacket)
register(0x3A, ClientboundPingPlayPacket)
register(0x3D, ClientboundPlayerChatMessagePacket)
register(0x76, ClientboundStartConfigurationPacket)
}
}
val serverboundGameProtocols = ProtocolStateRegistry().apply {
register(ProtocolState.HANDSHAKE) {
register(0x00, ServerboundHandshakePacket)
}
register(ProtocolState.CONFIGURATION) {
register(0x03, ServerboundAckFinishConfigurationPacket)
register(0x04, ServerboundKeepAliveConfigurationPacket)
register(0x05, ServerboundPongPacket)
register(0x07, ServerboundSelectKnownPacksPacket)
}
register(ProtocolState.LOGIN) {
register(0x00, ServerboundLoginStartPacket)
register(0x03, ServerboundLoginAcknowledgedPacket)
}
register(ProtocolState.PLAY) {
register(0x09, ServerboundChatMessagePacket)
register(0x0B, ServerboundPongPlayPacket)
register(0x0D, ServerboundConfigurationAcknowledgedPacket)
register(0x1C, ServerboundKeepAlivePlayPacket)
}
}
}
@@ -0,0 +1,12 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.protocol
internal enum class PacketDirection {
SERVERBOUND, CLIENTBOUND
}
@@ -0,0 +1,43 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.protocol.game
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.BytesBuffer
import kotlinx.serialization.Serializable
/**
* An integer/block position: x (-33 554 432 to 33 554 431), z (-33 554 432 to 33 554 431), y (-2048 to 2047)
* ref: https://minecraft.wiki/w/Java_Edition_protocol/Packets#Type:Position
*/
@Serializable
public data class BlockPos(val x: Int, val y: Int, val z: Int) {
public companion object : PacketCodec<BlockPos> {
private const val PACKED_X_MASK = 0x3FFFFFFL // 26 bits
private const val PACKED_Y_MASK = 0xFFFL // 12 bits
private const val PACKED_Z_MASK = 0x3FFFFFFL // 26 bits
override fun decode(buffer: BytesBuffer): BlockPos {
val packed = buffer.readLong()
val x = (packed shr 38).toInt()
val y = (packed shl 52 shr 52).toInt()
val z = (packed shl 26 shr 38).toInt()
return BlockPos(x, y, z)
}
override fun encode(buffer: BytesBuffer, value: BlockPos) {
val xLong = (value.x.toLong() and PACKED_X_MASK)
val yLong = (value.y.toLong() and PACKED_Y_MASK)
val zLong = (value.z.toLong() and PACKED_Z_MASK)
buffer.writeLong(xLong shl 38 or (zLong shl 12) or yLong)
}
}
}
internal fun BytesBuffer.readBlockPos(): BlockPos = BlockPos.decode(this)
internal fun BytesBuffer.writeBlockPos(pos: BlockPos) = BlockPos.encode(this, pos)
@@ -0,0 +1,26 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.protocol.game
public enum class GameMode(public val id: Byte) {
Survival(0),
Creative(1),
Adventure(2),
Spectator(3),
Undefined(-1),
/**
* reserved
*/
Unknown(-99);
public companion object {
public fun fromID(id: Byte): GameMode = entries.firstOrNull { it.id == id } ?: Unknown
public fun fromID(id: UByte): GameMode = fromID(id.toByte())
}
}
@@ -0,0 +1,38 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.protocol.game
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.readMcString
import cn.rtast.libmc.common.writeMcString
import kotlin.jvm.JvmInline
/**
* ref: https://minecraft.wiki/w/Java_Edition_protocol/Packets#Identifier
*/
@JvmInline
public value class Identifier(public val full: String) {
public val namespace: String get() = if (full.contains(':')) full.substringBefore(':') else "minecraft"
public val path: String get() = if (full.contains(':')) full.substringAfter(':') else full
override fun toString(): String = "$namespace:$path"
public companion object Codec : PacketCodec<Identifier> {
public fun of(namespace: String, path: String): Identifier = Identifier("$namespace:$path")
override fun encode(buffer: BytesBuffer, value: Identifier) {
buffer.writeMcString(value.toString())
}
override fun decode(buffer: BytesBuffer): Identifier = Identifier(buffer.readMcString())
}
}
internal fun BytesBuffer.readIdentifier(): Identifier = Identifier.decode(this)
internal fun BytesBuffer.writeIdentifier(identifier: Identifier) = Identifier.encode(this, identifier)
@@ -0,0 +1,24 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.protocol.state
import kotlin.jvm.JvmInline
@JvmInline
public value class HandshakeIntent internal constructor(public val intentID: Int) {
public companion object {
public val STATUS: HandshakeIntent = HandshakeIntent(1)
public val LOGIN: HandshakeIntent = HandshakeIntent(2)
public val TRANSFER: HandshakeIntent = HandshakeIntent(3)
public fun fromID(intentID: Int): HandshakeIntent = when (intentID) {
1 -> STATUS; 2 -> LOGIN; 3 -> TRANSFER
else -> throw IllegalArgumentException("Unknown Handshake Intent ID")
}
}
}
@@ -0,0 +1,16 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.protocol.state
public enum class ProtocolState {
HANDSHAKE,
LOGIN,
CONFIGURATION,
PLAY,
DISCONNECTED
}
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.protocol.state
import cn.rtast.libmc.common.packet.PacketRegistry
import kotlin.enums.enumEntries
public class ProtocolStateRegistry {
// create registries for different state
private val registries = enumEntries<ProtocolState>().toTypedArray().associateWith { PacketRegistry() }
public fun getRegistry(state: ProtocolState): PacketRegistry =
requireNotNull(registries[state]) { "No registry found for state $state" }
public fun register(state: ProtocolState, block: PacketRegistry.() -> Unit) {
registries[state]?.apply(block)
}
}
@@ -0,0 +1,254 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.util
internal object CycloneMd5 {
const val BLOCK_SIZE = 64
const val DIGEST_SIZE = 16
const val MIN_PAD_SIZE = 9
val OID = byteArrayOf(
0x2A.toByte(), 0x86.toByte(), 0x48.toByte(), 0x86.toByte(),
0xF7.toByte(), 0x0D.toByte(), 0x02.toByte(), 0x05.toByte()
)
private val PADDING = ByteArray(64).apply { this[0] = 0x80.toByte() }
private val K = intArrayOf(
0xD76AA478.toInt(), 0xE8C7B756.toInt(), 0x242070DB, 0xC1BDCEEE.toInt(),
0xF57C0FAF.toInt(), 0x4787C62A, 0xA8304613.toInt(), 0xFD469501.toInt(),
0x698098D8, 0x8B44F7AF.toInt(), 0xFFFF5BB1.toInt(), 0x895CD7BE.toInt(),
0x6B901122, 0xFD987193.toInt(), 0xA679438E.toInt(), 0x49B40821,
0xF61E2562.toInt(), 0xC040B340.toInt(), 0x265E5A51, 0xE9B6C7AA.toInt(),
0xD62F105D.toInt(), 0x02441453, 0xD8A1E681.toInt(), 0xE7D3FBC8.toInt(),
0x21E1CDE6, 0xC33707D6.toInt(), 0xF4D50D87.toInt(), 0x455A14ED,
0xA9E3E905.toInt(), 0xFCEFA3F8.toInt(), 0x676F02D9, 0x8D2A4C8A.toInt(),
0xFFFA3942.toInt(), 0x8771F681.toInt(), 0x6D9D6122, 0xFDE5380C.toInt(),
0xA4BEEA44.toInt(), 0x4BDECFA9, 0xF6BB4B60.toInt(), 0xBEBFBC70.toInt(),
0x289B7EC6, 0xEAA127FA.toInt(), 0xD4EF3085.toInt(), 0x04881D05,
0xD9D4D039.toInt(), 0xE6DB99E5.toInt(), 0x1FA27CF8, 0xC4AC5665.toInt(),
0xF4292244.toInt(), 0x432AFF97, 0xAB9423A7.toInt(), 0xFC93A039.toInt(),
0x655B59C3, 0x8F0CCC92.toInt(), 0xFFEFF47D.toInt(), 0x85845DD1.toInt(),
0x6FA87E4F, 0xFE2CE6E0.toInt(), 0xA3014314.toInt(), 0x4E0811A1,
0xF7537E82.toInt(), 0xBD3AF235.toInt(), 0x2AD7D2BB, 0xEB86D391.toInt()
)
private class Context {
val h = IntArray(4)
val buffer = ByteArray(64)
val x = IntArray(16)
var size: Int = 0
var totalSize: Long = 0L
}
fun compute(data: ByteArray): ByteArray {
val digest = ByteArray(DIGEST_SIZE)
val context = Context()
initContext(context)
updateContext(context, data, 0, data.size)
finalContext(context, digest)
return digest
}
fun computeToHex(data: ByteArray): String {
return compute(data).toHexString()
}
fun computeToHex(text: String): String {
return compute(text.encodeToByteArray()).toHexString()
}
fun computeToBytes(data: ByteArray): ByteArray = compute(data)
private fun initContext(context: Context) {
context.h[0] = 0x67452301
context.h[1] = 0xEFCDAB89.toInt()
context.h[2] = 0x98BADCFE.toInt()
context.h[3] = 0x10325476
context.size = 0
context.totalSize = 0L
}
private fun updateContext(context: Context, data: ByteArray, offset: Int, length: Int) {
var dataOffset = offset
var remLength = length
while (remLength > 0) {
val n = minOf(remLength, 64 - context.size)
data.copyInto(context.buffer, context.size, dataOffset, dataOffset + n)
context.size += n
context.totalSize += n
dataOffset += n
remLength -= n
if (context.size == 64) {
processBlock(context)
context.size = 0
}
}
}
private fun finalContext(context: Context, digest: ByteArray) {
var totalBits: Long = context.totalSize * 8L
val paddingSize = if (context.size < 56) {
56 - context.size
} else {
64 + 56 - context.size
}
updateContext(context, PADDING, 0, paddingSize)
for (i in 0 until 8) {
context.buffer[56 + i] = (totalBits and 0xFFL).toByte()
totalBits = totalBits ushr 8
}
processBlock(context)
for (i in 0 until (DIGEST_SIZE / 4)) {
store32le(context.h[i], digest, i * 4)
}
}
private fun processBlock(context: Context) {
var a = context.h[0]
var b = context.h[1]
var c = context.h[2]
var d = context.h[3]
val x = context.x
for (i in 0 until 16) {
x[i] = load32le(context.buffer, i * 4)
}
// Round 1
a = ff(a, b, c, d, x[0], 7, K[0])
d = ff(d, a, b, c, x[1], 12, K[1])
c = ff(c, d, a, b, x[2], 17, K[2])
b = ff(b, c, d, a, x[3], 22, K[3])
a = ff(a, b, c, d, x[4], 7, K[4])
d = ff(d, a, b, c, x[5], 12, K[5])
c = ff(c, d, a, b, x[6], 17, K[6])
b = ff(b, c, d, a, x[7], 22, K[7])
a = ff(a, b, c, d, x[8], 7, K[8])
d = ff(d, a, b, c, x[9], 12, K[9])
c = ff(c, d, a, b, x[10], 17, K[10])
b = ff(b, c, d, a, x[11], 22, K[11])
a = ff(a, b, c, d, x[12], 7, K[12])
d = ff(d, a, b, c, x[13], 12, K[13])
c = ff(c, d, a, b, x[14], 17, K[14])
b = ff(b, c, d, a, x[15], 22, K[15])
// Round 2
a = gg(a, b, c, d, x[1], 5, K[16])
d = gg(d, a, b, c, x[6], 9, K[17])
c = gg(c, d, a, b, x[11], 14, K[18])
b = gg(b, c, d, a, x[0], 20, K[19])
a = gg(a, b, c, d, x[5], 5, K[20])
d = gg(d, a, b, c, x[10], 9, K[21])
c = gg(c, d, a, b, x[15], 14, K[22])
b = gg(b, c, d, a, x[4], 20, K[23])
a = gg(a, b, c, d, x[9], 5, K[24])
d = gg(d, a, b, c, x[14], 9, K[25])
c = gg(c, d, a, b, x[3], 14, K[26])
b = gg(b, c, d, a, x[8], 20, K[27])
a = gg(a, b, c, d, x[13], 5, K[28])
d = gg(d, a, b, c, x[2], 9, K[29])
c = gg(c, d, a, b, x[7], 14, K[30])
b = gg(b, c, d, a, x[12], 20, K[31])
// Round 3
a = hh(a, b, c, d, x[5], 4, K[32])
d = hh(d, a, b, c, x[8], 11, K[33])
c = hh(c, d, a, b, x[11], 16, K[34])
b = hh(b, c, d, a, x[14], 23, K[35])
a = hh(a, b, c, d, x[1], 4, K[36])
d = hh(d, a, b, c, x[4], 11, K[37])
c = hh(c, d, a, b, x[7], 16, K[38])
b = hh(b, c, d, a, x[10], 23, K[39])
a = hh(a, b, c, d, x[13], 4, K[40])
d = hh(d, a, b, c, x[0], 11, K[41])
c = hh(c, d, a, b, x[3], 16, K[42])
b = hh(b, c, d, a, x[6], 23, K[43])
a = hh(a, b, c, d, x[9], 4, K[44])
d = hh(d, a, b, c, x[12], 11, K[45])
c = hh(c, d, a, b, x[15], 16, K[46])
b = hh(b, c, d, a, x[2], 23, K[47])
// Round 4
a = ii(a, b, c, d, x[0], 6, K[48])
d = ii(d, a, b, c, x[7], 10, K[49])
c = ii(c, d, a, b, x[14], 15, K[50])
b = ii(b, c, d, a, x[5], 21, K[51])
a = ii(a, b, c, d, x[12], 6, K[52])
d = ii(d, a, b, c, x[3], 10, K[53])
c = ii(c, d, a, b, x[10], 15, K[54])
b = ii(b, c, d, a, x[1], 21, K[55])
a = ii(a, b, c, d, x[8], 6, K[56])
d = ii(d, a, b, c, x[15], 10, K[57])
c = ii(c, d, a, b, x[6], 15, K[58])
b = ii(b, c, d, a, x[13], 21, K[59])
a = ii(a, b, c, d, x[4], 6, K[60])
d = ii(d, a, b, c, x[11], 10, K[61])
c = ii(c, d, a, b, x[2], 15, K[62])
b = ii(b, c, d, a, x[9], 21, K[63])
context.h[0] += a
context.h[1] += b
context.h[2] += c
context.h[3] += d
}
private fun ByteArray.toHexString(): String {
val hexChars = CharArray(size * 2)
val hexArray = "0123456789abcdef".toCharArray()
for (i in indices) {
val v = this[i].toInt() and 0xFF
hexChars[i * 2] = hexArray[v ushr 4]
hexChars[i * 2 + 1] = hexArray[v and 0x0F]
}
return hexChars.concatToString()
}
private fun rol32(a: Int, s: Int): Int = (a shl s) or (a ushr (32 - s))
private fun load32le(buf: ByteArray, offset: Int): Int {
return (buf[offset].toInt() and 0xFF) or
((buf[offset + 1].toInt() and 0xFF) shl 8) or
((buf[offset + 2].toInt() and 0xFF) shl 16) or
((buf[offset + 3].toInt() and 0xFF) shl 24)
}
private fun store32le(val32: Int, buf: ByteArray, offset: Int) {
buf[offset] = (val32 and 0xFF).toByte()
buf[offset + 1] = ((val32 ushr 8) and 0xFF).toByte()
buf[offset + 2] = ((val32 ushr 16) and 0xFF).toByte()
buf[offset + 3] = ((val32 ushr 24) and 0xFF).toByte()
}
private fun f(x: Int, y: Int, z: Int): Int = (x and y) or (x.inv() and z)
private fun g(x: Int, y: Int, z: Int): Int = (x and z) or (y and z.inv())
private fun h(x: Int, y: Int, z: Int): Int = x xor y xor z
private fun i(x: Int, y: Int, z: Int): Int = y xor (x or z.inv())
private fun ff(a: Int, b: Int, c: Int, d: Int, x: Int, s: Int, k: Int): Int =
rol32(a + f(b, c, d) + x + k, s) + b
private fun gg(a: Int, b: Int, c: Int, d: Int, x: Int, s: Int, k: Int): Int =
rol32(a + g(b, c, d) + x + k, s) + b
private fun hh(a: Int, b: Int, c: Int, d: Int, x: Int, s: Int, k: Int): Int =
rol32(a + h(b, c, d) + x + k, s) + b
private fun ii(a: Int, b: Int, c: Int, d: Int, x: Int, s: Int, k: Int): Int =
rol32(a + i(b, c, d) + x + k, s) + b
}
internal fun ByteArray.digest(): ByteArray = CycloneMd5.computeToBytes(this)
@@ -0,0 +1,43 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.util
import cn.rtast.libmc.common.BytesBuffer
/**
* tmp
*/
internal fun BytesBuffer.writeMinimalTextNbt(text: String) {
writeByte(0x0A)
writeByte(0x08)
val keyBytes = "text".encodeToByteArray()
writeShort(keyBytes.size.toShort())
writeBytes(keyBytes)
val valBytes = text.encodeToByteArray()
require(valBytes.size <= 32767)
writeShort(valBytes.size.toShort())
writeBytes(valBytes)
writeByte(0x00)
}
internal fun BytesBuffer.readMinimalTextNbt(): String {
val rootTagType = readByte().toInt()
if (rootTagType != 0x0A) return ""
var resultText = ""
while (true) {
val tagType = readByte().toInt()
if (tagType == 0x00) break
val keyLength = readShort().toInt()
val key = readBytes(keyLength).decodeToString()
if (tagType == 0x08 && key == "text") {
val valLength = readShort().toInt()
resultText = readBytes(valLength).decodeToString()
} else break
}
return resultText
}
@@ -0,0 +1,30 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.util
import kotlin.uuid.Uuid
/**
* translate from PHP code
* ref: https://gist.github.com/TuxCoding/2b6a00a8fc21fd3b88375f03c9e2e603
*/
public fun generateOfflineUuid(username: String): Uuid {
val data = "OfflinePlayer:$username".encodeToByteArray().digest()
data[6] = ((data[6].toInt() and 0x0F) or 0x30).toByte()
data[8] = ((data[8].toInt() and 0x3F) or 0x80).toByte()
val hexChars = "0123456789abcdef"
val sb = StringBuilder(36)
for (i in 0 until 16) {
if (i == 4 || i == 6 || i == 8 || i == 10) sb.append('-')
val v = data[i].toInt() and 0xFF
sb.append(hexChars[v ushr 4])
sb.append(hexChars[v and 0x0F])
}
return Uuid.parse(sb.toString())
}
@@ -0,0 +1,21 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package test
import cn.rtast.libmc.protocol.client.MinecraftClient
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
class TestClient {
@Test
fun `test client`() = runTest {
val cli = MinecraftClient("127.0.0.1", 25565, "123")
cli.connect()
}
}