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

+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
}