From 15aefbb869bdfb66234d61d14f5c088d42b0ab50 Mon Sep 17 00:00:00 2001 From: RTAkland Date: Sat, 5 Sep 2026 00:17:22 +0800 Subject: [PATCH] Login into server complete --- gradle/libs.versions.toml | 6 + libmc-chat/build.gradle.kts | 36 +++++ .../kotlin/cn/rtast/libmc/chat/chat.kt | 146 ++++++++++++++++++ .../rtast/libmc/chat/chat/ChatFilterType.kt | 19 +++ .../libmc/chat/chat/PreviousMessageEntry.kt | 48 ++++++ .../libmc/chat/packet/PacketDirection.kt | 14 ++ .../AckFinishConfigurationPacket.kt | 23 +++ ...lientboundDisconnectConfigurationPacket.kt | 28 ++++ .../configuration/ClientboundPingPacket.kt | 25 +++ .../ClientboundSelectKnownPacksPacket.kt | 30 ++++ .../FinishConfigurationPacket.kt | 23 +++ .../packet/configuration/KeepAlivePacket.kt | 28 ++++ .../chat/packet/configuration/KnownPacks.kt | 37 +++++ .../configuration/ServerboundPongPacket.kt | 25 +++ .../ServerboundSelectKnownPacksPacket.kt | 30 ++++ .../chat/packet/handshake/HandshakePacket.kt | 36 +++++ .../login/ClientboundDisconnectLoginPacket.kt | 28 ++++ .../packet/login/LoginAcknowledgedPacket.kt | 23 +++ .../chat/packet/login/LoginStartPacket.kt | 32 ++++ .../chat/packet/login/LoginSuccessPacket.kt | 32 ++++ .../play/ClientboundDisconnectPlayPacket.kt | 28 ++++ .../play/ClientboundKeepAlivePlayPacket.kt | 26 ++++ .../packet/play/ClientboundLoginPlayPacket.kt | 82 ++++++++++ .../ClientboundPlayerChatMessagePacket.kt | 117 ++++++++++++++ .../play/ServerboundChatMessagePacket.kt | 33 ++++ .../play/ServerboundKeepAlivePlayPacket.kt | 26 ++++ .../rtast/libmc/chat/profile/GameProfile.kt | 60 +++++++ .../cn/rtast/libmc/chat/protocol/BlockPos.kt | 43 ++++++ .../cn/rtast/libmc/chat/protocol/GameMode.kt | 26 ++++ .../libmc/chat/protocol/HandshakeIntent.kt | 14 ++ .../rtast/libmc/chat/protocol/Identifier.kt | 38 +++++ .../libmc/chat/protocol/ProtocolState.kt | 15 ++ .../kotlin/cn/rtast/libmc/chat/util/nbt.kt | 43 ++++++ .../kotlin/cn/rtast/libmc/chat/util/uuid.kt | 14 ++ .../commonTest/kotlin/test/TestChatClient.kt | 22 +++ libmc-common/build.gradle.kts | 1 + .../kotlin/cn/rtast/libmc/common/buffer.kt | 32 +++- .../kotlin/cn/rtast/libmc/common/channels.kt | 14 ++ .../cn/rtast/libmc/common}/mc_primitives.kt | 35 ++--- .../kotlin/cn/rtast/libmc/common/packet.kt | 12 ++ .../cn/rtast/libmc/common}/packet_writer.kt | 13 +- .../cn/rtast/libmc/common/buffer.jvm.kt | 4 + .../cn/rtast/libmc/common/buffer.native.kt | 4 + libmc-mcping/build.gradle.kts | 3 +- .../cn/rtast/libmc/mcping/java/packet.kt | 17 +- .../cn/rtast/libmc/mcping/java/ping_java.kt | 23 +-- libmc-nbt/build.gradle.kts | 29 ++++ .../kotlin/cn/rtast/libmc/nbt/nbt_reader.kt | 95 ++++++++++++ .../kotlin/cn/rtast/libmc/nbt/nbt_tags.kt | 66 ++++++++ .../kotlin/cn/rtast/libmc/nbt/nbt_type.kt | 26 ++++ .../kotlin/cn/rtast/libmc/nbt/nbt_variant.kt | 19 +++ .../kotlin/cn/rtast/libmc/nbt/varint.kt | 11 ++ .../src/commonTest/resources/bedrock.nbt | Bin 0 -> 2761 bytes libmc-nbt/src/commonTest/resources/level.dat | Bin 0 -> 11353 bytes .../src/jvmTest/kotlin/test/TestNBTReader.kt | 24 +++ libmc-rconlib/build.gradle.kts | 1 + settings.gradle.kts | 2 + 57 files changed, 1622 insertions(+), 65 deletions(-) create mode 100644 libmc-chat/build.gradle.kts create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/ChatFilterType.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/PreviousMessageEntry.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/PacketDirection.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/AckFinishConfigurationPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundDisconnectConfigurationPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundPingPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundSelectKnownPacksPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/FinishConfigurationPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KeepAlivePacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KnownPacks.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundPongPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundSelectKnownPacksPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/handshake/HandshakePacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/ClientboundDisconnectLoginPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginAcknowledgedPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginStartPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginSuccessPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundDisconnectPlayPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundKeepAlivePlayPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundLoginPlayPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundPlayerChatMessagePacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundChatMessagePacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundKeepAlivePlayPacket.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/profile/GameProfile.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/BlockPos.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/GameMode.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/HandshakeIntent.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/Identifier.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/ProtocolState.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/nbt.kt create mode 100644 libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/uuid.kt create mode 100644 libmc-chat/src/commonTest/kotlin/test/TestChatClient.kt rename {libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java => libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common}/mc_primitives.kt (52%) create mode 100644 libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet.kt rename {libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java => libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common}/packet_writer.kt (53%) create mode 100644 libmc-nbt/build.gradle.kts create mode 100644 libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_reader.kt create mode 100644 libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_tags.kt create mode 100644 libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_type.kt create mode 100644 libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_variant.kt create mode 100644 libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/varint.kt create mode 100644 libmc-nbt/src/commonTest/resources/bedrock.nbt create mode 100644 libmc-nbt/src/commonTest/resources/level.dat create mode 100644 libmc-nbt/src/jvmTest/kotlin/test/TestNBTReader.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f6c84cf..5bac6b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,12 +3,18 @@ kotlin = "2.4.10" kotlinx-io = "0.9.1" ktor-network = "3.5.2" coroutines-test = "1.11.0" +kotlinx-serialization = "1.11.0" +kotlinx-coroutines = "1.11.0" [libraries] kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" } ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor-network" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines-test" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } maven-publish = { id = "maven-publish" } \ No newline at end of file diff --git a/libmc-chat/build.gradle.kts b/libmc-chat/build.gradle.kts new file mode 100644 index 0000000..3c2f3f6 --- /dev/null +++ b/libmc-chat/build.gradle.kts @@ -0,0 +1,36 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinx.serialization) +} + +kotlin { + explicitApi() + withSourcesJar() + + linuxX64() + linuxArm64() + macosArm64() + mingwX64() + iosArm64() + iosSimulatorArm64() + jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } + + sourceSets { + commonMain.dependencies { + implementation(project(":common")) + api(libs.kotlinx.serialization.core) + api(libs.kotlinx.serialization.json) + api(libs.kotlinx.coroutines) + } + + jvmMain.dependencies { + + } + + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat.kt new file mode 100644 index 0000000..a5b5266 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat.kt @@ -0,0 +1,146 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat + +import cn.rtast.libmc.chat.packet.configuration.AckFinishConfigurationPacket +import cn.rtast.libmc.chat.packet.configuration.ServerboundPongPacket +import cn.rtast.libmc.chat.packet.configuration.ServerboundSelectKnownPacksPacket +import cn.rtast.libmc.chat.packet.handshake.HandshakePacket +import cn.rtast.libmc.chat.packet.login.LoginAcknowledgedPacket +import cn.rtast.libmc.chat.packet.login.LoginStartPacket +import cn.rtast.libmc.chat.packet.play.ServerboundKeepAlivePlayPacket +import cn.rtast.libmc.chat.protocol.HandshakeIntent +import cn.rtast.libmc.chat.protocol.ProtocolState +import cn.rtast.libmc.chat.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 = HandshakePacket(776, host, port.toUShort(), HandshakeIntent.LOGIN) + output.sendPacket(handshakePacket, HandshakePacket) + state = ProtocolState.LOGIN + + val loginStartPacket = LoginStartPacket(username, uuid) + output.sendPacket(loginStartPacket, LoginStartPacket) + } + + 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 = _Buffer(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(LoginAcknowledgedPacket(), LoginAcknowledgedPacket) + 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: _Buffer, output: _WriteChannel) { + when (packetId) { + 0x0E -> { + println("received ClientboundSelectKnownPacksPacket") + } + + 0x03 -> { + output.sendPacket(AckFinishConfigurationPacket, AckFinishConfigurationPacket) + state = ProtocolState.PLAY + } + + 0x05 -> { + output.sendPacket(ServerboundPongPacket(0), ServerboundPongPacket) + } + + 0x01 -> println("configuration state disconnected") + } + } + + private fun handlePlayPackets(packetId: Int, packetBuffer: _Buffer, 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-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/ChatFilterType.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/ChatFilterType.kt new file mode 100644 index 0000000..f2c8e44 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/ChatFilterType.kt @@ -0,0 +1,19 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.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-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/PreviousMessageEntry.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/PreviousMessageEntry.kt new file mode 100644 index 0000000..7314492 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/PreviousMessageEntry.kt @@ -0,0 +1,48 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.chat + +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer +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: _Buffer, 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: _Buffer): 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-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/PacketDirection.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/PacketDirection.kt new file mode 100644 index 0000000..9542be3 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/PacketDirection.kt @@ -0,0 +1,14 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet + +internal sealed interface PacketDirection { + interface ServerboundPacket : PacketDirection + interface ClientboundPacket : PacketDirection + interface AcrossPacket : PacketDirection +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/AckFinishConfigurationPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/AckFinishConfigurationPacket.kt new file mode 100644 index 0000000..568095b --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/AckFinishConfigurationPacket.kt @@ -0,0 +1,23 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.configuration + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer + +internal data object AckFinishConfigurationPacket : MinecraftPacket, + PacketCodec, + PacketDirection.ServerboundPacket { + override val packetId: Int = 0x03 + + override fun encode(buffer: _Buffer, value: AckFinishConfigurationPacket) {} + + override fun decode(buffer: _Buffer): AckFinishConfigurationPacket = AckFinishConfigurationPacket +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundDisconnectConfigurationPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundDisconnectConfigurationPacket.kt new file mode 100644 index 0000000..001f781 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundDisconnectConfigurationPacket.kt @@ -0,0 +1,28 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.configuration + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.chat.util.readMinimalTextNbt +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer + +internal data class ClientboundDisconnectConfigurationPacket( + val reason: String, +) : MinecraftPacket, PacketDirection.ClientboundPacket { + override val packetId: Int = 0x02 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: ClientboundDisconnectConfigurationPacket) {} + override fun decode(buffer: _Buffer): ClientboundDisconnectConfigurationPacket { + val reasonText = buffer.readMinimalTextNbt() + return ClientboundDisconnectConfigurationPacket(reason = reasonText) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundPingPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundPingPacket.kt new file mode 100644 index 0000000..08d5390 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundPingPacket.kt @@ -0,0 +1,25 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.configuration + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer + +internal data class ClientboundPingPacket(val id: Int) : MinecraftPacket, PacketDirection.ClientboundPacket { + override val packetId: Int = 0x51 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: ClientboundPingPacket) { + buffer.writeInt(value.id) + } + + override fun decode(buffer: _Buffer): ClientboundPingPacket = ClientboundPingPacket(buffer.readInt()) + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundSelectKnownPacksPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundSelectKnownPacksPacket.kt new file mode 100644 index 0000000..5bb7a3a --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundSelectKnownPacksPacket.kt @@ -0,0 +1,30 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.configuration + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.* + +internal data class ClientboundSelectKnownPacksPacket( + val knownPacks: List, +) : MinecraftPacket, PacketDirection.AcrossPacket { + override val packetId: Int = 0x0e + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: ClientboundSelectKnownPacksPacket) { + buffer.writeVarInt(value.knownPacks.size) + value.knownPacks.forEach { KnownPacks.encode(buffer, it) } + } + + override fun decode(buffer: _Buffer): ClientboundSelectKnownPacksPacket { + val packsCount = buffer.readVarInt() + val packs = List(packsCount) { KnownPacks.decode(buffer) } + return ClientboundSelectKnownPacksPacket(packs) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/FinishConfigurationPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/FinishConfigurationPacket.kt new file mode 100644 index 0000000..d32dff9 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/FinishConfigurationPacket.kt @@ -0,0 +1,23 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.configuration + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer + +internal data object FinishConfigurationPacket : MinecraftPacket, + PacketCodec, + PacketDirection.ClientboundPacket { + override val packetId: Int = 0x03 + + override fun encode(buffer: _Buffer, value: FinishConfigurationPacket) {} + + override fun decode(buffer: _Buffer): FinishConfigurationPacket = FinishConfigurationPacket +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KeepAlivePacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KeepAlivePacket.kt new file mode 100644 index 0000000..f518755 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KeepAlivePacket.kt @@ -0,0 +1,28 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.configuration + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer + +internal data class KeepAlivePacket(val id: Long) : MinecraftPacket, PacketDirection.AcrossPacket { + override val packetId: Int = 0x04 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: KeepAlivePacket) { + buffer.writeLong(value.id) + } + + override fun decode(buffer: _Buffer): KeepAlivePacket { + val id = buffer.readLong() + return KeepAlivePacket(id) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KnownPacks.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KnownPacks.kt new file mode 100644 index 0000000..17f891b --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KnownPacks.kt @@ -0,0 +1,37 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.configuration + +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer +import cn.rtast.libmc.common.readMcString +import cn.rtast.libmc.common.writeMcString +import kotlinx.serialization.Serializable + +@Serializable +public data class KnownPacks( + val namespace: String, + val id: String, + val version: String, +) { + public companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: KnownPacks) { + buffer.writeMcString(value.namespace) + buffer.writeMcString(value.id) + buffer.writeMcString(value.version) + } + + override fun decode(buffer: _Buffer): KnownPacks { + val namespace = buffer.readMcString() + val id = buffer.readMcString() + val version = buffer.readMcString() + return KnownPacks(namespace, id, version) + } + } +} + diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundPongPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundPongPacket.kt new file mode 100644 index 0000000..5085281 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundPongPacket.kt @@ -0,0 +1,25 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.configuration + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer + +internal data class ServerboundPongPacket(val id: Int) : MinecraftPacket, PacketDirection.ServerboundPacket { + override val packetId: Int = 0x2D + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: ServerboundPongPacket) { + buffer.writeInt(value.id) + } + + override fun decode(buffer: _Buffer): ServerboundPongPacket = ServerboundPongPacket(buffer.readInt()) + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundSelectKnownPacksPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundSelectKnownPacksPacket.kt new file mode 100644 index 0000000..73b6f7d --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundSelectKnownPacksPacket.kt @@ -0,0 +1,30 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.configuration + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.* + +internal data class ServerboundSelectKnownPacksPacket( + val knownPacks: List, +) : MinecraftPacket, PacketDirection.AcrossPacket { + override val packetId: Int = 0x07 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: ServerboundSelectKnownPacksPacket) { + buffer.writeVarInt(value.knownPacks.size) + value.knownPacks.forEach { KnownPacks.encode(buffer, it) } + } + + override fun decode(buffer: _Buffer): ServerboundSelectKnownPacksPacket { + val packsCount = buffer.readVarInt() + val packs = List(packsCount) { KnownPacks.decode(buffer) } + return ServerboundSelectKnownPacksPacket(packs) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/handshake/HandshakePacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/handshake/HandshakePacket.kt new file mode 100644 index 0000000..84e3339 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/handshake/HandshakePacket.kt @@ -0,0 +1,36 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.handshake + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer +import cn.rtast.libmc.common.writeMcString +import cn.rtast.libmc.common.writeVarInt + +internal data class HandshakePacket( + val protocolVersion: Int, + val serverAddress: String, + val serverPort: UShort, + // 1 for Status, 2 for Login, 3 for Transfer + val intent: Int +) : MinecraftPacket, PacketDirection.ServerboundPacket { + override val packetId: Int = 0x00 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: HandshakePacket) { + buffer.writeVarInt(value.protocolVersion) + buffer.writeMcString(value.serverAddress) + buffer.writeShort(value.serverPort.toShort()) + buffer.writeVarInt(value.intent) + } + + override fun decode(buffer: _Buffer): HandshakePacket = throw UnsupportedOperationException() + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/ClientboundDisconnectLoginPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/ClientboundDisconnectLoginPacket.kt new file mode 100644 index 0000000..60b714b --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/ClientboundDisconnectLoginPacket.kt @@ -0,0 +1,28 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.login + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer +import cn.rtast.libmc.common.readMcString + +internal data class ClientboundDisconnectLoginPacket( + val reason: String, +) : MinecraftPacket, PacketDirection.ClientboundPacket { + override val packetId: Int = 0x00 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: ClientboundDisconnectLoginPacket) {} + override fun decode(buffer: _Buffer): ClientboundDisconnectLoginPacket { + val reasonJson = buffer.readMcString() + return ClientboundDisconnectLoginPacket(reason = reasonJson) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginAcknowledgedPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginAcknowledgedPacket.kt new file mode 100644 index 0000000..24f9402 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginAcknowledgedPacket.kt @@ -0,0 +1,23 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.login + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer + +internal data class LoginAcknowledgedPacket( + override val packetId: Int = 0x03, +) : MinecraftPacket, PacketDirection.ServerboundPacket { + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: LoginAcknowledgedPacket) {} + override fun decode(buffer: _Buffer): LoginAcknowledgedPacket = throw UnsupportedOperationException() + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginStartPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginStartPacket.kt new file mode 100644 index 0000000..f7dd3fe --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginStartPacket.kt @@ -0,0 +1,32 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.login + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer +import cn.rtast.libmc.common.writeMcString +import cn.rtast.libmc.common.writeUuid +import kotlin.uuid.Uuid + +internal data class LoginStartPacket( + val username: String, + val playerUuid: Uuid, +) : MinecraftPacket, PacketDirection.ServerboundPacket { + override val packetId: Int = 0x00 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: LoginStartPacket) { + buffer.writeMcString(value.username) + buffer.writeUuid(value.playerUuid) + } + + override fun decode(buffer: _Buffer): LoginStartPacket = throw UnsupportedOperationException() + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginSuccessPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginSuccessPacket.kt new file mode 100644 index 0000000..0ff7ac1 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginSuccessPacket.kt @@ -0,0 +1,32 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.login + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.chat.profile.GameProfile +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer +import cn.rtast.libmc.common.readUuid +import kotlin.uuid.Uuid + +internal data class LoginSuccessPacket( + val gameProfile: GameProfile, + val sessionId: Uuid, +) : MinecraftPacket, PacketDirection.ClientboundPacket { + override val packetId: Int = 0x02 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: LoginSuccessPacket) {} + override fun decode(buffer: _Buffer): LoginSuccessPacket { + val gameProfile = GameProfile.decode(buffer) + val sessionId = buffer.readUuid() + return LoginSuccessPacket(gameProfile, sessionId) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundDisconnectPlayPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundDisconnectPlayPacket.kt new file mode 100644 index 0000000..dea1032 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundDisconnectPlayPacket.kt @@ -0,0 +1,28 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.play + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.chat.util.readMinimalTextNbt +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer + +internal data class ClientboundDisconnectPlayPacket( + val reason: String, +) : MinecraftPacket, PacketDirection.ClientboundPacket { + override val packetId: Int = 0x28 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: ClientboundDisconnectPlayPacket) {} + override fun decode(buffer: _Buffer): ClientboundDisconnectPlayPacket { + val reasonText = buffer.readMinimalTextNbt() + return ClientboundDisconnectPlayPacket(reason = reasonText) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundKeepAlivePlayPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundKeepAlivePlayPacket.kt new file mode 100644 index 0000000..08b4072 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundKeepAlivePlayPacket.kt @@ -0,0 +1,26 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.play + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer + +internal data class ClientboundKeepAlivePlayPacket(val id: Long) : MinecraftPacket, PacketDirection.ClientboundPacket { + override val packetId: Int = 0x33 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: ClientboundKeepAlivePlayPacket) { + buffer.writeLong(value.id) + } + + override fun decode(buffer: _Buffer): ClientboundKeepAlivePlayPacket = + ClientboundKeepAlivePlayPacket(buffer.readLong()) + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundLoginPlayPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundLoginPlayPacket.kt new file mode 100644 index 0000000..caebd1a --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundLoginPlayPacket.kt @@ -0,0 +1,82 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.play + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.chat.protocol.* +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer +import cn.rtast.libmc.common.readVarInt + +internal data class ClientboundLoginPlayPacket( + val entityId: Int, + val isHardcore: Boolean, + val dimensionNames: List, + val maxPlayers: Int, + val viewDistance: Int, + val simulationDistance: Int, + val isReducedDebugInfo: Boolean, + val enableRespawnScreen: Boolean, + val doLimitedCrafting: Boolean, + val dimensionType: Int, + val dimensionName: Identifier, + val hashedSeed: Long, + val gameMode: GameMode, + val previousGameMode: GameMode, + val isDebug: Boolean, + val isFlat: Boolean, + val hasDeathLocation: Boolean, + val deathDimensionName: Identifier?, + val deathLocation: BlockPos?, + val portalCooldown: Int, + val seaLevel: Int, + val isOnlineMode: Boolean, + val enforceSecureChat: Boolean, +) : MinecraftPacket, PacketDirection.ClientboundPacket { + override val packetId: Int = 0x31 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: ClientboundLoginPlayPacket) {} + override fun decode(buffer: _Buffer): ClientboundLoginPlayPacket { + val entityId = buffer.readInt() + val isHardcore = buffer.readBoolean() + val dimensionNamesCount = buffer.readVarInt() + val dimensionNames = List(dimensionNamesCount) { buffer.readIdentifier() } + val maxPlayers = buffer.readVarInt() + val viewDistance = buffer.readVarInt() + val simulationDistance = buffer.readVarInt() + val isReducedDebugInfo = buffer.readBoolean() + val enableRespawnScreen = buffer.readBoolean() + val doLimitedCrafting = buffer.readBoolean() + val dimensionType = buffer.readVarInt() + val dimensionName = buffer.readIdentifier() + val hashedSeed = buffer.readLong() + val gameMode = GameMode.fromID(buffer.readByte().toUByte()) + val previousGameMode = GameMode.fromID(buffer.readByte()) + val isDebug = buffer.readBoolean() + val isFlat = buffer.readBoolean() + val hasDeathLocation = buffer.readBoolean() + val deathDimensionName = if (hasDeathLocation) buffer.readIdentifier() else null + val deathLocation = if (hasDeathLocation) buffer.readBlockPos() else null + val portalCooldown = buffer.readVarInt() + val seaLevel = buffer.readVarInt() + val isOnlineMode = buffer.readBoolean() + val isEnforcesSecureChat = buffer.readBoolean() + return ClientboundLoginPlayPacket( + entityId, isHardcore, dimensionNames, maxPlayers, + viewDistance, simulationDistance, isReducedDebugInfo, + enableRespawnScreen, doLimitedCrafting, dimensionType, + dimensionName, hashedSeed, gameMode, previousGameMode, + isDebug, isFlat, hasDeathLocation, deathDimensionName, + deathLocation, portalCooldown, seaLevel, isOnlineMode, + isEnforcesSecureChat + ) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundPlayerChatMessagePacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundPlayerChatMessagePacket.kt new file mode 100644 index 0000000..f6016cd --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundPlayerChatMessagePacket.kt @@ -0,0 +1,117 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.play + +import cn.rtast.libmc.chat.chat.ChatFilterType +import cn.rtast.libmc.chat.chat.PreviousMessageEntry +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.chat.util.writeMinimalTextNbt +import cn.rtast.libmc.common.* +import kotlin.uuid.Uuid + +internal data class ClientboundPlayerChatMessagePacket( + val globalIndex: Int, + val sender: Uuid, + val index: Int, + val messageSignature: ByteArray?, + val message: String, + val timestamp: Long, + val salt: Long, + val previousMessages: List, + val unsignedContent: String?, + val filterType: ChatFilterType, + val filterMaskBits: LongArray?, + val chatType: Int, + val senderName: String, + val targetName: String?, +) : MinecraftPacket, PacketDirection.ClientboundPacket { + override val packetId: Int = 0x41 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: ClientboundPlayerChatMessagePacket) { + buffer.writeVarInt(value.globalIndex) + buffer.writeUuid(value.sender) + buffer.writeVarInt(value.index) + val hasSignature = value.messageSignature != null + buffer.writeBoolean(hasSignature) + if (hasSignature) buffer.writeBytes(requireNotNull(value.messageSignature)) + + buffer.writeMcString(value.message) + buffer.writeLong(value.timestamp) + buffer.writeLong(value.salt) + + require(value.previousMessages.size == 20) + buffer.writeVarInt(value.previousMessages.size) + value.previousMessages.forEach { entry -> PreviousMessageEntry.encode(buffer, entry) } + + val hasUnsignedContent = value.unsignedContent != null + buffer.writeBoolean(hasUnsignedContent) + value.unsignedContent?.let { buffer.writeMinimalTextNbt(it) } + + buffer.writeVarInt(value.filterType.id) + if (value.filterType == ChatFilterType.PARTIALLY_FILTERED) { + val mask = requireNotNull(value.filterMaskBits) + buffer.writeVarInt(mask.size) + mask.forEach { buffer.writeLong(it) } + } + + buffer.writeVarInt(value.chatType) + buffer.writeMinimalTextNbt(value.senderName) + + val hasTargetName = value.targetName != null + buffer.writeBoolean(hasTargetName) + value.targetName?.let { buffer.writeMinimalTextNbt(it) } + } + + override fun decode(buffer: _Buffer): ClientboundPlayerChatMessagePacket = 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 ClientboundPlayerChatMessagePacket + + if (globalIndex != other.globalIndex) return false + if (index != other.index) return false + if (timestamp != other.timestamp) return false + if (salt != other.salt) return false + if (chatType != other.chatType) return false + if (packetId != other.packetId) return false + if (sender != other.sender) return false + if (!messageSignature.contentEquals(other.messageSignature)) return false + if (message != other.message) return false + if (previousMessages != other.previousMessages) return false + if (unsignedContent != other.unsignedContent) return false + if (filterType != other.filterType) return false + if (!filterMaskBits.contentEquals(other.filterMaskBits)) return false + if (senderName != other.senderName) return false + if (targetName != other.targetName) return false + + return true + } + + override fun hashCode(): Int { + var result = globalIndex + result = 31 * result + index + result = 31 * result + timestamp.hashCode() + result = 31 * result + salt.hashCode() + result = 31 * result + chatType + result = 31 * result + packetId + result = 31 * result + sender.hashCode() + result = 31 * result + (messageSignature?.contentHashCode() ?: 0) + result = 31 * result + message.hashCode() + result = 31 * result + previousMessages.hashCode() + result = 31 * result + unsignedContent.hashCode() + result = 31 * result + filterType.hashCode() + result = 31 * result + (filterMaskBits?.contentHashCode() ?: 0) + result = 31 * result + senderName.hashCode() + result = 31 * result + targetName.hashCode() + return result + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundChatMessagePacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundChatMessagePacket.kt new file mode 100644 index 0000000..195c886 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundChatMessagePacket.kt @@ -0,0 +1,33 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.play + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.* +import kotlin.time.Clock + +internal data class ServerboundChatMessagePacket( + val message: String, + val timestamp: Long = Clock.System.now().toEpochMilliseconds(), + val salt: Long = 0L, +) : MinecraftPacket, PacketDirection.ServerboundPacket { + override val packetId: Int = 0x09 + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: ServerboundChatMessagePacket) { + buffer.writeMcString(value.message) + buffer.writeLong(value.timestamp) + buffer.writeLong(value.salt) + buffer.writeBoolean(false) // has signature + buffer.writeVarInt(0) // message count + buffer.writeBytes(byteArrayOf(0, 0, 0)) + } + + override fun decode(buffer: _Buffer): ServerboundChatMessagePacket = throw UnsupportedOperationException() + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundKeepAlivePlayPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundKeepAlivePlayPacket.kt new file mode 100644 index 0000000..942b63d --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundKeepAlivePlayPacket.kt @@ -0,0 +1,26 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.packet.play + +import cn.rtast.libmc.chat.packet.PacketDirection +import cn.rtast.libmc.common.MinecraftPacket +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer + +internal data class ServerboundKeepAlivePlayPacket(val id: Long) : MinecraftPacket, PacketDirection.ServerboundPacket { + override val packetId: Int = 0x1C + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: ServerboundKeepAlivePlayPacket) { + buffer.writeLong(value.id) + } + + override fun decode(buffer: _Buffer): ServerboundKeepAlivePlayPacket = + ServerboundKeepAlivePlayPacket(buffer.readLong()) + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/profile/GameProfile.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/profile/GameProfile.kt new file mode 100644 index 0000000..3d9d6c5 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/profile/GameProfile.kt @@ -0,0 +1,60 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.profile + +import cn.rtast.libmc.common.* +import kotlinx.serialization.Serializable +import kotlin.uuid.Uuid + +@Serializable +public data class GameProfile( + val uuid: Uuid, + val username: String, + val properties: List, +) { + @Serializable + public data class Property( + val name: String, + val value: String, + val signature: String?, + ) { + public companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: Property) { + buffer.writeMcString(value.name) + buffer.writeMcString(value.value) + buffer.writeBoolean(value.signature != null) + value.signature?.let { buffer.writeMcString(it) } + } + + override fun decode(buffer: _Buffer): Property { + val name = buffer.readMcString() + val value = buffer.readMcString() + val hasSignature = buffer.readBoolean() + val signature = if (hasSignature) buffer.readMcString() else null + return Property(name, value, signature) + } + } + } + + public companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: GameProfile) { + buffer.writeUuid(value.uuid) + buffer.writeMcString(value.username) + buffer.writeVarInt(value.properties.size) // prefixed array + value.properties.forEach { prop -> Property.encode(buffer, prop) } + } + + override fun decode(buffer: _Buffer): GameProfile { + val uuid = buffer.readUuid() + val username = buffer.readMcString() + val propertyCount = buffer.readVarInt() + val properties = List(propertyCount) { Property.decode(buffer) } + return GameProfile(uuid, username, properties) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/BlockPos.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/BlockPos.kt new file mode 100644 index 0000000..3d73e0b --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/BlockPos.kt @@ -0,0 +1,43 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.protocol + +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer +import kotlinx.serialization.Serializable + +/** + * An integer/block position: x (-33 554 432 to 33 554 431), z (-33 554 432 to 33 554 431), y (-2048 to 2047) + * ref: https://minecraft.wiki/w/Java_Edition_protocol/Packets#Type:Position + */ +@Serializable +public data class BlockPos(val x: Int, val y: Int, val z: Int) { + public companion object : PacketCodec { + private const val PACKED_X_MASK = 0x3FFFFFFL // 26 bits + private const val PACKED_Y_MASK = 0xFFFL // 12 bits + private const val PACKED_Z_MASK = 0x3FFFFFFL // 26 bits + + override fun decode(buffer: _Buffer): BlockPos { + val packed = buffer.readLong() + val x = (packed shr 38).toInt() + val y = (packed shl 52 shr 52).toInt() + val z = (packed shl 26 shr 38).toInt() + return BlockPos(x, y, z) + } + + override fun encode(buffer: _Buffer, value: BlockPos) { + val xLong = (value.x.toLong() and PACKED_X_MASK) + val yLong = (value.y.toLong() and PACKED_Y_MASK) + val zLong = (value.z.toLong() and PACKED_Z_MASK) + buffer.writeLong(xLong shl 38 or (zLong shl 12) or yLong) + } + } +} + +internal fun _Buffer.readBlockPos(): BlockPos = BlockPos.decode(this) +internal fun _Buffer.writeBlockPos(pos: BlockPos) = BlockPos.encode(this, pos) \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/GameMode.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/GameMode.kt new file mode 100644 index 0000000..4f0d29e --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/GameMode.kt @@ -0,0 +1,26 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.protocol + +public enum class GameMode(public val id: Byte) { + Survival(0), + Creative(1), + Adventure(2), + Spectator(3), + Undefined(-1), + + /** + * reserved + */ + Unknown(-99); + + public companion object { + public fun fromID(id: Byte): GameMode = entries.firstOrNull { it.id == id } ?: Unknown + public fun fromID(id: UByte): GameMode = fromID(id.toByte()) + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/HandshakeIntent.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/HandshakeIntent.kt new file mode 100644 index 0000000..4515b75 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/HandshakeIntent.kt @@ -0,0 +1,14 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.protocol + +internal object HandshakeIntent { + const val STATUS = 1 + const val LOGIN = 2 + const val TRANSFER = 3 +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/Identifier.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/Identifier.kt new file mode 100644 index 0000000..be74559 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/Identifier.kt @@ -0,0 +1,38 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.protocol + +import cn.rtast.libmc.common.PacketCodec +import cn.rtast.libmc.common._Buffer +import cn.rtast.libmc.common.readMcString +import cn.rtast.libmc.common.writeMcString +import kotlin.jvm.JvmInline + +/** + * ref: https://minecraft.wiki/w/Java_Edition_protocol/Packets#Identifier + */ +@JvmInline +public value class Identifier(public val full: String) { + public val namespace: String get() = if (full.contains(':')) full.substringBefore(':') else "minecraft" + public val path: String get() = if (full.contains(':')) full.substringAfter(':') else full + + override fun toString(): String = "$namespace:$path" + + public companion object Codec : PacketCodec { + public fun of(namespace: String, path: String): Identifier = Identifier("$namespace:$path") + + override fun encode(buffer: _Buffer, value: Identifier) { + buffer.writeMcString(value.toString()) + } + + override fun decode(buffer: _Buffer): Identifier = Identifier(buffer.readMcString()) + } +} + +internal fun _Buffer.readIdentifier(): Identifier = Identifier.decode(this) +internal fun _Buffer.writeIdentifier(identifier: Identifier) = Identifier.encode(this, identifier) \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/ProtocolState.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/ProtocolState.kt new file mode 100644 index 0000000..e1edcc0 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/ProtocolState.kt @@ -0,0 +1,15 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.protocol + +internal enum class ProtocolState { + HANDSHAKE, + LOGIN, + CONFIGURATION, + PLAY +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/nbt.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/nbt.kt new file mode 100644 index 0000000..1c0c8a2 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/nbt.kt @@ -0,0 +1,43 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.util + +import cn.rtast.libmc.common._Buffer + +/** + * tmp + */ +internal fun _Buffer.writeMinimalTextNbt(text: String) { + writeByte(0x0A) + writeByte(0x08) + val keyBytes = "text".encodeToByteArray() + writeShort(keyBytes.size.toShort()) + writeBytes(keyBytes) + val valBytes = text.encodeToByteArray() + require(valBytes.size <= 32767) + writeShort(valBytes.size.toShort()) + writeBytes(valBytes) + writeByte(0x00) +} + +internal fun _Buffer.readMinimalTextNbt(): String { + val rootTagType = readByte().toInt() + if (rootTagType != 0x0A) return "" + var resultText = "" + while (true) { + val tagType = readByte().toInt() + if (tagType == 0x00) break + val keyLength = readShort().toInt() + val key = readBytes(keyLength).decodeToString() + if (tagType == 0x08 && key == "text") { + val valLength = readShort().toInt() + resultText = readBytes(valLength).decodeToString() + } else break + } + return resultText +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/uuid.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/uuid.kt new file mode 100644 index 0000000..2d0a7f7 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/uuid.kt @@ -0,0 +1,14 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.chat.util + +import kotlin.uuid.Uuid + + +public fun generateOfflineUuid(username: String): Uuid = + Uuid.fromByteArray("OfflinePlayer:$username".encodeToByteArray()) \ No newline at end of file diff --git a/libmc-chat/src/commonTest/kotlin/test/TestChatClient.kt b/libmc-chat/src/commonTest/kotlin/test/TestChatClient.kt new file mode 100644 index 0000000..8a4d3f9 --- /dev/null +++ b/libmc-chat/src/commonTest/kotlin/test/TestChatClient.kt @@ -0,0 +1,22 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package test + +import cn.rtast.libmc.chat.MinecraftChatClient +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.uuid.Uuid + +class TestChatClient { + + @Test + fun `test chat client`() = runTest { + val cli = MinecraftChatClient("127.0.0.1", 25565, "RTAkland", Uuid.parse("0dc6a9e9-a6df-3f3e-ae07-e6dbdf74b294")) + cli.start() + } +} \ No newline at end of file diff --git a/libmc-common/build.gradle.kts b/libmc-common/build.gradle.kts index ecbc18f..1ee18cc 100644 --- a/libmc-common/build.gradle.kts +++ b/libmc-common/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget kotlin { explicitApi() + withSourcesJar() linuxX64() linuxArm64() diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/buffer.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/buffer.kt index 3a1d9e1..3aad6ac 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/buffer.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/buffer.kt @@ -7,6 +7,8 @@ package cn.rtast.libmc.common +import kotlin.uuid.Uuid + @Suppress("CLASSNAME") public expect class _Buffer { public constructor() @@ -17,12 +19,15 @@ public expect class _Buffer { public fun writeInt(value: Int, endian: ByteOrder = ByteOrder.BIG_ENDIAN) public fun writeLong(value: Long, endian: ByteOrder = ByteOrder.BIG_ENDIAN) public fun writeBytes(bytes: ByteArray) + public fun writeBoolean(value: Boolean) public fun readByte(): Byte public fun readShort(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Short public fun readInt(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Int public fun readLong(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Long public fun readBytes(length: Int): ByteArray + public fun readBoolean(): Boolean + public fun toByteArray(): ByteArray public fun hasRemaining(): Boolean public fun close() @@ -30,4 +35,29 @@ public expect class _Buffer { public val remaining: Long } -public fun ByteArray.wrap(): _Buffer = _Buffer(this) \ No newline at end of file +public fun ByteArray.wrap(): _Buffer = _Buffer(this) + +public fun _Buffer.writeUuid(uuid: Uuid): Unit = uuid.toLongs { mostSignificantBits, leastSignificantBits -> + this.writeLong(mostSignificantBits) + this.writeLong(leastSignificantBits) +} + +public fun _Buffer.readUuid(): Uuid { + val most = this.readLong() + val least = this.readLong() + return Uuid.fromLongs(most, least) +} + +public fun _Buffer.writeVarInt(value: Int): Unit = VarIntCodec.encode(this, value) +public fun _Buffer.readVarInt(): Int = VarIntCodec.decode(this) + +public fun _Buffer.writeMcString(value: String): Unit = McStringCodec.encode(this, value) +public fun _Buffer.readMcString(): String = McStringCodec.decode(this) + +public fun _ReadChannel.readPacketFrame(): _Buffer { + val length = this.readVarInt() + val frameBytes = this.readBytes(length) + return _Buffer().apply { + writeBytes(frameBytes) + } +} \ No newline at end of file diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/channels.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/channels.kt index 186aed8..ee057d9 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/channels.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/channels.kt @@ -21,4 +21,18 @@ public expect class _ReadChannel { public expect class _WriteChannel { public fun writeFully(value: ByteArray, startIndex: Int = 0, endIndex: Int = value.size) public fun flush() +} + +public fun _ReadChannel.readVarInt(): Int { + var numRead = 0 + var result = 0 + var read: Byte + do { + read = this.readByte() + val value = (read.toInt() and 0x7F) + result = result or (value shl (7 * numRead)) + numRead++ + if (numRead > 5) throw IllegalArgumentException("VarInt is too big") + } while ((read.toInt() and 0x80) != 0) + return result } \ No newline at end of file diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/mc_primitives.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/mc_primitives.kt similarity index 52% rename from libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/mc_primitives.kt rename to libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/mc_primitives.kt index d9e4216..ec3c1ec 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/mc_primitives.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/mc_primitives.kt @@ -4,12 +4,9 @@ * Date: 2026/9/3 */ -package cn.rtast.libmc.mcping.java +package cn.rtast.libmc.common -import cn.rtast.libmc.common.PacketCodec -import cn.rtast.libmc.common._Buffer - -internal object VarIntCodec : PacketCodec { +public object VarIntCodec : PacketCodec { override fun encode(buffer: _Buffer, value: Int) { var v = value while (true) { @@ -23,20 +20,21 @@ internal object VarIntCodec : PacketCodec { } override fun decode(buffer: _Buffer): Int { - var value = 0 - var position = 0 - while (true) { - val currentByte = buffer.readByte().toInt() and 0xFF - value = value or ((currentByte and 0x7F) shl position) - if ((currentByte and 0x80) == 0) break - position += 7 - if (position >= 35) throw IllegalArgumentException("VarInt too long") - } - return value + var numRead = 0 + var result = 0 + var read: Byte + do { + read = buffer.readByte() + val value = (read.toInt() and 0x7F) + result = result or (value shl (7 * numRead)) + numRead++ + if (numRead > 5) throw IllegalArgumentException("VarInt is too big") + } while ((read.toInt() and 0x80) != 0) + return result } } -internal object McStringCodec : PacketCodec { +public object McStringCodec : PacketCodec { override fun encode(buffer: _Buffer, value: String) { val bytes = value.encodeToByteArray() VarIntCodec.encode(buffer, bytes.size) @@ -48,7 +46,4 @@ internal object McStringCodec : PacketCodec { val bytes = buffer.readBytes(length) return bytes.decodeToString() } -} - -internal fun _Buffer.writeVarInt(value: Int) = VarIntCodec.encode(this, value) -internal fun _Buffer.readVarInt(): Int = VarIntCodec.decode(this) \ No newline at end of file +} \ No newline at end of file diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet.kt new file mode 100644 index 0000000..b90d219 --- /dev/null +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet.kt @@ -0,0 +1,12 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.common + +public interface MinecraftPacket { + public val packetId: Int +} \ No newline at end of file diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet_writer.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet_writer.kt similarity index 53% rename from libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet_writer.kt rename to libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet_writer.kt index 7999991..4387fa4 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet_writer.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet_writer.kt @@ -1,20 +1,13 @@ /* * Copyright © 2026 RTAkland * Author: RTAkland - * Date: 2026/9/3 + * Date: 2026/9/4 */ -package cn.rtast.libmc.mcping.java +package cn.rtast.libmc.common -import cn.rtast.libmc.common.PacketCodec -import cn.rtast.libmc.common._Buffer -import cn.rtast.libmc.common._WriteChannel -import cn.rtast.libmc.common.write -import cn.rtast.libmc.common.writeBuffer - - -internal fun _WriteChannel.sendPacket(packet: T, codec: PacketCodec) { +public fun _WriteChannel.sendPacket(packet: T, codec: PacketCodec) { val bodyBuffer = _Buffer() bodyBuffer.write(packet.packetId, VarIntCodec) codec.encode(bodyBuffer, packet) diff --git a/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/buffer.jvm.kt b/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/buffer.jvm.kt index 80529c4..1677ac6 100644 --- a/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/buffer.jvm.kt +++ b/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/buffer.jvm.kt @@ -63,6 +63,8 @@ public actual class _Buffer { outStream.write(bytes) } + public actual fun writeBoolean(value: Boolean): Unit = writeByte(if (value) 0x01 else 0x00) + private fun ensureReadArray(): ByteArray { var buf = readBuffer if (buf == null) { @@ -89,6 +91,8 @@ public actual class _Buffer { return result } + public actual fun readBoolean(): Boolean = this.readByte() != 0x00.toByte() + public actual fun toByteArray(): ByteArray = outStream.toByteArray() public actual fun hasRemaining(): Boolean = readOffset < ensureReadArray().size diff --git a/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/buffer.native.kt b/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/buffer.native.kt index 7fd1346..695723a 100644 --- a/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/buffer.native.kt +++ b/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/buffer.native.kt @@ -40,6 +40,8 @@ public actual class _Buffer { } public actual fun writeBytes(bytes: ByteArray): Unit = _delegateBuf.write(bytes) + public actual fun writeBoolean(value: Boolean): Unit = _delegateBuf.writeByte(if (value) 0x01 else 0x00) + public actual fun readByte(): Byte = _delegateBuf.readByte() public actual fun readShort(endian: ByteOrder): Short { val v = _delegateBuf.readShort() @@ -57,6 +59,8 @@ public actual class _Buffer { } public actual fun readBytes(length: Int): ByteArray = _delegateBuf.readByteArray(length) + public actual fun readBoolean(): Boolean = _delegateBuf.readByte() != 0x00.toByte() + public actual fun toByteArray(): ByteArray { val copy = _delegateBuf.peek() return try { diff --git a/libmc-mcping/build.gradle.kts b/libmc-mcping/build.gradle.kts index 1dabf62..18f154e 100644 --- a/libmc-mcping/build.gradle.kts +++ b/libmc-mcping/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget kotlin { explicitApi() + withSourcesJar() linuxX64() linuxArm64() @@ -13,7 +14,7 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(project(":common")) + api(project(":common")) } commonTest.dependencies { diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet.kt b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet.kt index 25fc2a8..8d8c0c7 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet.kt +++ b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet.kt @@ -7,15 +7,10 @@ package cn.rtast.libmc.mcping.java -import cn.rtast.libmc.common.PacketCodec -import cn.rtast.libmc.common._Buffer - -internal interface MinecraftPacket { - val packetId: Int -} +import cn.rtast.libmc.common.* // ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Handshake -internal data class HandshakePacket( +public data class HandshakePacket( val protocolVersion: Int, val serverAddress: String, val serverPort: UShort, @@ -24,7 +19,7 @@ internal data class HandshakePacket( ) : MinecraftPacket { override val packetId: Int = 0x00 - companion object Codec : PacketCodec { + public companion object Codec : PacketCodec { override fun encode(buffer: _Buffer, value: HandshakePacket) { VarIntCodec.encode(buffer, value.protocolVersion) McStringCodec.encode(buffer, value.serverAddress) @@ -37,17 +32,17 @@ internal data class HandshakePacket( } // ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Status -internal data object StatusRequestPacket : MinecraftPacket, PacketCodec { +public data object StatusRequestPacket : MinecraftPacket, PacketCodec { override val packetId: Int = 0x00 override fun encode(buffer: _Buffer, value: StatusRequestPacket) {} override fun decode(buffer: _Buffer): StatusRequestPacket = throw UnsupportedOperationException() } -internal data class PingPacket(val currentTime: Long) : MinecraftPacket { +public data class PingPacket(val currentTime: Long) : MinecraftPacket { override val packetId: Int = 0x01 - companion object : PacketCodec { + public companion object : PacketCodec { override fun encode(buffer: _Buffer, value: PingPacket) { buffer.writeLong(value.currentTime) } diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/ping_java.kt b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/ping_java.kt index 0b5742b..3e11d1c 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/ping_java.kt +++ b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/ping_java.kt @@ -7,10 +7,7 @@ package cn.rtast.libmc.mcping.java -import cn.rtast.libmc.common.LibMCContext -import cn.rtast.libmc.common._Buffer -import cn.rtast.libmc.common._ReadChannel -import cn.rtast.libmc.common._Socket +import cn.rtast.libmc.common.* import cn.rtast.libmc.mcping.PingResponse import kotlin.time.Clock @@ -50,22 +47,4 @@ internal fun pingJavaServer(host: String, port: Int, context: LibMCContext): Pin } finally { socket.close() } -} - -private fun _ReadChannel.readVarIntWithCodec(): Int { - val tempBuffer = _Buffer() - while (true) { - val byte = this.readByte() - tempBuffer.writeByte(byte) - if ((byte.toInt() and 0x80) == 0) break - } - return VarIntCodec.decode(tempBuffer) -} - -private fun _ReadChannel.readPacketFrame(): _Buffer { - val length = this.readVarIntWithCodec() - val frameBytes = this.readBytes(length) - return _Buffer().apply { - writeBytes(frameBytes) - } } \ No newline at end of file diff --git a/libmc-nbt/build.gradle.kts b/libmc-nbt/build.gradle.kts new file mode 100644 index 0000000..9cb79a5 --- /dev/null +++ b/libmc-nbt/build.gradle.kts @@ -0,0 +1,29 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +kotlin { + explicitApi() + withSourcesJar() + + linuxX64() + linuxArm64() + macosArm64() + mingwX64() + iosArm64() + iosSimulatorArm64() + jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } + + sourceSets { + commonMain.dependencies { + implementation(project(":common")) + } + + jvmMain.dependencies { + + } + + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + } + } +} \ No newline at end of file diff --git a/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_reader.kt b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_reader.kt new file mode 100644 index 0000000..4857e2a --- /dev/null +++ b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_reader.kt @@ -0,0 +1,95 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.nbt + +import cn.rtast.libmc.common._Buffer +import cn.rtast.libmc.common.readVarInt + +public class NbtReader( + private val buffer: _Buffer, + private val variant: NbtVariant = NbtVariant.JAVA, +) { + private fun readShort(): Short = buffer.readShort(variant.order) + private fun readInt(): Int = buffer.readInt(variant.order) + private fun readLong(): Long = buffer.readLong(variant.order) + private fun readFloat(): Float = Float.fromBits(readInt()) + private fun readDouble(): Double = Double.fromBits(readLong()) + + private fun readString(): String { + val length = if (variant.isNetwork) buffer.readVarInt() else readShort().toInt() and 0xFFFF + if (length == 0) return "" + return buffer.readBytes(length).decodeToString() + } + + private fun readTagType(): Byte = if (variant.isNetwork) buffer.readVarInt().toByte() else buffer.readByte() + + public fun readRoot(): Pair { + val type = readTagType() + require(type == NBTType.COMPOUND) { "Expected Compound Tag (10), got $type" } + val name = readString() + val root = readCompound() + return name to root + } + + private fun readCompound(): NbtCompound { + val map = mutableMapOf() + while (true) { + val type = readTagType() + if (type == NBTType.END) break + + val name = readString() + map[name] = readPayload(type) + } + return NbtCompound(map) + } + + private fun readList(): NbtList { + val elementType = readTagType() + val size = if (variant.isNetwork) buffer.readVarInt() else readInt() + if (size <= 0) return NbtList(elementType, emptyList()) + val list = mutableListOf() + (0 until size).forEach { _ -> list.add(readPayload(elementType)) } + return NbtList(elementType, list) + } + + private fun readPayload(type: Byte): NBTElement { + return when (type) { + NBTType.BYTE -> NbtByte(buffer.readByte()) + NBTType.SHORT -> NbtShort(readShort()) + NBTType.INT -> { + val value = if (variant.isNetwork) buffer.readVarInt().decodeZigZag() else readInt() + NbtInt(value) + } + + NBTType.LONG -> NbtLong(readLong()) + NBTType.FLOAT -> NbtFloat(readFloat()) + NBTType.DOUBLE -> NbtDouble(readDouble()) + NBTType.BYTE_ARRAY -> { + val len = if (variant.isNetwork) buffer.readVarInt() else readInt() + NbtByteArray(buffer.readBytes(len)) + } + + NBTType.STRING -> NbtString(readString()) + NBTType.LIST -> readList() + NBTType.COMPOUND -> readCompound() + NBTType.INT_ARRAY -> { + val len = if (variant.isNetwork) buffer.readVarInt() else readInt() + val array = IntArray(len) { readInt() } + NbtIntArray(array) + } + + NBTType.LONG_ARRAY -> { + val len = if (variant.isNetwork) buffer.readVarInt() else readInt() + val array = LongArray(len) { readLong() } + NbtLongArray(array) + } + + else -> throw IllegalArgumentException("Unknown Tag type: $type") + } + } +} \ No newline at end of file diff --git a/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_tags.kt b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_tags.kt new file mode 100644 index 0000000..1601d61 --- /dev/null +++ b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_tags.kt @@ -0,0 +1,66 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.nbt + +public sealed interface NBTElement { + public val typeId: NBTTypeID +} + +public data class NbtByte(val value: Byte) : NBTElement { + override val typeId: NBTTypeID = NBTType.BYTE +} + +public data class NbtShort(val value: Short) : NBTElement { + override val typeId: NBTTypeID = NBTType.SHORT +} + +public data class NbtInt(val value: Int) : NBTElement { + override val typeId: NBTTypeID = NBTType.INT +} + +public data class NbtLong(val value: Long) : NBTElement { + override val typeId: NBTTypeID = NBTType.LONG +} + +public data class NbtFloat(val value: Float) : NBTElement { + override val typeId: NBTTypeID = NBTType.FLOAT +} + +public data class NbtDouble(val value: Double) : NBTElement { + override val typeId: NBTTypeID = NBTType.DOUBLE +} + +public data class NbtByteArray(val value: ByteArray) : NBTElement { + override val typeId: NBTTypeID = NBTType.BYTE_ARRAY + override fun equals(other: Any?): Boolean = other is NbtByteArray && value.contentEquals(other.value) + override fun hashCode(): Int = value.contentHashCode() +} + +public data class NbtString(val value: String) : NBTElement { + override val typeId: NBTTypeID = NBTType.STRING +} + +public data class NbtList(val elementType: Byte, val elements: List) : NBTElement { + override val typeId: NBTTypeID = NBTType.LIST +} + +public data class NbtCompound(val map: Map) : NBTElement { + override val typeId: NBTTypeID = NBTType.COMPOUND +} + +public data class NbtIntArray(val value: IntArray) : NBTElement { + override val typeId: NBTTypeID = NBTType.INT_ARRAY + override fun equals(other: Any?): Boolean = other is NbtIntArray && value.contentEquals(other.value) + override fun hashCode(): Int = value.contentHashCode() +} + +public data class NbtLongArray(val value: LongArray) : NBTElement { + override val typeId: NBTTypeID = NBTType.LONG_ARRAY + override fun equals(other: Any?): Boolean = other is NbtLongArray && value.contentEquals(other.value) + override fun hashCode(): Int = value.contentHashCode() +} \ No newline at end of file diff --git a/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_type.kt b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_type.kt new file mode 100644 index 0000000..e87667c --- /dev/null +++ b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_type.kt @@ -0,0 +1,26 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.nbt + +public typealias NBTTypeID = Byte + +public object NBTType { + public const val END: NBTTypeID = 0 + public const val BYTE: NBTTypeID = 1 + public const val SHORT: NBTTypeID = 2 + public const val INT: NBTTypeID = 3 + public const val LONG: NBTTypeID = 4 + public const val FLOAT: NBTTypeID = 5 + public const val DOUBLE: NBTTypeID = 6 + public const val BYTE_ARRAY: NBTTypeID = 7 + public const val STRING: NBTTypeID = 8 + public const val LIST: NBTTypeID = 9 + public const val COMPOUND: NBTTypeID = 10 + public const val INT_ARRAY: NBTTypeID = 11 + public const val LONG_ARRAY: NBTTypeID = 12 +} \ No newline at end of file diff --git a/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_variant.kt b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_variant.kt new file mode 100644 index 0000000..c5860f9 --- /dev/null +++ b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_variant.kt @@ -0,0 +1,19 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.nbt + +import cn.rtast.libmc.common.ByteOrder + +public enum class NbtVariant( + public val order: ByteOrder, + public val isNetwork: Boolean, +) { + JAVA(ByteOrder.BIG_ENDIAN, isNetwork = false), + BEDROCK_DISK(ByteOrder.LITTLE_ENDIAN, isNetwork = false), + BEDROCK_NETWORK(ByteOrder.LITTLE_ENDIAN, isNetwork = true) +} \ No newline at end of file diff --git a/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/varint.kt b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/varint.kt new file mode 100644 index 0000000..21c4499 --- /dev/null +++ b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/varint.kt @@ -0,0 +1,11 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.nbt + +internal fun Int.decodeZigZag(): Int = (this ushr 1) xor -(this and 1) +internal fun Int.encodeZigZag(): Int = (this shl 1) xor (this shr 31) diff --git a/libmc-nbt/src/commonTest/resources/bedrock.nbt b/libmc-nbt/src/commonTest/resources/bedrock.nbt new file mode 100644 index 0000000000000000000000000000000000000000..21d0771357975ceba3d3ba138ef5ade71343f82a GIT binary patch literal 2761 zcmaJ@&2C%A6+SnV9Fnpn*^-+S1%ifMG%W<93W_2S5EKz(2L>q1uoTC3P!Pj=56OY% z&fLt*rNjhA7A=sxKwhB*`VL*R>+}J#>$;1ei!KYO-bN;^bo#_(M zU%N#3Yu}=M?MgnJvG=-QqU;X!*+%vUstU*M(Cdk|l)XiLXUE!?Ts$@^j-4+DE}QTzF{qc6zz1ZN%A^JO5L;&^}&jzWaYPCtihcsk-~}cRM|{ zxhu4t43Utao$c-YW~UdTuk%=Y9=a+*7f?^Kbi5T#gG*AG)EvXw3ku!}m)xp@2Y6I2z~c4)veHjm)O+w`|TnfqtabICKO za}GbPts6z9tr{5)-~ZwFtt}epQb#UQ0UH=R`{w-o{FOV;&%Z<@uR8PMdl>J$MF-mI zvM&3stW?yn(l;7GE+TBLLQA6G#`hh3FFx4@9RN|fN+^-MQFix-q9$%?t zGTg=$t#y&0N+%0D7RW^YFCZ*9ZN0HvE)&~aD7(nz6J-lm9x)g5um1J3>W?q}oNdxk zrB3bB-KW|ZH9?iAb)%6zyCXSTES~>X3@jEeo+ChbbcD*{%)%oyA+4;?u`c-^-$@3b zbCXii7aAgL-JxgugID<1?T{L2qod}KbxFmj@{`hyLI&J5t0r|2tkS5~rpVxa;hYbM zE1c?>PjPg$O=B}Zsu1Phza6}v-H;{vfN-g{A*ytg06=YLwGq6chLm;4=*cwNq7~Kfx4%TotB@Fod@V5vdp!TGAxBD@#P1=*88ss$-UIUy}yDxO3w|zPw;% zjq=)ip!HbiC*S=g6^UB6s8I71eSe-K7x)0Jg}adEA{BQEhod^qL1GnR??M13<%r8N zI9E7bf>yNx>2eTUNzx&;!If4k3A1pf>BdBeDcKzeQE1~Ff)OK9bvcdQEL(#hY9quc zHrKH{MN-fl3%4NgE-4qP1RS&02EhqnGhr_=l>n~|X?-{zvrhrODa#xk?Cb;F2Tok3 zsk=seR#}Dhkg^ZG*W?T#bz3DKz$pl^wujnF;T^cK67LLgZ>AG0m zq_JoI{9@hOrg3d^acEKHl&Q_1WH5NtIQY@erUrk#I` z&Jl81Zam@OYA|=Ru}&ycjYcq>GDIkg4(Ud@`hLnC0aiNPKNDng@xZ(Cs$ABM&0{Xn zmJyC@L0{xe)~|^#iRS}jP(I{t6WT{8u;Guxg&RMC?AqzWG^83s1ZbQvSLj+)1tWzz zcgbKn9HJvZ?x?|{$*xiGFHmfuKl!Y#U-oz!AtD6&myIvn_0Jlz+mKz(j1Rer(@oL6 zF8@Rq3;A8}#o3~2`4rq>*iPc~{2aAgBj9=BHIEaXx5x_Gl<_=irX>oVwKfTWD|L`j z9WI^w5jCXu_y?PbN`JiA48f$tOmIbx5#yT_rtWm)d;vy5>*ipH zjU>y#`*_{}c_4+`NmK|elpF%$Ps}W5kqrFqQ1a(M#tDXNT6k6HI>3rR7jwZ!!4Mvc zOH$qjx1R|!NQmJ{6E(2AMiK9mCMsOMMWBO&pk$_dv3F=OIaW64R324Fa&uo^d;h^a S6?5J7W@lJ|3v-M+qW=QCeP@~g literal 0 HcmV?d00001 diff --git a/libmc-nbt/src/commonTest/resources/level.dat b/libmc-nbt/src/commonTest/resources/level.dat new file mode 100644 index 0000000000000000000000000000000000000000..5d1749a2937b1647e3ccc547ddb8ff868ed7a089 GIT binary patch literal 11353 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