diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet/MinecraftPacket.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet/MinecraftPacket.kt index 6f3b56f..8d5f601 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet/MinecraftPacket.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet/MinecraftPacket.kt @@ -7,4 +7,4 @@ package cn.rtast.libmc.common.packet -public interface MinecraftPacket \ No newline at end of file +public interface MinecraftPacket : PacketEvent \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/codec/PacketCodec.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet/PacketEvent.kt similarity index 54% rename from libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/codec/PacketCodec.kt rename to libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet/PacketEvent.kt index 56ed718..0bfcc0c 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/codec/PacketCodec.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet/PacketEvent.kt @@ -5,5 +5,6 @@ */ -package cn.rtast.libmc.protocol.codec +package cn.rtast.libmc.common.packet +public interface PacketEvent \ No newline at end of file diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/zlib.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/zlib.kt index e36ca76..31a6615 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/zlib.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/zlib.kt @@ -7,4 +7,8 @@ package cn.rtast.libmc.common -internal expect fun ByteArray.zlibDecompress(): ByteArray \ No newline at end of file +public expect fun ByteArray.zlibDecompress(): ByteArray + +public expect fun ByteArray.zlibDecompress(expectedSize: Int): ByteArray + +public expect fun ByteArray.zlibCompress(): ByteArray \ No newline at end of file diff --git a/libmc-common/src/commonTest/kotlin/test/TestZlib.kt b/libmc-common/src/commonTest/kotlin/test/TestZlib.kt new file mode 100644 index 0000000..e87bdd6 --- /dev/null +++ b/libmc-common/src/commonTest/kotlin/test/TestZlib.kt @@ -0,0 +1,29 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/5 + */ + + +package test + +import cn.rtast.libmc.common.zlibCompress +import cn.rtast.libmc.common.zlibDecompress +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class TestZlib { + + @Test + fun `test zlib compress decompress`() { + val originalText = "Minecraft Protocol Compression".repeat(10) + val originalBytes = originalText.encodeToByteArray() + val compressedBytes = originalBytes.zlibCompress() + assertTrue(compressedBytes.size < originalBytes.size) + val decompressedBytes = compressedBytes.zlibDecompress(originalBytes.size) + assertEquals(originalBytes.size, decompressedBytes.size) + assertContentEquals(originalBytes, decompressedBytes) + } +} \ No newline at end of file diff --git a/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/zlib.jvm.kt b/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/zlib.jvm.kt index d97e719..bafb6c6 100644 --- a/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/zlib.jvm.kt +++ b/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/zlib.jvm.kt @@ -8,6 +8,7 @@ package cn.rtast.libmc.common import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.util.zip.Deflater import java.util.zip.GZIPInputStream import java.util.zip.Inflater @@ -41,4 +42,27 @@ private fun ByteArray.gzipDecompress(): ByteArray { return outputStream.toByteArray() } } +} + +public actual fun ByteArray.zlibDecompress(expectedSize: Int): ByteArray { + val inflater = Inflater() + inflater.setInput(this) + val result = ByteArray(expectedSize) + try { + val resultLength = inflater.inflate(result) + check(resultLength == expectedSize) { "Decompression failed: expected $expectedSize bytes, but got $resultLength" } + return result + } finally { + inflater.end() + } +} + +public actual fun ByteArray.zlibCompress(): ByteArray { + val deflater = Deflater() + deflater.setInput(this) + deflater.finish() + val output = ByteArray(this.size + 64) + val compressedSize = deflater.deflate(output) + deflater.end() + return output.copyOf(compressedSize) } \ No newline at end of file diff --git a/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/zlib.native.kt b/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/zlib.native.kt index 6de10d8..a6b2c80 100644 --- a/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/zlib.native.kt +++ b/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/zlib.native.kt @@ -4,11 +4,12 @@ * Date: 2026/9/4 */ -@file:OptIn(ExperimentalForeignApi::class) +@file:OptIn(ExperimentalForeignApi::class, UnsafeNumber::class) package cn.rtast.libmc.common import kotlinx.cinterop.* +import platform.posix.u_longVar import platform.zlib.* private const val ENABLE_ZLIB_GZIP_HEADER = 15 + 32 @@ -65,4 +66,39 @@ public actual fun ByteArray.zlibDecompress(): ByteArray { inputPinned.unpin() } } +} + +public actual fun ByteArray.zlibDecompress(expectedSize: Int): ByteArray { + val result = ByteArray(expectedSize) + if (this.isEmpty()) return result + memScoped { + val destLen = alloc() + destLen.value = expectedSize.toUInt() + val res = uncompress( + result.refTo(0).getPointer(this).reinterpret(), + destLen.ptr, + this@zlibDecompress.refTo(0).getPointer(this).reinterpret(), + this@zlibDecompress.size.toUInt() + ) + check(res == Z_OK) { "zlib uncompress failed with error code: $res" } + } + return result +} + +public actual fun ByteArray.zlibCompress(): ByteArray { + if (this.isEmpty()) return byteArrayOf() + val maxCompressedLen = compressBound(this.size.toUInt()).toInt() + val output = ByteArray(maxCompressedLen) + memScoped { + val destLen = alloc() + destLen.value = maxCompressedLen.toUInt() + val res = compress( + output.refTo(0).getPointer(this).reinterpret(), + destLen.ptr, + this@zlibCompress.refTo(0).getPointer(this).reinterpret(), + this@zlibCompress.size.toUInt() + ) + check(res == Z_OK) { "zlib compress failed with error code: $res" } + return output.copyOf(destLen.value.toInt()) + } } \ No newline at end of file diff --git a/libmc-protocol/build.gradle.kts b/libmc-protocol/build.gradle.kts index 3c2f3f6..d1ea2e5 100644 --- a/libmc-protocol/build.gradle.kts +++ b/libmc-protocol/build.gradle.kts @@ -18,7 +18,7 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(project(":common")) + api(project(":common")) api(libs.kotlinx.serialization.core) api(libs.kotlinx.serialization.json) api(libs.kotlinx.coroutines) diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/chat.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/chat.kt deleted file mode 100644 index 077df18..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/chat.kt +++ /dev/null @@ -1,146 +0,0 @@ -///* -// * Copyright © 2026 RTAkland -// * Author: RTAkland -// * Date: 2026/9/4 -// */ -// -// -//package cn.rtast.libmc.protocol -// -//import cn.rtast.libmc.protocol.packet.configuration.ServerboundAckFinishConfigurationPacket -//import cn.rtast.libmc.protocol.packet.configuration.ServerboundPongPacket -//import cn.rtast.libmc.protocol.packet.configuration.ServerboundSelectKnownPacksPacket -//import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket -//import cn.rtast.libmc.protocol.packet.login.ServerboundLoginAcknowledgedPacket -//import cn.rtast.libmc.protocol.packet.login.ServerboundLoginStartPacket -//import cn.rtast.libmc.protocol.packet.play.ServerboundKeepAlivePlayPacket -//import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent -//import cn.rtast.libmc.protocol.protocol.state.ProtocolState -//import cn.rtast.libmc.protocol.util.generateOfflineUuid -//import cn.rtast.libmc.common.* -//import kotlinx.coroutines.Dispatchers -//import kotlinx.coroutines.coroutineScope -//import kotlinx.coroutines.currentCoroutineContext -//import kotlinx.coroutines.isActive -//import kotlinx.coroutines.launch -//import kotlin.uuid.Uuid -// -// -//public class MinecraftChatClient( -// private val host: String, -// private val port: Int, -// private val username: String, -// private val uuid: Uuid = generateOfflineUuid(username), -// private val context: LibMCContext = LibMCContext(), -//) { -// private var state = ProtocolState.HANDSHAKE -// -// public suspend fun start(): Unit = coroutineScope { -// val socket = Socket(host, port, context) -// val input = socket.openReadChannel() -// val output = socket.openWriteChannel() -// -// executeInitHandshake(output) -// -// val readerJob = launch(Dispatchers.Default) { -// handleIncomingPackets(input, output) -// } -// -// readerJob.join() -// } -// -// private fun executeInitHandshake(output: WriteChannel) { -// val handshakePacket = ServerboundHandshakePacket(776, host, port.toUShort(), HandshakeIntent.LOGIN) -// output.sendPacket(handshakePacket, ServerboundHandshakePacket) -// state = ProtocolState.LOGIN -// -// val loginStartPacket = ServerboundLoginStartPacket(username, uuid) -// output.sendPacket(loginStartPacket, ServerboundLoginStartPacket) -// } -// -// private suspend fun handleIncomingPackets(input: ReadChannel, output: WriteChannel) { -// try { -// while (currentCoroutineContext().isActive) { -// val packetLength = input.readVarInt() -// if (packetLength <= 0) continue -// -// val packetBytes = ByteArray(packetLength) -// input.readFully(packetBytes, 0, packetLength) -// -// val buffer = BytesBuffer(packetBytes) -// val packetId = buffer.readVarInt() -// println("received -> State: $state | ID: 0x${packetId.toString(16).uppercase()} | Length: $packetLength") -// try { -// when (state) { -// ProtocolState.LOGIN -> handleLoginPackets(packetId, output) -// ProtocolState.CONFIGURATION -> handleConfigurationPackets(packetId, buffer, output) -// ProtocolState.PLAY -> handlePlayPackets(packetId, buffer, output) -// else -> {} -// } -// } catch (e: Exception) { -// println("parsing 0x${packetId.toString(16).uppercase()} Payload failed: ${e.message}") -// } -// } -// } catch (e: Exception) { -// e.printStackTrace() -// println("disconnecting: ${e.message}") -// } -// } -// -// private fun handleLoginPackets(packetId: Int, output: WriteChannel) { -// when (packetId) { -// 0x02 -> { -// output.sendPacket(ServerboundLoginAcknowledgedPacket(), ServerboundLoginAcknowledgedPacket) -// state = ProtocolState.CONFIGURATION -// println("[3/4] sent LoginAcknowledgedPacket -> switching to CONFIGURATION state") -// -// output.sendPacket( -// ServerboundSelectKnownPacksPacket(knownPacks = emptyList()), -// ServerboundSelectKnownPacksPacket -// ) -// } -// -// 0x00 -> { -// println("login denied (ClientboundDisconnectLoginPacket)") -// } -// } -// } -// -// private fun handleConfigurationPackets(packetId: Int, packetBuffer: BytesBuffer, output: WriteChannel) { -// when (packetId) { -// 0x0E -> { -// println("received ClientboundSelectKnownPacksPacket") -// } -// -// 0x03 -> { -// output.sendPacket(ServerboundAckFinishConfigurationPacket, ServerboundAckFinishConfigurationPacket) -// state = ProtocolState.PLAY -// } -// -// 0x05 -> { -// output.sendPacket(ServerboundPongPacket(0), ServerboundPongPacket) -// } -// -// 0x01 -> println("configuration state disconnected") -// } -// } -// -// private fun handlePlayPackets(packetId: Int, packetBuffer: BytesBuffer, output: WriteChannel) { -// try { -// when (packetId) { -// 0x2B -> println("[PLAY] Joined world") -// -// 0x2c -> { -// val keepAliveId = packetBuffer.readLong() -// output.sendPacket(ServerboundKeepAlivePlayPacket(id = keepAliveId), ServerboundKeepAlivePlayPacket) -// println("[PLAY] reply keep alive packet $keepAliveId") -// } -// -// 0x1D -> println("[PLAY] disconnected (ClientboundDisconnectPlayPacket)") -// else -> {} -// } -// } catch (e: Exception) { -// println("parsing 0x${packetId.toString(16).uppercase()} failed, skipped: ${e.message}") -// } -// } -//} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/chat/ChatFilterType.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/chat/ChatFilterType.kt deleted file mode 100644 index 37dafc3..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/chat/ChatFilterType.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/4 - */ - - -package cn.rtast.libmc.protocol.chat - -public enum class ChatFilterType(public val id: Int) { - PASS_THROUGH(0), - FULLY_FILTERED(1), - PARTIALLY_FILTERED(2); - - public companion object { - public fun fromId(id: Int): ChatFilterType = - entries.firstOrNull { it.id == id } ?: PASS_THROUGH - } -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/chat/PreviousMessageEntry.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/chat/PreviousMessageEntry.kt deleted file mode 100644 index cb6b87d..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/chat/PreviousMessageEntry.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/4 - */ - - -package cn.rtast.libmc.protocol.chat - -import cn.rtast.libmc.common.PacketCodec -import cn.rtast.libmc.common.BytesBuffer -import cn.rtast.libmc.common.writeVarInt - -public data class PreviousMessageEntry( - val messageId: Int, - val signature: ByteArray?, -) { - public companion object Codec : PacketCodec { - override fun encode(buffer: BytesBuffer, value: PreviousMessageEntry) { - buffer.writeVarInt(value.messageId) - if (value.messageId == 0) { - val sig = requireNotNull(value.signature) { "signature must be present when messageId is 0" } - require(sig.size == 256) - buffer.writeBytes(sig) - } - } - - override fun decode(buffer: BytesBuffer): PreviousMessageEntry = throw UnsupportedOperationException() // TODO - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as PreviousMessageEntry - - if (messageId != other.messageId) return false - if (!signature.contentEquals(other.signature)) return false - - return true - } - - override fun hashCode(): Int { - var result = messageId - result = 31 * result + (signature?.contentHashCode() ?: 0) - return result - } -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/InternalPacketDispatcher.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/InternalPacketDispatcher.kt new file mode 100644 index 0000000..6c13bf6 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/InternalPacketDispatcher.kt @@ -0,0 +1,73 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/5 + */ + + +package cn.rtast.libmc.protocol.client + +import cn.rtast.libmc.common.packet.MinecraftPacket +import cn.rtast.libmc.protocol.packet.configuration.* +import cn.rtast.libmc.protocol.packet.login.ClientboundDisconnectLoginPacket +import cn.rtast.libmc.protocol.packet.login.ClientboundLoginSuccessPacket +import cn.rtast.libmc.protocol.packet.login.ServerboundLoginAcknowledgedPacket +import cn.rtast.libmc.protocol.packet.play.* +import cn.rtast.libmc.protocol.protocol.state.ProtocolState + +internal class InternalPacketDispatcher(private val client: MinecraftClient) { + suspend fun dispatchEvent(packet: MinecraftPacket) = client.dispatch(packet) + + suspend fun handleIncomingPackets(packet: MinecraftPacket) { + this.dispatchEvent(packet) + when (packet) { + is ClientboundLoginSuccessPacket -> { + client.networkChannel.sendPacket(ServerboundLoginAcknowledgedPacket) + client.stateMachine.transitionTo(ProtocolState.CONFIGURATION) + } + + is ClientboundDisconnectLoginPacket -> { + println("Login denied: ${packet.reason}") +// close() + } + + is ClientboundSelectKnownPacksPacket -> { + client.networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList())) // TODO empty resource packs list + } + + is ClientboundPingPacket -> client.networkChannel.sendPacket(ServerboundPongPacket(packet.id)) + + is ClientboundKeepAliveConfigurationPacket -> { + client.networkChannel.sendPacket(ServerboundKeepAliveConfigurationPacket(packet.id)) + } + + is ClientboundFinishConfigurationPacket -> { + client.networkChannel.sendPacket(ServerboundAckFinishConfigurationPacket) + client.stateMachine.transitionTo(ProtocolState.PLAY) + } + + is ClientboundDisconnectConfigurationPacket -> { + println("Configuration disconnected: ${packet.reason}") +// close() + } + + is ClientboundLoginPlayPacket -> { + println("Successfully joined world! Entity ID: ${packet.entityId}") + } + + is ClientboundKeepAlivePlayPacket -> { + client.networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(id = packet.id)) + } + + is ClientboundStartConfigurationPacket -> { + client.networkChannel.sendPacket(ServerboundConfigurationAcknowledgedPacket) + client.stateMachine.transitionTo(ProtocolState.CONFIGURATION) + } + + is ClientboundDisconnectPlayPacket -> { + println("Disconnected from play session: ${packet.reason}") +// close() + } + } + } +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/MinecraftClient.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/MinecraftClient.kt index 5081fb0..73a59fa 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/MinecraftClient.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/client/MinecraftClient.kt @@ -7,60 +7,53 @@ package cn.rtast.libmc.protocol.client import cn.rtast.libmc.common.LibMCContext -import cn.rtast.libmc.common.packet.MinecraftPacket -import cn.rtast.libmc.common.packet.UnknownPacket +import cn.rtast.libmc.protocol.event.PacketEventDispatcher import cn.rtast.libmc.protocol.network.NetworkChannel -import cn.rtast.libmc.protocol.packet.configuration.* import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket -import cn.rtast.libmc.protocol.packet.login.ClientboundDisconnectLoginPacket -import cn.rtast.libmc.protocol.packet.login.ClientboundLoginSuccessPacket -import cn.rtast.libmc.protocol.packet.login.ServerboundLoginAcknowledgedPacket import cn.rtast.libmc.protocol.packet.login.ServerboundLoginStartPacket -import cn.rtast.libmc.protocol.packet.play.* -import cn.rtast.libmc.protocol.protocol.GameProtocols import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent import cn.rtast.libmc.protocol.protocol.state.ProtocolState import cn.rtast.libmc.protocol.util.generateOfflineUuid import kotlinx.coroutines.* +import kotlin.coroutines.CoroutineContext import kotlin.uuid.Uuid -public class MinecraftClient( +public class MinecraftClient internal constructor( private val host: String, private val port: Int = 25565, private val username: String, - private val uuid: Uuid = generateOfflineUuid(username), - private val context: LibMCContext = LibMCContext(), -) { - private val stateMachine = ClientStateMachine() - private val networkChannel = NetworkChannel(host, port, context, stateMachine) - private val listeners = mutableListOf<(MinecraftPacket) -> Unit>() + private val uuid: Uuid, + context: LibMCContext, + parentJob: Job?, + private val ioDispatcher: CoroutineDispatcher, +) : PacketEventDispatcher(), CoroutineScope { + + internal val stateMachine = ClientStateMachine() + internal val networkChannel = NetworkChannel(host, port, context, stateMachine) + private val internalPacketDispatcher = InternalPacketDispatcher(this) + + private val clientJob = SupervisorJob(parentJob) private var listenJob: Job? = null - public suspend fun connect(protocolVersion: Int = 776) { + override val coroutineContext: CoroutineContext + get() = clientJob + ioDispatcher + CoroutineName("LibMC-MinecraftClient-$username") + + public fun connect(protocolVersion: Int = 776) { networkChannel.connect() startListening() networkChannel.sendPacket( - ServerboundHandshakePacket( - protocolVersion, host, - port.toUShort(), - HandshakeIntent.LOGIN - ) + ServerboundHandshakePacket(protocolVersion, host, port.toUShort(), HandshakeIntent.LOGIN) ) stateMachine.transitionTo(ProtocolState.LOGIN) networkChannel.sendPacket(ServerboundLoginStartPacket(username, uuid)) - listenJob?.join() } private fun startListening() { - listenJob = CoroutineScope(Dispatchers.IO).launch { + listenJob = launch { try { - while (isActive) { - val packet = networkChannel.readNextPacket() - handleIncomingPackets(packet) - listeners.forEach { it.invoke(packet) } - } + while (isActive) internalPacketDispatcher.handleIncomingPackets(networkChannel.readNextPacket()) } catch (e: Exception) { - e.printStackTrace() + if (e is CancellationException) throw e if (isActive) { println("Network read loop exception: ${e.message}") close() @@ -69,65 +62,18 @@ public class MinecraftClient( } } - private fun handleIncomingPackets(packet: MinecraftPacket) { - when (packet) { - is ClientboundLoginSuccessPacket -> { - networkChannel.sendPacket(ServerboundLoginAcknowledgedPacket) - stateMachine.transitionTo(ProtocolState.CONFIGURATION) - } - - is ClientboundDisconnectLoginPacket -> { - println("Login denied: ${packet.reason}") - close() - } - - is ClientboundSelectKnownPacksPacket -> { - networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList())) // TODO empty resource packs list - } - - is ClientboundPingPacket -> networkChannel.sendPacket(ServerboundPongPacket(packet.id)) - - is ClientboundKeepAliveConfigurationPacket -> { - networkChannel.sendPacket(ServerboundKeepAliveConfigurationPacket(packet.id)) - } - - is ClientboundFinishConfigurationPacket -> { - networkChannel.sendPacket(ServerboundAckFinishConfigurationPacket) - stateMachine.transitionTo(ProtocolState.PLAY) - } - - is ClientboundDisconnectConfigurationPacket -> { - println("Configuration disconnected: ${packet.reason}") - close() - } - - is ClientboundLoginPlayPacket -> { - println("Successfully joined world! Entity ID: ${packet.entityId}") - } - - is ClientboundKeepAlivePlayPacket -> { - networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(id = packet.id)) - } - - is ClientboundStartConfigurationPacket -> { - networkChannel.sendPacket(ServerboundConfigurationAcknowledgedPacket) - stateMachine.transitionTo(ProtocolState.CONFIGURATION) - } - - is ClientboundDisconnectPlayPacket -> { - println("Disconnected from play session: ${packet.reason}") - close() - } -// else -> println((packet as? UnknownPacket)?.data?.contentToString() ?: packet) - } - } - - public fun onPacket(listener: (MinecraftPacket) -> Unit) { - listeners.add(listener) - } - public fun close() { - listenJob?.cancel() networkChannel.close() + clientJob.cancel() } -} \ No newline at end of file +} + +public fun createMinecraftClient( + host: String, + port: Int, + username: String, + uuid: Uuid = generateOfflineUuid(username), + context: LibMCContext = LibMCContext(), + parentJob: Job? = null, + ioDispatcher: CoroutineDispatcher = Dispatchers.IO, +): MinecraftClient = MinecraftClient(host, port, username, uuid, context, parentJob, ioDispatcher) \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/event/PacketEventDispatcher.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/event/PacketEventDispatcher.kt new file mode 100644 index 0000000..8296626 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/event/PacketEventDispatcher.kt @@ -0,0 +1,26 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/5 + */ + + +package cn.rtast.libmc.protocol.event + +import cn.rtast.libmc.common.packet.PacketEvent +import kotlin.reflect.KClass + +public open class PacketEventDispatcher { + @PublishedApi + internal val eventHandlers: MutableMap, MutableList Unit>> = + mutableMapOf() + + internal suspend fun dispatch(event: PacketEvent) { + eventHandlers[event::class]?.forEach { it.invoke(event) } + } + + public inline fun on(crossinline block: suspend (T) -> Unit) { + val handlers = eventHandlers.getOrPut(T::class) { mutableListOf() } + handlers.add { event -> block(event as T) } + } +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkChannel.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkChannel.kt index 8fa9128..9096c58 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkChannel.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkChannel.kt @@ -4,7 +4,6 @@ * Date: 2026/9/5 */ - package cn.rtast.libmc.protocol.network import cn.rtast.libmc.common.* @@ -21,6 +20,7 @@ internal class NetworkChannel( private var socket: Socket? = null private var readChannel: ReadChannel? = null private var writeChannel: WriteChannel? = null + private var threshold = -1 fun connect() { val sk = Socket(host, port, context) @@ -29,13 +29,25 @@ internal class NetworkChannel( this.writeChannel = sk.openWriteChannel() } + fun setCompression(threshold: Int) { + this.threshold = threshold + } + fun readNextPacket(): MinecraftPacket { val channel = requireNotNull(readChannel) { "ReadChannel not connected" } - val length = channel.readVarInt() - val buf = channel.readBytes(length).wrap() + val packetLength = channel.readVarInt() + val rawFrameBytes = channel.readBytes(packetLength) + val payloadBuf = if (threshold < 0) rawFrameBytes.wrap() else { + val frameBuf = rawFrameBytes.wrap() + val dataLength = frameBuf.readVarInt() + if (dataLength == 0) { + frameBuf.readBytes(frameBuf.remaining.toInt()).wrap() + } else frameBuf.readBytes(frameBuf.remaining.toInt()).zlibDecompress(dataLength).wrap() + } + val currentState = stateMachine.currentState - val packetId = buf.readVarInt() - return GameProtocols.clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, buf) + val packetId = payloadBuf.readVarInt() + return GameProtocols.clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, payloadBuf) } fun sendPacket(packet: MinecraftPacket) { @@ -43,8 +55,28 @@ internal class NetworkChannel( val bodyBuffer = BytesBuffer() GameProtocols.serverboundGameProtocols.getRegistry(stateMachine.currentState).encodePacket(bodyBuffer, packet) val frameBuffer = BytesBuffer().apply { - writeVarInt(bodyBuffer.size) - writeBuffer(bodyBuffer) + if (threshold < 0) { + writeVarInt(bodyBuffer.size) + writeBuffer(bodyBuffer) + } else { + val uncompressedData = bodyBuffer.toByteArray() + if (uncompressedData.size < threshold) { + val contentBuf = BytesBuffer().apply { + writeVarInt(0) + writeBytes(uncompressedData) + } + writeVarInt(contentBuf.size) + writeBuffer(contentBuf) + } else { + val compressedData = uncompressedData.zlibCompress() + val contentBuf = BytesBuffer().apply { + writeVarInt(uncompressedData.size) + writeBytes(compressedData) + } + writeVarInt(contentBuf.size) + writeBuffer(contentBuf) + } + } } channel.writeFully(frameBuffer.toByteArray()) channel.flush() diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/login/ClientboundSetCompressionPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/login/ClientboundSetCompressionPacket.kt new file mode 100644 index 0000000..b2aebb4 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/login/ClientboundSetCompressionPacket.kt @@ -0,0 +1,26 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/5 + */ + + +package cn.rtast.libmc.protocol.packet.login + +import cn.rtast.libmc.common.BytesBuffer +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common.packet.MinecraftPacket +import cn.rtast.libmc.common.readVarInt +import cn.rtast.libmc.common.writeVarInt + +public data class ClientboundSetCompressionPacket(val threshold: Int) : MinecraftPacket { + public companion object Codec : PacketCodec { + override fun encode(buffer: BytesBuffer, value: ClientboundSetCompressionPacket) { + buffer.writeVarInt(value.threshold) + } + + override fun decode(buffer: BytesBuffer): ClientboundSetCompressionPacket { + return ClientboundSetCompressionPacket(buffer.readVarInt()) + } + } +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/ClientboundPlayerChatMessagePacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/ClientboundPlayerChatMessagePacket.kt index 13cde32..de9cd75 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/ClientboundPlayerChatMessagePacket.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/ClientboundPlayerChatMessagePacket.kt @@ -9,8 +9,6 @@ package cn.rtast.libmc.protocol.packet.play import cn.rtast.libmc.common.* import cn.rtast.libmc.common.packet.MinecraftPacket -import cn.rtast.libmc.protocol.chat.ChatFilterType -import cn.rtast.libmc.protocol.chat.PreviousMessageEntry import cn.rtast.libmc.protocol.util.writeMinimalTextNbt import kotlin.uuid.Uuid @@ -30,6 +28,53 @@ public data class ClientboundPlayerChatMessagePacket( val senderName: String, val targetName: String?, ) : MinecraftPacket { + public enum class ChatFilterType(public val id: Int) { + PASS_THROUGH(0), + FULLY_FILTERED(1), + PARTIALLY_FILTERED(2); + + public companion object { + public fun fromId(id: Int): ChatFilterType = + entries.firstOrNull { it.id == id } ?: PASS_THROUGH + } + } + + public data class PreviousMessageEntry( + val messageId: Int, + val signature: ByteArray?, + ) { + public companion object Codec : PacketCodec { + override fun encode(buffer: BytesBuffer, value: PreviousMessageEntry) { + buffer.writeVarInt(value.messageId) + if (value.messageId == 0) { + val sig = requireNotNull(value.signature) { "signature must be present when messageId is 0" } + require(sig.size == 256) + buffer.writeBytes(sig) + } + } + + override fun decode(buffer: BytesBuffer): PreviousMessageEntry = throw UnsupportedOperationException() // TODO + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as PreviousMessageEntry + + if (messageId != other.messageId) return false + if (!signature.contentEquals(other.signature)) return false + + return true + } + + override fun hashCode(): Int { + var result = messageId + result = 31 * result + (signature?.contentHashCode() ?: 0) + return result + } + } + public companion object Codec : PacketCodec { override fun encode(buffer: BytesBuffer, value: ClientboundPlayerChatMessagePacket) { buffer.writeVarInt(value.globalIndex) diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GameProtocols.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GameProtocols.kt index 30c2514..8886973 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GameProtocols.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GameProtocols.kt @@ -10,6 +10,7 @@ import cn.rtast.libmc.protocol.packet.configuration.* import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket import cn.rtast.libmc.protocol.packet.login.ClientboundDisconnectLoginPacket import cn.rtast.libmc.protocol.packet.login.ClientboundLoginSuccessPacket +import cn.rtast.libmc.protocol.packet.login.ClientboundSetCompressionPacket import cn.rtast.libmc.protocol.packet.login.ServerboundLoginAcknowledgedPacket import cn.rtast.libmc.protocol.packet.login.ServerboundLoginStartPacket import cn.rtast.libmc.protocol.packet.play.* @@ -30,6 +31,7 @@ internal object GameProtocols { register(ProtocolState.LOGIN) { register(0x00, ClientboundDisconnectLoginPacket) register(0x02, ClientboundLoginSuccessPacket) + register(0x03, ClientboundSetCompressionPacket) } register(ProtocolState.PLAY) { register(0x2C, ClientboundKeepAlivePlayPacket) diff --git a/libmc-protocol/src/commonTest/kotlin/test/TestClient.kt b/libmc-protocol/src/commonTest/kotlin/test/TestClient.kt index 6b0eec9..c0b9d85 100644 --- a/libmc-protocol/src/commonTest/kotlin/test/TestClient.kt +++ b/libmc-protocol/src/commonTest/kotlin/test/TestClient.kt @@ -7,7 +7,8 @@ package test -import cn.rtast.libmc.protocol.client.MinecraftClient +import cn.rtast.libmc.protocol.client.createMinecraftClient +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import kotlin.test.Test @@ -15,7 +16,10 @@ class TestClient { @Test fun `test client`() = runTest { - val cli = MinecraftClient("127.0.0.1", 25565, "123") - cli.connect() + val cli = createMinecraftClient("127.0.0.1", 25565, "123") + cli.launch { cli.connect() } + + while (true) { + } } } \ No newline at end of file