Login into server complete
This commit is contained in:
57 files changed
+1622
-65
No files matched your search
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user