Login into server complete
This commit is contained in:
57 files changed
+1622
-65
No files matched your search
@@ -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" }
|
||||
@@ -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
|
||||
}
|
||||
+23
@@ -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
|
||||
}
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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())
|
||||
}
|
||||
}
|
||||
+30
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -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
|
||||
}
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -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())
|
||||
}
|
||||
}
|
||||
+30
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -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()
|
||||
}
|
||||
}
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -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()
|
||||
}
|
||||
}
|
||||
+32
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -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())
|
||||
}
|
||||
}
|
||||
+82
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
@@ -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
|
||||
}
|
||||
}
|
||||
+33
@@ -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()
|
||||
}
|
||||
}
|
||||
+26
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -31,3 +36,28 @@ public expect class _Buffer {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -22,3 +22,17 @@ 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
|
||||
}
|
||||
+14
-19
@@ -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)
|
||||
@@ -49,6 +47,3 @@ internal object McStringCodec : PacketCodec<String> {
|
||||
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
|
||||
}
|
||||
+3
-10
@@ -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,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
|
||||
|
||||
@@ -51,21 +48,3 @@ internal fun pingJavaServer(host: String, port: Int, context: LibMCContext): Pin
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
kotlin {
|
||||
explicitApi()
|
||||
withSourcesJar()
|
||||
|
||||
linuxX64()
|
||||
linuxArm64()
|
||||
|
||||
@@ -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(":")}")
|
||||
|
||||
Reference in New Issue
Block a user