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

@@ -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) {
}