diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f6c84cf..5bac6b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } \ No newline at end of file diff --git a/libmc-chat/build.gradle.kts b/libmc-chat/build.gradle.kts new file mode 100644 index 0000000..3c2f3f6 --- /dev/null +++ b/libmc-chat/build.gradle.kts @@ -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) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat.kt new file mode 100644 index 0000000..a5b5266 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat.kt @@ -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}") + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/ChatFilterType.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/ChatFilterType.kt new file mode 100644 index 0000000..f2c8e44 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/ChatFilterType.kt @@ -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 + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/PreviousMessageEntry.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/PreviousMessageEntry.kt new file mode 100644 index 0000000..7314492 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/chat/PreviousMessageEntry.kt @@ -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 { + 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 + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/PacketDirection.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/PacketDirection.kt new file mode 100644 index 0000000..9542be3 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/PacketDirection.kt @@ -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 +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/AckFinishConfigurationPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/AckFinishConfigurationPacket.kt new file mode 100644 index 0000000..568095b --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/AckFinishConfigurationPacket.kt @@ -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, + PacketDirection.ServerboundPacket { + override val packetId: Int = 0x03 + + override fun encode(buffer: _Buffer, value: AckFinishConfigurationPacket) {} + + override fun decode(buffer: _Buffer): AckFinishConfigurationPacket = AckFinishConfigurationPacket +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundDisconnectConfigurationPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundDisconnectConfigurationPacket.kt new file mode 100644 index 0000000..001f781 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundDisconnectConfigurationPacket.kt @@ -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 { + override fun encode(buffer: _Buffer, value: ClientboundDisconnectConfigurationPacket) {} + override fun decode(buffer: _Buffer): ClientboundDisconnectConfigurationPacket { + val reasonText = buffer.readMinimalTextNbt() + return ClientboundDisconnectConfigurationPacket(reason = reasonText) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundPingPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundPingPacket.kt new file mode 100644 index 0000000..08d5390 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundPingPacket.kt @@ -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 { + override fun encode(buffer: _Buffer, value: ClientboundPingPacket) { + buffer.writeInt(value.id) + } + + override fun decode(buffer: _Buffer): ClientboundPingPacket = ClientboundPingPacket(buffer.readInt()) + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundSelectKnownPacksPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundSelectKnownPacksPacket.kt new file mode 100644 index 0000000..5bb7a3a --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ClientboundSelectKnownPacksPacket.kt @@ -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, +) : MinecraftPacket, PacketDirection.AcrossPacket { + override val packetId: Int = 0x0e + + companion object Codec : PacketCodec { + 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) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/FinishConfigurationPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/FinishConfigurationPacket.kt new file mode 100644 index 0000000..d32dff9 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/FinishConfigurationPacket.kt @@ -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, + PacketDirection.ClientboundPacket { + override val packetId: Int = 0x03 + + override fun encode(buffer: _Buffer, value: FinishConfigurationPacket) {} + + override fun decode(buffer: _Buffer): FinishConfigurationPacket = FinishConfigurationPacket +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KeepAlivePacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KeepAlivePacket.kt new file mode 100644 index 0000000..f518755 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KeepAlivePacket.kt @@ -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 { + override fun encode(buffer: _Buffer, value: KeepAlivePacket) { + buffer.writeLong(value.id) + } + + override fun decode(buffer: _Buffer): KeepAlivePacket { + val id = buffer.readLong() + return KeepAlivePacket(id) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KnownPacks.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KnownPacks.kt new file mode 100644 index 0000000..17f891b --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/KnownPacks.kt @@ -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 { + 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) + } + } +} + diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundPongPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundPongPacket.kt new file mode 100644 index 0000000..5085281 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundPongPacket.kt @@ -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 { + override fun encode(buffer: _Buffer, value: ServerboundPongPacket) { + buffer.writeInt(value.id) + } + + override fun decode(buffer: _Buffer): ServerboundPongPacket = ServerboundPongPacket(buffer.readInt()) + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundSelectKnownPacksPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundSelectKnownPacksPacket.kt new file mode 100644 index 0000000..73b6f7d --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/configuration/ServerboundSelectKnownPacksPacket.kt @@ -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, +) : MinecraftPacket, PacketDirection.AcrossPacket { + override val packetId: Int = 0x07 + + companion object Codec : PacketCodec { + 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) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/handshake/HandshakePacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/handshake/HandshakePacket.kt new file mode 100644 index 0000000..84e3339 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/handshake/HandshakePacket.kt @@ -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 { + 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() + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/ClientboundDisconnectLoginPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/ClientboundDisconnectLoginPacket.kt new file mode 100644 index 0000000..60b714b --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/ClientboundDisconnectLoginPacket.kt @@ -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 { + override fun encode(buffer: _Buffer, value: ClientboundDisconnectLoginPacket) {} + override fun decode(buffer: _Buffer): ClientboundDisconnectLoginPacket { + val reasonJson = buffer.readMcString() + return ClientboundDisconnectLoginPacket(reason = reasonJson) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginAcknowledgedPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginAcknowledgedPacket.kt new file mode 100644 index 0000000..24f9402 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginAcknowledgedPacket.kt @@ -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 { + override fun encode(buffer: _Buffer, value: LoginAcknowledgedPacket) {} + override fun decode(buffer: _Buffer): LoginAcknowledgedPacket = throw UnsupportedOperationException() + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginStartPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginStartPacket.kt new file mode 100644 index 0000000..f7dd3fe --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginStartPacket.kt @@ -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 { + override fun encode(buffer: _Buffer, value: LoginStartPacket) { + buffer.writeMcString(value.username) + buffer.writeUuid(value.playerUuid) + } + + override fun decode(buffer: _Buffer): LoginStartPacket = throw UnsupportedOperationException() + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginSuccessPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginSuccessPacket.kt new file mode 100644 index 0000000..0ff7ac1 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/login/LoginSuccessPacket.kt @@ -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 { + 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) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundDisconnectPlayPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundDisconnectPlayPacket.kt new file mode 100644 index 0000000..dea1032 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundDisconnectPlayPacket.kt @@ -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 { + override fun encode(buffer: _Buffer, value: ClientboundDisconnectPlayPacket) {} + override fun decode(buffer: _Buffer): ClientboundDisconnectPlayPacket { + val reasonText = buffer.readMinimalTextNbt() + return ClientboundDisconnectPlayPacket(reason = reasonText) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundKeepAlivePlayPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundKeepAlivePlayPacket.kt new file mode 100644 index 0000000..08b4072 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundKeepAlivePlayPacket.kt @@ -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 { + override fun encode(buffer: _Buffer, value: ClientboundKeepAlivePlayPacket) { + buffer.writeLong(value.id) + } + + override fun decode(buffer: _Buffer): ClientboundKeepAlivePlayPacket = + ClientboundKeepAlivePlayPacket(buffer.readLong()) + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundLoginPlayPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundLoginPlayPacket.kt new file mode 100644 index 0000000..caebd1a --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundLoginPlayPacket.kt @@ -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, + 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 { + 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 + ) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundPlayerChatMessagePacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundPlayerChatMessagePacket.kt new file mode 100644 index 0000000..f6016cd --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ClientboundPlayerChatMessagePacket.kt @@ -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, + 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 { + 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 + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundChatMessagePacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundChatMessagePacket.kt new file mode 100644 index 0000000..195c886 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundChatMessagePacket.kt @@ -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 { + 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() + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundKeepAlivePlayPacket.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundKeepAlivePlayPacket.kt new file mode 100644 index 0000000..942b63d --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/packet/play/ServerboundKeepAlivePlayPacket.kt @@ -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 { + override fun encode(buffer: _Buffer, value: ServerboundKeepAlivePlayPacket) { + buffer.writeLong(value.id) + } + + override fun decode(buffer: _Buffer): ServerboundKeepAlivePlayPacket = + ServerboundKeepAlivePlayPacket(buffer.readLong()) + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/profile/GameProfile.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/profile/GameProfile.kt new file mode 100644 index 0000000..3d9d6c5 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/profile/GameProfile.kt @@ -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, +) { + @Serializable + public data class Property( + val name: String, + val value: String, + val signature: String?, + ) { + public companion object Codec : PacketCodec { + 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 { + 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) + } + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/BlockPos.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/BlockPos.kt new file mode 100644 index 0000000..3d73e0b --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/BlockPos.kt @@ -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 { + 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) \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/GameMode.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/GameMode.kt new file mode 100644 index 0000000..4f0d29e --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/GameMode.kt @@ -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()) + } +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/HandshakeIntent.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/HandshakeIntent.kt new file mode 100644 index 0000000..4515b75 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/HandshakeIntent.kt @@ -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 +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/Identifier.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/Identifier.kt new file mode 100644 index 0000000..be74559 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/Identifier.kt @@ -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 { + 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) \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/ProtocolState.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/ProtocolState.kt new file mode 100644 index 0000000..e1edcc0 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/protocol/ProtocolState.kt @@ -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 +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/nbt.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/nbt.kt new file mode 100644 index 0000000..1c0c8a2 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/nbt.kt @@ -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 +} \ No newline at end of file diff --git a/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/uuid.kt b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/uuid.kt new file mode 100644 index 0000000..2d0a7f7 --- /dev/null +++ b/libmc-chat/src/commonMain/kotlin/cn/rtast/libmc/chat/util/uuid.kt @@ -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()) \ No newline at end of file diff --git a/libmc-chat/src/commonTest/kotlin/test/TestChatClient.kt b/libmc-chat/src/commonTest/kotlin/test/TestChatClient.kt new file mode 100644 index 0000000..8a4d3f9 --- /dev/null +++ b/libmc-chat/src/commonTest/kotlin/test/TestChatClient.kt @@ -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() + } +} \ No newline at end of file diff --git a/libmc-common/build.gradle.kts b/libmc-common/build.gradle.kts index ecbc18f..1ee18cc 100644 --- a/libmc-common/build.gradle.kts +++ b/libmc-common/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget kotlin { explicitApi() + withSourcesJar() linuxX64() linuxArm64() diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/buffer.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/buffer.kt index 3a1d9e1..3aad6ac 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/buffer.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/buffer.kt @@ -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) \ No newline at end of file +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) + } +} \ No newline at end of file diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/channels.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/channels.kt index 186aed8..ee057d9 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/channels.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/channels.kt @@ -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 } \ No newline at end of file diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/mc_primitives.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/mc_primitives.kt similarity index 52% rename from libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/mc_primitives.kt rename to libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/mc_primitives.kt index d9e4216..ec3c1ec 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/mc_primitives.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/mc_primitives.kt @@ -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 { +public object VarIntCodec : PacketCodec { override fun encode(buffer: _Buffer, value: Int) { var v = value while (true) { @@ -23,20 +20,21 @@ internal object VarIntCodec : PacketCodec { } 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 { +public object McStringCodec : PacketCodec { override fun encode(buffer: _Buffer, value: String) { val bytes = value.encodeToByteArray() VarIntCodec.encode(buffer, bytes.size) @@ -48,7 +46,4 @@ internal object McStringCodec : PacketCodec { 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) \ No newline at end of file +} \ No newline at end of file diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet.kt new file mode 100644 index 0000000..b90d219 --- /dev/null +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet.kt @@ -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 +} \ No newline at end of file diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet_writer.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet_writer.kt similarity index 53% rename from libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet_writer.kt rename to libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet_writer.kt index 7999991..4387fa4 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet_writer.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/packet_writer.kt @@ -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 _WriteChannel.sendPacket(packet: T, codec: PacketCodec) { +public fun _WriteChannel.sendPacket(packet: T, codec: PacketCodec) { val bodyBuffer = _Buffer() bodyBuffer.write(packet.packetId, VarIntCodec) codec.encode(bodyBuffer, packet) diff --git a/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/buffer.jvm.kt b/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/buffer.jvm.kt index 80529c4..1677ac6 100644 --- a/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/buffer.jvm.kt +++ b/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/buffer.jvm.kt @@ -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 diff --git a/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/buffer.native.kt b/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/buffer.native.kt index 7fd1346..695723a 100644 --- a/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/buffer.native.kt +++ b/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/buffer.native.kt @@ -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 { diff --git a/libmc-mcping/build.gradle.kts b/libmc-mcping/build.gradle.kts index 1dabf62..18f154e 100644 --- a/libmc-mcping/build.gradle.kts +++ b/libmc-mcping/build.gradle.kts @@ -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 { diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet.kt b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet.kt index 25fc2a8..8d8c0c7 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet.kt +++ b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet.kt @@ -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 { + public companion object Codec : PacketCodec { 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 { +public data object StatusRequestPacket : MinecraftPacket, PacketCodec { 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 { + public companion object : PacketCodec { override fun encode(buffer: _Buffer, value: PingPacket) { buffer.writeLong(value.currentTime) } diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/ping_java.kt b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/ping_java.kt index 0b5742b..3e11d1c 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/ping_java.kt +++ b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/ping_java.kt @@ -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) - } } \ No newline at end of file diff --git a/libmc-nbt/build.gradle.kts b/libmc-nbt/build.gradle.kts new file mode 100644 index 0000000..9cb79a5 --- /dev/null +++ b/libmc-nbt/build.gradle.kts @@ -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) + } + } +} \ No newline at end of file diff --git a/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_reader.kt b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_reader.kt new file mode 100644 index 0000000..4857e2a --- /dev/null +++ b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_reader.kt @@ -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 { + 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() + 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() + (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") + } + } +} \ No newline at end of file diff --git a/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_tags.kt b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_tags.kt new file mode 100644 index 0000000..1601d61 --- /dev/null +++ b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_tags.kt @@ -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 { + override val typeId: NBTTypeID = NBTType.LIST +} + +public data class NbtCompound(val map: Map) : 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() +} \ No newline at end of file diff --git a/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_type.kt b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_type.kt new file mode 100644 index 0000000..e87667c --- /dev/null +++ b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_type.kt @@ -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 +} \ No newline at end of file diff --git a/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_variant.kt b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_variant.kt new file mode 100644 index 0000000..c5860f9 --- /dev/null +++ b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/nbt_variant.kt @@ -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) +} \ No newline at end of file diff --git a/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/varint.kt b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/varint.kt new file mode 100644 index 0000000..21c4499 --- /dev/null +++ b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/nbt/varint.kt @@ -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) diff --git a/libmc-nbt/src/commonTest/resources/bedrock.nbt b/libmc-nbt/src/commonTest/resources/bedrock.nbt new file mode 100644 index 0000000..21d0771 Binary files /dev/null and b/libmc-nbt/src/commonTest/resources/bedrock.nbt differ diff --git a/libmc-nbt/src/commonTest/resources/level.dat b/libmc-nbt/src/commonTest/resources/level.dat new file mode 100644 index 0000000..5d1749a Binary files /dev/null and b/libmc-nbt/src/commonTest/resources/level.dat differ diff --git a/libmc-nbt/src/jvmTest/kotlin/test/TestNBTReader.kt b/libmc-nbt/src/jvmTest/kotlin/test/TestNBTReader.kt new file mode 100644 index 0000000..b3d4b2d --- /dev/null +++ b/libmc-nbt/src/jvmTest/kotlin/test/TestNBTReader.kt @@ -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) + } +} \ No newline at end of file diff --git a/libmc-rconlib/build.gradle.kts b/libmc-rconlib/build.gradle.kts index 039da51..9cb79a5 100644 --- a/libmc-rconlib/build.gradle.kts +++ b/libmc-rconlib/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget kotlin { explicitApi() + withSourcesJar() linuxX64() linuxArm64() diff --git a/settings.gradle.kts b/settings.gradle.kts index 8062980..a099062 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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(":")}")