Login into server complete

This commit is contained in:
2026-09-05 00:17:22 +08:00
parent dd760b7b9b
commit 15aefbb869
57 files changed
+1622 -65

No files matched your search

+6
View File
@@ -3,12 +3,18 @@ kotlin = "2.4.10"
kotlinx-io = "0.9.1"
ktor-network = "3.5.2"
coroutines-test = "1.11.0"
kotlinx-serialization = "1.11.0"
kotlinx-coroutines = "1.11.0"
[libraries]
kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" }
ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor-network" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines-test" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
maven-publish = { id = "maven-publish" }
+36
View File
@@ -0,0 +1,36 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinx.serialization)
}
kotlin {
explicitApi()
withSourcesJar()
linuxX64()
linuxArm64()
macosArm64()
mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets {
commonMain.dependencies {
implementation(project(":common"))
api(libs.kotlinx.serialization.core)
api(libs.kotlinx.serialization.json)
api(libs.kotlinx.coroutines)
}
jvmMain.dependencies {
}
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)
}
}
}
@@ -0,0 +1,146 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat
import cn.rtast.libmc.chat.packet.configuration.AckFinishConfigurationPacket
import cn.rtast.libmc.chat.packet.configuration.ServerboundPongPacket
import cn.rtast.libmc.chat.packet.configuration.ServerboundSelectKnownPacksPacket
import cn.rtast.libmc.chat.packet.handshake.HandshakePacket
import cn.rtast.libmc.chat.packet.login.LoginAcknowledgedPacket
import cn.rtast.libmc.chat.packet.login.LoginStartPacket
import cn.rtast.libmc.chat.packet.play.ServerboundKeepAlivePlayPacket
import cn.rtast.libmc.chat.protocol.HandshakeIntent
import cn.rtast.libmc.chat.protocol.ProtocolState
import cn.rtast.libmc.chat.util.generateOfflineUuid
import cn.rtast.libmc.common.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.uuid.Uuid
public class MinecraftChatClient(
private val host: String,
private val port: Int,
private val username: String,
private val uuid: Uuid = generateOfflineUuid(username),
private val context: LibMCContext = LibMCContext(),
) {
private var state = ProtocolState.HANDSHAKE
public suspend fun start(): Unit = coroutineScope {
val socket = _Socket(host, port, context)
val input = socket.openReadChannel()
val output = socket.openWriteChannel()
executeInitHandshake(output)
val readerJob = launch(Dispatchers.Default) {
handleIncomingPackets(input, output)
}
readerJob.join()
}
private fun executeInitHandshake(output: _WriteChannel) {
val handshakePacket = HandshakePacket(776, host, port.toUShort(), HandshakeIntent.LOGIN)
output.sendPacket(handshakePacket, HandshakePacket)
state = ProtocolState.LOGIN
val loginStartPacket = LoginStartPacket(username, uuid)
output.sendPacket(loginStartPacket, LoginStartPacket)
}
private suspend fun handleIncomingPackets(input: _ReadChannel, output: _WriteChannel) {
try {
while (currentCoroutineContext().isActive) {
val packetLength = input.readVarInt()
if (packetLength <= 0) continue
val packetBytes = ByteArray(packetLength)
input.readFully(packetBytes, 0, packetLength)
val buffer = _Buffer(packetBytes)
val packetId = buffer.readVarInt()
println("received -> State: $state | ID: 0x${packetId.toString(16).uppercase()} | Length: $packetLength")
try {
when (state) {
ProtocolState.LOGIN -> handleLoginPackets(packetId, output)
ProtocolState.CONFIGURATION -> handleConfigurationPackets(packetId, buffer, output)
ProtocolState.PLAY -> handlePlayPackets(packetId, buffer, output)
else -> {}
}
} catch (e: Exception) {
println("parsing 0x${packetId.toString(16).uppercase()} Payload failed: ${e.message}")
}
}
} catch (e: Exception) {
e.printStackTrace()
println("disconnecting: ${e.message}")
}
}
private fun handleLoginPackets(packetId: Int, output: _WriteChannel) {
when (packetId) {
0x02 -> {
output.sendPacket(LoginAcknowledgedPacket(), LoginAcknowledgedPacket)
state = ProtocolState.CONFIGURATION
println("[3/4] sent LoginAcknowledgedPacket -> switching to CONFIGURATION state")
output.sendPacket(
ServerboundSelectKnownPacksPacket(knownPacks = emptyList()),
ServerboundSelectKnownPacksPacket
)
}
0x00 -> {
println("login denied (ClientboundDisconnectLoginPacket)")
}
}
}
private fun handleConfigurationPackets(packetId: Int, packetBuffer: _Buffer, output: _WriteChannel) {
when (packetId) {
0x0E -> {
println("received ClientboundSelectKnownPacksPacket")
}
0x03 -> {
output.sendPacket(AckFinishConfigurationPacket, AckFinishConfigurationPacket)
state = ProtocolState.PLAY
}
0x05 -> {
output.sendPacket(ServerboundPongPacket(0), ServerboundPongPacket)
}
0x01 -> println("configuration state disconnected")
}
}
private fun handlePlayPackets(packetId: Int, packetBuffer: _Buffer, output: _WriteChannel) {
try {
when (packetId) {
0x2B -> println("[PLAY] Joined world")
0x2c -> {
val keepAliveId = packetBuffer.readLong()
output.sendPacket(ServerboundKeepAlivePlayPacket(id = keepAliveId), ServerboundKeepAlivePlayPacket)
println("[PLAY] reply keep alive packet $keepAliveId")
}
0x1D -> println("[PLAY] disconnected (ClientboundDisconnectPlayPacket)")
else -> {}
}
} catch (e: Exception) {
println("parsing 0x${packetId.toString(16).uppercase()} failed, skipped: ${e.message}")
}
}
}
@@ -0,0 +1,19 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.chat
public enum class ChatFilterType(public val id: Int) {
PASS_THROUGH(0),
FULLY_FILTERED(1),
PARTIALLY_FILTERED(2);
public companion object {
public fun fromId(id: Int): ChatFilterType =
entries.firstOrNull { it.id == id } ?: PASS_THROUGH
}
}
@@ -0,0 +1,48 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.chat
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common.writeVarInt
public data class PreviousMessageEntry(
val messageId: Int,
val signature: ByteArray?,
) {
public companion object Codec : PacketCodec<PreviousMessageEntry> {
override fun encode(buffer: _Buffer, value: PreviousMessageEntry) {
buffer.writeVarInt(value.messageId)
if (value.messageId == 0) {
val sig = requireNotNull(value.signature) { "signature must be present when messageId is 0" }
require(sig.size == 256)
buffer.writeBytes(sig)
}
}
override fun decode(buffer: _Buffer): PreviousMessageEntry = throw UnsupportedOperationException() // TODO
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as PreviousMessageEntry
if (messageId != other.messageId) return false
if (!signature.contentEquals(other.signature)) return false
return true
}
override fun hashCode(): Int {
var result = messageId
result = 31 * result + (signature?.contentHashCode() ?: 0)
return result
}
}
@@ -0,0 +1,14 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet
internal sealed interface PacketDirection {
interface ServerboundPacket : PacketDirection
interface ClientboundPacket : PacketDirection
interface AcrossPacket : PacketDirection
}
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.configuration
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
internal data object AckFinishConfigurationPacket : MinecraftPacket,
PacketCodec<AckFinishConfigurationPacket>,
PacketDirection.ServerboundPacket {
override val packetId: Int = 0x03
override fun encode(buffer: _Buffer, value: AckFinishConfigurationPacket) {}
override fun decode(buffer: _Buffer): AckFinishConfigurationPacket = AckFinishConfigurationPacket
}
@@ -0,0 +1,28 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.configuration
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.chat.util.readMinimalTextNbt
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
internal data class ClientboundDisconnectConfigurationPacket(
val reason: String,
) : MinecraftPacket, PacketDirection.ClientboundPacket {
override val packetId: Int = 0x02
companion object Codec : PacketCodec<ClientboundDisconnectConfigurationPacket> {
override fun encode(buffer: _Buffer, value: ClientboundDisconnectConfigurationPacket) {}
override fun decode(buffer: _Buffer): ClientboundDisconnectConfigurationPacket {
val reasonText = buffer.readMinimalTextNbt()
return ClientboundDisconnectConfigurationPacket(reason = reasonText)
}
}
}
@@ -0,0 +1,25 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.configuration
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
internal data class ClientboundPingPacket(val id: Int) : MinecraftPacket, PacketDirection.ClientboundPacket {
override val packetId: Int = 0x51
companion object Codec : PacketCodec<ClientboundPingPacket> {
override fun encode(buffer: _Buffer, value: ClientboundPingPacket) {
buffer.writeInt(value.id)
}
override fun decode(buffer: _Buffer): ClientboundPingPacket = ClientboundPingPacket(buffer.readInt())
}
}
@@ -0,0 +1,30 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.configuration
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.*
internal data class ClientboundSelectKnownPacksPacket(
val knownPacks: List<KnownPacks>,
) : MinecraftPacket, PacketDirection.AcrossPacket {
override val packetId: Int = 0x0e
companion object Codec : PacketCodec<ClientboundSelectKnownPacksPacket> {
override fun encode(buffer: _Buffer, value: ClientboundSelectKnownPacksPacket) {
buffer.writeVarInt(value.knownPacks.size)
value.knownPacks.forEach { KnownPacks.encode(buffer, it) }
}
override fun decode(buffer: _Buffer): ClientboundSelectKnownPacksPacket {
val packsCount = buffer.readVarInt()
val packs = List(packsCount) { KnownPacks.decode(buffer) }
return ClientboundSelectKnownPacksPacket(packs)
}
}
}
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.configuration
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
internal data object FinishConfigurationPacket : MinecraftPacket,
PacketCodec<FinishConfigurationPacket>,
PacketDirection.ClientboundPacket {
override val packetId: Int = 0x03
override fun encode(buffer: _Buffer, value: FinishConfigurationPacket) {}
override fun decode(buffer: _Buffer): FinishConfigurationPacket = FinishConfigurationPacket
}
@@ -0,0 +1,28 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.configuration
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
internal data class KeepAlivePacket(val id: Long) : MinecraftPacket, PacketDirection.AcrossPacket {
override val packetId: Int = 0x04
companion object Codec : PacketCodec<KeepAlivePacket> {
override fun encode(buffer: _Buffer, value: KeepAlivePacket) {
buffer.writeLong(value.id)
}
override fun decode(buffer: _Buffer): KeepAlivePacket {
val id = buffer.readLong()
return KeepAlivePacket(id)
}
}
}
@@ -0,0 +1,37 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.configuration
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common.readMcString
import cn.rtast.libmc.common.writeMcString
import kotlinx.serialization.Serializable
@Serializable
public data class KnownPacks(
val namespace: String,
val id: String,
val version: String,
) {
public companion object Codec : PacketCodec<KnownPacks> {
override fun encode(buffer: _Buffer, value: KnownPacks) {
buffer.writeMcString(value.namespace)
buffer.writeMcString(value.id)
buffer.writeMcString(value.version)
}
override fun decode(buffer: _Buffer): KnownPacks {
val namespace = buffer.readMcString()
val id = buffer.readMcString()
val version = buffer.readMcString()
return KnownPacks(namespace, id, version)
}
}
}
@@ -0,0 +1,25 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.configuration
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
internal data class ServerboundPongPacket(val id: Int) : MinecraftPacket, PacketDirection.ServerboundPacket {
override val packetId: Int = 0x2D
companion object Codec : PacketCodec<ServerboundPongPacket> {
override fun encode(buffer: _Buffer, value: ServerboundPongPacket) {
buffer.writeInt(value.id)
}
override fun decode(buffer: _Buffer): ServerboundPongPacket = ServerboundPongPacket(buffer.readInt())
}
}
@@ -0,0 +1,30 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.configuration
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.*
internal data class ServerboundSelectKnownPacksPacket(
val knownPacks: List<KnownPacks>,
) : MinecraftPacket, PacketDirection.AcrossPacket {
override val packetId: Int = 0x07
companion object Codec : PacketCodec<ServerboundSelectKnownPacksPacket> {
override fun encode(buffer: _Buffer, value: ServerboundSelectKnownPacksPacket) {
buffer.writeVarInt(value.knownPacks.size)
value.knownPacks.forEach { KnownPacks.encode(buffer, it) }
}
override fun decode(buffer: _Buffer): ServerboundSelectKnownPacksPacket {
val packsCount = buffer.readVarInt()
val packs = List(packsCount) { KnownPacks.decode(buffer) }
return ServerboundSelectKnownPacksPacket(packs)
}
}
}
@@ -0,0 +1,36 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.handshake
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common.writeMcString
import cn.rtast.libmc.common.writeVarInt
internal data class HandshakePacket(
val protocolVersion: Int,
val serverAddress: String,
val serverPort: UShort,
// 1 for Status, 2 for Login, 3 for Transfer
val intent: Int
) : MinecraftPacket, PacketDirection.ServerboundPacket {
override val packetId: Int = 0x00
companion object Codec : PacketCodec<HandshakePacket> {
override fun encode(buffer: _Buffer, value: HandshakePacket) {
buffer.writeVarInt(value.protocolVersion)
buffer.writeMcString(value.serverAddress)
buffer.writeShort(value.serverPort.toShort())
buffer.writeVarInt(value.intent)
}
override fun decode(buffer: _Buffer): HandshakePacket = throw UnsupportedOperationException()
}
}
@@ -0,0 +1,28 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.login
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common.readMcString
internal data class ClientboundDisconnectLoginPacket(
val reason: String,
) : MinecraftPacket, PacketDirection.ClientboundPacket {
override val packetId: Int = 0x00
companion object Codec : PacketCodec<ClientboundDisconnectLoginPacket> {
override fun encode(buffer: _Buffer, value: ClientboundDisconnectLoginPacket) {}
override fun decode(buffer: _Buffer): ClientboundDisconnectLoginPacket {
val reasonJson = buffer.readMcString()
return ClientboundDisconnectLoginPacket(reason = reasonJson)
}
}
}
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.login
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
internal data class LoginAcknowledgedPacket(
override val packetId: Int = 0x03,
) : MinecraftPacket, PacketDirection.ServerboundPacket {
companion object Codec : PacketCodec<LoginAcknowledgedPacket> {
override fun encode(buffer: _Buffer, value: LoginAcknowledgedPacket) {}
override fun decode(buffer: _Buffer): LoginAcknowledgedPacket = throw UnsupportedOperationException()
}
}
@@ -0,0 +1,32 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.login
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common.writeMcString
import cn.rtast.libmc.common.writeUuid
import kotlin.uuid.Uuid
internal data class LoginStartPacket(
val username: String,
val playerUuid: Uuid,
) : MinecraftPacket, PacketDirection.ServerboundPacket {
override val packetId: Int = 0x00
companion object Codec : PacketCodec<LoginStartPacket> {
override fun encode(buffer: _Buffer, value: LoginStartPacket) {
buffer.writeMcString(value.username)
buffer.writeUuid(value.playerUuid)
}
override fun decode(buffer: _Buffer): LoginStartPacket = throw UnsupportedOperationException()
}
}
@@ -0,0 +1,32 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.login
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.chat.profile.GameProfile
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common.readUuid
import kotlin.uuid.Uuid
internal data class LoginSuccessPacket(
val gameProfile: GameProfile,
val sessionId: Uuid,
) : MinecraftPacket, PacketDirection.ClientboundPacket {
override val packetId: Int = 0x02
companion object Codec : PacketCodec<LoginSuccessPacket> {
override fun encode(buffer: _Buffer, value: LoginSuccessPacket) {}
override fun decode(buffer: _Buffer): LoginSuccessPacket {
val gameProfile = GameProfile.decode(buffer)
val sessionId = buffer.readUuid()
return LoginSuccessPacket(gameProfile, sessionId)
}
}
}
@@ -0,0 +1,28 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.play
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.chat.util.readMinimalTextNbt
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
internal data class ClientboundDisconnectPlayPacket(
val reason: String,
) : MinecraftPacket, PacketDirection.ClientboundPacket {
override val packetId: Int = 0x28
companion object Codec : PacketCodec<ClientboundDisconnectPlayPacket> {
override fun encode(buffer: _Buffer, value: ClientboundDisconnectPlayPacket) {}
override fun decode(buffer: _Buffer): ClientboundDisconnectPlayPacket {
val reasonText = buffer.readMinimalTextNbt()
return ClientboundDisconnectPlayPacket(reason = reasonText)
}
}
}
@@ -0,0 +1,26 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.play
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
internal data class ClientboundKeepAlivePlayPacket(val id: Long) : MinecraftPacket, PacketDirection.ClientboundPacket {
override val packetId: Int = 0x33
companion object Codec : PacketCodec<ClientboundKeepAlivePlayPacket> {
override fun encode(buffer: _Buffer, value: ClientboundKeepAlivePlayPacket) {
buffer.writeLong(value.id)
}
override fun decode(buffer: _Buffer): ClientboundKeepAlivePlayPacket =
ClientboundKeepAlivePlayPacket(buffer.readLong())
}
}
@@ -0,0 +1,82 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.play
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.chat.protocol.*
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common.readVarInt
internal data class ClientboundLoginPlayPacket(
val entityId: Int,
val isHardcore: Boolean,
val dimensionNames: List<Identifier>,
val maxPlayers: Int,
val viewDistance: Int,
val simulationDistance: Int,
val isReducedDebugInfo: Boolean,
val enableRespawnScreen: Boolean,
val doLimitedCrafting: Boolean,
val dimensionType: Int,
val dimensionName: Identifier,
val hashedSeed: Long,
val gameMode: GameMode,
val previousGameMode: GameMode,
val isDebug: Boolean,
val isFlat: Boolean,
val hasDeathLocation: Boolean,
val deathDimensionName: Identifier?,
val deathLocation: BlockPos?,
val portalCooldown: Int,
val seaLevel: Int,
val isOnlineMode: Boolean,
val enforceSecureChat: Boolean,
) : MinecraftPacket, PacketDirection.ClientboundPacket {
override val packetId: Int = 0x31
companion object Codec : PacketCodec<ClientboundLoginPlayPacket> {
override fun encode(buffer: _Buffer, value: ClientboundLoginPlayPacket) {}
override fun decode(buffer: _Buffer): ClientboundLoginPlayPacket {
val entityId = buffer.readInt()
val isHardcore = buffer.readBoolean()
val dimensionNamesCount = buffer.readVarInt()
val dimensionNames = List(dimensionNamesCount) { buffer.readIdentifier() }
val maxPlayers = buffer.readVarInt()
val viewDistance = buffer.readVarInt()
val simulationDistance = buffer.readVarInt()
val isReducedDebugInfo = buffer.readBoolean()
val enableRespawnScreen = buffer.readBoolean()
val doLimitedCrafting = buffer.readBoolean()
val dimensionType = buffer.readVarInt()
val dimensionName = buffer.readIdentifier()
val hashedSeed = buffer.readLong()
val gameMode = GameMode.fromID(buffer.readByte().toUByte())
val previousGameMode = GameMode.fromID(buffer.readByte())
val isDebug = buffer.readBoolean()
val isFlat = buffer.readBoolean()
val hasDeathLocation = buffer.readBoolean()
val deathDimensionName = if (hasDeathLocation) buffer.readIdentifier() else null
val deathLocation = if (hasDeathLocation) buffer.readBlockPos() else null
val portalCooldown = buffer.readVarInt()
val seaLevel = buffer.readVarInt()
val isOnlineMode = buffer.readBoolean()
val isEnforcesSecureChat = buffer.readBoolean()
return ClientboundLoginPlayPacket(
entityId, isHardcore, dimensionNames, maxPlayers,
viewDistance, simulationDistance, isReducedDebugInfo,
enableRespawnScreen, doLimitedCrafting, dimensionType,
dimensionName, hashedSeed, gameMode, previousGameMode,
isDebug, isFlat, hasDeathLocation, deathDimensionName,
deathLocation, portalCooldown, seaLevel, isOnlineMode,
isEnforcesSecureChat
)
}
}
}
@@ -0,0 +1,117 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.play
import cn.rtast.libmc.chat.chat.ChatFilterType
import cn.rtast.libmc.chat.chat.PreviousMessageEntry
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.chat.util.writeMinimalTextNbt
import cn.rtast.libmc.common.*
import kotlin.uuid.Uuid
internal data class ClientboundPlayerChatMessagePacket(
val globalIndex: Int,
val sender: Uuid,
val index: Int,
val messageSignature: ByteArray?,
val message: String,
val timestamp: Long,
val salt: Long,
val previousMessages: List<PreviousMessageEntry>,
val unsignedContent: String?,
val filterType: ChatFilterType,
val filterMaskBits: LongArray?,
val chatType: Int,
val senderName: String,
val targetName: String?,
) : MinecraftPacket, PacketDirection.ClientboundPacket {
override val packetId: Int = 0x41
companion object Codec : PacketCodec<ClientboundPlayerChatMessagePacket> {
override fun encode(buffer: _Buffer, value: ClientboundPlayerChatMessagePacket) {
buffer.writeVarInt(value.globalIndex)
buffer.writeUuid(value.sender)
buffer.writeVarInt(value.index)
val hasSignature = value.messageSignature != null
buffer.writeBoolean(hasSignature)
if (hasSignature) buffer.writeBytes(requireNotNull(value.messageSignature))
buffer.writeMcString(value.message)
buffer.writeLong(value.timestamp)
buffer.writeLong(value.salt)
require(value.previousMessages.size == 20)
buffer.writeVarInt(value.previousMessages.size)
value.previousMessages.forEach { entry -> PreviousMessageEntry.encode(buffer, entry) }
val hasUnsignedContent = value.unsignedContent != null
buffer.writeBoolean(hasUnsignedContent)
value.unsignedContent?.let { buffer.writeMinimalTextNbt(it) }
buffer.writeVarInt(value.filterType.id)
if (value.filterType == ChatFilterType.PARTIALLY_FILTERED) {
val mask = requireNotNull(value.filterMaskBits)
buffer.writeVarInt(mask.size)
mask.forEach { buffer.writeLong(it) }
}
buffer.writeVarInt(value.chatType)
buffer.writeMinimalTextNbt(value.senderName)
val hasTargetName = value.targetName != null
buffer.writeBoolean(hasTargetName)
value.targetName?.let { buffer.writeMinimalTextNbt(it) }
}
override fun decode(buffer: _Buffer): ClientboundPlayerChatMessagePacket = throw UnsupportedOperationException() // TODO
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ClientboundPlayerChatMessagePacket
if (globalIndex != other.globalIndex) return false
if (index != other.index) return false
if (timestamp != other.timestamp) return false
if (salt != other.salt) return false
if (chatType != other.chatType) return false
if (packetId != other.packetId) return false
if (sender != other.sender) return false
if (!messageSignature.contentEquals(other.messageSignature)) return false
if (message != other.message) return false
if (previousMessages != other.previousMessages) return false
if (unsignedContent != other.unsignedContent) return false
if (filterType != other.filterType) return false
if (!filterMaskBits.contentEquals(other.filterMaskBits)) return false
if (senderName != other.senderName) return false
if (targetName != other.targetName) return false
return true
}
override fun hashCode(): Int {
var result = globalIndex
result = 31 * result + index
result = 31 * result + timestamp.hashCode()
result = 31 * result + salt.hashCode()
result = 31 * result + chatType
result = 31 * result + packetId
result = 31 * result + sender.hashCode()
result = 31 * result + (messageSignature?.contentHashCode() ?: 0)
result = 31 * result + message.hashCode()
result = 31 * result + previousMessages.hashCode()
result = 31 * result + unsignedContent.hashCode()
result = 31 * result + filterType.hashCode()
result = 31 * result + (filterMaskBits?.contentHashCode() ?: 0)
result = 31 * result + senderName.hashCode()
result = 31 * result + targetName.hashCode()
return result
}
}
@@ -0,0 +1,33 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.play
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.*
import kotlin.time.Clock
internal data class ServerboundChatMessagePacket(
val message: String,
val timestamp: Long = Clock.System.now().toEpochMilliseconds(),
val salt: Long = 0L,
) : MinecraftPacket, PacketDirection.ServerboundPacket {
override val packetId: Int = 0x09
companion object Codec : PacketCodec<ServerboundChatMessagePacket> {
override fun encode(buffer: _Buffer, value: ServerboundChatMessagePacket) {
buffer.writeMcString(value.message)
buffer.writeLong(value.timestamp)
buffer.writeLong(value.salt)
buffer.writeBoolean(false) // has signature
buffer.writeVarInt(0) // message count
buffer.writeBytes(byteArrayOf(0, 0, 0))
}
override fun decode(buffer: _Buffer): ServerboundChatMessagePacket = throw UnsupportedOperationException()
}
}
@@ -0,0 +1,26 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.packet.play
import cn.rtast.libmc.chat.packet.PacketDirection
import cn.rtast.libmc.common.MinecraftPacket
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
internal data class ServerboundKeepAlivePlayPacket(val id: Long) : MinecraftPacket, PacketDirection.ServerboundPacket {
override val packetId: Int = 0x1C
companion object Codec : PacketCodec<ServerboundKeepAlivePlayPacket> {
override fun encode(buffer: _Buffer, value: ServerboundKeepAlivePlayPacket) {
buffer.writeLong(value.id)
}
override fun decode(buffer: _Buffer): ServerboundKeepAlivePlayPacket =
ServerboundKeepAlivePlayPacket(buffer.readLong())
}
}
@@ -0,0 +1,60 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.profile
import cn.rtast.libmc.common.*
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
public data class GameProfile(
val uuid: Uuid,
val username: String,
val properties: List<Property>,
) {
@Serializable
public data class Property(
val name: String,
val value: String,
val signature: String?,
) {
public companion object Codec : PacketCodec<Property> {
override fun encode(buffer: _Buffer, value: Property) {
buffer.writeMcString(value.name)
buffer.writeMcString(value.value)
buffer.writeBoolean(value.signature != null)
value.signature?.let { buffer.writeMcString(it) }
}
override fun decode(buffer: _Buffer): Property {
val name = buffer.readMcString()
val value = buffer.readMcString()
val hasSignature = buffer.readBoolean()
val signature = if (hasSignature) buffer.readMcString() else null
return Property(name, value, signature)
}
}
}
public companion object Codec : PacketCodec<GameProfile> {
override fun encode(buffer: _Buffer, value: GameProfile) {
buffer.writeUuid(value.uuid)
buffer.writeMcString(value.username)
buffer.writeVarInt(value.properties.size) // prefixed array
value.properties.forEach { prop -> Property.encode(buffer, prop) }
}
override fun decode(buffer: _Buffer): GameProfile {
val uuid = buffer.readUuid()
val username = buffer.readMcString()
val propertyCount = buffer.readVarInt()
val properties = List(propertyCount) { Property.decode(buffer) }
return GameProfile(uuid, username, properties)
}
}
}
@@ -0,0 +1,43 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.protocol
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
import kotlinx.serialization.Serializable
/**
* An integer/block position: x (-33 554 432 to 33 554 431), z (-33 554 432 to 33 554 431), y (-2048 to 2047)
* ref: https://minecraft.wiki/w/Java_Edition_protocol/Packets#Type:Position
*/
@Serializable
public data class BlockPos(val x: Int, val y: Int, val z: Int) {
public companion object : PacketCodec<BlockPos> {
private const val PACKED_X_MASK = 0x3FFFFFFL // 26 bits
private const val PACKED_Y_MASK = 0xFFFL // 12 bits
private const val PACKED_Z_MASK = 0x3FFFFFFL // 26 bits
override fun decode(buffer: _Buffer): BlockPos {
val packed = buffer.readLong()
val x = (packed shr 38).toInt()
val y = (packed shl 52 shr 52).toInt()
val z = (packed shl 26 shr 38).toInt()
return BlockPos(x, y, z)
}
override fun encode(buffer: _Buffer, value: BlockPos) {
val xLong = (value.x.toLong() and PACKED_X_MASK)
val yLong = (value.y.toLong() and PACKED_Y_MASK)
val zLong = (value.z.toLong() and PACKED_Z_MASK)
buffer.writeLong(xLong shl 38 or (zLong shl 12) or yLong)
}
}
}
internal fun _Buffer.readBlockPos(): BlockPos = BlockPos.decode(this)
internal fun _Buffer.writeBlockPos(pos: BlockPos) = BlockPos.encode(this, pos)
@@ -0,0 +1,26 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.protocol
public enum class GameMode(public val id: Byte) {
Survival(0),
Creative(1),
Adventure(2),
Spectator(3),
Undefined(-1),
/**
* reserved
*/
Unknown(-99);
public companion object {
public fun fromID(id: Byte): GameMode = entries.firstOrNull { it.id == id } ?: Unknown
public fun fromID(id: UByte): GameMode = fromID(id.toByte())
}
}
@@ -0,0 +1,14 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.protocol
internal object HandshakeIntent {
const val STATUS = 1
const val LOGIN = 2
const val TRANSFER = 3
}
@@ -0,0 +1,38 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.protocol
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common.readMcString
import cn.rtast.libmc.common.writeMcString
import kotlin.jvm.JvmInline
/**
* ref: https://minecraft.wiki/w/Java_Edition_protocol/Packets#Identifier
*/
@JvmInline
public value class Identifier(public val full: String) {
public val namespace: String get() = if (full.contains(':')) full.substringBefore(':') else "minecraft"
public val path: String get() = if (full.contains(':')) full.substringAfter(':') else full
override fun toString(): String = "$namespace:$path"
public companion object Codec : PacketCodec<Identifier> {
public fun of(namespace: String, path: String): Identifier = Identifier("$namespace:$path")
override fun encode(buffer: _Buffer, value: Identifier) {
buffer.writeMcString(value.toString())
}
override fun decode(buffer: _Buffer): Identifier = Identifier(buffer.readMcString())
}
}
internal fun _Buffer.readIdentifier(): Identifier = Identifier.decode(this)
internal fun _Buffer.writeIdentifier(identifier: Identifier) = Identifier.encode(this, identifier)
@@ -0,0 +1,15 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.protocol
internal enum class ProtocolState {
HANDSHAKE,
LOGIN,
CONFIGURATION,
PLAY
}
@@ -0,0 +1,43 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.util
import cn.rtast.libmc.common._Buffer
/**
* tmp
*/
internal fun _Buffer.writeMinimalTextNbt(text: String) {
writeByte(0x0A)
writeByte(0x08)
val keyBytes = "text".encodeToByteArray()
writeShort(keyBytes.size.toShort())
writeBytes(keyBytes)
val valBytes = text.encodeToByteArray()
require(valBytes.size <= 32767)
writeShort(valBytes.size.toShort())
writeBytes(valBytes)
writeByte(0x00)
}
internal fun _Buffer.readMinimalTextNbt(): String {
val rootTagType = readByte().toInt()
if (rootTagType != 0x0A) return ""
var resultText = ""
while (true) {
val tagType = readByte().toInt()
if (tagType == 0x00) break
val keyLength = readShort().toInt()
val key = readBytes(keyLength).decodeToString()
if (tagType == 0x08 && key == "text") {
val valLength = readShort().toInt()
resultText = readBytes(valLength).decodeToString()
} else break
}
return resultText
}
@@ -0,0 +1,14 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.chat.util
import kotlin.uuid.Uuid
public fun generateOfflineUuid(username: String): Uuid =
Uuid.fromByteArray("OfflinePlayer:$username".encodeToByteArray())
@@ -0,0 +1,22 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package test
import cn.rtast.libmc.chat.MinecraftChatClient
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.uuid.Uuid
class TestChatClient {
@Test
fun `test chat client`() = runTest {
val cli = MinecraftChatClient("127.0.0.1", 25565, "RTAkland", Uuid.parse("0dc6a9e9-a6df-3f3e-ae07-e6dbdf74b294"))
cli.start()
}
}
+1
View File
@@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
kotlin {
explicitApi()
withSourcesJar()
linuxX64()
linuxArm64()
@@ -7,6 +7,8 @@
package cn.rtast.libmc.common
import kotlin.uuid.Uuid
@Suppress("CLASSNAME")
public expect class _Buffer {
public constructor()
@@ -17,12 +19,15 @@ public expect class _Buffer {
public fun writeInt(value: Int, endian: ByteOrder = ByteOrder.BIG_ENDIAN)
public fun writeLong(value: Long, endian: ByteOrder = ByteOrder.BIG_ENDIAN)
public fun writeBytes(bytes: ByteArray)
public fun writeBoolean(value: Boolean)
public fun readByte(): Byte
public fun readShort(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Short
public fun readInt(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Int
public fun readLong(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Long
public fun readBytes(length: Int): ByteArray
public fun readBoolean(): Boolean
public fun toByteArray(): ByteArray
public fun hasRemaining(): Boolean
public fun close()
@@ -30,4 +35,29 @@ public expect class _Buffer {
public val remaining: Long
}
public fun ByteArray.wrap(): _Buffer = _Buffer(this)
public fun ByteArray.wrap(): _Buffer = _Buffer(this)
public fun _Buffer.writeUuid(uuid: Uuid): Unit = uuid.toLongs { mostSignificantBits, leastSignificantBits ->
this.writeLong(mostSignificantBits)
this.writeLong(leastSignificantBits)
}
public fun _Buffer.readUuid(): Uuid {
val most = this.readLong()
val least = this.readLong()
return Uuid.fromLongs(most, least)
}
public fun _Buffer.writeVarInt(value: Int): Unit = VarIntCodec.encode(this, value)
public fun _Buffer.readVarInt(): Int = VarIntCodec.decode(this)
public fun _Buffer.writeMcString(value: String): Unit = McStringCodec.encode(this, value)
public fun _Buffer.readMcString(): String = McStringCodec.decode(this)
public fun _ReadChannel.readPacketFrame(): _Buffer {
val length = this.readVarInt()
val frameBytes = this.readBytes(length)
return _Buffer().apply {
writeBytes(frameBytes)
}
}
@@ -21,4 +21,18 @@ public expect class _ReadChannel {
public expect class _WriteChannel {
public fun writeFully(value: ByteArray, startIndex: Int = 0, endIndex: Int = value.size)
public fun flush()
}
public fun _ReadChannel.readVarInt(): Int {
var numRead = 0
var result = 0
var read: Byte
do {
read = this.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
}
@@ -4,12 +4,9 @@
* Date: 2026/9/3
*/
package cn.rtast.libmc.mcping.java
package cn.rtast.libmc.common
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
internal object VarIntCodec : PacketCodec<Int> {
public object VarIntCodec : PacketCodec<Int> {
override fun encode(buffer: _Buffer, value: Int) {
var v = value
while (true) {
@@ -23,20 +20,21 @@ internal object VarIntCodec : PacketCodec<Int> {
}
override fun decode(buffer: _Buffer): Int {
var value = 0
var position = 0
while (true) {
val currentByte = buffer.readByte().toInt() and 0xFF
value = value or ((currentByte and 0x7F) shl position)
if ((currentByte and 0x80) == 0) break
position += 7
if (position >= 35) throw IllegalArgumentException("VarInt too long")
}
return value
var numRead = 0
var result = 0
var read: Byte
do {
read = buffer.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
}
}
internal object McStringCodec : PacketCodec<String> {
public object McStringCodec : PacketCodec<String> {
override fun encode(buffer: _Buffer, value: String) {
val bytes = value.encodeToByteArray()
VarIntCodec.encode(buffer, bytes.size)
@@ -48,7 +46,4 @@ internal object McStringCodec : PacketCodec<String> {
val bytes = buffer.readBytes(length)
return bytes.decodeToString()
}
}
internal fun _Buffer.writeVarInt(value: Int) = VarIntCodec.encode(this, value)
internal fun _Buffer.readVarInt(): Int = VarIntCodec.decode(this)
}
@@ -0,0 +1,12 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.common
public interface MinecraftPacket {
public val packetId: Int
}
@@ -1,20 +1,13 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
* Date: 2026/9/4
*/
package cn.rtast.libmc.mcping.java
package cn.rtast.libmc.common
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common._WriteChannel
import cn.rtast.libmc.common.write
import cn.rtast.libmc.common.writeBuffer
internal fun <T : MinecraftPacket> _WriteChannel.sendPacket(packet: T, codec: PacketCodec<T>) {
public fun <T : MinecraftPacket> _WriteChannel.sendPacket(packet: T, codec: PacketCodec<T>) {
val bodyBuffer = _Buffer()
bodyBuffer.write(packet.packetId, VarIntCodec)
codec.encode(bodyBuffer, packet)
@@ -63,6 +63,8 @@ public actual class _Buffer {
outStream.write(bytes)
}
public actual fun writeBoolean(value: Boolean): Unit = writeByte(if (value) 0x01 else 0x00)
private fun ensureReadArray(): ByteArray {
var buf = readBuffer
if (buf == null) {
@@ -89,6 +91,8 @@ public actual class _Buffer {
return result
}
public actual fun readBoolean(): Boolean = this.readByte() != 0x00.toByte()
public actual fun toByteArray(): ByteArray = outStream.toByteArray()
public actual fun hasRemaining(): Boolean = readOffset < ensureReadArray().size
@@ -40,6 +40,8 @@ public actual class _Buffer {
}
public actual fun writeBytes(bytes: ByteArray): Unit = _delegateBuf.write(bytes)
public actual fun writeBoolean(value: Boolean): Unit = _delegateBuf.writeByte(if (value) 0x01 else 0x00)
public actual fun readByte(): Byte = _delegateBuf.readByte()
public actual fun readShort(endian: ByteOrder): Short {
val v = _delegateBuf.readShort()
@@ -57,6 +59,8 @@ public actual class _Buffer {
}
public actual fun readBytes(length: Int): ByteArray = _delegateBuf.readByteArray(length)
public actual fun readBoolean(): Boolean = _delegateBuf.readByte() != 0x00.toByte()
public actual fun toByteArray(): ByteArray {
val copy = _delegateBuf.peek()
return try {
+2 -1
View File
@@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
kotlin {
explicitApi()
withSourcesJar()
linuxX64()
linuxArm64()
@@ -13,7 +14,7 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(project(":common"))
api(project(":common"))
}
commonTest.dependencies {
@@ -7,15 +7,10 @@
package cn.rtast.libmc.mcping.java
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer
internal interface MinecraftPacket {
val packetId: Int
}
import cn.rtast.libmc.common.*
// ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Handshake
internal data class HandshakePacket(
public data class HandshakePacket(
val protocolVersion: Int,
val serverAddress: String,
val serverPort: UShort,
@@ -24,7 +19,7 @@ internal data class HandshakePacket(
) : MinecraftPacket {
override val packetId: Int = 0x00
companion object Codec : PacketCodec<HandshakePacket> {
public companion object Codec : PacketCodec<HandshakePacket> {
override fun encode(buffer: _Buffer, value: HandshakePacket) {
VarIntCodec.encode(buffer, value.protocolVersion)
McStringCodec.encode(buffer, value.serverAddress)
@@ -37,17 +32,17 @@ internal data class HandshakePacket(
}
// ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Status
internal data object StatusRequestPacket : MinecraftPacket, PacketCodec<StatusRequestPacket> {
public data object StatusRequestPacket : MinecraftPacket, PacketCodec<StatusRequestPacket> {
override val packetId: Int = 0x00
override fun encode(buffer: _Buffer, value: StatusRequestPacket) {}
override fun decode(buffer: _Buffer): StatusRequestPacket = throw UnsupportedOperationException()
}
internal data class PingPacket(val currentTime: Long) : MinecraftPacket {
public data class PingPacket(val currentTime: Long) : MinecraftPacket {
override val packetId: Int = 0x01
companion object : PacketCodec<PingPacket> {
public companion object : PacketCodec<PingPacket> {
override fun encode(buffer: _Buffer, value: PingPacket) {
buffer.writeLong(value.currentTime)
}
@@ -7,10 +7,7 @@
package cn.rtast.libmc.mcping.java
import cn.rtast.libmc.common.LibMCContext
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common._ReadChannel
import cn.rtast.libmc.common._Socket
import cn.rtast.libmc.common.*
import cn.rtast.libmc.mcping.PingResponse
import kotlin.time.Clock
@@ -50,22 +47,4 @@ internal fun pingJavaServer(host: String, port: Int, context: LibMCContext): Pin
} finally {
socket.close()
}
}
private fun _ReadChannel.readVarIntWithCodec(): Int {
val tempBuffer = _Buffer()
while (true) {
val byte = this.readByte()
tempBuffer.writeByte(byte)
if ((byte.toInt() and 0x80) == 0) break
}
return VarIntCodec.decode(tempBuffer)
}
private fun _ReadChannel.readPacketFrame(): _Buffer {
val length = this.readVarIntWithCodec()
val frameBytes = this.readBytes(length)
return _Buffer().apply {
writeBytes(frameBytes)
}
}
+29
View File
@@ -0,0 +1,29 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
kotlin {
explicitApi()
withSourcesJar()
linuxX64()
linuxArm64()
macosArm64()
mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets {
commonMain.dependencies {
implementation(project(":common"))
}
jvmMain.dependencies {
}
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)
}
}
}
@@ -0,0 +1,95 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.nbt
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common.readVarInt
public class NbtReader(
private val buffer: _Buffer,
private val variant: NbtVariant = NbtVariant.JAVA,
) {
private fun readShort(): Short = buffer.readShort(variant.order)
private fun readInt(): Int = buffer.readInt(variant.order)
private fun readLong(): Long = buffer.readLong(variant.order)
private fun readFloat(): Float = Float.fromBits(readInt())
private fun readDouble(): Double = Double.fromBits(readLong())
private fun readString(): String {
val length = if (variant.isNetwork) buffer.readVarInt() else readShort().toInt() and 0xFFFF
if (length == 0) return ""
return buffer.readBytes(length).decodeToString()
}
private fun readTagType(): Byte = if (variant.isNetwork) buffer.readVarInt().toByte() else buffer.readByte()
public fun readRoot(): Pair<String, NbtCompound> {
val type = readTagType()
require(type == NBTType.COMPOUND) { "Expected Compound Tag (10), got $type" }
val name = readString()
val root = readCompound()
return name to root
}
private fun readCompound(): NbtCompound {
val map = mutableMapOf<String, NBTElement>()
while (true) {
val type = readTagType()
if (type == NBTType.END) break
val name = readString()
map[name] = readPayload(type)
}
return NbtCompound(map)
}
private fun readList(): NbtList {
val elementType = readTagType()
val size = if (variant.isNetwork) buffer.readVarInt() else readInt()
if (size <= 0) return NbtList(elementType, emptyList())
val list = mutableListOf<NBTElement>()
(0 until size).forEach { _ -> list.add(readPayload(elementType)) }
return NbtList(elementType, list)
}
private fun readPayload(type: Byte): NBTElement {
return when (type) {
NBTType.BYTE -> NbtByte(buffer.readByte())
NBTType.SHORT -> NbtShort(readShort())
NBTType.INT -> {
val value = if (variant.isNetwork) buffer.readVarInt().decodeZigZag() else readInt()
NbtInt(value)
}
NBTType.LONG -> NbtLong(readLong())
NBTType.FLOAT -> NbtFloat(readFloat())
NBTType.DOUBLE -> NbtDouble(readDouble())
NBTType.BYTE_ARRAY -> {
val len = if (variant.isNetwork) buffer.readVarInt() else readInt()
NbtByteArray(buffer.readBytes(len))
}
NBTType.STRING -> NbtString(readString())
NBTType.LIST -> readList()
NBTType.COMPOUND -> readCompound()
NBTType.INT_ARRAY -> {
val len = if (variant.isNetwork) buffer.readVarInt() else readInt()
val array = IntArray(len) { readInt() }
NbtIntArray(array)
}
NBTType.LONG_ARRAY -> {
val len = if (variant.isNetwork) buffer.readVarInt() else readInt()
val array = LongArray(len) { readLong() }
NbtLongArray(array)
}
else -> throw IllegalArgumentException("Unknown Tag type: $type")
}
}
}
@@ -0,0 +1,66 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.nbt
public sealed interface NBTElement {
public val typeId: NBTTypeID
}
public data class NbtByte(val value: Byte) : NBTElement {
override val typeId: NBTTypeID = NBTType.BYTE
}
public data class NbtShort(val value: Short) : NBTElement {
override val typeId: NBTTypeID = NBTType.SHORT
}
public data class NbtInt(val value: Int) : NBTElement {
override val typeId: NBTTypeID = NBTType.INT
}
public data class NbtLong(val value: Long) : NBTElement {
override val typeId: NBTTypeID = NBTType.LONG
}
public data class NbtFloat(val value: Float) : NBTElement {
override val typeId: NBTTypeID = NBTType.FLOAT
}
public data class NbtDouble(val value: Double) : NBTElement {
override val typeId: NBTTypeID = NBTType.DOUBLE
}
public data class NbtByteArray(val value: ByteArray) : NBTElement {
override val typeId: NBTTypeID = NBTType.BYTE_ARRAY
override fun equals(other: Any?): Boolean = other is NbtByteArray && value.contentEquals(other.value)
override fun hashCode(): Int = value.contentHashCode()
}
public data class NbtString(val value: String) : NBTElement {
override val typeId: NBTTypeID = NBTType.STRING
}
public data class NbtList(val elementType: Byte, val elements: List<NBTElement>) : NBTElement {
override val typeId: NBTTypeID = NBTType.LIST
}
public data class NbtCompound(val map: Map<String, NBTElement>) : NBTElement {
override val typeId: NBTTypeID = NBTType.COMPOUND
}
public data class NbtIntArray(val value: IntArray) : NBTElement {
override val typeId: NBTTypeID = NBTType.INT_ARRAY
override fun equals(other: Any?): Boolean = other is NbtIntArray && value.contentEquals(other.value)
override fun hashCode(): Int = value.contentHashCode()
}
public data class NbtLongArray(val value: LongArray) : NBTElement {
override val typeId: NBTTypeID = NBTType.LONG_ARRAY
override fun equals(other: Any?): Boolean = other is NbtLongArray && value.contentEquals(other.value)
override fun hashCode(): Int = value.contentHashCode()
}
@@ -0,0 +1,26 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.nbt
public typealias NBTTypeID = Byte
public object NBTType {
public const val END: NBTTypeID = 0
public const val BYTE: NBTTypeID = 1
public const val SHORT: NBTTypeID = 2
public const val INT: NBTTypeID = 3
public const val LONG: NBTTypeID = 4
public const val FLOAT: NBTTypeID = 5
public const val DOUBLE: NBTTypeID = 6
public const val BYTE_ARRAY: NBTTypeID = 7
public const val STRING: NBTTypeID = 8
public const val LIST: NBTTypeID = 9
public const val COMPOUND: NBTTypeID = 10
public const val INT_ARRAY: NBTTypeID = 11
public const val LONG_ARRAY: NBTTypeID = 12
}
@@ -0,0 +1,19 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.nbt
import cn.rtast.libmc.common.ByteOrder
public enum class NbtVariant(
public val order: ByteOrder,
public val isNetwork: Boolean,
) {
JAVA(ByteOrder.BIG_ENDIAN, isNetwork = false),
BEDROCK_DISK(ByteOrder.LITTLE_ENDIAN, isNetwork = false),
BEDROCK_NETWORK(ByteOrder.LITTLE_ENDIAN, isNetwork = true)
}
@@ -0,0 +1,11 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.nbt
internal fun Int.decodeZigZag(): Int = (this ushr 1) xor -(this and 1)
internal fun Int.encodeZigZag(): Int = (this shl 1) xor (this shr 31)
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,24 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package test
import cn.rtast.libmc.common.wrap
import cn.rtast.libmc.nbt.NbtReader
import org.junit.Test
import java.io.File
class TestNBTReader {
private val javaNBTBuffer = File("src/commonTest/resources/level.dat").readBytes().wrap()
@Test
fun `test read java nbt`() {
val readRoot = NbtReader(javaNBTBuffer).readRoot()
println(readRoot)
}
}
+1
View File
@@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
kotlin {
explicitApi()
withSourcesJar()
linuxX64()
linuxArm64()
+2
View File
@@ -6,6 +6,8 @@ rootProject.name = "libmc"
includeSubModule(":common")
includeSubModule(":mcping")
includeSubModule(":rconlib")
includeSubModule(":chat")
//includeSubModule(":nbt")
fun includeSubModule(name: String) = include(name).also {
project(name).projectDir = file("libmc-${name.removePrefix(":")}")