Implement join online-mode server process
This commit is contained in:
44 files changed
+696
-699
No files matched your search
+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
|
||||
|
||||
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)
|
||||
}
|
||||
+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.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()
|
||||
}
|
||||
}
|
||||
+12
-19
@@ -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()
|
||||
}
|
||||
}
|
||||
+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(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)
|
||||
|
||||
-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.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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
+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
|
||||
}
|
||||
Reference in New Issue
Block a user