Add all packets handler

This commit is contained in:
2026-09-07 10:07:10 +08:00
parent d7300451f8
commit c0f9679090
25 files changed
+202 -105

No files matched your search

+2
View File
@@ -13,6 +13,8 @@ A lightweight minecraft client-side protocol library and related library, includ
# libmc-protocol
[Use libmc-protocol](https://repo.rtast.cn/packages/-/cn.rtast.libmc:protocol)
---
# Minecraft Protocol Library Status & Roadmap
+1 -1
View File
@@ -7,7 +7,7 @@ allprojects {
group = "cn.rtast.libmc"
val libVersion = getProperty("libVersion")
version = when (name) {
"protocol" -> "${getProperty("protocolVersion")}-$libVersion"
"protocol" -> getProperty("protocolVersion")
else -> libVersion
}
+8 -22
View File
@@ -4,27 +4,13 @@
* **Auth Join Request** (`AuthenticationProvider`): Sends the HTTP POST request to Mojang's session server
(`https://sessionserver.mojang.com/session/minecraft/join`).
[Use libmc-protocol-encrypt](https://repo.rtast.cn/packages/-/cn.rtast.libmc:protocol-encrypt)
```kotlin
val client = createMinecraftClient(
host = "127.0.0.1",
username = "Player",
accessToken = accessToken
) {
// 1. AES-128-CFB8 Cipher Implementation
cipherFactory = { sharedKey -> JvmAesCipher(sharedKey) }
// 2. SHA-1 Hasher Implementation
sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey ->
/* your SHA-1 implementation */
}
// 3. RSA-1024 Encryptor Implementation
rsaEncryptor = RSA1024Encryptor { publicKey, data ->
/* your RSA encryption implementation */
}
// 4. Mojang Auth HTTP Join Provider
authProvider = AuthenticationProvider { accessToken, uuid, serverIdHash ->
/* send HTTP POST to session server using your preferred HTTP client (Ktor, OkHttp, etc.) */
}
fun main() {
val client = createMinecraftClient(
// other parameter
crypto = DefaultProtocolContext
)
}
```
+2 -2
View File
@@ -3,5 +3,5 @@ kotlin.native.ignoreDisabledTargets=true
kotlin.native.enableKlibsCrossCompilation=true
kotlin.daemon.jvmargs=-Xmx2048M
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
libVersion=0.1.0
protocolVersion=26.2
libVersion=0.1.1
protocolVersion=26.2-0.1.1
+3
View File
@@ -6,6 +6,7 @@ coroutines-test = "1.11.0"
kotlinx-coroutines = "1.11.0"
ktor-core = "3.5.2"
cryptography-core = "0.6.0"
kotlinx-atomicfu = "0.33.0"
#kotlinx-serialization = "1.11.0"
[libraries]
@@ -20,10 +21,12 @@ ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "kto
ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor-core" }
ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor-core" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor-core" }
kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinx-atomicfu" }
#kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
#kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
maven-publish = { id = "maven-publish" }
kotlinx-atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "kotlinx-atomicfu" }
#kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
@@ -15,9 +15,7 @@ public class PacketRegistry {
private val idToCodec = mutableMapOf<Int, PacketCodec<out MinecraftPacket>>()
private val classToInfo = mutableMapOf<KClass<out MinecraftPacket>, RegisteredPacket<*>>()
private data class RegisteredPacket<P : MinecraftPacket>(
val id: Int, val codec: PacketCodec<P>,
)
private data class RegisteredPacket<P : MinecraftPacket>(val id: Int, val codec: PacketCodec<P>)
public fun <T : MinecraftPacket> register(id: Int, kClass: KClass<T>, codec: PacketCodec<T>) {
idToCodec[id] = codec
@@ -40,8 +40,3 @@ public expect class BytesBuffer {
@Suppress("NOTHING_TO_INLINE")
public inline fun ByteArray.wrap(): BytesBuffer = BytesBuffer(this)
public suspend fun ReadChannel.readPacketFrame(): BytesBuffer {
val length = this.readVarInt()
return this.readBytes(length).wrap()
}
@@ -7,6 +7,9 @@
package cn.rtast.libmc.stream
/**
* Platform specified raw byte read channel
*/
public expect open class ReadChannel() {
public open suspend fun readByte(): Byte
public open suspend fun readShort(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Short
@@ -16,21 +19,10 @@ public expect open class ReadChannel() {
public open suspend fun readFully(out: ByteArray, start: Int = 0, end: Int = out.size)
}
/**
* Platform specified raw byte write channel
*/
public expect open class WriteChannel() {
public open suspend fun writeFully(value: ByteArray, startIndex: Int = 0, endIndex: Int = value.size)
public open suspend fun flush()
}
public suspend fun ReadChannel.readVarInt(): Int {
var numRead = 0
var result = 0
var read: Byte
do {
read = this.readByte()
val value = (read.toInt() and 0x7F)
result = result or (value shl (7 * numRead))
numRead++
if (numRead > 5) throw IllegalArgumentException("VarInt is too big")
} while ((read.toInt() and 0x80) != 0)
return result
}
@@ -6,7 +6,7 @@
package cn.rtast.libmc.stream
import cn.rtast.libmc.common.LibMCContext
import cn.rtast.libmc.LibMCContext
import io.ktor.network.sockets.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.runBlocking
@@ -7,7 +7,7 @@
package cn.rtast.libmc.mcping.bedrock
import cn.rtast.libmc.common.LibMCContext
import cn.rtast.libmc.LibMCContext
import cn.rtast.libmc.stream.UdpSocket
import cn.rtast.libmc.stream.wrap
import cn.rtast.libmc.mcping.PingResponse
@@ -0,0 +1,31 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/7
*/
package cn.rtast.libmc.mcping.java
import cn.rtast.libmc.stream.BytesBuffer
import cn.rtast.libmc.stream.ReadChannel
import cn.rtast.libmc.stream.wrap
private suspend fun ReadChannel.readVarInt(): Int {
var numRead = 0
var result = 0
var read: Byte
do {
read = this.readByte()
val value = (read.toInt() and 0x7F)
result = result or (value shl (7 * numRead))
numRead++
if (numRead > 5) throw IllegalArgumentException("VarInt is too big")
} while ((read.toInt() and 0x80) != 0)
return result
}
public suspend fun ReadChannel.readPacketFrame(): BytesBuffer {
val length = this.readVarInt()
return this.readBytes(length).wrap()
}
@@ -7,13 +7,12 @@
package cn.rtast.libmc.mcping.java
import cn.rtast.libmc.common.*
import cn.rtast.libmc.LibMCContext
import cn.rtast.libmc.mcping.PingResponse
import cn.rtast.libmc.mcping.sendPacket
import cn.rtast.libmc.primitives.McStringCodec
import cn.rtast.libmc.primitives.VarIntCodec
import cn.rtast.libmc.stream.Socket
import cn.rtast.libmc.stream.readPacketFrame
import cn.rtast.libmc.mcping.PingResponse
import cn.rtast.libmc.mcping.sendPacket
import kotlin.time.Clock
internal suspend fun pingJavaServer(host: String, port: Int, context: LibMCContext): PingResponse {
@@ -9,7 +9,7 @@
package cn.rtast.libmc.mcping
import cn.rtast.libmc.common.LibMCContext
import cn.rtast.libmc.LibMCContext
import cn.rtast.libmc.mcping.bedrock.pingBedrockServer
import cn.rtast.libmc.mcping.java.pingJavaServer
import kotlin.jvm.JvmName
@@ -33,7 +33,7 @@ class TestJvmClient {
)
// cli.on<ClientboundSystemChatMessagePacket> { println(it) }
// cli.on<ClientboundLoginSuccessPacket> { println(it) }
cli.on<MinecraftPacket> { println(it) }
cli.onPacket<MinecraftPacket> { println(it) }
cli.launch { cli.connect() }
while (true) {
}
+14 -1
View File
@@ -22,11 +22,24 @@ kotlin {
commonTest.dependencies {
implementation(kotlin("test"))
implementation(project(":protocol-encrypt"))
implementation(libs.kotlinx.coroutines.test)
}
jvmTest.dependencies {
implementation(project(":protocol-encrypt"))
implementation(libs.ktor.client.okhttp)
}
linuxTest.dependencies {
implementation(libs.ktor.client.curl)
}
mingwTest.dependencies {
implementation(libs.ktor.client.winhttp)
}
appleTest.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}
@@ -39,11 +39,7 @@ public class MinecraftClient internal constructor(
internal val stateMachine = ClientStateMachine()
public val networkChannel: NetworkChannel = NetworkChannel(
host = host,
port = port,
context = context,
stateMachine = stateMachine,
cipherProvider = cryptoContext.cipherFactory
host, port, context, stateMachine, cryptoContext.cipherFactory, this
)
private val internalPacketDispatcher = InternalPacketDispatcher(this, authProvider)
@@ -57,13 +53,18 @@ public class MinecraftClient internal constructor(
networkChannel.connect()
startListening()
networkChannel.sendPacket(
ServerboundHandshakePacket(protocolVersion, host, port.toUShort(), HandshakeIntent.LOGIN)
ServerboundHandshakePacket(
protocolVersion,
host, port.toUShort(),
HandshakeIntent.LOGIN
)
)
stateMachine.transitionTo(ProtocolState.LOGIN)
networkChannel.sendPacket(ServerboundLoginStartPacket(username, uuid))
}
public fun setCompression(threshold: Int): Unit = networkChannel.setCompression(threshold)
private fun startListening() {
listenJob = launch {
try {
@@ -28,13 +28,16 @@ import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundPongPlayPacket
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
import cn.rtast.libmc.protocol.util.generateRandom16Bytes
/**
* Internal simple state machine trigger,
* Auto respond packets the server needed.
* Only including `Handshake`, `Login` and `Configuration` State
*/
public class InternalPacketDispatcher(
private val client: MinecraftClient,
private val authProvider: AuthenticationProvider,
) {
private suspend fun dispatchEvent(packet: MinecraftPacket) = client.dispatch(packet)
public suspend fun handleIncomingPackets(packet: MinecraftPacket) {
this.dispatchEvent(packet)
when (packet) {
// login
is ClientboundDisconnectLoginPacket -> client.close()
@@ -4,23 +4,77 @@
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.event
import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.protocol.protocol.PacketDirection
import kotlin.concurrent.Volatile
import kotlin.reflect.KClass
public open class PacketEventDispatcher {
@PublishedApi
internal val eventHandlers: MutableMap<KClass<out MinecraftPacket>, MutableList<suspend (MinecraftPacket) -> Unit>> =
mutableMapOf()
private typealias Handler = suspend (MinecraftPacket) -> Unit
private typealias DirectionalHandler = suspend (MinecraftPacket, PacketDirection) -> Unit
internal suspend fun dispatch(event: MinecraftPacket) {
eventHandlers[event::class]?.forEach { it.invoke(event) }
public open class PacketEventDispatcher {
@Volatile
@PublishedApi
internal var receiveHandlers: Map<KClass<out MinecraftPacket>, List<Handler>> = emptyMap()
@Volatile
@PublishedApi
internal var sentHandlers: Map<KClass<out MinecraftPacket>, List<Handler>> = emptyMap()
@Volatile
@PublishedApi
internal var globalHandlers: List<DirectionalHandler> = emptyList()
@PublishedApi
internal fun <T : MinecraftPacket> addTypedHandler(
isReceive: Boolean,
key: KClass<T>,
handler: Handler,
) {
if (isReceive) {
val current = receiveHandlers[key] ?: emptyList()
receiveHandlers = receiveHandlers + (key to (current + handler))
} else {
val current = sentHandlers[key] ?: emptyList()
sentHandlers = sentHandlers + (key to (current + handler))
}
}
public inline fun <reified T : MinecraftPacket> on(crossinline block: suspend (T) -> Unit) {
val handlers = eventHandlers.getOrPut(T::class) { mutableListOf() }
handlers.add { event -> block(event as T) }
private suspend fun dispatch(
handlersMap: Map<KClass<out MinecraftPacket>, List<Handler>>,
packet: MinecraftPacket,
direction: PacketDirection,
) {
handlersMap[packet::class]?.forEach { handler -> handler(packet) }
globalHandlers.forEach { handler -> handler(packet, direction) }
}
internal suspend fun dispatchReceive(packet: MinecraftPacket) =
dispatch(receiveHandlers, packet, PacketDirection.CLIENTBOUND)
internal suspend fun dispatchSent(packet: MinecraftPacket) =
dispatch(sentHandlers, packet, PacketDirection.SERVERBOUND)
/**
* Lambda will be invoked when received a packet
*/
public inline fun <reified T : MinecraftPacket> onPacket(crossinline block: suspend (T) -> Unit) {
addTypedHandler(true, T::class) { block(it as T) }
}
/**
* Lambda will be invoked after a packet sent
*/
public inline fun <reified T : MinecraftPacket> onSent(crossinline block: suspend (T) -> Unit) {
addTypedHandler(false, T::class) { block(it as T) }
}
/**
* All packets will be appeared here, including `Outbound(Serverbound)` and `Inbound(Clientbound)` packet
*/
public fun on(block: suspend (packet: MinecraftPacket, direction: PacketDirection) -> Unit) {
globalHandlers = globalHandlers + block
}
}
@@ -11,6 +11,9 @@ import cn.rtast.libmc.crypto.NetworkCipher
import cn.rtast.libmc.stream.ReadChannel
import cn.rtast.libmc.stream.WriteChannel
/**
* AES-128-CFB8 ***ciphered*** read channel
*/
internal class CipherReadChannel(
private val delegate: ReadChannel,
private val crypto: NetworkCipher,
@@ -34,6 +37,9 @@ internal class CipherReadChannel(
}
}
/**
* AES-128-CFB8 ***ciphered*** write channel
*/
internal class CipherWriteChannel(
private val delegate: WriteChannel,
private val crypto: NetworkCipher,
@@ -13,6 +13,7 @@ import cn.rtast.libmc.packet.writeBuffer
import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.primitives.writeVarInt
import cn.rtast.libmc.protocol.client.ClientStateMachine
import cn.rtast.libmc.protocol.event.PacketEventDispatcher
import cn.rtast.libmc.protocol.protocol.GameProtocols
import cn.rtast.libmc.stream.BytesBuffer
import cn.rtast.libmc.stream.wrap
@@ -26,6 +27,7 @@ public class NetworkChannel internal constructor(
context: LibMCContext,
private val stateMachine: ClientStateMachine,
cipherProvider: (ByteArray) -> NetworkCipher,
private val dispatcher: PacketEventDispatcher,
) {
internal val session: NetworkSession = NetworkSession(host, port, context, cipherProvider)
@@ -35,6 +37,11 @@ public class NetworkChannel internal constructor(
public fun connect(): Unit = session.connect()
public fun setCompression(threshold: Int): Unit = run { this.threshold = threshold }
/**
* This function receive all inbound packets, and dispatch as a packet event
* See [PacketEventDispatcher.dispatchReceive],
* Use [PacketEventDispatcher.onPacket] to get packet event
*/
public suspend fun readNextPacket(): MinecraftPacket {
val packetLength = session.readVarInt()
val rawFrameBytes = session.readBytes(packetLength)
@@ -46,11 +53,18 @@ public class NetworkChannel internal constructor(
}
val currentState = stateMachine.currentState
val packetId = payloadBuf.readVarInt()
return GameProtocols.clientboundGameProtocols
val packet = GameProtocols.clientboundGameProtocols
.getRegistry(currentState)
.decodePacket(packetId, payloadBuf)
dispatcher.dispatchReceive(packet)
return packet
}
/**
* This function will dispatch a packet event when packet was sent.
* See [PacketEventDispatcher.dispatchSent],
* Use [PacketEventDispatcher.onSent] to get packet event
*/
public suspend fun sendPacket(packet: MinecraftPacket) {
val uncompressedBodyBuf = BytesBuffer()
GameProtocols.serverboundGameProtocols
@@ -75,6 +89,7 @@ public class NetworkChannel internal constructor(
frameBuffer.writeBuffer(contentBuf)
}
session.writeFully(frameBuffer.toByteArray())
dispatcher.dispatchSent(packet)
}
public fun close(): Unit = session.close()
@@ -7,6 +7,6 @@
package cn.rtast.libmc.protocol.protocol
internal enum class PacketDirection {
public enum class PacketDirection {
SERVERBOUND, CLIENTBOUND
}
@@ -8,7 +8,6 @@
package test
import cn.rtast.libmc.protocol.client.createMinecraftClient
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundAwardStatisticsPacket
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
@@ -28,7 +27,7 @@ class TestClient {
// rsaEncryptor = RSA1024Encryptor { data, sharedKey -> }
}
cli.launch { cli.connect() }
cli.on<ClientboundAwardStatisticsPacket> { println(it) }
cli.on { packet, direction -> println("${direction} -> $packet") }
while (true) {
}
}
@@ -7,12 +7,11 @@
package test
import cn.rtast.libmc.protocol.client.createMinecraftClient
import cn.rtast.libmc.crypto.AuthenticationProvider
import cn.rtast.libmc.crypto.RSA1024Encryptor
import cn.rtast.libmc.crypto.Sha1Hasher
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundLoginSuccessPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundSystemChatMessagePacket
import cn.rtast.libmc.crypto.AuthenticationProvider
import cn.rtast.libmc.protocol.client.createMinecraftClient
import cn.rtast.libmc.protocol.crypto.DefaultProtocolContext
import cn.rtast.libmc.protocol.util.generateOfflineUuid
import kotlinx.coroutines.launch
import org.junit.Test
@@ -28,7 +27,6 @@ import kotlin.uuid.Uuid
class TestClientTestInJvm {
val accessToken = File("src/jvmTest/resources/accessToken.txt").readText()
fun encrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray {
@@ -63,25 +61,27 @@ class TestClientTestInJvm {
// generateOfflineUuid("RTAkland"),
Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
// null,
accessToken
) {
rsaEncryptor = RSA1024Encryptor { key, data -> encrypt(key, data) }
sha1Hasher =
Sha1Hasher { serverId, secretKey, publicKey -> minecraftServerIdHash(serverId, secretKey, publicKey) }
cipherFactory = { key -> JvmAesCipher(key) }
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash ->
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.doOutput = true
connection.setRequestProperty("Content-Type", "application/json")
connection.getOutputStream()
.use { it.write("{\"accessToken\":\"$accessToken\", \"selectedProfile\":\"$uuid\", \"serverId\":\"$serverIdHash\"}".encodeToByteArray()) }
connection.disconnect()
}
}
cli.on<ClientboundSystemChatMessagePacket> { println(it) }
cli.on<ClientboundLoginSuccessPacket> { println(it) }
accessToken,
crypto = DefaultProtocolContext
)
// {
// rsaEncryptor = RSA1024Encryptor { key, data -> encrypt(key, data) }
// sha1Hasher =
// Sha1Hasher { serverId, secretKey, publicKey -> minecraftServerIdHash(serverId, secretKey, publicKey) }
// cipherFactory = { key -> JvmAesCipher(key) }
// authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash ->
// val connection = URL(url).openConnection() as HttpURLConnection
// connection.requestMethod = "POST"
// connection.doOutput = true
// connection.setRequestProperty("Content-Type", "application/json")
// connection.getOutputStream()
// .use { it.write("{\"accessToken\":\"$accessToken\", \"selectedProfile\":\"$uuid\", \"serverId\":\"$serverIdHash\"}".encodeToByteArray()) }
// connection.disconnect()
// }
// }
// cli.onPacket<ClientboundSystemChatMessagePacket> { println(it) }
// cli.onPacket<ClientboundLoginSuccessPacket> { println(it) }
cli.on { packet, direction -> println("${direction} -> $packet") }
cli.launch { cli.connect() }
while (true) {
}
@@ -8,7 +8,7 @@
package cn.rtast.libmc.mcping.rconlib
import cn.rtast.libmc.common.LibMCContext
import cn.rtast.libmc.LibMCContext
import cn.rtast.libmc.stream.ReadChannel
import cn.rtast.libmc.stream.Socket
import cn.rtast.libmc.stream.WriteChannel
+1 -1
View File
@@ -10,7 +10,7 @@ includeSubModule(":rconlib")
includeSubModule(":protocol")
includeSubModule(":protocol-encrypt")
includeSubModule(":nbt")
includeSubModule(":snbt")
//includeSubModule(":snbt")
fun includeSubModule(name: String, path: String? = null) = include(name).also {
project(name).projectDir = file(path ?: "libmc-${name.removePrefix(":")}")