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] **Structured `TextComponent` Parser**: TextComponent AST decoder
- [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)
- [ ] **Light Engine Update**: Sky & Block light nibble array parser
- [ ] **Explosion Event Decoder**: Knockback vectors and destroyed block offsets array
+25 -2
View File
@@ -6,7 +6,7 @@ public fun main() = runBlocking {
"127.0.0.1", 25566, "MyBot",
generateOfflineUuid("MyBot"),
accessToken = null,
context = DefaultProtocolContext.withCustom {
context = {
socketEngine = KtorNetworkEngine()
}
)
@@ -81,11 +81,34 @@ 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
> This part uses math calculations
When joined to the level(aka `world`), the server will send a packet
When joined to the level (aka `world`), the server will send a packet
`ClientboundSetEntityVelocityPacket` to the client, packet contains a vec3 and entity id,
The client sync this data to the player and sends it to the server during the next tick loop
to inform the server: "Hi, I know my current position; here is the result of my calculations. I'm sending it to you".
@@ -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.ProtocolContextBuilder
import cn.rtast.libmc.protocol.network.NetworkChannel
import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundLoginStartPacket
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
import cn.rtast.libmc.protocol.protocol.event.PacketEventDispatcher
import cn.rtast.libmc.protocol.protocol.session.Session
import cn.rtast.libmc.protocol.protocol.session.SessionEvent
import cn.rtast.libmc.protocol.protocol.session.SessionImpl
import cn.rtast.libmc.protocol.util.TransactionIdManager
import cn.rtast.libmc.protocol.util.generateOfflineUuid
import kotlinx.coroutines.*
@@ -20,68 +20,51 @@ import kotlin.coroutines.CoroutineContext
import kotlin.uuid.Uuid
public class MinecraftClient internal constructor(
private val host: String,
private val port: Int,
private val username: String,
internal val host: String,
internal val port: Int,
internal val username: String,
internal val uuid: Uuid,
internal val accessToken: String?,
parentJob: Job?,
private val ioDispatcher: CoroutineDispatcher,
internal val protocolContext: ProtocolContext,
) : PacketEventDispatcher(), CoroutineScope {
public val session: SessionImpl = SessionImpl(),
) : PacketEventDispatcher(), CoroutineScope, Session by session {
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 var listenJob: Job? = null
public val isOnlineMode: Boolean = accessToken != null
public val transactionManager: TransactionIdManager = TransactionIdManager()
public val clientTickingLoop: ClientTickingLoop = ClientTickingLoop(this)
/**
* Register a client ticking event callback.
* 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))
init {
session.attachClient(this)
}
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() {
listenJob = launch {
try {
while (isActive) networkChannel.readNextPacket()
} catch (e: Exception) {
if (e is CancellationException) throw e
if (isActive) {
e.printStackTrace()
} catch (e: Throwable) {
if (e is CancellationException) return@launch
println("Network read loop exception: ${e.message}")
close()
}
} finally {
networkChannel.close()
}
}
}
public fun close() {
networkChannel.close()
clientTickingLoop.stop()
listenJob?.cancel()
clientJob.cancel()
cancel()
}
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
import cn.rtast.libmc.crypto.ProtocolContext
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.network.wrap
import cn.rtast.libmc.packet.MinecraftPacket
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.client.PacketEventDispatcher
import cn.rtast.libmc.protocol.client.MinecraftClient
import cn.rtast.libmc.protocol.protocol.event.PacketEventDispatcher
import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec.clientboundGameProtocols
import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec.serverboundGameProtocols
import cn.rtast.libmc.zlibCompress
import cn.rtast.libmc.zlibDecompress
import kotlin.concurrent.Volatile
public class NetworkChannel internal constructor(
host: String,
port: Int,
private val stateMachine: ClientStateMachine,
private val dispatcher: PacketEventDispatcher,
protocolContext: ProtocolContext,
) {
internal val session: NetworkSession = NetworkSession(host, port, protocolContext)
public class NetworkChannel internal constructor(private val client: MinecraftClient) {
internal val networkSession: NetworkSession = NetworkSession(client)
@Volatile
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 }
/**
@@ -42,8 +35,8 @@ public class NetworkChannel internal constructor(
* Use [PacketEventDispatcher.onPacket] to get packet event
*/
public suspend fun readNextPacket(): MinecraftPacket {
val packetLength = session.readVarInt()
val frameBuf = session.readBytes(packetLength).wrap()
val packetLength = networkSession.readVarInt()
val frameBuf = networkSession.readBytes(packetLength).wrap()
val payloadBuf = if (threshold < 0) frameBuf else {
val dataLength = frameBuf.readVarInt()
if (dataLength == 0) frameBuf else {
@@ -51,10 +44,10 @@ public class NetworkChannel internal constructor(
compressedBytes.zlibDecompress(dataLength).wrap()
}
}
val currentState = stateMachine.currentState
val currentState = client.stateMachine.currentState
val packetId = payloadBuf.readVarInt()
val packet = clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, payloadBuf)
dispatcher.dispatchReceive(packet)
client.dispatchReceive(packet, client.session)
return packet
}
@@ -65,7 +58,7 @@ public class NetworkChannel internal constructor(
*/
public suspend fun sendPacket(packet: MinecraftPacket) {
val uncompressedBodyBuf = BytesBuffer()
serverboundGameProtocols.getRegistry(stateMachine.currentState).encodePacket(uncompressedBodyBuf, packet)
serverboundGameProtocols.getRegistry(client.stateMachine.currentState).encodePacket(uncompressedBodyBuf, packet)
val uncompressedData = uncompressedBodyBuf.toByteArray()
val frameBuffer = BytesBuffer()
if (threshold < 0) {
@@ -84,9 +77,9 @@ public class NetworkChannel internal constructor(
frameBuffer.writeVarInt(contentBuf.size)
frameBuffer.writeBuffer(contentBuf)
}
session.writeFully(frameBuffer.toByteArray())
dispatcher.dispatchSent(packet)
networkSession.writeFully(frameBuffer.toByteArray())
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
import cn.rtast.libmc.crypto.ProtocolContext
import cn.rtast.libmc.network.RawSocket
import cn.rtast.libmc.network.ReadChannel
import cn.rtast.libmc.network.WriteChannel
import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.protocol.client.MinecraftClient
import cn.rtast.libmc.protocol.crypto.Aes128Cfb8ChannelCipher
public class NetworkSession internal constructor(
private val host: String,
private val port: Int,
private val context: ProtocolContext,
) {
public class NetworkSession internal constructor(private val client: MinecraftClient) {
private var socket: RawSocket? = null
public var readChannel: ReadChannel? = null
private set
@@ -26,7 +22,7 @@ public class NetworkSession internal constructor(
private set
public suspend fun connect() {
val sk = context.createSocket(host, port)
val sk = client.protocolContext.createSocket(client.host, client.port)
sk.connect()
this.socket = sk
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.NBTTag
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.util.readNetworkNBTCompound
import cn.rtast.libmc.protocol.protocol.util.writeNetworkNBTCompound
import cn.rtast.libmc.network.BytesBuffer
import kotlin.uuid.Uuid
public data class TextComponent(
@@ -223,6 +224,8 @@ public data class TextComponent(
}
}
public fun toJsonString(): String = this.toNBTCompound().toJsonString()
public companion object {
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 {
val type = tag.getStringOrNull("type")
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 {
val map = mutableMapOf<String, NBTTag>("action" to NBTTag.StringTag(event.action.text))
when (event) {
@@ -575,7 +578,7 @@ private fun encodeClickEventToNbt(event: TextComponent.ClickEvent): NBTTag.Compo
private fun encodeHoverEventToNbt(event: TextComponent.HoverEvent): NBTTag.CompoundTag {
val map = mutableMapOf<String, NBTTag>("action" to NBTTag.StringTag(event.action.text))
when (event) {
is TextComponent.HoverEvent.ShowText -> map["value"] = event.component.toNbt()
is TextComponent.HoverEvent.ShowText -> map["value"] = event.component.toNBTCompound()
is TextComponent.HoverEvent.ShowItem -> {
map["id"] = NBTTag.StringTag(event.id.toString())
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)
)
}
event.name?.let { map["name"] = it.toNbt() }
event.name?.let { map["name"] = it.toNBTCompound() }
}
}
return NBTTag.CompoundTag(map)
@@ -617,6 +620,6 @@ internal fun BytesBuffer.readTextComponent(): TextComponent =
this.readNetworkNBTCompound().element.toTextComponent()
internal fun BytesBuffer.writeTextComponent(component: TextComponent) {
val nbt = component.toNbt()
val nbt = component.toNBTCompound()
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
import cn.rtast.libmc.crypto.AuthenticationProvider
import cn.rtast.libmc.packet.ClientboundUnknownPacket
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.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.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 io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.utils.io.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlinx.io.buffered
import kotlinx.io.files.Path
import kotlinx.io.files.SystemFileSystem
import test.KtorNetworkEngine
import kotlin.random.Random
import kotlin.test.Test
import kotlin.time.Clock
import kotlin.uuid.Uuid
@@ -40,7 +35,7 @@ class TestClient {
private val httpClient = HttpClient()
@Test
fun `test client`() {
fun `test client`() = runTest {
val cli = createMinecraftClient(
"127.0.0.1", 25565, "RTAkland",
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.launch { cli.connect() }
cli.connect()
// awaitCancellation()
while (true) {
}
}
@Test
fun `test client offline mode`() {
fun `test client offline mode`() = runTest {
val cli = createMinecraftClient(
"127.0.0.1", 25566, "11",
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<ClientboundStepTickPacket> { println(it) }
cli.launch { cli.connect() }
cli.on { packet, direction -> println("$direction -> $packet") }
cli.connect()
// awaitCancellation()
while (true) {
}
}
@@ -16,6 +16,7 @@ import io.ktor.network.sockets.*
import io.ktor.utils.io.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlin.coroutines.cancellation.CancellationException
class KtorNetworkEngine : SocketEngine {
override fun create(host: String, port: Int): RawSocket = KtorNetworkSocket(host, port)
@@ -24,21 +25,61 @@ class KtorNetworkEngine : SocketEngine {
class KtorNetworkSocket(private val host: String, private val port: Int) : RawSocket {
private val sm = SelectorManager(Dispatchers.IO)
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 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() {
socket.close()
try {
ktorReadChannel?.cancel(CancellationException("Socket closed"))
ktorWriteChannel?.close(null)
if (::socket.isInitialized) socket.dispose()
} catch (_: Exception) {
} finally {
sm.close()
}
}
}
class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel {
override suspend fun readByte(): Byte = readChannel.readByte()
override suspend fun readBytes(length: Int): ByteArray = readChannel.readByteArray(length)
override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit =
private inline fun <T> wrapChannelException(block: () -> T): T {
return try {
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)
}
}
class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel {