Implemented consumer interface API, support TextComponent serialize to json string, fix some bugs

This commit is contained in:
2026-09-10 02:19:22 +08:00
parent 19697d2022
commit 62a59bd184
16 files changed
+650 -413

No files matched your search

+1 -1
View File
@@ -19,7 +19,7 @@ A lightweight minecraft client-side protocol library for Kotlin Native and JVM
- [x] **Online Mode Authentication & Encryption/Decryption**: See [Embedded cryptography](docs/Embedded-cryptography.md) - [x] **Online Mode Authentication & Encryption/Decryption**: See [Embedded cryptography](docs/Embedded-cryptography.md)
- [x] **Structured `TextComponent` Parser**: TextComponent AST decoder - [x] **Structured `TextComponent` Parser**: TextComponent AST decoder
- [x] **Command Tree Parser**: Full binary graph decoder for brigadier nodes, argument types, and suggestions - [x] **Command Tree Parser**: Full binary graph decoder for brigadier nodes, argument types, and suggestions
- [ ] **Recipe Book & Recipe Data**: Recipe layout declarations and client-side recipe settings - [x] **Recipe Book & Recipe Data**
- [ ] **Chunk & World Data**: Level Chunk Data with Light decoder (Paletted Containers, Direct/Indirect Palettes) - [ ] **Chunk & World Data**: Level Chunk Data with Light decoder (Paletted Containers, Direct/Indirect Palettes)
- [ ] **Light Engine Update**: Sky & Block light nibble array parser - [ ] **Light Engine Update**: Sky & Block light nibble array parser
- [ ] **Explosion Event Decoder**: Knockback vectors and destroyed block offsets array - [ ] **Explosion Event Decoder**: Knockback vectors and destroyed block offsets array
+24 -1
View File
@@ -6,7 +6,7 @@ public fun main() = runBlocking {
"127.0.0.1", 25566, "MyBot", "127.0.0.1", 25566, "MyBot",
generateOfflineUuid("MyBot"), generateOfflineUuid("MyBot"),
accessToken = null, accessToken = null,
context = DefaultProtocolContext.withCustom { context = {
socketEngine = KtorNetworkEngine() socketEngine = KtorNetworkEngine()
} }
) )
@@ -81,6 +81,29 @@ client.networkChannel.sendPacket(
) )
``` ```
# Get server MOTD
```kotlin
fun main() {
val cli = createMinecraftClient(
"127.0.0.1", 25566, "11",
generateOfflineUuid("11"), null,
context = {
socketEngine = KtorNetworkEngine()
}
)
cli.session.onEvent<SessionEvent.ConnectedEvent> {
println(status())
disconnect()
}
cli.session.onEvent<SessionEvent.DisconnectedEvent> {
println(it.reason.toJsonString())
}
cli.connect()
awaitCancellation()
}
```
# Respond velocity and update client motion # Respond velocity and update client motion
> This part uses math calculations > This part uses math calculations
@@ -0,0 +1,55 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/10
*/
package cn.rtast.libmc.nbt
public fun NBTTag.toJsonString(): String = when (this) {
is NBTTag.StringTag -> buildString {
append('"')
for (ch in value) {
when (ch) {
'\\' -> append("\\\\")
'"' -> append("\\\"")
'\b' -> append("\\b")
'\u000C' -> append("\\f")
'\n' -> append("\\n")
'\r' -> append("\\r")
'\t' -> append("\\t")
else -> {
if (ch < ' ') {
val hex = ch.code.toString(16).padStart(4, '0')
append("\\u").append(hex)
} else {
append(ch)
}
}
}
}
append('"')
}
is NBTTag.ByteTag -> if (value == 1.toByte() || value == 0.toByte()) {
if (value == 1.toByte()) "true" else "false"
} else value.toString()
is NBTTag.ShortTag -> value.toString()
is NBTTag.IntTag -> value.toString()
is NBTTag.LongTag -> value.toString()
is NBTTag.FloatTag -> value.toString()
is NBTTag.DoubleTag -> value.toString()
is NBTTag.ByteArrayTag -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
is NBTTag.IntArrayTag -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
is NBTTag.LongArrayTag -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
is NBTTag.ListTag -> value.joinToString(prefix = "[", postfix = "]") { it.toJsonString() }
is NBTTag.CompoundTag -> value.entries.joinToString(prefix = "{", postfix = "}") { (k, v) ->
val escapedKey = NBTTag.StringTag(k).toJsonString()
"$escapedKey:${v.toJsonString()}"
}
}
@@ -1,127 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/9
*/
package cn.rtast.libmc.protocol.client
import cn.rtast.libmc.protocol.crypto.minecraftServerIdHash
import cn.rtast.libmc.protocol.crypto.rsaEncrypt
import cn.rtast.libmc.protocol.packet.configuration.clientbound.*
import cn.rtast.libmc.protocol.packet.configuration.serverbound.*
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundDisconnectLoginPacket
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundHelloPacket
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundLoginSuccessPacket
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundSetCompressionPacket
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundKeyPacket
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundLoginAcknowledgedPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.*
import cn.rtast.libmc.protocol.packet.play.serverbound.*
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
import cn.rtast.libmc.protocol.util.generateRandom16Bytes
import kotlinx.coroutines.*
import kotlin.concurrent.Volatile
import kotlin.math.abs
import kotlin.time.Clock
import kotlin.time.Duration.Companion.milliseconds
public class ClientTickingLoop internal constructor(private val client: MinecraftClient) {
private var tickJob: Job? = null
private val tickIntervalMs = 50L
public var currentTick: Long = 0L
private set
@Volatile
public var isRunning: Boolean = false
private set
private val listeners = mutableListOf<suspend (Long) -> Unit>()
internal fun registerListener(action: suspend (Long) -> Unit) = listeners.add(action)
init {
client.onPacket<ClientboundLoginSuccessPacket> { handleLoginSuccess() }
client.onPacket<ClientboundSetCompressionPacket> { client.networkChannel.setCompression(it.threshold) }
client.onPacket<ClientboundHelloPacket> { handleEncryptRequest(it) }
client.onPacket<ClientboundDisconnectLoginPacket> { client.close() }
client.onPacket<ClientboundDisconnectPlayPacket> { client.close() }
client.onPacket<ClientboundDisconnectConfigurationPacket> { client.close() }
client.onPacket<ClientboundSetTimePacket> { syncServerTick(it.worldAge) }
client.onPacket<ClientboundPingPacket> { client.networkChannel.sendPacket(ServerboundPongPlayPacket(it.id)) }
client.onPacket<ClientboundSelectKnownPacksPacket> {
client.networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList()))
}
client.onPacket<ClientboundCodeOfConductPacket> {
client.networkChannel.sendPacket(ServerboundAcceptCodeOfConductPacket)
}
client.onPacket<ClientboundKeepAlivePlayPacket> {
client.networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(it.id))
}
client.onPacket<ClientboundPingConfigurationPacket> {
client.networkChannel.sendPacket(ServerboundPongConfigurationPacket(it.id))
}
client.onPacket<ClientboundStartConfigurationPacket> {
client.networkChannel.sendPacket(ServerboundConfigurationAcknowledgedPacket)
client.stateMachine.transitionTo(ProtocolState.CONFIGURATION)
}
client.onPacket<ClientboundKeepAliveConfigurationPacket> {
client.networkChannel.sendPacket(ServerboundKeepAliveConfigurationPacket(it.id))
}
client.onPacket<ClientboundFinishConfigurationPacket> {
client.networkChannel.sendPacket(ServerboundAckFinishConfigurationPacket)
client.stateMachine.transitionTo(ProtocolState.PLAY)
}
}
internal suspend fun handleLoginSuccess() {
client.networkChannel.sendPacket(ServerboundLoginAcknowledgedPacket)
client.stateMachine.transitionTo(ProtocolState.CONFIGURATION)
}
internal suspend fun handleEncryptRequest(packet: ClientboundHelloPacket) {
val sharedSecret = generateRandom16Bytes()
if (client.isOnlineMode) {
val serverHash = minecraftServerIdHash(packet.serverId, sharedSecret, packet.publicKey)
client.protocolContext.authProvider!!.joinServer(
"https://sessionserver.mojang.com/session/minecraft/join",
client.accessToken!!, client.uuid.toString().replace("-", ""), serverHash
)
}
val encryptedSecret = rsaEncrypt(packet.publicKey, sharedSecret)
val encryptedVerifyToken = rsaEncrypt(packet.publicKey, packet.verifyToken)
client.networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken))
client.networkChannel.session.enableEncryption(sharedSecret)
}
internal fun syncServerTick(serverWorldAge: Long) {
if (abs(this.currentTick - serverWorldAge) > 2) this.currentTick = serverWorldAge
}
internal fun start() {
if (isRunning) return
isRunning = true
tickJob = client.launch(CoroutineName("LibMC-ClientTickingLoop")) {
var nextTickTime = Clock.System.now().toEpochMilliseconds()
while (isActive && isRunning) {
val now = Clock.System.now().toEpochMilliseconds()
if (now >= nextTickTime) {
try {
listeners.forEach { it.invoke(currentTick) }
} catch (e: Exception) {
if (e is CancellationException) throw e
e.printStackTrace()
}
currentTick++
nextTickTime += tickIntervalMs
if (now - nextTickTime > tickIntervalMs * 5) nextTickTime = now + tickIntervalMs
} else delay((nextTickTime - now).milliseconds)
}
}
}
internal fun stop() {
isRunning = false
tickJob?.cancel()
tickJob = null
}
}
@@ -9,10 +9,10 @@ package cn.rtast.libmc.protocol.client
import cn.rtast.libmc.crypto.ProtocolContext import cn.rtast.libmc.crypto.ProtocolContext
import cn.rtast.libmc.crypto.ProtocolContextBuilder import cn.rtast.libmc.crypto.ProtocolContextBuilder
import cn.rtast.libmc.protocol.network.NetworkChannel import cn.rtast.libmc.protocol.network.NetworkChannel
import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket import cn.rtast.libmc.protocol.protocol.event.PacketEventDispatcher
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundLoginStartPacket import cn.rtast.libmc.protocol.protocol.session.Session
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent import cn.rtast.libmc.protocol.protocol.session.SessionEvent
import cn.rtast.libmc.protocol.protocol.state.ProtocolState import cn.rtast.libmc.protocol.protocol.session.SessionImpl
import cn.rtast.libmc.protocol.util.TransactionIdManager import cn.rtast.libmc.protocol.util.TransactionIdManager
import cn.rtast.libmc.protocol.util.generateOfflineUuid import cn.rtast.libmc.protocol.util.generateOfflineUuid
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -20,68 +20,51 @@ import kotlin.coroutines.CoroutineContext
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
public class MinecraftClient internal constructor( public class MinecraftClient internal constructor(
private val host: String, internal val host: String,
private val port: Int, internal val port: Int,
private val username: String, internal val username: String,
internal val uuid: Uuid, internal val uuid: Uuid,
internal val accessToken: String?, internal val accessToken: String?,
parentJob: Job?, parentJob: Job?,
private val ioDispatcher: CoroutineDispatcher, private val ioDispatcher: CoroutineDispatcher,
internal val protocolContext: ProtocolContext, internal val protocolContext: ProtocolContext,
) : PacketEventDispatcher(), CoroutineScope { public val session: SessionImpl = SessionImpl(),
) : PacketEventDispatcher(), CoroutineScope, Session by session {
internal val stateMachine = ClientStateMachine() internal val stateMachine = ClientStateMachine()
public val networkChannel: NetworkChannel = NetworkChannel(host, port, stateMachine, this, protocolContext) public val networkChannel: NetworkChannel = NetworkChannel(this)
private val clientJob = SupervisorJob(parentJob) private val clientJob = SupervisorJob(parentJob)
private var listenJob: Job? = null private var listenJob: Job? = null
public val isOnlineMode: Boolean = accessToken != null
public val transactionManager: TransactionIdManager = TransactionIdManager() public val transactionManager: TransactionIdManager = TransactionIdManager()
public val clientTickingLoop: ClientTickingLoop = ClientTickingLoop(this)
/** init {
* Register a client ticking event callback. session.attachClient(this)
* NOTE: Blocking operations will **block** the bot thread.
* Using #launch to avoid blocking.
*/
public fun onTick(action: suspend (Long) -> Unit): Unit = run { clientTickingLoop.registerListener(action) }
public suspend fun connect(protocolVersion: Int = CURRENT_MINECRAFT_PROTOCOL_VERSION) {
networkChannel.connect()
startListening()
clientTickingLoop.start()
networkChannel.sendPacket(
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) public suspend fun connect() {
networkChannel.connect()
startListening()
session.init()
session.emitEvent(SessionEvent.ConnectedEvent)
}
private fun startListening() { private fun startListening() {
listenJob = launch { listenJob = launch {
try { try {
while (isActive) networkChannel.readNextPacket() while (isActive) networkChannel.readNextPacket()
} catch (e: Exception) { } catch (e: Throwable) {
if (e is CancellationException) throw e if (e is CancellationException) return@launch
if (isActive) {
e.printStackTrace()
println("Network read loop exception: ${e.message}") println("Network read loop exception: ${e.message}")
close() } finally {
} networkChannel.close()
} }
} }
} }
public fun close() { public fun close() {
networkChannel.close() networkChannel.close()
clientTickingLoop.stop() listenJob?.cancel()
clientJob.cancel() clientJob.cancel()
cancel()
} }
public override val coroutineContext: CoroutineContext public override val coroutineContext: CoroutineContext
@@ -1,80 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.client
import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.protocol.protocol.PacketDirection
import kotlin.concurrent.Volatile
import kotlin.reflect.KClass
private typealias Handler = suspend (MinecraftPacket) -> Unit
private typealias DirectionalHandler = suspend (MinecraftPacket, PacketDirection) -> Unit
public abstract 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))
}
}
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
}
}
@@ -6,34 +6,27 @@
package cn.rtast.libmc.protocol.network package cn.rtast.libmc.protocol.network
import cn.rtast.libmc.crypto.ProtocolContext
import cn.rtast.libmc.network.BytesBuffer import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.network.wrap import cn.rtast.libmc.network.wrap
import cn.rtast.libmc.packet.MinecraftPacket import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.packet.writeBuffer import cn.rtast.libmc.packet.writeBuffer
import cn.rtast.libmc.primitives.readVarInt import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.primitives.writeVarInt import cn.rtast.libmc.primitives.writeVarInt
import cn.rtast.libmc.protocol.client.ClientStateMachine import cn.rtast.libmc.protocol.client.MinecraftClient
import cn.rtast.libmc.protocol.client.PacketEventDispatcher import cn.rtast.libmc.protocol.protocol.event.PacketEventDispatcher
import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec.clientboundGameProtocols import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec.clientboundGameProtocols
import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec.serverboundGameProtocols import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec.serverboundGameProtocols
import cn.rtast.libmc.zlibCompress import cn.rtast.libmc.zlibCompress
import cn.rtast.libmc.zlibDecompress import cn.rtast.libmc.zlibDecompress
import kotlin.concurrent.Volatile import kotlin.concurrent.Volatile
public class NetworkChannel internal constructor( public class NetworkChannel internal constructor(private val client: MinecraftClient) {
host: String, internal val networkSession: NetworkSession = NetworkSession(client)
port: Int,
private val stateMachine: ClientStateMachine,
private val dispatcher: PacketEventDispatcher,
protocolContext: ProtocolContext,
) {
internal val session: NetworkSession = NetworkSession(host, port, protocolContext)
@Volatile @Volatile
private var threshold = -1 private var threshold = -1
public suspend fun connect(): Unit = session.connect() public suspend fun connect(): Unit = networkSession.connect()
public fun setCompression(threshold: Int): Unit = run { this.threshold = threshold } public fun setCompression(threshold: Int): Unit = run { this.threshold = threshold }
/** /**
@@ -42,8 +35,8 @@ public class NetworkChannel internal constructor(
* Use [PacketEventDispatcher.onPacket] to get packet event * Use [PacketEventDispatcher.onPacket] to get packet event
*/ */
public suspend fun readNextPacket(): MinecraftPacket { public suspend fun readNextPacket(): MinecraftPacket {
val packetLength = session.readVarInt() val packetLength = networkSession.readVarInt()
val frameBuf = session.readBytes(packetLength).wrap() val frameBuf = networkSession.readBytes(packetLength).wrap()
val payloadBuf = if (threshold < 0) frameBuf else { val payloadBuf = if (threshold < 0) frameBuf else {
val dataLength = frameBuf.readVarInt() val dataLength = frameBuf.readVarInt()
if (dataLength == 0) frameBuf else { if (dataLength == 0) frameBuf else {
@@ -51,10 +44,10 @@ public class NetworkChannel internal constructor(
compressedBytes.zlibDecompress(dataLength).wrap() compressedBytes.zlibDecompress(dataLength).wrap()
} }
} }
val currentState = stateMachine.currentState val currentState = client.stateMachine.currentState
val packetId = payloadBuf.readVarInt() val packetId = payloadBuf.readVarInt()
val packet = clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, payloadBuf) val packet = clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, payloadBuf)
dispatcher.dispatchReceive(packet) client.dispatchReceive(packet, client.session)
return packet return packet
} }
@@ -65,7 +58,7 @@ public class NetworkChannel internal constructor(
*/ */
public suspend fun sendPacket(packet: MinecraftPacket) { public suspend fun sendPacket(packet: MinecraftPacket) {
val uncompressedBodyBuf = BytesBuffer() val uncompressedBodyBuf = BytesBuffer()
serverboundGameProtocols.getRegistry(stateMachine.currentState).encodePacket(uncompressedBodyBuf, packet) serverboundGameProtocols.getRegistry(client.stateMachine.currentState).encodePacket(uncompressedBodyBuf, packet)
val uncompressedData = uncompressedBodyBuf.toByteArray() val uncompressedData = uncompressedBodyBuf.toByteArray()
val frameBuffer = BytesBuffer() val frameBuffer = BytesBuffer()
if (threshold < 0) { if (threshold < 0) {
@@ -84,9 +77,9 @@ public class NetworkChannel internal constructor(
frameBuffer.writeVarInt(contentBuf.size) frameBuffer.writeVarInt(contentBuf.size)
frameBuffer.writeBuffer(contentBuf) frameBuffer.writeBuffer(contentBuf)
} }
session.writeFully(frameBuffer.toByteArray()) networkSession.writeFully(frameBuffer.toByteArray())
dispatcher.dispatchSent(packet) client.dispatchSent(packet, client.session)
} }
public fun close(): Unit = session.close() public fun close(): Unit = networkSession.close()
} }
@@ -6,18 +6,14 @@
package cn.rtast.libmc.protocol.network package cn.rtast.libmc.protocol.network
import cn.rtast.libmc.crypto.ProtocolContext
import cn.rtast.libmc.network.RawSocket import cn.rtast.libmc.network.RawSocket
import cn.rtast.libmc.network.ReadChannel import cn.rtast.libmc.network.ReadChannel
import cn.rtast.libmc.network.WriteChannel import cn.rtast.libmc.network.WriteChannel
import cn.rtast.libmc.primitives.readVarInt import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.protocol.client.MinecraftClient
import cn.rtast.libmc.protocol.crypto.Aes128Cfb8ChannelCipher import cn.rtast.libmc.protocol.crypto.Aes128Cfb8ChannelCipher
public class NetworkSession internal constructor( public class NetworkSession internal constructor(private val client: MinecraftClient) {
private val host: String,
private val port: Int,
private val context: ProtocolContext,
) {
private var socket: RawSocket? = null private var socket: RawSocket? = null
public var readChannel: ReadChannel? = null public var readChannel: ReadChannel? = null
private set private set
@@ -26,7 +22,7 @@ public class NetworkSession internal constructor(
private set private set
public suspend fun connect() { public suspend fun connect() {
val sk = context.createSocket(host, port) val sk = client.protocolContext.createSocket(client.host, client.port)
sk.connect() sk.connect()
this.socket = sk this.socket = sk
this.readChannel = sk.openReadChannel() this.readChannel = sk.openReadChannel()
@@ -0,0 +1,12 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/10
*/
package cn.rtast.libmc.protocol.protocol.event
public fun interface ListenerRegistration {
public suspend fun unregister()
}
@@ -0,0 +1,102 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.protocol.event
import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.protocol.protocol.PacketDirection
import cn.rtast.libmc.protocol.protocol.session.Session
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.reflect.KClass
private typealias Handler = suspend Session.(MinecraftPacket) -> Unit
private typealias DirectionalHandler = suspend Session.(MinecraftPacket, PacketDirection) -> Unit
public abstract class PacketEventDispatcher {
private val mutex = Mutex()
@PublishedApi
internal var receiveHandlers: Map<KClass<out MinecraftPacket>, List<Handler>> = emptyMap()
@PublishedApi
internal var sentHandlers: Map<KClass<out MinecraftPacket>, List<Handler>> = emptyMap()
@PublishedApi
internal var globalHandlers: List<DirectionalHandler> = emptyList()
@PublishedApi
internal suspend fun <T : MinecraftPacket> addTypedHandler(
isReceive: Boolean,
key: KClass<T>,
handler: Handler,
): ListenerRegistration {
mutex.withLock {
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))
}
}
return ListenerRegistration { removeTypedHandler(isReceive, key, handler) }
}
@PublishedApi
internal suspend fun <T : MinecraftPacket> removeTypedHandler(
isReceive: Boolean,
key: KClass<T>,
handler: Handler,
) {
mutex.withLock {
if (isReceive) {
val current = receiveHandlers[key] ?: return@withLock
val updated = current - handler
receiveHandlers = if (updated.isEmpty()) receiveHandlers - key else receiveHandlers + (key to updated)
} else {
val current = sentHandlers[key] ?: return@withLock
val updated = current - handler
sentHandlers = if (updated.isEmpty()) sentHandlers - key else sentHandlers + (key to updated)
}
}
}
private suspend fun dispatch(
session: Session,
isReceive: Boolean,
packet: MinecraftPacket,
direction: PacketDirection,
) {
val (typeHandlers, globals) = mutex.withLock {
val map = if (isReceive) receiveHandlers else sentHandlers
Pair(map[packet::class], globalHandlers)
}
typeHandlers?.forEach { handler -> handler(session, packet) }
globals.forEach { handler -> handler(session, packet, direction) }
}
internal suspend fun dispatchReceive(packet: MinecraftPacket, session: Session) =
dispatch(session, true, packet, PacketDirection.CLIENTBOUND)
internal suspend fun dispatchSent(packet: MinecraftPacket, session: Session) =
dispatch(session, false, packet, PacketDirection.SERVERBOUND)
public suspend inline fun <reified T : MinecraftPacket> onPacket(noinline block: suspend Session.(T) -> Unit): ListenerRegistration {
val handler: Handler = { block(it as T) }
return addTypedHandler(true, T::class, handler)
}
public suspend inline fun <reified T : MinecraftPacket> onSent(noinline block: suspend Session.(T) -> Unit): ListenerRegistration {
val handler: Handler = { block(it as T) }
return addTypedHandler(false, T::class, handler)
}
public suspend fun on(block: suspend Session.(packet: MinecraftPacket, direction: PacketDirection) -> Unit): ListenerRegistration {
mutex.withLock { globalHandlers = globalHandlers + block }
return ListenerRegistration { mutex.withLock { globalHandlers = globalHandlers - block } }
}
}
@@ -9,10 +9,11 @@ package cn.rtast.libmc.protocol.protocol.game.chat
import cn.rtast.libmc.nbt.NBTCompound import cn.rtast.libmc.nbt.NBTCompound
import cn.rtast.libmc.nbt.NBTTag import cn.rtast.libmc.nbt.NBTTag
import cn.rtast.libmc.nbt.NBTType import cn.rtast.libmc.nbt.NBTType
import cn.rtast.libmc.nbt.toJsonString
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.protocol.protocol.game.Identifier import cn.rtast.libmc.protocol.protocol.game.Identifier
import cn.rtast.libmc.protocol.protocol.util.readNetworkNBTCompound import cn.rtast.libmc.protocol.protocol.util.readNetworkNBTCompound
import cn.rtast.libmc.protocol.protocol.util.writeNetworkNBTCompound import cn.rtast.libmc.protocol.protocol.util.writeNetworkNBTCompound
import cn.rtast.libmc.network.BytesBuffer
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
public data class TextComponent( public data class TextComponent(
@@ -223,6 +224,8 @@ public data class TextComponent(
} }
} }
public fun toJsonString(): String = this.toNBTCompound().toJsonString()
public companion object { public companion object {
public fun of(text: String): TextComponent = TextComponent(content = Content.PlainText(text)) public fun of(text: String): TextComponent = TextComponent(content = Content.PlainText(text))
} }
@@ -261,6 +264,117 @@ public fun NBTTag.toTextComponent(): TextComponent {
} }
} }
public fun TextComponent.toNBTCompound(): NBTTag.CompoundTag {
val map = mutableMapOf<String, NBTTag>()
when (val c = this.content) {
is TextComponent.Content.PlainText -> {
map["type"] = NBTTag.StringTag("text")
map["text"] = NBTTag.StringTag(c.text)
}
is TextComponent.Content.Translatable -> {
map["type"] = NBTTag.StringTag("translatable")
map["translate"] = NBTTag.StringTag(c.key)
c.fallback?.let { map["fallback"] = NBTTag.StringTag(it) }
if (c.args.isNotEmpty()) {
map["with"] = NBTTag.ListTag(NBTType.List, c.args.map { it.toNBTCompound() as NBTTag }.toMutableList())
}
}
is TextComponent.Content.Score -> {
map["type"] = NBTTag.StringTag("score")
map["score"] = NBTTag.CompoundTag(
mutableMapOf(
"name" to NBTTag.StringTag(c.name),
"objective" to NBTTag.StringTag(c.objective)
)
)
}
is TextComponent.Content.Selector -> {
map["type"] = NBTTag.StringTag("selector")
map["selector"] = NBTTag.StringTag(c.selector)
c.separator?.let { map["separator"] = it.toNBTCompound() }
}
is TextComponent.Content.Keybind -> {
map["type"] = NBTTag.StringTag("keybind")
map["keybind"] = NBTTag.StringTag(c.keybind)
}
is TextComponent.Content.Nbt -> {
map["type"] = NBTTag.StringTag("nbt")
map["nbt"] = NBTTag.StringTag(c.path)
map["interpret"] = NBTTag.ByteTag(if (c.interpret) 1 else 0)
map["plain"] = NBTTag.ByteTag(if (c.plain) 1 else 0)
c.separator?.let { map["separator"] = it.toNBTCompound() }
when (val s = c.source) {
is TextComponent.Content.Nbt.NbtSource.Entity -> map["entity"] = NBTTag.StringTag(s.selector)
is TextComponent.Content.Nbt.NbtSource.Block -> map["block"] = NBTTag.StringTag(s.coordinates)
is TextComponent.Content.Nbt.NbtSource.Storage -> map["storage"] = NBTTag.StringTag(s.id.toString())
}
}
is TextComponent.Content.ObjectContent.Atlas -> {
map["type"] = NBTTag.StringTag("object")
map["object"] = NBTTag.StringTag("atlas")
map["sprite"] = NBTTag.StringTag(c.sprite.toString())
map["atlas"] = NBTTag.StringTag(c.atlas.toString())
}
is TextComponent.Content.ObjectContent.Player -> {
map["type"] = NBTTag.StringTag("object")
map["object"] = NBTTag.StringTag("player")
map["hat"] = NBTTag.ByteTag(if (c.hat) 1 else 0)
when (val p = c.profile) {
is TextComponent.PlayerProfile.Name -> map["player"] = NBTTag.StringTag(p.name)
is TextComponent.PlayerProfile.FullProfile -> {
val pMap = mutableMapOf<String, NBTTag>()
p.name?.let { pMap["name"] = NBTTag.StringTag(it) }
p.id?.let { pMap["id"] = NBTTag.StringTag(it.toString()) }
map["player"] = NBTTag.CompoundTag(pMap)
}
}
}
}
this.style.color?.let {
map["color"] = NBTTag.StringTag(
when (it) {
is TextComponent.TextColor.Named -> it.name
is TextComponent.TextColor.Hex -> it.hex
}
)
}
this.style.font?.let { map["font"] = NBTTag.StringTag(it.toString()) }
this.style.bold?.let { map["bold"] = NBTTag.ByteTag(if (it) 1 else 0) }
this.style.italic?.let { map["italic"] = NBTTag.ByteTag(if (it) 1 else 0) }
this.style.underlined?.let { map["underlined"] = NBTTag.ByteTag(if (it) 1 else 0) }
this.style.strikethrough?.let { map["strikethrough"] = NBTTag.ByteTag(if (it) 1 else 0) }
this.style.obfuscated?.let { map["obfuscated"] = NBTTag.ByteTag(if (it) 1 else 0) }
this.style.shadowColor?.let {
when (it) {
is TextComponent.ShadowColor.ArgbInt -> map["shadow_color"] = NBTTag.IntTag(it.argb.toInt())
is TextComponent.ShadowColor.RgbaFloat -> map["shadow_color"] = NBTTag.ListTag(
NBTType.List, mutableListOf(
NBTTag.FloatTag(it.red),
NBTTag.FloatTag(it.green),
NBTTag.FloatTag(it.blue),
NBTTag.FloatTag(it.alpha)
)
)
}
}
if (this.extra.isNotEmpty()) map["extra"] =
NBTTag.ListTag(NBTType.List, this.extra.map { it.toNBTCompound() as NBTTag }.toMutableList())
this.insertion?.let { map["insertion"] = NBTTag.StringTag(it) }
this.clickEvent?.let { map["click_event"] = encodeClickEventToNbt(it) }
this.hoverEvent?.let { map["hover_event"] = encodeHoverEventToNbt(it) }
return NBTTag.CompoundTag(map)
}
private fun parseContentFromNbt(tag: NBTTag.CompoundTag): TextComponent.Content { private fun parseContentFromNbt(tag: NBTTag.CompoundTag): TextComponent.Content {
val type = tag.getStringOrNull("type") val type = tag.getStringOrNull("type")
return when { return when {
@@ -437,117 +551,6 @@ private fun parseHoverEventFromNbt(tag: NBTTag.CompoundTag): TextComponent.Hover
} }
} }
public fun TextComponent.toNbt(): NBTTag.CompoundTag {
val map = mutableMapOf<String, NBTTag>()
when (val c = this.content) {
is TextComponent.Content.PlainText -> {
map["type"] = NBTTag.StringTag("text")
map["text"] = NBTTag.StringTag(c.text)
}
is TextComponent.Content.Translatable -> {
map["type"] = NBTTag.StringTag("translatable")
map["translate"] = NBTTag.StringTag(c.key)
c.fallback?.let { map["fallback"] = NBTTag.StringTag(it) }
if (c.args.isNotEmpty()) {
map["with"] = NBTTag.ListTag(NBTType.List, c.args.map { it.toNbt() as NBTTag }.toMutableList())
}
}
is TextComponent.Content.Score -> {
map["type"] = NBTTag.StringTag("score")
map["score"] = NBTTag.CompoundTag(
mutableMapOf(
"name" to NBTTag.StringTag(c.name),
"objective" to NBTTag.StringTag(c.objective)
)
)
}
is TextComponent.Content.Selector -> {
map["type"] = NBTTag.StringTag("selector")
map["selector"] = NBTTag.StringTag(c.selector)
c.separator?.let { map["separator"] = it.toNbt() }
}
is TextComponent.Content.Keybind -> {
map["type"] = NBTTag.StringTag("keybind")
map["keybind"] = NBTTag.StringTag(c.keybind)
}
is TextComponent.Content.Nbt -> {
map["type"] = NBTTag.StringTag("nbt")
map["nbt"] = NBTTag.StringTag(c.path)
map["interpret"] = NBTTag.ByteTag(if (c.interpret) 1 else 0)
map["plain"] = NBTTag.ByteTag(if (c.plain) 1 else 0)
c.separator?.let { map["separator"] = it.toNbt() }
when (val s = c.source) {
is TextComponent.Content.Nbt.NbtSource.Entity -> map["entity"] = NBTTag.StringTag(s.selector)
is TextComponent.Content.Nbt.NbtSource.Block -> map["block"] = NBTTag.StringTag(s.coordinates)
is TextComponent.Content.Nbt.NbtSource.Storage -> map["storage"] = NBTTag.StringTag(s.id.toString())
}
}
is TextComponent.Content.ObjectContent.Atlas -> {
map["type"] = NBTTag.StringTag("object")
map["object"] = NBTTag.StringTag("atlas")
map["sprite"] = NBTTag.StringTag(c.sprite.toString())
map["atlas"] = NBTTag.StringTag(c.atlas.toString())
}
is TextComponent.Content.ObjectContent.Player -> {
map["type"] = NBTTag.StringTag("object")
map["object"] = NBTTag.StringTag("player")
map["hat"] = NBTTag.ByteTag(if (c.hat) 1 else 0)
when (val p = c.profile) {
is TextComponent.PlayerProfile.Name -> map["player"] = NBTTag.StringTag(p.name)
is TextComponent.PlayerProfile.FullProfile -> {
val pMap = mutableMapOf<String, NBTTag>()
p.name?.let { pMap["name"] = NBTTag.StringTag(it) }
p.id?.let { pMap["id"] = NBTTag.StringTag(it.toString()) }
map["player"] = NBTTag.CompoundTag(pMap)
}
}
}
}
this.style.color?.let {
map["color"] = NBTTag.StringTag(
when (it) {
is TextComponent.TextColor.Named -> it.name
is TextComponent.TextColor.Hex -> it.hex
}
)
}
this.style.font?.let { map["font"] = NBTTag.StringTag(it.toString()) }
this.style.bold?.let { map["bold"] = NBTTag.ByteTag(if (it) 1 else 0) }
this.style.italic?.let { map["italic"] = NBTTag.ByteTag(if (it) 1 else 0) }
this.style.underlined?.let { map["underlined"] = NBTTag.ByteTag(if (it) 1 else 0) }
this.style.strikethrough?.let { map["strikethrough"] = NBTTag.ByteTag(if (it) 1 else 0) }
this.style.obfuscated?.let { map["obfuscated"] = NBTTag.ByteTag(if (it) 1 else 0) }
this.style.shadowColor?.let {
when (it) {
is TextComponent.ShadowColor.ArgbInt -> map["shadow_color"] = NBTTag.IntTag(it.argb.toInt())
is TextComponent.ShadowColor.RgbaFloat -> map["shadow_color"] = NBTTag.ListTag(
NBTType.List, mutableListOf(
NBTTag.FloatTag(it.red),
NBTTag.FloatTag(it.green),
NBTTag.FloatTag(it.blue),
NBTTag.FloatTag(it.alpha)
)
)
}
}
if (this.extra.isNotEmpty()) map["extra"] =
NBTTag.ListTag(NBTType.List, this.extra.map { it.toNbt() as NBTTag }.toMutableList())
this.insertion?.let { map["insertion"] = NBTTag.StringTag(it) }
this.clickEvent?.let { map["click_event"] = encodeClickEventToNbt(it) }
this.hoverEvent?.let { map["hover_event"] = encodeHoverEventToNbt(it) }
return NBTTag.CompoundTag(map)
}
private fun encodeClickEventToNbt(event: TextComponent.ClickEvent): NBTTag.CompoundTag { private fun encodeClickEventToNbt(event: TextComponent.ClickEvent): NBTTag.CompoundTag {
val map = mutableMapOf<String, NBTTag>("action" to NBTTag.StringTag(event.action.text)) val map = mutableMapOf<String, NBTTag>("action" to NBTTag.StringTag(event.action.text))
when (event) { when (event) {
@@ -575,7 +578,7 @@ private fun encodeClickEventToNbt(event: TextComponent.ClickEvent): NBTTag.Compo
private fun encodeHoverEventToNbt(event: TextComponent.HoverEvent): NBTTag.CompoundTag { private fun encodeHoverEventToNbt(event: TextComponent.HoverEvent): NBTTag.CompoundTag {
val map = mutableMapOf<String, NBTTag>("action" to NBTTag.StringTag(event.action.text)) val map = mutableMapOf<String, NBTTag>("action" to NBTTag.StringTag(event.action.text))
when (event) { when (event) {
is TextComponent.HoverEvent.ShowText -> map["value"] = event.component.toNbt() is TextComponent.HoverEvent.ShowText -> map["value"] = event.component.toNBTCompound()
is TextComponent.HoverEvent.ShowItem -> { is TextComponent.HoverEvent.ShowItem -> {
map["id"] = NBTTag.StringTag(event.id.toString()) map["id"] = NBTTag.StringTag(event.id.toString())
map["count"] = NBTTag.IntTag(event.count) map["count"] = NBTTag.IntTag(event.count)
@@ -590,7 +593,7 @@ private fun encodeHoverEventToNbt(event: TextComponent.HoverEvent): NBTTag.Compo
intArrayOf(u.i1, u.i2, u.i3, u.i4) intArrayOf(u.i1, u.i2, u.i3, u.i4)
) )
} }
event.name?.let { map["name"] = it.toNbt() } event.name?.let { map["name"] = it.toNBTCompound() }
} }
} }
return NBTTag.CompoundTag(map) return NBTTag.CompoundTag(map)
@@ -617,6 +620,6 @@ internal fun BytesBuffer.readTextComponent(): TextComponent =
this.readNetworkNBTCompound().element.toTextComponent() this.readNetworkNBTCompound().element.toTextComponent()
internal fun BytesBuffer.writeTextComponent(component: TextComponent) { internal fun BytesBuffer.writeTextComponent(component: TextComponent) {
val nbt = component.toNbt() val nbt = component.toNBTCompound()
this.writeNetworkNBTCompound(NBTCompound("", nbt)) this.writeNetworkNBTCompound(NBTCompound("", nbt))
} }
@@ -0,0 +1,31 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/10
*/
package cn.rtast.libmc.protocol.protocol.session
import cn.rtast.libmc.protocol.client.CURRENT_MINECRAFT_PROTOCOL_VERSION
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
import kotlin.reflect.KClass
public interface Session {
@Suppress("FunctionName")
public fun <T : SessionEvent> _onEvent(clazz: KClass<T>, block: suspend Session.(T) -> Unit)
public suspend fun emitEvent(event: SessionEvent): Unit?
public suspend fun login(protocolVersion: Int = CURRENT_MINECRAFT_PROTOCOL_VERSION)
public suspend fun handshake(
protocolVersion: Int = CURRENT_MINECRAFT_PROTOCOL_VERSION,
intent: HandshakeIntent = HandshakeIntent.LOGIN,
)
public suspend fun status(protocolVersion: Int = CURRENT_MINECRAFT_PROTOCOL_VERSION): String
public suspend fun disconnect()
public suspend fun init()
}
public inline fun <reified T : SessionEvent> Session.onEvent(noinline block: suspend Session.(T) -> Unit) {
_onEvent(T::class, block)
}
@@ -0,0 +1,16 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/10
*/
package cn.rtast.libmc.protocol.protocol.session
import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
public sealed interface SessionEvent {
public data class DisconnectedEvent(val reason: TextComponent, val state: ProtocolState) : SessionEvent
public object ConnectedEvent : SessionEvent
}
@@ -0,0 +1,172 @@
package cn.rtast.libmc.protocol.protocol.session
import cn.rtast.libmc.protocol.client.MinecraftClient
import cn.rtast.libmc.protocol.crypto.minecraftServerIdHash
import cn.rtast.libmc.protocol.crypto.rsaEncrypt
import cn.rtast.libmc.protocol.packet.configuration.clientbound.*
import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundAckFinishConfigurationPacket
import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundKeepAliveConfigurationPacket
import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundPongConfigurationPacket
import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundSelectKnownPacksPacket
import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundDisconnectLoginPacket
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundHelloPacket
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundLoginSuccessPacket
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundSetCompressionPacket
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundKeyPacket
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundLoginAcknowledgedPacket
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundLoginStartPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundDisconnectPlayPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundKeepAlivePlayPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundPingPacket
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundKeepAlivePlayPacket
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundPongPlayPacket
import cn.rtast.libmc.protocol.packet.status.clientbound.ClientboundStatusResponsePacket
import cn.rtast.libmc.protocol.packet.status.serverbound.ServerboundStatusRequestPacket
import cn.rtast.libmc.protocol.protocol.event.ListenerRegistration
import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
import cn.rtast.libmc.protocol.util.generateRandom16Bytes
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.reflect.KClass
private typealias EventHandler = suspend Session.(SessionEvent) -> Unit
public class SessionImpl internal constructor() : Session {
private lateinit var client: MinecraftClient
internal fun attachClient(client: MinecraftClient) {
this.client = client
}
private val stateMachine get() = client.stateMachine
private val networkChannel get() = client.networkChannel
@PublishedApi
internal val eventListener: HashMap<KClass<out SessionEvent>, MutableList<EventHandler>> = hashMapOf()
override suspend fun init() {
client.onPacket<ClientboundDisconnectLoginPacket> {
emitEvent(SessionEvent.DisconnectedEvent(it.reason, stateMachine.currentState))
}
client.onPacket<ClientboundDisconnectPlayPacket> {
emitEvent(SessionEvent.DisconnectedEvent(it.reason, stateMachine.currentState))
}
client.onPacket<ClientboundDisconnectConfigurationPacket> {
emitEvent(SessionEvent.DisconnectedEvent(it.reason, stateMachine.currentState))
}
client.onPacket<ClientboundHelloPacket> { acceptEncryption(it) }
client.onPacket<ClientboundSetCompressionPacket> { setCompression(it.threshold) }
client.onPacket<ClientboundLoginSuccessPacket> { acknowledgeLogin() }
client.onPacket<ClientboundFinishConfigurationPacket> { finishConfiguration() }
client.onPacket<ClientboundPingPacket> { networkChannel.sendPacket(ServerboundPongPlayPacket(it.id)) }
client.onPacket<ClientboundKeepAlivePlayPacket> { networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(it.id)) }
client.onPacket<ClientboundKeepAliveConfigurationPacket> {
networkChannel.sendPacket(ServerboundKeepAliveConfigurationPacket(it.id))
}
client.onPacket<ClientboundPingConfigurationPacket> {
networkChannel.sendPacket(ServerboundPongConfigurationPacket(it.id))
}
client.onPacket<ClientboundSelectKnownPacksPacket> {
networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList()))
}
}
override fun <T : SessionEvent> _onEvent(clazz: KClass<T>, block: suspend Session.(T) -> Unit) {
@Suppress("UNCHECKED_CAST")
this.eventListener.getOrPut(clazz) { mutableListOf() }.add { block(it as T) }
}
override suspend fun emitEvent(event: SessionEvent): Unit? =
eventListener[event::class]?.forEach { handler -> handler(event) }
override suspend fun login(protocolVersion: Int) {
handshake(protocolVersion, HandshakeIntent.LOGIN)
loginStart()
}
override suspend fun handshake(protocolVersion: Int, intent: HandshakeIntent) {
ensureState(ProtocolState.HANDSHAKE)
client.networkChannel.sendPacket(
ServerboundHandshakePacket(protocolVersion, client.host, client.port.toUShort(), intent)
)
val targetState = if (intent == HandshakeIntent.LOGIN) ProtocolState.LOGIN else {
if (intent == HandshakeIntent.TRANSFER) ProtocolState.HANDSHAKE else ProtocolState.STATUS
}
stateMachine.transitionTo(targetState)
}
override suspend fun status(protocolVersion: Int): String =
suspendCancellableCoroutine { continuation ->
client.launch {
try {
var reg: ListenerRegistration? = null
reg = client.onPacket<ClientboundStatusResponsePacket> { packet ->
reg?.unregister()
if (continuation.isActive) continuation.resume(packet.jsonResponse)
}
handshake(protocolVersion, HandshakeIntent.STATUS)
networkChannel.sendPacket(ServerboundStatusRequestPacket)
} catch (e: Exception) {
if (continuation.isActive) continuation.resumeWithException(e)
}
}
}
internal suspend fun loginStart() {
ensureState(ProtocolState.LOGIN)
networkChannel.sendPacket(ServerboundLoginStartPacket(client.username, client.uuid))
}
internal suspend fun acceptEncryption(context: ClientboundHelloPacket) {
ensureState(ProtocolState.LOGIN)
val sharedSecret = generateRandom16Bytes()
val serverHash = minecraftServerIdHash(context.serverId, sharedSecret, context.publicKey)
client.protocolContext.authProvider!!.joinServer(
"https://sessionserver.mojang.com/session/minecraft/join",
client.accessToken!!, client.uuid.toString().replace("-", ""), serverHash
)
val encryptedSecret = rsaEncrypt(context.publicKey, sharedSecret)
val encryptedVerifyToken = rsaEncrypt(context.publicKey, context.verifyToken)
networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken))
networkChannel.networkSession.enableEncryption(sharedSecret)
}
internal suspend fun acknowledgeLogin() {
ensureState(ProtocolState.LOGIN)
networkChannel.sendPacket(ServerboundLoginAcknowledgedPacket)
stateMachine.transitionTo(ProtocolState.CONFIGURATION)
}
internal suspend fun finishConfiguration() {
ensureState(ProtocolState.CONFIGURATION)
networkChannel.sendPacket(ServerboundAckFinishConfigurationPacket)
stateMachine.transitionTo(ProtocolState.PLAY)
}
internal fun setCompression(threshold: Int) {
ensureState(ProtocolState.LOGIN, ProtocolState.CONFIGURATION)
networkChannel.setCompression(threshold)
}
override suspend fun disconnect(): Unit = client.close().apply {
emitEvent(
SessionEvent.DisconnectedEvent(
TextComponent(TextComponent.Content.PlainText("LibMC-Disconnected by calling disconnect()")),
stateMachine.currentState
)
)
}
private fun ensureState(vararg allowedStates: ProtocolState) {
if (stateMachine.currentState !in allowedStates) throw IllegalStateException(
"Cannot perform this action in state ${stateMachine.currentState}. Expected state: ${
allowedStates.joinToString(",").removeSuffix(",")
}."
)
}
}
@@ -8,28 +8,23 @@
package client package client
import cn.rtast.libmc.crypto.AuthenticationProvider import cn.rtast.libmc.crypto.AuthenticationProvider
import cn.rtast.libmc.packet.ClientboundUnknownPacket
import cn.rtast.libmc.protocol.client.createMinecraftClient import cn.rtast.libmc.protocol.client.createMinecraftClient
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundLevelParticlePacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundPlayerChatMessagePacket import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundPlayerChatMessagePacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundRecipeBookRemovePacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundRecipeBookSettingsPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundStepTickPacket import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundStepTickPacket
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundChatMessagePacket import cn.rtast.libmc.protocol.protocol.session.SessionEvent
import cn.rtast.libmc.protocol.protocol.session.onEvent
import cn.rtast.libmc.protocol.util.generateOfflineUuid import cn.rtast.libmc.protocol.util.generateOfflineUuid
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.utils.io.* import io.ktor.utils.io.*
import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest
import kotlinx.io.buffered import kotlinx.io.buffered
import kotlinx.io.files.Path import kotlinx.io.files.Path
import kotlinx.io.files.SystemFileSystem import kotlinx.io.files.SystemFileSystem
import test.KtorNetworkEngine import test.KtorNetworkEngine
import kotlin.random.Random
import kotlin.test.Test import kotlin.test.Test
import kotlin.time.Clock
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
@@ -40,7 +35,7 @@ class TestClient {
private val httpClient = HttpClient() private val httpClient = HttpClient()
@Test @Test
fun `test client`() { fun `test client`() = runTest {
val cli = createMinecraftClient( val cli = createMinecraftClient(
"127.0.0.1", 25565, "RTAkland", "127.0.0.1", 25565, "RTAkland",
Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"), Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
@@ -56,14 +51,25 @@ class TestClient {
} }
} }
) )
cli.onEvent<SessionEvent.ConnectedEvent> {
// println(status())
login()
// disconnect()
}
cli.onEvent<SessionEvent.DisconnectedEvent> {
println(it.reason.toJsonString())
}
cli.onPacket<ClientboundPlayerChatMessagePacket> { chatTracker.onReceivePlayerChat(it.messageSignature) }
cli.onPacket<ClientboundStepTickPacket> { println(it) }
cli.on { packet, direction -> println("$direction -> $packet") } cli.on { packet, direction -> println("$direction -> $packet") }
cli.launch { cli.connect() } cli.connect()
// awaitCancellation()
while (true) { while (true) {
} }
} }
@Test @Test
fun `test client offline mode`() { fun `test client offline mode`() = runTest {
val cli = createMinecraftClient( val cli = createMinecraftClient(
"127.0.0.1", 25566, "11", "127.0.0.1", 25566, "11",
generateOfflineUuid("11"), null, generateOfflineUuid("11"), null,
@@ -83,9 +89,20 @@ class TestClient {
// ) // )
// ) // )
// } // }
cli.onEvent<SessionEvent.ConnectedEvent> {
// println(status())
login()
// disconnect()
}
cli.onEvent<SessionEvent.DisconnectedEvent> {
println(it.reason.toJsonString())
}
cli.onPacket<ClientboundPlayerChatMessagePacket> { chatTracker.onReceivePlayerChat(it.messageSignature) } cli.onPacket<ClientboundPlayerChatMessagePacket> { chatTracker.onReceivePlayerChat(it.messageSignature) }
cli.onPacket<ClientboundStepTickPacket> { println(it) } cli.onPacket<ClientboundStepTickPacket> { println(it) }
cli.launch { cli.connect() } cli.on { packet, direction -> println("$direction -> $packet") }
cli.connect()
// awaitCancellation()
while (true) { while (true) {
} }
} }
@@ -16,6 +16,7 @@ import io.ktor.network.sockets.*
import io.ktor.utils.io.* import io.ktor.utils.io.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import kotlin.coroutines.cancellation.CancellationException
class KtorNetworkEngine : SocketEngine { class KtorNetworkEngine : SocketEngine {
override fun create(host: String, port: Int): RawSocket = KtorNetworkSocket(host, port) override fun create(host: String, port: Int): RawSocket = KtorNetworkSocket(host, port)
@@ -24,22 +25,62 @@ class KtorNetworkEngine : SocketEngine {
class KtorNetworkSocket(private val host: String, private val port: Int) : RawSocket { class KtorNetworkSocket(private val host: String, private val port: Int) : RawSocket {
private val sm = SelectorManager(Dispatchers.IO) private val sm = SelectorManager(Dispatchers.IO)
private lateinit var socket: Socket private lateinit var socket: Socket
private var ktorReadChannel: ByteReadChannel? = null
private var ktorWriteChannel: ByteWriteChannel? = null
override suspend fun connect(): Unit = run { socket = aSocket(sm).tcp().connect(host, port) } override suspend fun connect(): Unit = run { socket = aSocket(sm).tcp().connect(host, port) }
override fun openReadChannel(): ReadChannel = KtorReadChannel(socket.openReadChannel())
override fun openWriteChannel(): WriteChannel = KtorWriteChannel(socket.openWriteChannel()) override fun openReadChannel(): ReadChannel {
val channel = socket.openReadChannel()
this.ktorReadChannel = channel
return KtorReadChannel(channel)
}
override fun openWriteChannel(): WriteChannel {
val channel = socket.openWriteChannel(autoFlush = false)
this.ktorWriteChannel = channel
return KtorWriteChannel(channel)
}
override fun close() { override fun close() {
socket.close() try {
ktorReadChannel?.cancel(CancellationException("Socket closed"))
ktorWriteChannel?.close(null)
if (::socket.isInitialized) socket.dispose()
} catch (_: Exception) {
} finally {
sm.close() sm.close()
} }
} }
}
class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel { class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel {
override suspend fun readByte(): Byte = readChannel.readByte() private inline fun <T> wrapChannelException(block: () -> T): T {
override suspend fun readBytes(length: Int): ByteArray = readChannel.readByteArray(length) return try {
override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit = block()
} catch (e: Throwable) {
if (e is ClosedByteChannelException || e is CancellationException || readChannel.isClosedForRead) {
throw CancellationException("ReadChannel was closed", e)
}
throw e
}
}
override suspend fun readByte(): Byte = wrapChannelException {
if (readChannel.isClosedForRead) throw CancellationException("ReadChannel is closed for read")
readChannel.readByte()
}
override suspend fun readBytes(length: Int): ByteArray = wrapChannelException {
if (readChannel.isClosedForRead) throw CancellationException("ReadChannel is closed for read")
readChannel.readByteArray(length)
}
override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit = wrapChannelException {
if (readChannel.isClosedForRead) throw CancellationException("ReadChannel is closed for read")
readChannel.readFully(out, start, end) readChannel.readFully(out, start, end)
} }
}
class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel { class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel {
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) { override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {