From 5dc4e8bc0d19828d4a6be1a36a5dd28d1512ecb6 Mon Sep 17 00:00:00 2001 From: RTAkland Date: Mon, 7 Sep 2026 03:15:53 +0800 Subject: [PATCH] Implement join online-mode server process --- .gitignore | 3 +- README.md | 20 ++- docs/implement-encryption.md | 30 +++++ gradle/libs.versions.toml | 14 +- libmc-common/build.gradle.kts | 2 - .../kotlin/cn/rtast/libmc/common/channels.kt | 20 +-- .../cn/rtast/libmc/common/channels.jvm.kt | 52 ++++---- .../cn/rtast/libmc/common/channels.native.kt | 51 ++++--- libmc-mcping/build.gradle.kts | 2 - libmc-nbt/build.gradle.kts | 2 - libmc-protocol/build.gradle.kts | 11 +- .../protocol/util/aes_shared_key.apple.kt | 20 +++ .../libmc/protocol/client/MinecraftClient.kt | 49 +++++-- .../libmc/protocol/crypto/NetworkCipher.kt | 13 ++ .../libmc/protocol/crypto/ProtocolContext.kt | 40 ++++++ .../event/InternalPacketDispatcher.kt | 100 ++++++-------- .../libmc/protocol/network/CipherChannel.kt | 51 +++++++ .../libmc/protocol/network/NetworkChannel.kt | 31 ++--- .../libmc/protocol/network/NetworkSession.kt | 77 +++++++++++ .../ClientboundContainerSetContentPacket.kt | 38 ------ .../ClientboundContainerSetSlotPacket.kt | 32 ----- .../ClientboundSetCursorItemPacket.kt | 23 ---- .../ClientboundSetEquipmentPacket.kt | 39 ------ ...ClientboundSetPlayerInventorySlotPacket.kt | 26 ---- ...rverboundChangeContainerSlotStatePacket.kt | 27 ---- .../ServerboundContainerClickPacket.kt | 73 ---------- .../ServerboundSetCreativeModeSlotPacket.kt | 26 ---- .../libmc/protocol/protocol/GameProtocols.kt | 16 +-- .../protocol/game/inventory/ChangedSlot.kt | 28 ---- .../protocol/game/inventory/EquipmentEntry.kt | 13 -- .../protocol/game/inventory/EquipmentSlot.kt | 33 ----- .../protocol/game/inventory/HashedSlot.kt | 65 --------- .../protocol/protocol/game/inventory/Slot.kt | 93 ------------- .../session/AuthenticationProvider.kt | 12 ++ .../libmc/protocol/util/aes_shared_key.kt | 10 ++ .../src/commonTest/kotlin/test/TestClient.kt | 11 +- .../libmc/protocol/util/aes_shared_key.jvm.kt | 18 +++ .../src/jvmTest/kotlin/test/JvmAesCipher.kt | 31 +++++ .../kotlin/test/TestClientTestInJvm.kt | 125 ++++++++++++++++++ .../protocol/util/aes_shared_key.linux.kt | 33 +++++ .../protocol/util/aes_shared_key.mingw.kt | 30 +++++ libmc-rconlib/build.gradle.kts | 2 - libmc-snbt/build.gradle.kts | 2 - settings.gradle.kts | 1 + 44 files changed, 696 insertions(+), 699 deletions(-) create mode 100644 docs/implement-encryption.md create mode 100644 libmc-protocol/src/appleMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.apple.kt create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/NetworkCipher.kt create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/ProtocolContext.kt create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/CipherChannel.kt create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkSession.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundContainerSetContentPacket.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundContainerSetSlotPacket.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSetCursorItemPacket.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSetEquipmentPacket.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSetPlayerInventorySlotPacket.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/serverbound/ServerboundChangeContainerSlotStatePacket.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/serverbound/ServerboundContainerClickPacket.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/serverbound/ServerboundSetCreativeModeSlotPacket.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/ChangedSlot.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/EquipmentEntry.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/EquipmentSlot.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/HashedSlot.kt delete mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/Slot.kt create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/session/AuthenticationProvider.kt create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.kt create mode 100644 libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.jvm.kt create mode 100644 libmc-protocol/src/jvmTest/kotlin/test/JvmAesCipher.kt create mode 100644 libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt create mode 100644 libmc-protocol/src/linuxMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.linux.kt create mode 100644 libmc-protocol/src/mingwMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.mingw.kt diff --git a/.gitignore b/.gitignore index 6420b18..6812e96 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,5 @@ bin/ .idea/ .kotlin/ -**/build/* \ No newline at end of file +**/build/* +/libmc-protocol/src/jvmTest/resources/accessToken.txt diff --git a/README.md b/README.md index a54e44a..1057ef9 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,6 @@ A lightweight minecraft client-side protocol library and related library, includ - MingwX64 - LinuxArm64 - LinuxX64 -- iosArm64 -- iosSimulatorArm64 - MacosArm64 # libmc-protocol @@ -19,10 +17,9 @@ A lightweight minecraft client-side protocol library and related library, includ # Minecraft Protocol Library Status & Roadmap -## Unimplemented features +## Implementation details -- [ ] **Online Mode Authentication & Encryption**: Full Mojang/Microsoft auth pipeline, JoinServer request, and AES-CFB8 - stream cipher stream wrapper. +- [ ] **Online Mode Authentication & Encryption**: See [Implement Encryption](docs/implement-encryption.md) - [ ] **Structured `TextComponent` Parser**: Rich Chat Component AST decoder (currently falling back to raw `NBTCompound`). - [ ] **Command Tree Parser (0x10)**: Full binary graph decoder for brigadier nodes, argument types, and suggestions. @@ -33,7 +30,8 @@ A lightweight minecraft client-side protocol library and related library, includ - [ ] **Light Engine Update (0x30)**: Sky & Block light nibble array parser. - [ ] **Explosion Event Decoder (0x24)**: Knockback vectors and destroyed block offsets array. - [ ] **Debug Packets Parsing (0x1A - 0x1E)**: Debug subs, block/entity states, and game performance sample events. -- [ ] **Particle parsing** +- [ ] **Particle Parsing** +- [ ] **Slot Data Parsing** --- @@ -146,9 +144,9 @@ A lightweight minecraft client-side protocol library and related library, includ - [x] `0x0F` Command Suggestions (`ClientboundCommandSuggestionsPacket`) - [ ] `0x10` Commands (`ClientboundCommandsPacket`) - [x] `0x11` Container Close (`ClientboundContainerClosePacket`) -- [x] `0x12` Container Set Content (`ClientboundContainerSetContentPacket`) +- [ ] `0x12` Container Set Content (`ClientboundContainerSetContentPacket`) - [x] `0x13` Container Set Data (`ClientboundContainerSetDataPacket`) -- [x] `0x14` Container Set Slot (`ClientboundContainerSetSlotPacket`) +- [ ] `0x14` Container Set Slot (`ClientboundContainerSetSlotPacket`) - [x] `0x15` Cookie Request (`ClientboundCookieRequestPacket`) - [x] `0x16` Cooldown (`ClientboundCooldownPacket`) - [x] `0x17` Custom Chat Completions (`ClientboundCustomChatCompletionsPacket`) @@ -224,19 +222,19 @@ A lightweight minecraft client-side protocol library and related library, includ - [x] `0x5D` Set Camera (`ClientboundSetCameraPacket`) - [x] `0x5E` Set Center Chunk (`ClientboundSetCenterChunkPacket`) - [x] `0x5F` Set Render Distance (`ClientboundSetRenderDistancePacket`) -- [x] `0x60` Set Cursor Item (`ClientboundSetCursorItemPacket`) +- [ ] `0x60` Set Cursor Item (`ClientboundSetCursorItemPacket`) - [x] `0x61` Set Default Spawn Position (`ClientboundSetDefaultSpawnPositionPacket`) - [x] `0x62` Set Display Objective (`ClientboundSetDisplayObjectivePacket`) - [ ] `0x63` Set Entity Metadata (`ClientboundSetEntityMetadataPacket`) - [x] `0x64` Link Entities (`ClientboundLinkEntitiesPacket`) - [x] `0x65` Set Entity Velocity (`ClientboundSetEntityVelocityPacket`) -- [x] `0x66` Set Equipment (`ClientboundSetEquipmentPacket`) +- [ ] `0x66` Set Equipment (`ClientboundSetEquipmentPacket`) - [x] `0x67` Set Experience (`ClientboundSetExperiencePacket`) - [x] `0x68` Set Health (`ClientboundSetHealthPacket`) - [x] `0x69` Set Carried Item (`ClientboundSetCarriedItemPacket`) - [x] `0x6A` Update Objective (`ClientboundUpdateObjectivePacket`) - [x] `0x6B` Set Passengers (`ClientboundSetPassengersPacket`) -- [x] `0x6C` Set Player Inventory Slot (`ClientboundSetPlayerInventorySlotPacket`) +- [ ] `0x6C` Set Player Inventory Slot (`ClientboundSetPlayerInventorySlotPacket`) - [ ] `0x6D` Set Player Team (`ClientboundSetPlayerTeamPacket`) - [x] `0x6E` Update Score (`ClientboundUpdateScorePacket`) - [x] `0x6F` Set Simulation Distance (`ClientboundSetSimulationDistancePacket`) diff --git a/docs/implement-encryption.md b/docs/implement-encryption.md new file mode 100644 index 0000000..efd105c --- /dev/null +++ b/docs/implement-encryption.md @@ -0,0 +1,30 @@ +* **AES-128-CFB8** (`NetworkCipher`): Handles in-place network packet encryption and decryption. +* **SHA-1** (`Sha1Hasher`): Computes the Minecraft server ID hash for online-mode authentication. +* **RSA-1024** (`RSA1024Encryptor`): Encrypts the shared secret and verify token during the login handshake. +* **Auth Join Request** (`AuthenticationProvider`): Sends the HTTP POST request to Mojang's session server + (`https://sessionserver.mojang.com/session/minecraft/join`). + +```kotlin +val client = createMinecraftClient( + host = "127.0.0.1", + username = "Player", + accessToken = accessToken +) { + // 1. AES-128-CFB8 Cipher Implementation + cipherFactory = { sharedKey -> JvmAesCipher(sharedKey) } + + // 2. SHA-1 Hasher Implementation + sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> + /* your SHA-1 implementation */ + } + + // 3. RSA-1024 Encryptor Implementation + rsaEncryptor = RSA1024Encryptor { publicKey, data -> + /* your RSA encryption implementation */ + } + + // 4. Mojang Auth HTTP Join Provider + authProvider = AuthenticationProvider { accessToken, uuid, serverIdHash -> + /* send HTTP POST to session server using your preferred HTTP client (Ktor, OkHttp, etc.) */ + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5bac6b6..9e07c61 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,18 +3,24 @@ 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-serialization = "1.11.0" kotlinx-coroutines = "1.11.0" +ktor-core = "3.5.2" [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-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" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-core" } +#ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor-core" } +#ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor-core" } +#ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor-core" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor-core" } [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 +maven-publish = { id = "maven-publish" } diff --git a/libmc-common/build.gradle.kts b/libmc-common/build.gradle.kts index 1ee18cc..06093ff 100644 --- a/libmc-common/build.gradle.kts +++ b/libmc-common/build.gradle.kts @@ -8,8 +8,6 @@ kotlin { linuxArm64() macosArm64() mingwX64() - iosArm64() - iosSimulatorArm64() jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } sourceSets { 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 b952056..072d2ea 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 @@ -7,18 +7,18 @@ package cn.rtast.libmc.common -public expect class ReadChannel { - 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 readFully(out: ByteArray, start: Int = 0, end: Int = out.size) +public expect open class ReadChannel() { + public open fun readByte(): Byte + public open fun readShort(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Short + public open fun readInt(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Int + public open fun readLong(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Long + public open fun readBytes(length: Int): ByteArray + public open fun readFully(out: ByteArray, start: Int = 0, end: Int = out.size) } -public expect class WriteChannel { - public fun writeFully(value: ByteArray, startIndex: Int = 0, endIndex: Int = value.size) - public fun flush() +public expect open class WriteChannel() { + public open fun writeFully(value: ByteArray, startIndex: Int = 0, endIndex: Int = value.size) + public open fun flush() } public fun ReadChannel.readVarInt(): Int { diff --git a/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/channels.jvm.kt b/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/channels.jvm.kt index 5e1e5ad..588a58c 100644 --- a/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/channels.jvm.kt +++ b/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/channels.jvm.kt @@ -4,53 +4,55 @@ * Date: 2026/9/3 */ - package cn.rtast.libmc.common import java.io.EOFException import java.io.InputStream import java.io.OutputStream -public actual class ReadChannel(private val _inputStream: InputStream) { +public actual open class ReadChannel public actual constructor() { + private lateinit var _inputStream: InputStream - public actual fun readByte(): Byte = _inputStream.read().toByte() - public actual fun readBytes(length: Int): ByteArray = _inputStream.readNBytes(length) + public constructor(inputStream: InputStream) : this() { + this._inputStream = inputStream + } - public actual fun readFully(out: ByteArray, start: Int, end: Int) { + public actual open fun readFully(out: ByteArray, start: Int, end: Int) { var bytesRead = 0 val length = end - start while (bytesRead < length) { val read = _inputStream.read(out, start + bytesRead, length - bytesRead) - if (read == -1) throw IllegalStateException("End of stream") + if (read == -1) throw EOFException("End of stream reached") bytesRead += read } } - public actual fun readShort(endian: ByteOrder): Short = readBytes(2).toShort(endian) - public actual fun readInt(endian: ByteOrder): Int = readBytes(4).toInt(endian) - public actual fun readLong(endian: ByteOrder): Long = readBytes(8).toLong(endian) + public actual open fun readByte(): Byte { + val buf = ByteArray(1) + readFully(buf, 0, 1) + return buf[0] + } + + public actual open fun readBytes(length: Int): ByteArray { + val bytes = ByteArray(length) + readFully(bytes, 0, length) + return bytes + } + + public actual open fun readShort(endian: ByteOrder): Short = readBytes(2).toShort(endian) + public actual open fun readInt(endian: ByteOrder): Int = readBytes(4).toInt(endian) + public actual open fun readLong(endian: ByteOrder): Long = readBytes(8).toLong(endian) } -public actual class WriteChannel { - private val _outputStream: OutputStream +public actual open class WriteChannel public actual constructor() { + private lateinit var _outputStream: OutputStream - public constructor(outputStream: OutputStream) { + public constructor(outputStream: OutputStream) : this() { _outputStream = outputStream } - public actual fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int): Unit = + public actual open fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int): Unit = _outputStream.write(value, startIndex, endIndex - startIndex) - public actual fun flush(): Unit = _outputStream.flush() -} - -private fun InputStream.readNBytes(length: Int): ByteArray { - val buffer = ByteArray(length) - var totalRead = 0 - while (totalRead < length) { - val read = this.read(buffer, totalRead, length - totalRead) - if (read == -1) throw EOFException() - totalRead += read - } - return buffer + public actual open fun flush(): Unit = _outputStream.flush() } \ No newline at end of file diff --git a/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/channels.native.kt b/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/channels.native.kt index 88df3ec..39a844c 100644 --- a/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/channels.native.kt +++ b/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/channels.native.kt @@ -10,38 +10,53 @@ import io.ktor.utils.io.* import io.ktor.utils.io.bits.* import kotlinx.coroutines.runBlocking -public actual class ReadChannel(private val _readChannel: ByteReadChannel) { +public actual open class ReadChannel public actual constructor() { - public actual fun readByte(): Byte = runBlocking { _readChannel.readByte() } - public actual fun readBytes(length: Int): ByteArray = runBlocking { _readChannel.readByteArray(length) } - public actual fun readFully(out: ByteArray, start: Int, end: Int): Unit = + private lateinit var _readChannel: ByteReadChannel + + public constructor(readChannel: ByteReadChannel) : this() { + this._readChannel = readChannel + } + + public actual open fun readByte(): Byte = runBlocking { _readChannel.readByte() } + public actual open fun readBytes(length: Int): ByteArray = runBlocking { _readChannel.readByteArray(length) } + public actual open fun readFully(out: ByteArray, start: Int, end: Int): Unit = runBlocking { _readChannel.readFully(out, start, end) } - public actual fun readShort(endian: ByteOrder): Short = runBlocking { - val v = _readChannel.readShort() - if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder() + public actual open fun readShort(endian: ByteOrder): Short { + val bytes = readBytes(2) + val v = ((bytes[0].toInt() and 0xFF shl 8) or (bytes[1].toInt() and 0xFF)).toShort() + return if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder() } - public actual fun readInt(endian: ByteOrder): Int = runBlocking { - val v = _readChannel.readInt() - if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder() + public actual open fun readInt(endian: ByteOrder): Int { + val bytes = readBytes(4) + val v = (bytes[0].toInt() and 0xFF shl 24) or + (bytes[1].toInt() and 0xFF shl 16) or + (bytes[2].toInt() and 0xFF shl 8) or + (bytes[3].toInt() and 0xFF) + return if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder() } - public actual fun readLong(endian: ByteOrder): Long = runBlocking { - val v = _readChannel.readLong() - if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder() + public actual open fun readLong(endian: ByteOrder): Long { + val bytes = readBytes(8) + var v = 0L + for (i in 0 until 8) { + v = (v shl 8) or (bytes[i].toLong() and 0xFF) + } + return if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder() } } -public actual class WriteChannel { - private val _writeChannel: ByteWriteChannel +public actual open class WriteChannel public actual constructor() { + private lateinit var _writeChannel: ByteWriteChannel - public constructor(writeChannel: ByteWriteChannel) { + public constructor(writeChannel: ByteWriteChannel) : this() { _writeChannel = writeChannel } - public actual fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int): Unit = + public actual open fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int): Unit = runBlocking { _writeChannel.writeFully(value, startIndex, endIndex) } - public actual fun flush(): Unit = runBlocking { _writeChannel.flush() } + public actual open fun flush(): Unit = runBlocking { _writeChannel.flush() } } \ No newline at end of file diff --git a/libmc-mcping/build.gradle.kts b/libmc-mcping/build.gradle.kts index 18f154e..dc2a247 100644 --- a/libmc-mcping/build.gradle.kts +++ b/libmc-mcping/build.gradle.kts @@ -8,8 +8,6 @@ kotlin { linuxArm64() macosArm64() mingwX64() - iosArm64() - iosSimulatorArm64() jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } sourceSets { diff --git a/libmc-nbt/build.gradle.kts b/libmc-nbt/build.gradle.kts index fe2882f..5418968 100644 --- a/libmc-nbt/build.gradle.kts +++ b/libmc-nbt/build.gradle.kts @@ -8,8 +8,6 @@ kotlin { linuxArm64() macosArm64() mingwX64() - iosArm64() - iosSimulatorArm64() jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } sourceSets { diff --git a/libmc-protocol/build.gradle.kts b/libmc-protocol/build.gradle.kts index b877846..847de0d 100644 --- a/libmc-protocol/build.gradle.kts +++ b/libmc-protocol/build.gradle.kts @@ -1,9 +1,5 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget -plugins { - alias(libs.plugins.kotlinx.serialization) -} - kotlin { explicitApi() withSourcesJar() @@ -12,8 +8,6 @@ kotlin { linuxArm64() macosArm64() mingwX64() - iosArm64() - iosSimulatorArm64() jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } sourceSets { @@ -31,5 +25,10 @@ kotlin { implementation(kotlin("test")) implementation(libs.kotlinx.coroutines.test) } + + jvmTest.dependencies { + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.okhttp) + } } } \ No newline at end of file diff --git a/libmc-protocol/src/appleMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.apple.kt b/libmc-protocol/src/appleMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.apple.kt new file mode 100644 index 0000000..56e2bfe --- /dev/null +++ b/libmc-protocol/src/appleMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.apple.kt @@ -0,0 +1,20 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/6 + */ + +@file:OptIn(ExperimentalForeignApi::class) + +package cn.rtast.libmc.protocol.util + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned +import platform.posix.arc4random_buf + +public actual fun generateRandom16Bytes(): ByteArray { + val bytes = ByteArray(16) + bytes.usePinned { pinned -> arc4random_buf(pinned.addressOf(0), 16.toULong()) } + return bytes +} \ 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 e1b39ec..df6c6c4 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,6 +7,8 @@ package cn.rtast.libmc.protocol.client import cn.rtast.libmc.common.LibMCContext +import cn.rtast.libmc.protocol.crypto.ProtocolContext +import cn.rtast.libmc.protocol.crypto.ProtocolContextBuilder import cn.rtast.libmc.protocol.event.InternalPacketDispatcher import cn.rtast.libmc.protocol.event.PacketEventDispatcher import cn.rtast.libmc.protocol.network.NetworkChannel @@ -24,21 +26,33 @@ public class MinecraftClient internal constructor( private val host: String, private val port: Int = 25565, private val username: String, - private val uuid: Uuid, + internal val uuid: Uuid, + internal val accessToken: String?, context: LibMCContext, parentJob: Job?, private val ioDispatcher: CoroutineDispatcher, + cryptoContext: ProtocolContext, ) : PacketEventDispatcher(), CoroutineScope { - + internal val rsa1024Encryptor = cryptoContext.rsaEncryptor + internal val serverIdHasher = cryptoContext.sha1Hasher + internal val authProvider = cryptoContext.authProvider internal val stateMachine = ClientStateMachine() - internal val networkChannel = NetworkChannel(host, port, context, stateMachine) - private val internalPacketDispatcher = InternalPacketDispatcher(this) + internal val networkChannel = NetworkChannel( + host = host, + port = port, + context = context, + stateMachine = stateMachine, + cipherProvider = cryptoContext.cipherFactory + ) + + internal val session get() = networkChannel.session + private val internalPacketDispatcher = InternalPacketDispatcher(this, authProvider) private val clientJob = SupervisorJob(parentJob) private var listenJob: Job? = null + public val isOnlineMode: Boolean get() = accessToken != null public val transactionManager: TransactionIdManager = TransactionIdManager() - override val coroutineContext: CoroutineContext get() = clientJob + ioDispatcher + CoroutineName("LibMC-MinecraftClient-$username") @@ -52,10 +66,14 @@ public class MinecraftClient internal constructor( networkChannel.sendPacket(ServerboundLoginStartPacket(username, uuid)) } + public fun setCompression(threshold: Int): Unit = networkChannel.setCompression(threshold) + private fun startListening() { listenJob = launch { try { - while (isActive) internalPacketDispatcher.handleIncomingPackets(networkChannel.readNextPacket()) + while (isActive) { + internalPacketDispatcher.handleIncomingPackets(networkChannel.readNextPacket()) + } } catch (e: Exception) { if (e is CancellationException) throw e if (isActive) { @@ -75,10 +93,25 @@ public class MinecraftClient internal constructor( public fun createMinecraftClient( host: String, - port: Int, + port: Int = 25565, username: String, uuid: Uuid = generateOfflineUuid(username), + accessToken: String?, 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 + crypto: ProtocolContextBuilder.() -> Unit, +): MinecraftClient { + val cryptoContext = ProtocolContextBuilder(accessToken != null).apply(crypto).build() + return MinecraftClient( + host = host, + port = port, + username = username, + uuid = uuid, + accessToken = accessToken, + context = context, + parentJob = parentJob, + ioDispatcher = ioDispatcher, + cryptoContext = cryptoContext + ) +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/NetworkCipher.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/NetworkCipher.kt new file mode 100644 index 0000000..4d1a151 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/NetworkCipher.kt @@ -0,0 +1,13 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/7 + */ + + +package cn.rtast.libmc.protocol.crypto + +public interface NetworkCipher { + public fun encrypt(buffer: ByteArray, offset: Int, length: Int) + public fun decrypt(buffer: ByteArray, offset: Int, length: Int) +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/ProtocolContext.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/ProtocolContext.kt new file mode 100644 index 0000000..b4fcf04 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/crypto/ProtocolContext.kt @@ -0,0 +1,40 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/7 + */ + + +package cn.rtast.libmc.protocol.crypto + +import cn.rtast.libmc.protocol.session.AuthenticationProvider + +public data class ProtocolContext( + val rsaEncryptor: RSA1024Encryptor, + val sha1Hasher: Sha1Hasher, + val cipherFactory: (sharedKey: ByteArray) -> NetworkCipher, + val authProvider: AuthenticationProvider, +) + +public class ProtocolContextBuilder internal constructor(private val onlineMode: Boolean) { + public lateinit var rsaEncryptor: RSA1024Encryptor + public lateinit var sha1Hasher: Sha1Hasher + public lateinit var cipherFactory: (sharedKey: ByteArray) -> NetworkCipher + public lateinit var authProvider: AuthenticationProvider + + internal fun build(): ProtocolContext = ProtocolContext( + rsaEncryptor = if (::rsaEncryptor.isInitialized) rsaEncryptor else error("rsaEncryptor is required"), + sha1Hasher = if (::sha1Hasher.isInitialized) sha1Hasher else error("sha1Hasher is required"), + cipherFactory = if (::cipherFactory.isInitialized) cipherFactory else error("cipherFactory is required"), + authProvider = if (::authProvider.isInitialized && !onlineMode) authProvider else error("authProvider is required") + ) +} + +public fun interface RSA1024Encryptor { + public fun encrypt(key: ByteArray, data: ByteArray): ByteArray +} + + +public fun interface Sha1Hasher { + public fun hash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/event/InternalPacketDispatcher.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/event/InternalPacketDispatcher.kt index b80ad3b..bc45425 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/event/InternalPacketDispatcher.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/event/InternalPacketDispatcher.kt @@ -10,22 +10,25 @@ package cn.rtast.libmc.protocol.event import cn.rtast.libmc.common.packet.MinecraftPacket import cn.rtast.libmc.protocol.client.MinecraftClient import cn.rtast.libmc.protocol.packet.configuration.clientbound.* -import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundAckFinishConfigurationPacket -import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundKeepAliveConfigurationPacket -import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundPongConfigurationPacket -import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundSelectKnownPacksPacket +import cn.rtast.libmc.protocol.packet.configuration.serverbound.* import cn.rtast.libmc.protocol.packet.login.clientbound.* +import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundKeyPacket import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundLoginAcknowledgedPacket import cn.rtast.libmc.protocol.packet.play.clientbound.* import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundConfigurationAcknowledgedPacket import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundKeepAlivePlayPacket import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundPongPlayPacket import cn.rtast.libmc.protocol.protocol.state.ProtocolState +import cn.rtast.libmc.protocol.session.AuthenticationProvider +import cn.rtast.libmc.protocol.util.generateRandom16Bytes -internal class InternalPacketDispatcher(private val client: MinecraftClient) { - suspend fun dispatchEvent(packet: MinecraftPacket) = client.dispatch(packet) +public class InternalPacketDispatcher( + private val client: MinecraftClient, + private val authProvider: AuthenticationProvider, +) { + private suspend fun dispatchEvent(packet: MinecraftPacket) = client.dispatch(packet) - suspend fun handleIncomingPackets(packet: MinecraftPacket) { + public suspend fun handleIncomingPackets(packet: MinecraftPacket) { this.dispatchEvent(packet) when (packet) { is ClientboundLoginPacket -> this.handleLoginPackets(packet) @@ -34,7 +37,7 @@ internal class InternalPacketDispatcher(private val client: MinecraftClient) { } } - private fun handleLoginPackets(packet: ClientboundLoginPacket) { + private suspend fun handleLoginPackets(packet: ClientboundLoginPacket) { when (packet) { is ClientboundDisconnectLoginPacket -> { println("Login denied: ${packet.reason}") @@ -47,25 +50,30 @@ internal class InternalPacketDispatcher(private val client: MinecraftClient) { client.stateMachine.transitionTo(ProtocolState.CONFIGURATION) } - is ClientboundCustomQueryPacket -> {} - is ClientboundHelloPacket -> {} + is ClientboundHelloPacket -> { + val sharedSecret = generateRandom16Bytes() + if (client.isOnlineMode) { + val serverHash = client.serverIdHasher.hash(packet.serverId, sharedSecret, packet.publicKey) + authProvider.joinServer( + "https://sessionserver.mojang.com/session/minecraft/join", + client.accessToken!!, + client.uuid.toString().replace("-", ""), + serverHash + ) + } + val encryptedSecret = client.rsa1024Encryptor.encrypt(packet.publicKey, sharedSecret) + val encryptedVerifyToken = client.rsa1024Encryptor.encrypt(packet.publicKey, packet.verifyToken) + client.networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken)) + client.session.enableEncryption(sharedSecret) + } + + else -> {} } } private fun handleConfigurationPackets(packet: ClientboundConfigurationPacket) { when (packet) { - is ClientboundCookieRequestPacket -> { - // TODO - } - - is ClientboundCustomPayloadPacket -> { - // TODO - } - - is ClientboundDisconnectConfigurationPacket -> { - println("Configuration disconnected: ${packet.reason}") - } - + is ClientboundDisconnectConfigurationPacket -> println("Configuration disconnected: ${packet.reason}") ClientboundFinishConfigurationPacket -> { client.networkChannel.sendPacket(ServerboundAckFinishConfigurationPacket) client.stateMachine.transitionTo(ProtocolState.PLAY) @@ -79,23 +87,11 @@ internal class InternalPacketDispatcher(private val client: MinecraftClient) { ServerboundPongConfigurationPacket(packet.id) ) - is ClientboundSelectKnownPacksPacket -> { - client.networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList())) // TODO empty resource packs list - } - - is ClientboundAddResourcePackPacket -> {} - ClientboundClearDialogPacket -> {} - is ClientboundCodeOfConductPacket -> {} - is ClientboundConfigurationShowDialogPacket -> {} - is ClientboundCustomReportDetailsPacket -> {} - is ClientboundRegistryDataPacket -> {} - is ClientboundRemoveResourcePackPacket -> {} - ClientboundResetChatPacket -> {} - is ClientboundServerLinksPacket -> {} - is ClientboundStoreCookiePacket -> {} - is ClientboundTransferPacket -> {} - is ClientboundUpdateEnabledFeaturesPacket -> {} - is ClientboundUpdateTagsPacket -> {} + is ClientboundSelectKnownPacksPacket -> client.networkChannel.sendPacket( + ServerboundSelectKnownPacksPacket(emptyList()) + ) // TODO empty resource packs list + is ClientboundCodeOfConductPacket -> client.networkChannel.sendPacket(ServerboundAcceptCodeOfConductPacket) + else -> {} } } @@ -105,34 +101,14 @@ internal class InternalPacketDispatcher(private val client: MinecraftClient) { is ClientboundKeepAlivePlayPacket -> client.networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(id = packet.id)) is ClientboundLoginPlayPacket -> println("Successfully joined world Entity ID: ${packet.entityId}") is ClientboundPingPacket -> client.networkChannel.sendPacket(ServerboundPongPlayPacket(packet.id)) - is ClientboundPlayerChatMessagePacket -> { - println("Received player chat message $packet") - // TODO - } - + is ClientboundPlayerChatMessagePacket -> println("[Player Chat Message] ${packet.message}") ClientboundStartConfigurationPacket -> { client.networkChannel.sendPacket(ServerboundConfigurationAcknowledgedPacket) client.stateMachine.transitionTo(ProtocolState.CONFIGURATION) } - is ClientboundSystemChatMessagePacket -> {} - is ClientboundAcknowledgeBlockChangePacket -> {} - is ClientboundAwardStatisticsPacket -> {} - is ClientboundBlockDestructionPacket -> {} - is ClientboundBlockEntityDataPacket -> {} - ClientboundDelimiterPacket -> {} - is ClientboundEntityAnimationPacket -> {} - is ClientboundSpawnEntityPacket -> {} - is ClientboundShowDialogPacket -> {} - is ClientboundBlockEventPacket -> TODO() - is ClientboundBlockUpdatePacket -> TODO() - is ClientboundBossEventPacket -> TODO() - is ClientboundChangeDifficultyPacket -> TODO() - is ClientboundChunkBatchFinishedPacket -> TODO() - ClientboundChunkBatchStartPacket -> TODO() - is ClientboundChunksBiomesPacket -> TODO() - is ClientboundClearTitlesPacket -> TODO() - is ClientboundCommandSuggestionsPacket -> TODO() + is ClientboundSystemChatMessagePacket -> println("[System message] ${packet.content}") + else -> println(packet) } } } \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/CipherChannel.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/CipherChannel.kt new file mode 100644 index 0000000..e42673e --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/CipherChannel.kt @@ -0,0 +1,51 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/6 + */ + + +package cn.rtast.libmc.protocol.network + +import cn.rtast.libmc.common.ReadChannel +import cn.rtast.libmc.common.WriteChannel +import cn.rtast.libmc.protocol.crypto.NetworkCipher + +internal class CipherReadChannel( + private val delegate: ReadChannel, + private val crypto: NetworkCipher +) : ReadChannel() { + override fun readFully(out: ByteArray, start: Int, end: Int) { + delegate.readFully(out, start, end) + val length = end - start + if (length > 0) crypto.decrypt(out, start, length) + } + + override fun readByte(): Byte { + val buf = ByteArray(1) + readFully(buf, 0, 1) + return buf[0] + } + + override fun readBytes(length: Int): ByteArray { + val bytes = ByteArray(length) + readFully(bytes, 0, length) + return bytes + } +} + +internal class CipherWriteChannel( + private val delegate: WriteChannel, + private val crypto: NetworkCipher +) : WriteChannel() { + override fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) { + val length = endIndex - startIndex + if (length <= 0) return + val encrypted = value.copyOfRange(startIndex, endIndex) + crypto.encrypt(encrypted, 0, length) + delegate.writeFully(encrypted, 0, length) + } + override fun flush() { + delegate.flush() + } +} \ 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 959bc40..1c98eff 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 @@ -9,27 +9,24 @@ package cn.rtast.libmc.protocol.network import cn.rtast.libmc.common.* import cn.rtast.libmc.common.packet.MinecraftPacket import cn.rtast.libmc.protocol.client.ClientStateMachine +import cn.rtast.libmc.protocol.crypto.NetworkCipher import cn.rtast.libmc.protocol.protocol.GameProtocols import kotlin.concurrent.Volatile internal class NetworkChannel( - private val host: String, - private val port: Int, - private val context: LibMCContext, + host: String, + port: Int, + context: LibMCContext, private val stateMachine: ClientStateMachine, + cipherProvider: (ByteArray) -> NetworkCipher, ) { - private var socket: Socket? = null - private var readChannel: ReadChannel? = null - private var writeChannel: WriteChannel? = null + val session: NetworkSession = NetworkSession(host, port, context, cipherProvider) @Volatile private var threshold = -1 fun connect() { - val sk = Socket(host, port, context) - this.socket = sk - this.readChannel = sk.openReadChannel() - this.writeChannel = sk.openWriteChannel() + session.connect() } fun setCompression(threshold: Int) { @@ -37,9 +34,8 @@ internal class NetworkChannel( } fun readNextPacket(): MinecraftPacket { - val channel = requireNotNull(readChannel) { "ReadChannel not connected" } - val packetLength = channel.readVarInt() - val rawFrameBytes = channel.readBytes(packetLength) + val packetLength = session.readVarInt() + val rawFrameBytes = session.readBytes(packetLength) val frameBuf = rawFrameBytes.wrap() val payloadBuf = if (threshold < 0) frameBuf else { val dataLength = frameBuf.readVarInt() @@ -48,14 +44,12 @@ internal class NetworkChannel( } val currentState = stateMachine.currentState val packetId = payloadBuf.readVarInt() - val packet = GameProtocols.clientboundGameProtocols + return GameProtocols.clientboundGameProtocols .getRegistry(currentState) .decodePacket(packetId, payloadBuf) - return packet } fun sendPacket(packet: MinecraftPacket) { - val channel = requireNotNull(writeChannel) { "WriteChannel not connected" } val uncompressedBodyBuf = BytesBuffer() GameProtocols.serverboundGameProtocols .getRegistry(stateMachine.currentState) @@ -78,11 +72,10 @@ internal class NetworkChannel( frameBuffer.writeVarInt(contentBuf.size) frameBuffer.writeBuffer(contentBuf) } - channel.writeFully(frameBuffer.toByteArray()) - channel.flush() + session.writeFully(frameBuffer.toByteArray()) } fun close() { - socket?.close() + session.close() } } \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkSession.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkSession.kt new file mode 100644 index 0000000..ed706d6 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkSession.kt @@ -0,0 +1,77 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/6 + */ + +package cn.rtast.libmc.protocol.network + +import cn.rtast.libmc.common.LibMCContext +import cn.rtast.libmc.common.ReadChannel +import cn.rtast.libmc.common.Socket +import cn.rtast.libmc.common.WriteChannel +import cn.rtast.libmc.protocol.crypto.NetworkCipher + +internal class NetworkSession( + private val host: String, + private val port: Int, + private val context: LibMCContext, + private var cipherProvider: (ByteArray) -> NetworkCipher, +) { + private var socket: Socket? = null + + var readChannel: ReadChannel? = null + private set + + var writeChannel: WriteChannel? = null + private set + + fun connect() { + val sk = Socket(host, port, context) + this.socket = sk + this.readChannel = sk.openReadChannel() + this.writeChannel = sk.openWriteChannel() + } + + fun enableEncryption(sharedKey: ByteArray) { + val currentRead = requireNotNull(readChannel) { "ReadChannel not connected" } + val currentWrite = requireNotNull(writeChannel) { "WriteChannel not connected" } + val cipher = cipherProvider(sharedKey) + this.readChannel = CipherReadChannel(currentRead, cipher) + this.writeChannel = CipherWriteChannel(currentWrite, cipher) + } + + fun readByte(): Byte { + val channel = requireNotNull(readChannel) { "ReadChannel not connected" } + return channel.readByte() + } + + fun readBytes(length: Int): ByteArray { + val channel = requireNotNull(readChannel) { "ReadChannel not connected" } + return channel.readBytes(length) + } + + fun readVarInt(): Int { + var numRead = 0 + var result = 0 + var read: Byte + do { + read = 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 + } + + fun writeFully(data: ByteArray) { + val channel = requireNotNull(writeChannel) { "WriteChannel not connected" } + channel.writeFully(data, 0, data.size) + channel.flush() + } + + fun close() { + socket?.close() + } +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundContainerSetContentPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundContainerSetContentPacket.kt deleted file mode 100644 index da96989..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundContainerSetContentPacket.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/6 - */ - - -package cn.rtast.libmc.protocol.packet.play.clientbound - -import cn.rtast.libmc.common.BytesBuffer -import cn.rtast.libmc.common.PacketCodec -import cn.rtast.libmc.common.readVarInt -import cn.rtast.libmc.protocol.protocol.game.inventory.Slot -import cn.rtast.libmc.protocol.protocol.game.inventory.readSlot - -public data class ClientboundContainerSetContentPacket( - val windowId: Int, - /** - * A server-managed sequence number used to avoid desynchronization - * see https://minecraft.wiki/w/Java_Edition_protocol/Packets#Click_Container - */ - val stateId: Int, - val slotData: List, - val carriedItem: Slot, -) : ClientboundPlayPacket { - internal companion object Codec : PacketCodec { - override fun encode(buffer: BytesBuffer, value: ClientboundContainerSetContentPacket) {} - override fun decode(buffer: BytesBuffer): ClientboundContainerSetContentPacket { - val windowId = buffer.readVarInt() - val stateId = buffer.readVarInt() - val slotDataCount = buffer.readVarInt() - val slotData = ArrayList(slotDataCount) - repeat(slotDataCount) { slotData.add(buffer.readSlot()) } - val carriedItem = buffer.readSlot() - return ClientboundContainerSetContentPacket(windowId, stateId, slotData, carriedItem) - } - } -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundContainerSetSlotPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundContainerSetSlotPacket.kt deleted file mode 100644 index 6fff33d..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundContainerSetSlotPacket.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/6 - */ - - -package cn.rtast.libmc.protocol.packet.play.clientbound - -import cn.rtast.libmc.common.BytesBuffer -import cn.rtast.libmc.common.PacketCodec -import cn.rtast.libmc.common.readVarInt -import cn.rtast.libmc.protocol.protocol.game.inventory.Slot -import cn.rtast.libmc.protocol.protocol.game.inventory.readSlot - -public data class ClientboundContainerSetSlotPacket( - val windowId: Int, - val stateId: Int, - val slot: Short, - val slotData: Slot, -) : ClientboundPlayPacket { - internal companion object Codec : PacketCodec { - override fun encode(buffer: BytesBuffer, value: ClientboundContainerSetSlotPacket) {} - override fun decode(buffer: BytesBuffer): ClientboundContainerSetSlotPacket { - val windowId = buffer.readVarInt() - val stateId = buffer.readVarInt() - val slot = buffer.readShort() - val slotData = buffer.readSlot() - return ClientboundContainerSetSlotPacket(windowId, stateId, slot, slotData) - } - } -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSetCursorItemPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSetCursorItemPacket.kt deleted file mode 100644 index 06cbd2c..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSetCursorItemPacket.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/6 - */ - - -package cn.rtast.libmc.protocol.packet.play.clientbound - -import cn.rtast.libmc.common.BytesBuffer -import cn.rtast.libmc.common.PacketCodec -import cn.rtast.libmc.common.packet.MinecraftPacket -import cn.rtast.libmc.protocol.protocol.game.inventory.Slot -import cn.rtast.libmc.protocol.protocol.game.inventory.readSlot - -public data class ClientboundSetCursorItemPacket(val carriedItem: Slot) : MinecraftPacket { - internal companion object Codec : PacketCodec { - override fun encode(buffer: BytesBuffer, value: ClientboundSetCursorItemPacket) {} - override fun decode(buffer: BytesBuffer): ClientboundSetCursorItemPacket { - return ClientboundSetCursorItemPacket(buffer.readSlot()) - } - } -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSetEquipmentPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSetEquipmentPacket.kt deleted file mode 100644 index bdfd025..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSetEquipmentPacket.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/6 - */ - - -package cn.rtast.libmc.protocol.packet.play.clientbound - -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.protocol.protocol.game.inventory.EquipmentEntry -import cn.rtast.libmc.protocol.protocol.game.inventory.EquipmentSlot -import cn.rtast.libmc.protocol.protocol.game.inventory.readSlot - -public data class ClientboundSetEquipmentPacket(val entityId: Int, val equipment: List) : - MinecraftPacket { - internal companion object Codec : PacketCodec { - private const val MASK_HAS_NEXT: Int = 0x80 - private const val MASK_SLOT_ID: Int = 0x7F - - override fun encode(buffer: BytesBuffer, value: ClientboundSetEquipmentPacket) {} - override fun decode(buffer: BytesBuffer): ClientboundSetEquipmentPacket { - val entityId = buffer.readVarInt() - val equipment = ArrayList() - while (true) { - val rawSlot = buffer.readByte().toInt() - val slotId = rawSlot and MASK_SLOT_ID - val slot = EquipmentSlot.fromID(slotId) - val item = buffer.readSlot() - equipment.add(EquipmentEntry(slot, item)) - if ((rawSlot and MASK_HAS_NEXT) == 0) break - } - return ClientboundSetEquipmentPacket(entityId, equipment) - } - } -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSetPlayerInventorySlotPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSetPlayerInventorySlotPacket.kt deleted file mode 100644 index 772db11..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSetPlayerInventorySlotPacket.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/6 - */ - - -package cn.rtast.libmc.protocol.packet.play.clientbound - -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.protocol.protocol.game.inventory.Slot -import cn.rtast.libmc.protocol.protocol.game.inventory.readSlot - -public data class ClientboundSetPlayerInventorySlotPacket(val slot: Int, val data: Slot) : MinecraftPacket { - internal companion object Codec : PacketCodec { - override fun encode(buffer: BytesBuffer, value: ClientboundSetPlayerInventorySlotPacket) {} - override fun decode(buffer: BytesBuffer): ClientboundSetPlayerInventorySlotPacket { - val slot = buffer.readVarInt() - val slotData = buffer.readSlot() - return ClientboundSetPlayerInventorySlotPacket(slot, slotData) - } - } -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/serverbound/ServerboundChangeContainerSlotStatePacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/serverbound/ServerboundChangeContainerSlotStatePacket.kt deleted file mode 100644 index 55e46a2..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/serverbound/ServerboundChangeContainerSlotStatePacket.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/5 - */ - - -package cn.rtast.libmc.protocol.packet.play.serverbound - -import cn.rtast.libmc.common.BytesBuffer -import cn.rtast.libmc.common.PacketCodec -import cn.rtast.libmc.common.packet.MinecraftPacket -import cn.rtast.libmc.common.writeVarInt - -public data class ServerboundChangeContainerSlotStatePacket(val slotId: Int, val windowId: Int, val state: Boolean) : - MinecraftPacket { - internal companion object Codec : PacketCodec { - override fun encode(buffer: BytesBuffer, value: ServerboundChangeContainerSlotStatePacket) { - buffer.writeVarInt(value.slotId) - buffer.writeVarInt(value.windowId) - buffer.writeBoolean(value.state) - } - - override fun decode(buffer: BytesBuffer): ServerboundChangeContainerSlotStatePacket = - throw UnsupportedOperationException() - } -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/serverbound/ServerboundContainerClickPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/serverbound/ServerboundContainerClickPacket.kt deleted file mode 100644 index 83669a4..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/serverbound/ServerboundContainerClickPacket.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/5 - */ - - -package cn.rtast.libmc.protocol.packet.play.serverbound - -import cn.rtast.libmc.common.BytesBuffer -import cn.rtast.libmc.common.PacketCodec -import cn.rtast.libmc.common.packet.MinecraftPacket -import cn.rtast.libmc.common.writeVarInt -import cn.rtast.libmc.protocol.protocol.game.container.ContainerButton -import cn.rtast.libmc.protocol.protocol.game.inventory.ChangedSlot -import cn.rtast.libmc.protocol.protocol.game.inventory.HashedSlot -import cn.rtast.libmc.protocol.protocol.game.inventory.InventoryClickType - -public data class ServerboundContainerClickPacket( - val windowId: Int, - val stateId: Int, - val slot: Short, - val button: Byte, - val mode: Int, - val changedSlots: List, - val carriedItem: HashedSlot, -) : MinecraftPacket { - public constructor( - windowId: Int, - stateId: Int, - slot: Short, - mouseButton: ContainerButton.Mouse, - changedSlots: List, - carriedItem: HashedSlot, - ) : this( - windowId = windowId, - stateId = stateId, - slot = slot, - button = mouseButton.id, - mode = InventoryClickType.PICKUP.id, - changedSlots = changedSlots, - carriedItem = carriedItem - ) - - public constructor( - windowId: Int, - stateId: Int, - slot: Short, - swapButton: ContainerButton.Swap, - changedSlots: List, - carriedItem: HashedSlot, - ) : this( - windowId = windowId, - stateId = stateId, - slot = slot, - button = swapButton.id, - mode = InventoryClickType.SWAP.id, - changedSlots = changedSlots, - carriedItem = carriedItem - ) - - internal companion object Codec : PacketCodec { - override fun encode(buffer: BytesBuffer, value: ServerboundContainerClickPacket) { - buffer.writeVarInt(value.windowId) - buffer.writeVarInt(value.stateId) - buffer.writeShort(value.slot) - buffer.writeByte(value.button) - } - - override fun decode(buffer: BytesBuffer): ServerboundContainerClickPacket = - throw UnsupportedOperationException() - } -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/serverbound/ServerboundSetCreativeModeSlotPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/serverbound/ServerboundSetCreativeModeSlotPacket.kt deleted file mode 100644 index 2248725..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/serverbound/ServerboundSetCreativeModeSlotPacket.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/5 - */ - - -package cn.rtast.libmc.protocol.packet.play.serverbound - -import cn.rtast.libmc.common.BytesBuffer -import cn.rtast.libmc.common.PacketCodec -import cn.rtast.libmc.common.packet.MinecraftPacket -import cn.rtast.libmc.protocol.protocol.game.inventory.Slot -import cn.rtast.libmc.protocol.protocol.game.inventory.writeSlot - -public data class ServerboundSetCreativeModeSlotPacket(val slot: Short, val clickedItem: Slot) : MinecraftPacket { - internal companion object Codec : PacketCodec { - override fun encode(buffer: BytesBuffer, value: ServerboundSetCreativeModeSlotPacket) { - buffer.writeShort(value.slot) - buffer.writeSlot(value.clickedItem) - } - - override fun decode(buffer: BytesBuffer): ServerboundSetCreativeModeSlotPacket = - throw UnsupportedOperationException() - } -} \ No newline at end of file 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 f257eec..331fe61 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 @@ -78,9 +78,9 @@ internal object GameProtocols { register(0x0f, ClientboundCommandSuggestionsPacket) // register(0x10, ClientboundCommandsPacket) register(0x11, ClientboundContainerClosePacket) - register(0x12, ClientboundContainerSetContentPacket) +// register(0x12, ClientboundContainerSetContentPacket) register(0x13, ClientboundContainerSetDataPacket) - register(0x14, ClientboundContainerSetSlotPacket) +// register(0x14, ClientboundContainerSetSlotPacket) register(0x15, ClientboundCookieRequestPacket) register(0x16, ClientboundCooldownPacket) register(0x17, ClientboundCustomChatCompletionsPacket) @@ -156,19 +156,19 @@ internal object GameProtocols { register(0x5d, ClientboundSetCameraPacket) register(0x5e, ClientboundSetCenterChunkPacket) register(0x5f, ClientboundSetRenderDistancePacket) - register(0x60, ClientboundSetCursorItemPacket) +// register(0x60, ClientboundSetCursorItemPacket) register(0x61, ClientboundSetDefaultSpawnPositionPacket) register(0x62, ClientboundSetDisplayObjectivePacket) // register(0x63, ClientboundSetEntityMetadataPacket) register(0x64, ClientboundLinkEntitiesPacket) register(0x65, ClientboundSetEntityVelocityPacket) - register(0x66, ClientboundSetEquipmentPacket) +// register(0x66, ClientboundSetEquipmentPacket) register(0x67, ClientboundSetExperiencePacket) register(0x68, ClientboundSetHealthPacket) register(0x69, ClientboundSetCarriedItemPacket) register(0x6a, ClientboundUpdateObjectivePacket) register(0x6b, ClientboundSetPassengersPacket) - register(0x6c, ClientboundSetPlayerInventorySlotPacket) +// register(0x6c, ClientboundSetPlayerInventorySlotPacket) // register(0x6d, ClientboundSetPlayerTeamPacket) register(0x6e, ClientboundUpdateScorePacket) register(0x6f, ClientboundSetSimulationDistancePacket) @@ -249,9 +249,9 @@ internal object GameProtocols { register(0x0f, ServerboundCommandSuggestionRequestPacket) register(0x10, ServerboundConfigurationAcknowledgedPacket) register(0x11, ServerboundContainerClickButtonPacket) - register(0x12, ServerboundContainerClickPacket) +// register(0x12, ServerboundContainerClickPacket) register(0x13, ServerboundContainerClosePacket) - register(0x14, ServerboundChangeContainerSlotStatePacket) +// register(0x14, ServerboundChangeContainerSlotStatePacket) register(0x15, ServerboundCookieResponsePacket) register(0x16, ServerboundCustomPayloadPacket) // register(0x17, ServerboundDebugSubscriptionRequestPacket) @@ -287,7 +287,7 @@ internal object GameProtocols { register(0x35, ServerboundSetCarriedItemPacket) register(0x36, ServerboundSetCommandBlockPacket) register(0x37, ServerboundSetCommandMinecartPacket) - register(0x38, ServerboundSetCreativeModeSlotPacket) +// register(0x38, ServerboundSetCreativeModeSlotPacket) register(0x39, ServerboundSetGameRulePacket) register(0x3a, ServerboundSetJigsawBlockPacket) register(0x3b, ServerboundSetStructureBlockPacket) diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/ChangedSlot.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/ChangedSlot.kt deleted file mode 100644 index a1dc19b..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/ChangedSlot.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/5 - */ - - -package cn.rtast.libmc.protocol.protocol.game.inventory - -import cn.rtast.libmc.common.BytesBuffer -import cn.rtast.libmc.common.PacketCodec -import cn.rtast.libmc.common.readVarInt -import cn.rtast.libmc.common.writeVarInt - -public data class ChangedSlot(val slotNumber: Int, val item: HashedSlot) { - internal companion object Codec : PacketCodec { - override fun encode(buffer: BytesBuffer, value: ChangedSlot) { - buffer.writeVarInt(value.slotNumber) - HashedSlot.encode(buffer, value.item) - } - - override fun decode(buffer: BytesBuffer): ChangedSlot { - val slotNumber = buffer.readVarInt() - val item = HashedSlot.decode(buffer) - return ChangedSlot(slotNumber, item) - } - } -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/EquipmentEntry.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/EquipmentEntry.kt deleted file mode 100644 index 88255c1..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/EquipmentEntry.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/6 - */ - - -package cn.rtast.libmc.protocol.protocol.game.inventory - -public data class EquipmentEntry( - val slot: EquipmentSlot, - val item: Slot, -) \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/EquipmentSlot.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/EquipmentSlot.kt deleted file mode 100644 index b3e72ad..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/EquipmentSlot.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/6 - */ - - -package cn.rtast.libmc.protocol.protocol.game.inventory - -public enum class EquipmentSlot(public val rawId: Int) { - MAIN_HAND(0), - OFF_HAND(1), - FEET(2), - LEGS(3), - CHEST(4), - HEAD(5), - BODY(6), - SADDLE(7); - - public companion object { - public fun fromID(id: Int): EquipmentSlot = when (id) { - 0 -> MAIN_HAND - 1 -> OFF_HAND - 2 -> FEET - 3 -> LEGS - 4 -> CHEST - 5 -> HEAD - 6 -> BODY - 7 -> SADDLE - else -> MAIN_HAND - } - } -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/HashedSlot.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/HashedSlot.kt deleted file mode 100644 index 3003a45..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/HashedSlot.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/5 - */ - - -package cn.rtast.libmc.protocol.protocol.game.inventory - -import cn.rtast.libmc.common.BytesBuffer -import cn.rtast.libmc.common.PacketCodec -import cn.rtast.libmc.common.readVarInt -import cn.rtast.libmc.common.writeVarInt - -public data class HashedSlot( - val hasItem: Boolean, - val itemId: Int?, - val itemCount: Int?, - val componentsToAdd: List?, - val componentsToRemove: List?, -) { - public data class ComponentAddEntry(val typeId: Int, val dataHash: Int) { - internal companion object Codec : PacketCodec { - override fun encode(buffer: BytesBuffer, value: ComponentAddEntry) { - buffer.writeVarInt(value.typeId) - buffer.writeVarInt(value.dataHash) - } - - override fun decode(buffer: BytesBuffer): ComponentAddEntry { - val typeId = buffer.readVarInt() - val dataHash = buffer.readVarInt() - return ComponentAddEntry(typeId, dataHash) - } - } - } - - public companion object Codec : PacketCodec { - public val EMPTY: HashedSlot = HashedSlot(false, null, null, null, null) - - override fun encode(buffer: BytesBuffer, value: HashedSlot) { - buffer.writeBoolean(value.hasItem) - if (!value.hasItem) return - buffer.writeVarInt(requireNotNull(value.itemId) { "itemId must not be null when hasItem is true" }) - buffer.writeVarInt(requireNotNull(value.itemCount) { "itemCount must not be null when hasItem is true" }) - val addList = value.componentsToAdd ?: emptyList() - buffer.writeVarInt(addList.size) - addList.forEach { ComponentAddEntry.encode(buffer, it) } - val removeList = value.componentsToRemove ?: emptyList() - buffer.writeVarInt(removeList.size) - removeList.forEach { buffer.writeVarInt(it) } - } - - override fun decode(buffer: BytesBuffer): HashedSlot { - val hasItem = buffer.readBoolean() - if (!hasItem) return EMPTY - val itemId = buffer.readVarInt() - val itemCount = buffer.readVarInt() - val addCount = buffer.readVarInt() - val componentsToAdd = List(addCount) { ComponentAddEntry.decode(buffer) } - val removeCount = buffer.readVarInt() - val componentsToRemove = List(removeCount) { buffer.readVarInt() } - return HashedSlot(hasItem, itemId, itemCount, componentsToAdd, componentsToRemove) - } - } -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/Slot.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/Slot.kt deleted file mode 100644 index d3746f3..0000000 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/inventory/Slot.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright © 2026 RTAkland - * Author: RTAkland - * Date: 2026/9/5 - */ - - -package cn.rtast.libmc.protocol.protocol.game.inventory - -import cn.rtast.libmc.common.BytesBuffer -import cn.rtast.libmc.common.readPrefixedByteArray -import cn.rtast.libmc.common.readVarInt -import cn.rtast.libmc.common.writeVarInt - -public data class Slot( - val count: Int, - val itemId: Int?, - val componentsToAdd: List, - val componentsToRemove: List, -) { - public data class DataComponentToAdd(val typeId: Int, val data: ByteArray) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as DataComponentToAdd - - if (typeId != other.typeId) return false - if (!data.contentEquals(other.data)) return false - - return true - } - - override fun hashCode(): Int { - var result = typeId - result = 31 * result + data.contentHashCode() - return result - } - } - - public val isEmpty: Boolean get() = count <= 0 - - public companion object { - /** - * air - */ - public val EMPTY: Slot = Slot( - count = 0, - itemId = null, - componentsToAdd = emptyList(), - componentsToRemove = emptyList() - ) - } -} - -internal fun BytesBuffer.writeSlot(value: Slot) { - if (value.isEmpty) { - writeVarInt(0) - return - } - val itemId = requireNotNull(value.itemId) { "itemId must not be null when slot is not empty" } - writeVarInt(value.count) - writeVarInt(itemId) - writeVarInt(value.componentsToAdd.size) - writeVarInt(value.componentsToRemove.size) - for ((typeId, data) in value.componentsToAdd) { - writeVarInt(typeId) - writeBytes(data) - } - for (typeId in value.componentsToRemove) writeVarInt(typeId) -} - -internal fun BytesBuffer.readSlot(): Slot { - val count = readVarInt() - if (count <= 0) return Slot.EMPTY - val itemId = readVarInt() - val componentsToAddCount = readVarInt() - val componentsToRemoveCount = readVarInt() - val componentsToAdd = ArrayList(componentsToAddCount) - repeat(componentsToAddCount) { - val typeId = readVarInt() - val data = readPrefixedByteArray() - componentsToAdd.add(Slot.DataComponentToAdd(typeId, data)) - } - val componentsToRemove = ArrayList(componentsToRemoveCount) - repeat(componentsToRemoveCount) { componentsToRemove.add(readVarInt()) } - return Slot( - count = count, - itemId = itemId, - componentsToAdd = componentsToAdd, - componentsToRemove = componentsToRemove - ) -} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/session/AuthenticationProvider.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/session/AuthenticationProvider.kt new file mode 100644 index 0000000..4b672b7 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/session/AuthenticationProvider.kt @@ -0,0 +1,12 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/7 + */ + + +package cn.rtast.libmc.protocol.session + +public fun interface AuthenticationProvider { + public suspend fun joinServer(url: String, accessToken: String, uuid: String, serverIdHash: String) +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.kt new file mode 100644 index 0000000..3037cc8 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.kt @@ -0,0 +1,10 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/6 + */ + + +package cn.rtast.libmc.protocol.util + +public expect fun generateRandom16Bytes(): ByteArray \ No newline at end of file diff --git a/libmc-protocol/src/commonTest/kotlin/test/TestClient.kt b/libmc-protocol/src/commonTest/kotlin/test/TestClient.kt index 6d91183..d59a939 100644 --- a/libmc-protocol/src/commonTest/kotlin/test/TestClient.kt +++ b/libmc-protocol/src/commonTest/kotlin/test/TestClient.kt @@ -12,12 +12,21 @@ import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundAwardStatistic import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import kotlin.test.Test +import kotlin.uuid.Uuid class TestClient { @Test fun `test client`() = runTest { - val cli = createMinecraftClient("127.0.0.1", 25565, "123") + val cli = createMinecraftClient( + "127.0.0.1", + 25565, + "RTAkland", + Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"), + null + ) { +// rsaEncryptor = RSA1024Encryptor { data, sharedKey -> } + } cli.launch { cli.connect() } cli.on { println(it) } while (true) { diff --git a/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.jvm.kt b/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.jvm.kt new file mode 100644 index 0000000..a9bcfc9 --- /dev/null +++ b/libmc-protocol/src/jvmMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.jvm.kt @@ -0,0 +1,18 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/6 + */ + + +package cn.rtast.libmc.protocol.util + +import java.security.SecureRandom + +private val secureRandom = SecureRandom() + +public actual fun generateRandom16Bytes(): ByteArray { + val bytes = ByteArray(16) + secureRandom.nextBytes(bytes) + return bytes +} \ No newline at end of file diff --git a/libmc-protocol/src/jvmTest/kotlin/test/JvmAesCipher.kt b/libmc-protocol/src/jvmTest/kotlin/test/JvmAesCipher.kt new file mode 100644 index 0000000..68f2d99 --- /dev/null +++ b/libmc-protocol/src/jvmTest/kotlin/test/JvmAesCipher.kt @@ -0,0 +1,31 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/7 + */ + + +package test + +import cn.rtast.libmc.protocol.crypto.NetworkCipher +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +class JvmAesCipher(sharedKey: ByteArray) : NetworkCipher { + private val encryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply { + init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey)) + } + + private val decryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply { + init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey)) + } + + override fun encrypt(buffer: ByteArray, offset: Int, length: Int) { + encryptCipher.update(buffer, offset, length, buffer, offset) + } + + override fun decrypt(buffer: ByteArray, offset: Int, length: Int) { + decryptCipher.update(buffer, offset, length, buffer, offset) + } +} \ No newline at end of file diff --git a/libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt b/libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt new file mode 100644 index 0000000..bb3c7b1 --- /dev/null +++ b/libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt @@ -0,0 +1,125 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/7 + */ + + +package test + +import cn.rtast.libmc.protocol.client.createMinecraftClient +import cn.rtast.libmc.protocol.crypto.RSA1024Encryptor +import cn.rtast.libmc.protocol.crypto.Sha1Hasher +import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundLoginSuccessPacket +import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundSystemChatMessagePacket +import cn.rtast.libmc.protocol.session.AuthenticationProvider +import cn.rtast.libmc.protocol.util.generateOfflineUuid +import kotlinx.coroutines.launch +import org.junit.Test +import java.io.File +import java.math.BigInteger +import java.net.HttpURLConnection +import java.net.URL +import java.security.KeyFactory +import java.security.MessageDigest +import java.security.spec.X509EncodedKeySpec +import javax.crypto.Cipher +import kotlin.uuid.Uuid + + +class TestClientTestInJvm { + + val accessToken = File("src/jvmTest/resources/accessToken.txt").readText() + + fun encrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray { + val keySpec = X509EncodedKeySpec(publicKeyBytes) + val keyFactory = KeyFactory.getInstance("RSA") + val publicKey = keyFactory.generatePublic(keySpec) + val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding") + cipher.init(Cipher.ENCRYPT_MODE, publicKey) + return cipher.doFinal(data) + } + + fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String { + val serverIdBytes: ByteArray = serverId.encodeToByteArray() + for (b in serverIdBytes) { + require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" } + } + val data = ByteArray(serverIdBytes.size + secretKey.size + publicKey.size) + System.arraycopy(serverIdBytes, 0, data, 0, serverIdBytes.size) + System.arraycopy(secretKey, 0, data, serverIdBytes.size, secretKey.size) + System.arraycopy(publicKey, 0, data, serverIdBytes.size + secretKey.size, publicKey.size) + val digest = MessageDigest.getInstance("SHA-1") + val hash = digest.digest(data) + return BigInteger(hash).toString(16) + } + + @Test + fun `test client`() { + val cli = createMinecraftClient( + "127.0.0.1", + 25565, + "RTAkland", +// generateOfflineUuid("RTAkland"), + Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"), +// null, + accessToken + ) { + rsaEncryptor = RSA1024Encryptor { key, data -> encrypt(key, data) } + sha1Hasher = + Sha1Hasher { serverId, secretKey, publicKey -> minecraftServerIdHash(serverId, secretKey, publicKey) } + cipherFactory = { key -> JvmAesCipher(key) } + + authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> + val connection = URL(url).openConnection() as HttpURLConnection + connection.requestMethod = "POST" + connection.doOutput = true + connection.setRequestProperty("Content-Type", "application/json") + connection.getOutputStream() + .use { it.write("{\"accessToken\":\"$accessToken\", \"selectedProfile\":\"$uuid\", \"serverId\":\"$serverIdHash\"}".encodeToByteArray()) } + connection.disconnect() + } + } + cli.on { println(it) } + cli.on { println(it) } + cli.launch { cli.connect() } + while (true) { + } + } + + @Test + fun `test client offline mode`() { + val cli = createMinecraftClient( + "127.0.0.1", + 25566, + "11", + generateOfflineUuid("11"), + null, + ) { + rsaEncryptor = RSA1024Encryptor { key, data -> encrypt(key, data) } + sha1Hasher = + Sha1Hasher { serverId, secretKey, publicKey -> + minecraftServerIdHash( + serverId, + secretKey, + publicKey + ) + } + cipherFactory = { key -> JvmAesCipher(key) } +// authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> +// val connection = URL(url).openConnection() as HttpURLConnection +// connection.requestMethod = "POST" +// connection.doOutput = true +// connection.setRequestProperty("Content-Type", "application/json") +// connection.getOutputStream() +// .use { it.write("{\"accessToken\":\"$accessToken\", \"selectedProfile\":\"$uuid\", \"serverId\":\"$serverIdHash\"}".encodeToByteArray()) } +// connection.disconnect() +// } + } +// cli.on { println(it) } +// cli.on { println(it) } + cli.launch { cli.connect() } + while (true) { + } + } +} \ No newline at end of file diff --git a/libmc-protocol/src/linuxMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.linux.kt b/libmc-protocol/src/linuxMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.linux.kt new file mode 100644 index 0000000..ccc4717 --- /dev/null +++ b/libmc-protocol/src/linuxMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.linux.kt @@ -0,0 +1,33 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/7 + */ + + +@file:OptIn(ExperimentalForeignApi::class) + +package cn.rtast.libmc.protocol.util + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned +import platform.posix.O_RDONLY +import platform.posix.close +import platform.posix.open +import platform.posix.read + +public actual fun generateRandom16Bytes(): ByteArray { + val bytes = ByteArray(16) + val fd = open("/dev/urandom", O_RDONLY) + if (fd < 0) throw IllegalStateException("Failed to open /dev/urandom via POSIX open") + try { + bytes.usePinned { pinned -> + val readBytes = read(fd, pinned.addressOf(0), 16u) + if (readBytes < 16L) throw IllegalStateException("Failed to read 16 bytes, read count: $readBytes") + } + } finally { + close(fd) + } + return bytes +} \ No newline at end of file diff --git a/libmc-protocol/src/mingwMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.mingw.kt b/libmc-protocol/src/mingwMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.mingw.kt new file mode 100644 index 0000000..04dc976 --- /dev/null +++ b/libmc-protocol/src/mingwMain/kotlin/cn/rtast/libmc/protocol/util/aes_shared_key.mingw.kt @@ -0,0 +1,30 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/6 + */ + +@file:OptIn(ExperimentalForeignApi::class) + +package cn.rtast.libmc.protocol.util + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.usePinned +import platform.windows.BCRYPT_USE_SYSTEM_PREFERRED_RNG +import platform.windows.BCryptGenRandom + +public actual fun generateRandom16Bytes(): ByteArray { + val bytes = ByteArray(16) + bytes.usePinned { pinned -> + val status = BCryptGenRandom( + hAlgorithm = null, + pbBuffer = pinned.addressOf(0).reinterpret(), + cbBuffer = 16u, + dwFlags = BCRYPT_USE_SYSTEM_PREFERRED_RNG.toUInt() + ) + if (status != 0) throw IllegalStateException("BCryptGenRandom failed with status: $status") + } + return bytes +} \ No newline at end of file diff --git a/libmc-rconlib/build.gradle.kts b/libmc-rconlib/build.gradle.kts index fe2882f..5418968 100644 --- a/libmc-rconlib/build.gradle.kts +++ b/libmc-rconlib/build.gradle.kts @@ -8,8 +8,6 @@ kotlin { linuxArm64() macosArm64() mingwX64() - iosArm64() - iosSimulatorArm64() jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } sourceSets { diff --git a/libmc-snbt/build.gradle.kts b/libmc-snbt/build.gradle.kts index fe2882f..5418968 100644 --- a/libmc-snbt/build.gradle.kts +++ b/libmc-snbt/build.gradle.kts @@ -8,8 +8,6 @@ kotlin { linuxArm64() macosArm64() mingwX64() - iosArm64() - iosSimulatorArm64() jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } sourceSets { diff --git a/settings.gradle.kts b/settings.gradle.kts index 190c36c..d95b7d4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,7 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } + rootProject.name = "libmc" includeSubModule(":common")