Implement join online-mode server process
This commit is contained in:
44 files changed
+696
-699
No files matched your search
@@ -47,3 +47,4 @@ bin/
|
|||||||
.idea/
|
.idea/
|
||||||
.kotlin/
|
.kotlin/
|
||||||
**/build/*
|
**/build/*
|
||||||
|
/libmc-protocol/src/jvmTest/resources/accessToken.txt
|
||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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.) */
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" }
|
||||||
|
|||||||
@@ -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 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 {
|
public actual open class WriteChannel public actual constructor() {
|
||||||
private val _outputStream: OutputStream
|
private lateinit var _outputStream: OutputStream
|
||||||
|
|
||||||
public constructor(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() }
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+20
@@ -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
|
||||||
|
}
|
||||||
+41
-8
@@ -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)
|
||||||
|
}
|
||||||
+40
@@ -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
|
||||||
|
}
|
||||||
+38
-62
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-19
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+77
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
-38
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-32
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-23
@@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-39
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-26
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-27
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-73
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-26
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+8
-8
@@ -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)
|
||||||
|
|||||||
-28
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-13
@@ -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,
|
|
||||||
)
|
|
||||||
-33
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-65
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-93
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+12
@@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -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
|
||||||
|
}
|
||||||
+30
@@ -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
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,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")
|
||||||
|
|||||||
Reference in New Issue
Block a user