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/
.kotlin/
**/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
- 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`)
+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"
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" }
-2
View File
@@ -8,8 +8,6 @@ kotlin {
linuxArm64()
macosArm64()
mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets {
@@ -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 {
@@ -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()
}
@@ -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() }
}
-2
View File
@@ -8,8 +8,6 @@ kotlin {
linuxArm64()
macosArm64()
mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets {
-2
View File
@@ -8,8 +8,6 @@ kotlin {
linuxArm64()
macosArm64()
mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets {
+5 -6
View File
@@ -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)
}
}
}
@@ -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
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)
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.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)
}
}
}
@@ -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.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()
}
}
@@ -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(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)
@@ -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.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<ClientboundAwardStatisticsPacket> { println(it) }
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()
macosArm64()
mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets {
-2
View File
@@ -8,8 +8,6 @@ kotlin {
linuxArm64()
macosArm64()
mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets {
+1
View File
@@ -1,6 +1,7 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "libmc"
includeSubModule(":common")