Implement join online-mode server process

This commit is contained in:
2026-09-07 03:15:53 +08:00
parent a88888ba4d
commit 5dc4e8bc0d
44 files changed
+696 -699

No files matched your search

+1
View File
@@ -47,3 +47,4 @@ bin/
.idea/ .idea/
.kotlin/ .kotlin/
**/build/* **/build/*
/libmc-protocol/src/jvmTest/resources/accessToken.txt
+9 -11
View File
@@ -9,8 +9,6 @@ A lightweight minecraft client-side protocol library and related library, includ
- MingwX64 - MingwX64
- LinuxArm64 - LinuxArm64
- LinuxX64 - LinuxX64
- iosArm64
- iosSimulatorArm64
- MacosArm64 - MacosArm64
# libmc-protocol # libmc-protocol
@@ -19,10 +17,9 @@ A lightweight minecraft client-side protocol library and related library, includ
# Minecraft Protocol Library Status & Roadmap # Minecraft Protocol Library Status & Roadmap
## Unimplemented features ## Implementation details
- [ ] **Online Mode Authentication & Encryption**: Full Mojang/Microsoft auth pipeline, JoinServer request, and AES-CFB8 - [ ] **Online Mode Authentication & Encryption**: See [Implement Encryption](docs/implement-encryption.md)
stream cipher stream wrapper.
- [ ] **Structured `TextComponent` Parser**: Rich Chat Component AST decoder (currently falling back to raw - [ ] **Structured `TextComponent` Parser**: Rich Chat Component AST decoder (currently falling back to raw
`NBTCompound`). `NBTCompound`).
- [ ] **Command Tree Parser (0x10)**: Full binary graph decoder for brigadier nodes, argument types, and suggestions. - [ ] **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. - [ ] **Light Engine Update (0x30)**: Sky & Block light nibble array parser.
- [ ] **Explosion Event Decoder (0x24)**: Knockback vectors and destroyed block offsets array. - [ ] **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. - [ ] **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`) - [x] `0x0F` Command Suggestions (`ClientboundCommandSuggestionsPacket`)
- [ ] `0x10` Commands (`ClientboundCommandsPacket`) - [ ] `0x10` Commands (`ClientboundCommandsPacket`)
- [x] `0x11` Container Close (`ClientboundContainerClosePacket`) - [x] `0x11` Container Close (`ClientboundContainerClosePacket`)
- [x] `0x12` Container Set Content (`ClientboundContainerSetContentPacket`) - [ ] `0x12` Container Set Content (`ClientboundContainerSetContentPacket`)
- [x] `0x13` Container Set Data (`ClientboundContainerSetDataPacket`) - [x] `0x13` Container Set Data (`ClientboundContainerSetDataPacket`)
- [x] `0x14` Container Set Slot (`ClientboundContainerSetSlotPacket`) - [ ] `0x14` Container Set Slot (`ClientboundContainerSetSlotPacket`)
- [x] `0x15` Cookie Request (`ClientboundCookieRequestPacket`) - [x] `0x15` Cookie Request (`ClientboundCookieRequestPacket`)
- [x] `0x16` Cooldown (`ClientboundCooldownPacket`) - [x] `0x16` Cooldown (`ClientboundCooldownPacket`)
- [x] `0x17` Custom Chat Completions (`ClientboundCustomChatCompletionsPacket`) - [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] `0x5D` Set Camera (`ClientboundSetCameraPacket`)
- [x] `0x5E` Set Center Chunk (`ClientboundSetCenterChunkPacket`) - [x] `0x5E` Set Center Chunk (`ClientboundSetCenterChunkPacket`)
- [x] `0x5F` Set Render Distance (`ClientboundSetRenderDistancePacket`) - [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] `0x61` Set Default Spawn Position (`ClientboundSetDefaultSpawnPositionPacket`)
- [x] `0x62` Set Display Objective (`ClientboundSetDisplayObjectivePacket`) - [x] `0x62` Set Display Objective (`ClientboundSetDisplayObjectivePacket`)
- [ ] `0x63` Set Entity Metadata (`ClientboundSetEntityMetadataPacket`) - [ ] `0x63` Set Entity Metadata (`ClientboundSetEntityMetadataPacket`)
- [x] `0x64` Link Entities (`ClientboundLinkEntitiesPacket`) - [x] `0x64` Link Entities (`ClientboundLinkEntitiesPacket`)
- [x] `0x65` Set Entity Velocity (`ClientboundSetEntityVelocityPacket`) - [x] `0x65` Set Entity Velocity (`ClientboundSetEntityVelocityPacket`)
- [x] `0x66` Set Equipment (`ClientboundSetEquipmentPacket`) - [ ] `0x66` Set Equipment (`ClientboundSetEquipmentPacket`)
- [x] `0x67` Set Experience (`ClientboundSetExperiencePacket`) - [x] `0x67` Set Experience (`ClientboundSetExperiencePacket`)
- [x] `0x68` Set Health (`ClientboundSetHealthPacket`) - [x] `0x68` Set Health (`ClientboundSetHealthPacket`)
- [x] `0x69` Set Carried Item (`ClientboundSetCarriedItemPacket`) - [x] `0x69` Set Carried Item (`ClientboundSetCarriedItemPacket`)
- [x] `0x6A` Update Objective (`ClientboundUpdateObjectivePacket`) - [x] `0x6A` Update Objective (`ClientboundUpdateObjectivePacket`)
- [x] `0x6B` Set Passengers (`ClientboundSetPassengersPacket`) - [x] `0x6B` Set Passengers (`ClientboundSetPassengersPacket`)
- [x] `0x6C` Set Player Inventory Slot (`ClientboundSetPlayerInventorySlotPacket`) - [ ] `0x6C` Set Player Inventory Slot (`ClientboundSetPlayerInventorySlotPacket`)
- [ ] `0x6D` Set Player Team (`ClientboundSetPlayerTeamPacket`) - [ ] `0x6D` Set Player Team (`ClientboundSetPlayerTeamPacket`)
- [x] `0x6E` Update Score (`ClientboundUpdateScorePacket`) - [x] `0x6E` Update Score (`ClientboundUpdateScorePacket`)
- [x] `0x6F` Set Simulation Distance (`ClientboundSetSimulationDistancePacket`) - [x] `0x6F` Set Simulation Distance (`ClientboundSetSimulationDistancePacket`)
+30
View File
@@ -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.) */
}
}
+9 -3
View File
@@ -3,16 +3,22 @@ kotlin = "2.4.10"
kotlinx-io = "0.9.1" kotlinx-io = "0.9.1"
ktor-network = "3.5.2" ktor-network = "3.5.2"
coroutines-test = "1.11.0" coroutines-test = "1.11.0"
kotlinx-serialization = "1.11.0" #kotlinx-serialization = "1.11.0"
kotlinx-coroutines = "1.11.0" kotlinx-coroutines = "1.11.0"
ktor-core = "3.5.2"
[libraries] [libraries]
kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" } kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" }
ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor-network" } 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-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-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-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" } 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] [plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
-2
View File
@@ -8,8 +8,6 @@ kotlin {
linuxArm64() linuxArm64()
macosArm64() macosArm64()
mingwX64() mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets { sourceSets {
@@ -7,18 +7,18 @@
package cn.rtast.libmc.common package cn.rtast.libmc.common
public expect class ReadChannel { public expect open class ReadChannel() {
public fun readByte(): Byte public open fun readByte(): Byte
public fun readShort(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Short public open fun readShort(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Short
public fun readInt(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Int public open fun readInt(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Int
public fun readLong(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Long public open fun readLong(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Long
public fun readBytes(length: Int): ByteArray public open fun readBytes(length: Int): ByteArray
public fun readFully(out: ByteArray, start: Int = 0, end: Int = out.size) public open fun readFully(out: ByteArray, start: Int = 0, end: Int = out.size)
} }
public expect class WriteChannel { public expect open class WriteChannel() {
public fun writeFully(value: ByteArray, startIndex: Int = 0, endIndex: Int = value.size) public open fun writeFully(value: ByteArray, startIndex: Int = 0, endIndex: Int = value.size)
public fun flush() public open fun flush()
} }
public fun ReadChannel.readVarInt(): Int { public fun ReadChannel.readVarInt(): Int {
@@ -4,53 +4,55 @@
* Date: 2026/9/3 * Date: 2026/9/3
*/ */
package cn.rtast.libmc.common package cn.rtast.libmc.common
import java.io.EOFException import java.io.EOFException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream 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 constructor(inputStream: InputStream) : this() {
public actual fun readBytes(length: Int): ByteArray = _inputStream.readNBytes(length) 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 var bytesRead = 0
val length = end - start val length = end - start
while (bytesRead < length) { while (bytesRead < length) {
val read = _inputStream.read(out, start + bytesRead, length - bytesRead) 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 bytesRead += read
} }
} }
public actual fun readShort(endian: ByteOrder): Short = readBytes(2).toShort(endian) public actual open fun readByte(): Byte {
public actual fun readInt(endian: ByteOrder): Int = readBytes(4).toInt(endian) val buf = ByteArray(1)
public actual fun readLong(endian: ByteOrder): Long = readBytes(8).toLong(endian) readFully(buf, 0, 1)
return buf[0]
} }
public actual class WriteChannel { public actual open fun readBytes(length: Int): ByteArray {
private val _outputStream: OutputStream val bytes = ByteArray(length)
readFully(bytes, 0, length)
return bytes
}
public constructor(outputStream: OutputStream) { 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 open class WriteChannel public actual constructor() {
private lateinit var _outputStream: OutputStream
public constructor(outputStream: OutputStream) : this() {
_outputStream = outputStream _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) _outputStream.write(value, startIndex, endIndex - startIndex)
public actual fun flush(): Unit = _outputStream.flush() public actual open 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
} }
@@ -10,38 +10,53 @@ import io.ktor.utils.io.*
import io.ktor.utils.io.bits.* import io.ktor.utils.io.bits.*
import kotlinx.coroutines.runBlocking 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() } private lateinit var _readChannel: ByteReadChannel
public actual fun readBytes(length: Int): ByteArray = runBlocking { _readChannel.readByteArray(length) }
public actual fun readFully(out: ByteArray, start: Int, end: Int): Unit = 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) } runBlocking { _readChannel.readFully(out, start, end) }
public actual fun readShort(endian: ByteOrder): Short = runBlocking { public actual open fun readShort(endian: ByteOrder): Short {
val v = _readChannel.readShort() val bytes = readBytes(2)
if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder() 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 { public actual open fun readInt(endian: ByteOrder): Int {
val v = _readChannel.readInt() val bytes = readBytes(4)
if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder() 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 { public actual open fun readLong(endian: ByteOrder): Long {
val v = _readChannel.readLong() val bytes = readBytes(8)
if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder() 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 { public actual open class WriteChannel public actual constructor() {
private val _writeChannel: ByteWriteChannel private lateinit var _writeChannel: ByteWriteChannel
public constructor(writeChannel: ByteWriteChannel) { public constructor(writeChannel: ByteWriteChannel) : this() {
_writeChannel = writeChannel _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) } runBlocking { _writeChannel.writeFully(value, startIndex, endIndex) }
public actual fun flush(): Unit = runBlocking { _writeChannel.flush() } public actual open fun flush(): Unit = runBlocking { _writeChannel.flush() }
} }
-2
View File
@@ -8,8 +8,6 @@ kotlin {
linuxArm64() linuxArm64()
macosArm64() macosArm64()
mingwX64() mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets { sourceSets {
-2
View File
@@ -8,8 +8,6 @@ kotlin {
linuxArm64() linuxArm64()
macosArm64() macosArm64()
mingwX64() mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets { sourceSets {
+5 -6
View File
@@ -1,9 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinx.serialization)
}
kotlin { kotlin {
explicitApi() explicitApi()
withSourcesJar() withSourcesJar()
@@ -12,8 +8,6 @@ kotlin {
linuxArm64() linuxArm64()
macosArm64() macosArm64()
mingwX64() mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets { sourceSets {
@@ -31,5 +25,10 @@ kotlin {
implementation(kotlin("test")) implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test) implementation(libs.kotlinx.coroutines.test)
} }
jvmTest.dependencies {
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
}
} }
} }
@@ -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
}
@@ -7,6 +7,8 @@
package cn.rtast.libmc.protocol.client package cn.rtast.libmc.protocol.client
import cn.rtast.libmc.common.LibMCContext 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.InternalPacketDispatcher
import cn.rtast.libmc.protocol.event.PacketEventDispatcher import cn.rtast.libmc.protocol.event.PacketEventDispatcher
import cn.rtast.libmc.protocol.network.NetworkChannel import cn.rtast.libmc.protocol.network.NetworkChannel
@@ -24,21 +26,33 @@ public class MinecraftClient internal constructor(
private val host: String, private val host: String,
private val port: Int = 25565, private val port: Int = 25565,
private val username: String, private val username: String,
private val uuid: Uuid, internal val uuid: Uuid,
internal val accessToken: String?,
context: LibMCContext, context: LibMCContext,
parentJob: Job?, parentJob: Job?,
private val ioDispatcher: CoroutineDispatcher, private val ioDispatcher: CoroutineDispatcher,
cryptoContext: ProtocolContext,
) : PacketEventDispatcher(), CoroutineScope { ) : PacketEventDispatcher(), CoroutineScope {
internal val rsa1024Encryptor = cryptoContext.rsaEncryptor
internal val serverIdHasher = cryptoContext.sha1Hasher
internal val authProvider = cryptoContext.authProvider
internal val stateMachine = ClientStateMachine() 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 val clientJob = SupervisorJob(parentJob)
private var listenJob: Job? = null private var listenJob: Job? = null
public val isOnlineMode: Boolean get() = accessToken != null
public val transactionManager: TransactionIdManager = TransactionIdManager() public val transactionManager: TransactionIdManager = TransactionIdManager()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = clientJob + ioDispatcher + CoroutineName("LibMC-MinecraftClient-$username") get() = clientJob + ioDispatcher + CoroutineName("LibMC-MinecraftClient-$username")
@@ -52,10 +66,14 @@ public class MinecraftClient internal constructor(
networkChannel.sendPacket(ServerboundLoginStartPacket(username, uuid)) networkChannel.sendPacket(ServerboundLoginStartPacket(username, uuid))
} }
public fun setCompression(threshold: Int): Unit = networkChannel.setCompression(threshold)
private fun startListening() { private fun startListening() {
listenJob = launch { listenJob = launch {
try { try {
while (isActive) internalPacketDispatcher.handleIncomingPackets(networkChannel.readNextPacket()) while (isActive) {
internalPacketDispatcher.handleIncomingPackets(networkChannel.readNextPacket())
}
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
if (isActive) { if (isActive) {
@@ -75,10 +93,25 @@ public class MinecraftClient internal constructor(
public fun createMinecraftClient( public fun createMinecraftClient(
host: String, host: String,
port: Int, port: Int = 25565,
username: String, username: String,
uuid: Uuid = generateOfflineUuid(username), uuid: Uuid = generateOfflineUuid(username),
accessToken: String?,
context: LibMCContext = LibMCContext(), context: LibMCContext = LibMCContext(),
parentJob: Job? = null, parentJob: Job? = null,
ioDispatcher: CoroutineDispatcher = Dispatchers.IO, ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
): MinecraftClient = MinecraftClient(host, port, username, uuid, context, parentJob, ioDispatcher) 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
)
}
@@ -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)
}
@@ -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
}
@@ -10,22 +10,25 @@ package cn.rtast.libmc.protocol.event
import cn.rtast.libmc.common.packet.MinecraftPacket import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.protocol.client.MinecraftClient import cn.rtast.libmc.protocol.client.MinecraftClient
import cn.rtast.libmc.protocol.packet.configuration.clientbound.* 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.*
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.login.clientbound.* 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.login.serverbound.ServerboundLoginAcknowledgedPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.* 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.ServerboundConfigurationAcknowledgedPacket
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundKeepAlivePlayPacket import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundKeepAlivePlayPacket
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundPongPlayPacket import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundPongPlayPacket
import cn.rtast.libmc.protocol.protocol.state.ProtocolState 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) { public class InternalPacketDispatcher(
suspend fun dispatchEvent(packet: MinecraftPacket) = client.dispatch(packet) 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) this.dispatchEvent(packet)
when (packet) { when (packet) {
is ClientboundLoginPacket -> this.handleLoginPackets(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) { when (packet) {
is ClientboundDisconnectLoginPacket -> { is ClientboundDisconnectLoginPacket -> {
println("Login denied: ${packet.reason}") println("Login denied: ${packet.reason}")
@@ -47,25 +50,30 @@ internal class InternalPacketDispatcher(private val client: MinecraftClient) {
client.stateMachine.transitionTo(ProtocolState.CONFIGURATION) 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) { private fun handleConfigurationPackets(packet: ClientboundConfigurationPacket) {
when (packet) { when (packet) {
is ClientboundCookieRequestPacket -> { is ClientboundDisconnectConfigurationPacket -> println("Configuration disconnected: ${packet.reason}")
// TODO
}
is ClientboundCustomPayloadPacket -> {
// TODO
}
is ClientboundDisconnectConfigurationPacket -> {
println("Configuration disconnected: ${packet.reason}")
}
ClientboundFinishConfigurationPacket -> { ClientboundFinishConfigurationPacket -> {
client.networkChannel.sendPacket(ServerboundAckFinishConfigurationPacket) client.networkChannel.sendPacket(ServerboundAckFinishConfigurationPacket)
client.stateMachine.transitionTo(ProtocolState.PLAY) client.stateMachine.transitionTo(ProtocolState.PLAY)
@@ -79,23 +87,11 @@ internal class InternalPacketDispatcher(private val client: MinecraftClient) {
ServerboundPongConfigurationPacket(packet.id) ServerboundPongConfigurationPacket(packet.id)
) )
is ClientboundSelectKnownPacksPacket -> { is ClientboundSelectKnownPacksPacket -> client.networkChannel.sendPacket(
client.networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList())) // TODO empty resource packs list ServerboundSelectKnownPacksPacket(emptyList())
} ) // TODO empty resource packs list
is ClientboundCodeOfConductPacket -> client.networkChannel.sendPacket(ServerboundAcceptCodeOfConductPacket)
is ClientboundAddResourcePackPacket -> {} else -> {}
ClientboundClearDialogPacket -> {}
is ClientboundCodeOfConductPacket -> {}
is ClientboundConfigurationShowDialogPacket -> {}
is ClientboundCustomReportDetailsPacket -> {}
is ClientboundRegistryDataPacket -> {}
is ClientboundRemoveResourcePackPacket -> {}
ClientboundResetChatPacket -> {}
is ClientboundServerLinksPacket -> {}
is ClientboundStoreCookiePacket -> {}
is ClientboundTransferPacket -> {}
is ClientboundUpdateEnabledFeaturesPacket -> {}
is ClientboundUpdateTagsPacket -> {}
} }
} }
@@ -105,34 +101,14 @@ internal class InternalPacketDispatcher(private val client: MinecraftClient) {
is ClientboundKeepAlivePlayPacket -> client.networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(id = packet.id)) is ClientboundKeepAlivePlayPacket -> client.networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(id = packet.id))
is ClientboundLoginPlayPacket -> println("Successfully joined world Entity ID: ${packet.entityId}") is ClientboundLoginPlayPacket -> println("Successfully joined world Entity ID: ${packet.entityId}")
is ClientboundPingPacket -> client.networkChannel.sendPacket(ServerboundPongPlayPacket(packet.id)) is ClientboundPingPacket -> client.networkChannel.sendPacket(ServerboundPongPlayPacket(packet.id))
is ClientboundPlayerChatMessagePacket -> { is ClientboundPlayerChatMessagePacket -> println("[Player Chat Message] ${packet.message}")
println("Received player chat message $packet")
// TODO
}
ClientboundStartConfigurationPacket -> { ClientboundStartConfigurationPacket -> {
client.networkChannel.sendPacket(ServerboundConfigurationAcknowledgedPacket) client.networkChannel.sendPacket(ServerboundConfigurationAcknowledgedPacket)
client.stateMachine.transitionTo(ProtocolState.CONFIGURATION) client.stateMachine.transitionTo(ProtocolState.CONFIGURATION)
} }
is ClientboundSystemChatMessagePacket -> {} is ClientboundSystemChatMessagePacket -> println("[System message] ${packet.content}")
is ClientboundAcknowledgeBlockChangePacket -> {} else -> println(packet)
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()
} }
} }
} }
@@ -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()
}
}
@@ -9,27 +9,24 @@ package cn.rtast.libmc.protocol.network
import cn.rtast.libmc.common.* import cn.rtast.libmc.common.*
import cn.rtast.libmc.common.packet.MinecraftPacket import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.protocol.client.ClientStateMachine import cn.rtast.libmc.protocol.client.ClientStateMachine
import cn.rtast.libmc.protocol.crypto.NetworkCipher
import cn.rtast.libmc.protocol.protocol.GameProtocols import cn.rtast.libmc.protocol.protocol.GameProtocols
import kotlin.concurrent.Volatile import kotlin.concurrent.Volatile
internal class NetworkChannel( internal class NetworkChannel(
private val host: String, host: String,
private val port: Int, port: Int,
private val context: LibMCContext, context: LibMCContext,
private val stateMachine: ClientStateMachine, private val stateMachine: ClientStateMachine,
cipherProvider: (ByteArray) -> NetworkCipher,
) { ) {
private var socket: Socket? = null val session: NetworkSession = NetworkSession(host, port, context, cipherProvider)
private var readChannel: ReadChannel? = null
private var writeChannel: WriteChannel? = null
@Volatile @Volatile
private var threshold = -1 private var threshold = -1
fun connect() { fun connect() {
val sk = Socket(host, port, context) session.connect()
this.socket = sk
this.readChannel = sk.openReadChannel()
this.writeChannel = sk.openWriteChannel()
} }
fun setCompression(threshold: Int) { fun setCompression(threshold: Int) {
@@ -37,9 +34,8 @@ internal class NetworkChannel(
} }
fun readNextPacket(): MinecraftPacket { fun readNextPacket(): MinecraftPacket {
val channel = requireNotNull(readChannel) { "ReadChannel not connected" } val packetLength = session.readVarInt()
val packetLength = channel.readVarInt() val rawFrameBytes = session.readBytes(packetLength)
val rawFrameBytes = channel.readBytes(packetLength)
val frameBuf = rawFrameBytes.wrap() val frameBuf = rawFrameBytes.wrap()
val payloadBuf = if (threshold < 0) frameBuf else { val payloadBuf = if (threshold < 0) frameBuf else {
val dataLength = frameBuf.readVarInt() val dataLength = frameBuf.readVarInt()
@@ -48,14 +44,12 @@ internal class NetworkChannel(
} }
val currentState = stateMachine.currentState val currentState = stateMachine.currentState
val packetId = payloadBuf.readVarInt() val packetId = payloadBuf.readVarInt()
val packet = GameProtocols.clientboundGameProtocols return GameProtocols.clientboundGameProtocols
.getRegistry(currentState) .getRegistry(currentState)
.decodePacket(packetId, payloadBuf) .decodePacket(packetId, payloadBuf)
return packet
} }
fun sendPacket(packet: MinecraftPacket) { fun sendPacket(packet: MinecraftPacket) {
val channel = requireNotNull(writeChannel) { "WriteChannel not connected" }
val uncompressedBodyBuf = BytesBuffer() val uncompressedBodyBuf = BytesBuffer()
GameProtocols.serverboundGameProtocols GameProtocols.serverboundGameProtocols
.getRegistry(stateMachine.currentState) .getRegistry(stateMachine.currentState)
@@ -78,11 +72,10 @@ internal class NetworkChannel(
frameBuffer.writeVarInt(contentBuf.size) frameBuffer.writeVarInt(contentBuf.size)
frameBuffer.writeBuffer(contentBuf) frameBuffer.writeBuffer(contentBuf)
} }
channel.writeFully(frameBuffer.toByteArray()) session.writeFully(frameBuffer.toByteArray())
channel.flush()
} }
fun close() { fun close() {
socket?.close() session.close()
} }
} }
@@ -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()
}
}
@@ -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<Slot>,
val carriedItem: Slot,
) : ClientboundPlayPacket {
internal companion object Codec : PacketCodec<ClientboundContainerSetContentPacket> {
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<Slot>(slotDataCount)
repeat(slotDataCount) { slotData.add(buffer.readSlot()) }
val carriedItem = buffer.readSlot()
return ClientboundContainerSetContentPacket(windowId, stateId, slotData, carriedItem)
}
}
}
@@ -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<ClientboundContainerSetSlotPacket> {
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)
}
}
}
@@ -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<ClientboundSetCursorItemPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundSetCursorItemPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundSetCursorItemPacket {
return ClientboundSetCursorItemPacket(buffer.readSlot())
}
}
}
@@ -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<EquipmentEntry>) :
MinecraftPacket {
internal companion object Codec : PacketCodec<ClientboundSetEquipmentPacket> {
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<EquipmentEntry>()
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)
}
}
}
@@ -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<ClientboundSetPlayerInventorySlotPacket> {
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)
}
}
}
@@ -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<ServerboundChangeContainerSlotStatePacket> {
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()
}
}
@@ -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<ChangedSlot>,
val carriedItem: HashedSlot,
) : MinecraftPacket {
public constructor(
windowId: Int,
stateId: Int,
slot: Short,
mouseButton: ContainerButton.Mouse,
changedSlots: List<ChangedSlot>,
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<ChangedSlot>,
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<ServerboundContainerClickPacket> {
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()
}
}
@@ -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<ServerboundSetCreativeModeSlotPacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundSetCreativeModeSlotPacket) {
buffer.writeShort(value.slot)
buffer.writeSlot(value.clickedItem)
}
override fun decode(buffer: BytesBuffer): ServerboundSetCreativeModeSlotPacket =
throw UnsupportedOperationException()
}
}
@@ -78,9 +78,9 @@ internal object GameProtocols {
register(0x0f, ClientboundCommandSuggestionsPacket) register(0x0f, ClientboundCommandSuggestionsPacket)
// register(0x10, ClientboundCommandsPacket) // register(0x10, ClientboundCommandsPacket)
register(0x11, ClientboundContainerClosePacket) register(0x11, ClientboundContainerClosePacket)
register(0x12, ClientboundContainerSetContentPacket) // register(0x12, ClientboundContainerSetContentPacket)
register(0x13, ClientboundContainerSetDataPacket) register(0x13, ClientboundContainerSetDataPacket)
register(0x14, ClientboundContainerSetSlotPacket) // register(0x14, ClientboundContainerSetSlotPacket)
register(0x15, ClientboundCookieRequestPacket) register(0x15, ClientboundCookieRequestPacket)
register(0x16, ClientboundCooldownPacket) register(0x16, ClientboundCooldownPacket)
register(0x17, ClientboundCustomChatCompletionsPacket) register(0x17, ClientboundCustomChatCompletionsPacket)
@@ -156,19 +156,19 @@ internal object GameProtocols {
register(0x5d, ClientboundSetCameraPacket) register(0x5d, ClientboundSetCameraPacket)
register(0x5e, ClientboundSetCenterChunkPacket) register(0x5e, ClientboundSetCenterChunkPacket)
register(0x5f, ClientboundSetRenderDistancePacket) register(0x5f, ClientboundSetRenderDistancePacket)
register(0x60, ClientboundSetCursorItemPacket) // register(0x60, ClientboundSetCursorItemPacket)
register(0x61, ClientboundSetDefaultSpawnPositionPacket) register(0x61, ClientboundSetDefaultSpawnPositionPacket)
register(0x62, ClientboundSetDisplayObjectivePacket) register(0x62, ClientboundSetDisplayObjectivePacket)
// register(0x63, ClientboundSetEntityMetadataPacket) // register(0x63, ClientboundSetEntityMetadataPacket)
register(0x64, ClientboundLinkEntitiesPacket) register(0x64, ClientboundLinkEntitiesPacket)
register(0x65, ClientboundSetEntityVelocityPacket) register(0x65, ClientboundSetEntityVelocityPacket)
register(0x66, ClientboundSetEquipmentPacket) // register(0x66, ClientboundSetEquipmentPacket)
register(0x67, ClientboundSetExperiencePacket) register(0x67, ClientboundSetExperiencePacket)
register(0x68, ClientboundSetHealthPacket) register(0x68, ClientboundSetHealthPacket)
register(0x69, ClientboundSetCarriedItemPacket) register(0x69, ClientboundSetCarriedItemPacket)
register(0x6a, ClientboundUpdateObjectivePacket) register(0x6a, ClientboundUpdateObjectivePacket)
register(0x6b, ClientboundSetPassengersPacket) register(0x6b, ClientboundSetPassengersPacket)
register(0x6c, ClientboundSetPlayerInventorySlotPacket) // register(0x6c, ClientboundSetPlayerInventorySlotPacket)
// register(0x6d, ClientboundSetPlayerTeamPacket) // register(0x6d, ClientboundSetPlayerTeamPacket)
register(0x6e, ClientboundUpdateScorePacket) register(0x6e, ClientboundUpdateScorePacket)
register(0x6f, ClientboundSetSimulationDistancePacket) register(0x6f, ClientboundSetSimulationDistancePacket)
@@ -249,9 +249,9 @@ internal object GameProtocols {
register(0x0f, ServerboundCommandSuggestionRequestPacket) register(0x0f, ServerboundCommandSuggestionRequestPacket)
register(0x10, ServerboundConfigurationAcknowledgedPacket) register(0x10, ServerboundConfigurationAcknowledgedPacket)
register(0x11, ServerboundContainerClickButtonPacket) register(0x11, ServerboundContainerClickButtonPacket)
register(0x12, ServerboundContainerClickPacket) // register(0x12, ServerboundContainerClickPacket)
register(0x13, ServerboundContainerClosePacket) register(0x13, ServerboundContainerClosePacket)
register(0x14, ServerboundChangeContainerSlotStatePacket) // register(0x14, ServerboundChangeContainerSlotStatePacket)
register(0x15, ServerboundCookieResponsePacket) register(0x15, ServerboundCookieResponsePacket)
register(0x16, ServerboundCustomPayloadPacket) register(0x16, ServerboundCustomPayloadPacket)
// register(0x17, ServerboundDebugSubscriptionRequestPacket) // register(0x17, ServerboundDebugSubscriptionRequestPacket)
@@ -287,7 +287,7 @@ internal object GameProtocols {
register(0x35, ServerboundSetCarriedItemPacket) register(0x35, ServerboundSetCarriedItemPacket)
register(0x36, ServerboundSetCommandBlockPacket) register(0x36, ServerboundSetCommandBlockPacket)
register(0x37, ServerboundSetCommandMinecartPacket) register(0x37, ServerboundSetCommandMinecartPacket)
register(0x38, ServerboundSetCreativeModeSlotPacket) // register(0x38, ServerboundSetCreativeModeSlotPacket)
register(0x39, ServerboundSetGameRulePacket) register(0x39, ServerboundSetGameRulePacket)
register(0x3a, ServerboundSetJigsawBlockPacket) register(0x3a, ServerboundSetJigsawBlockPacket)
register(0x3b, ServerboundSetStructureBlockPacket) register(0x3b, ServerboundSetStructureBlockPacket)
@@ -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<ChangedSlot> {
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)
}
}
}
@@ -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,
)
@@ -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
}
}
}
@@ -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<ComponentAddEntry>?,
val componentsToRemove: List<Int>?,
) {
public data class ComponentAddEntry(val typeId: Int, val dataHash: Int) {
internal companion object Codec : PacketCodec<ComponentAddEntry> {
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<HashedSlot> {
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)
}
}
}
@@ -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<DataComponentToAdd>,
val componentsToRemove: List<Int>,
) {
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<Slot.DataComponentToAdd>(componentsToAddCount)
repeat(componentsToAddCount) {
val typeId = readVarInt()
val data = readPrefixedByteArray()
componentsToAdd.add(Slot.DataComponentToAdd(typeId, data))
}
val componentsToRemove = ArrayList<Int>(componentsToRemoveCount)
repeat(componentsToRemoveCount) { componentsToRemove.add(readVarInt()) }
return Slot(
count = count,
itemId = itemId,
componentsToAdd = componentsToAdd,
componentsToRemove = componentsToRemove
)
}
@@ -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)
}
@@ -0,0 +1,10 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/6
*/
package cn.rtast.libmc.protocol.util
public expect fun generateRandom16Bytes(): ByteArray
@@ -12,12 +12,21 @@ import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundAwardStatistic
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.uuid.Uuid
class TestClient { class TestClient {
@Test @Test
fun `test client`() = runTest { 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.launch { cli.connect() }
cli.on<ClientboundAwardStatisticsPacket> { println(it) } cli.on<ClientboundAwardStatisticsPacket> { println(it) }
while (true) { while (true) {
@@ -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
}
@@ -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)
}
}
@@ -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<ClientboundSystemChatMessagePacket> { println(it) }
cli.on<ClientboundLoginSuccessPacket> { 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<ClientboundSystemChatMessagePacket> { println(it) }
// cli.on<ClientboundLoginSuccessPacket> { println(it) }
cli.launch { cli.connect() }
while (true) {
}
}
}
@@ -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
}
@@ -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
}
-2
View File
@@ -8,8 +8,6 @@ kotlin {
linuxArm64() linuxArm64()
macosArm64() macosArm64()
mingwX64() mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets { sourceSets {
-2
View File
@@ -8,8 +8,6 @@ kotlin {
linuxArm64() linuxArm64()
macosArm64() macosArm64()
mingwX64() mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets { sourceSets {
+1
View File
@@ -1,6 +1,7 @@
plugins { plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
} }
rootProject.name = "libmc" rootProject.name = "libmc"
includeSubModule(":common") includeSubModule(":common")