Add event dispatcher

This commit is contained in:
2026-09-05 14:19:51 +08:00
parent 799edce45f
commit 206db390e9
18 files changed
+353 -318

No files matched your search

@@ -7,4 +7,4 @@
package cn.rtast.libmc.common.packet
public interface MinecraftPacket
public interface MinecraftPacket : PacketEvent
@@ -5,5 +5,6 @@
*/
package cn.rtast.libmc.protocol.codec
package cn.rtast.libmc.common.packet
public interface PacketEvent
@@ -7,4 +7,8 @@
package cn.rtast.libmc.common
internal expect fun ByteArray.zlibDecompress(): ByteArray
public expect fun ByteArray.zlibDecompress(): ByteArray
public expect fun ByteArray.zlibDecompress(expectedSize: Int): ByteArray
public expect fun ByteArray.zlibCompress(): ByteArray
@@ -0,0 +1,29 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package test
import cn.rtast.libmc.common.zlibCompress
import cn.rtast.libmc.common.zlibDecompress
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class TestZlib {
@Test
fun `test zlib compress decompress`() {
val originalText = "Minecraft Protocol Compression".repeat(10)
val originalBytes = originalText.encodeToByteArray()
val compressedBytes = originalBytes.zlibCompress()
assertTrue(compressedBytes.size < originalBytes.size)
val decompressedBytes = compressedBytes.zlibDecompress(originalBytes.size)
assertEquals(originalBytes.size, decompressedBytes.size)
assertContentEquals(originalBytes, decompressedBytes)
}
}
@@ -8,6 +8,7 @@ package cn.rtast.libmc.common
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.zip.Deflater
import java.util.zip.GZIPInputStream
import java.util.zip.Inflater
@@ -42,3 +43,26 @@ private fun ByteArray.gzipDecompress(): ByteArray {
}
}
}
public actual fun ByteArray.zlibDecompress(expectedSize: Int): ByteArray {
val inflater = Inflater()
inflater.setInput(this)
val result = ByteArray(expectedSize)
try {
val resultLength = inflater.inflate(result)
check(resultLength == expectedSize) { "Decompression failed: expected $expectedSize bytes, but got $resultLength" }
return result
} finally {
inflater.end()
}
}
public actual fun ByteArray.zlibCompress(): ByteArray {
val deflater = Deflater()
deflater.setInput(this)
deflater.finish()
val output = ByteArray(this.size + 64)
val compressedSize = deflater.deflate(output)
deflater.end()
return output.copyOf(compressedSize)
}
@@ -4,11 +4,12 @@
* Date: 2026/9/4
*/
@file:OptIn(ExperimentalForeignApi::class)
@file:OptIn(ExperimentalForeignApi::class, UnsafeNumber::class)
package cn.rtast.libmc.common
import kotlinx.cinterop.*
import platform.posix.u_longVar
import platform.zlib.*
private const val ENABLE_ZLIB_GZIP_HEADER = 15 + 32
@@ -66,3 +67,38 @@ public actual fun ByteArray.zlibDecompress(): ByteArray {
}
}
}
public actual fun ByteArray.zlibDecompress(expectedSize: Int): ByteArray {
val result = ByteArray(expectedSize)
if (this.isEmpty()) return result
memScoped {
val destLen = alloc<u_longVar>()
destLen.value = expectedSize.toUInt()
val res = uncompress(
result.refTo(0).getPointer(this).reinterpret(),
destLen.ptr,
this@zlibDecompress.refTo(0).getPointer(this).reinterpret(),
this@zlibDecompress.size.toUInt()
)
check(res == Z_OK) { "zlib uncompress failed with error code: $res" }
}
return result
}
public actual fun ByteArray.zlibCompress(): ByteArray {
if (this.isEmpty()) return byteArrayOf()
val maxCompressedLen = compressBound(this.size.toUInt()).toInt()
val output = ByteArray(maxCompressedLen)
memScoped {
val destLen = alloc<u_longVar>()
destLen.value = maxCompressedLen.toUInt()
val res = compress(
output.refTo(0).getPointer(this).reinterpret(),
destLen.ptr,
this@zlibCompress.refTo(0).getPointer(this).reinterpret(),
this@zlibCompress.size.toUInt()
)
check(res == Z_OK) { "zlib compress failed with error code: $res" }
return output.copyOf(destLen.value.toInt())
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(project(":common"))
api(project(":common"))
api(libs.kotlinx.serialization.core)
api(libs.kotlinx.serialization.json)
api(libs.kotlinx.coroutines)
@@ -1,146 +0,0 @@
///*
// * Copyright © 2026 RTAkland
// * Author: RTAkland
// * Date: 2026/9/4
// */
//
//
//package cn.rtast.libmc.protocol
//
//import cn.rtast.libmc.protocol.packet.configuration.ServerboundAckFinishConfigurationPacket
//import cn.rtast.libmc.protocol.packet.configuration.ServerboundPongPacket
//import cn.rtast.libmc.protocol.packet.configuration.ServerboundSelectKnownPacksPacket
//import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket
//import cn.rtast.libmc.protocol.packet.login.ServerboundLoginAcknowledgedPacket
//import cn.rtast.libmc.protocol.packet.login.ServerboundLoginStartPacket
//import cn.rtast.libmc.protocol.packet.play.ServerboundKeepAlivePlayPacket
//import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
//import cn.rtast.libmc.protocol.protocol.state.ProtocolState
//import cn.rtast.libmc.protocol.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 = ServerboundHandshakePacket(776, host, port.toUShort(), HandshakeIntent.LOGIN)
// output.sendPacket(handshakePacket, ServerboundHandshakePacket)
// state = ProtocolState.LOGIN
//
// val loginStartPacket = ServerboundLoginStartPacket(username, uuid)
// output.sendPacket(loginStartPacket, ServerboundLoginStartPacket)
// }
//
// 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 = BytesBuffer(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(ServerboundLoginAcknowledgedPacket(), ServerboundLoginAcknowledgedPacket)
// 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: BytesBuffer, output: WriteChannel) {
// when (packetId) {
// 0x0E -> {
// println("received ClientboundSelectKnownPacksPacket")
// }
//
// 0x03 -> {
// output.sendPacket(ServerboundAckFinishConfigurationPacket, ServerboundAckFinishConfigurationPacket)
// state = ProtocolState.PLAY
// }
//
// 0x05 -> {
// output.sendPacket(ServerboundPongPacket(0), ServerboundPongPacket)
// }
//
// 0x01 -> println("configuration state disconnected")
// }
// }
//
// private fun handlePlayPackets(packetId: Int, packetBuffer: BytesBuffer, 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}")
// }
// }
//}
@@ -1,19 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.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
}
}
@@ -1,48 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.protocol.chat
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.BytesBuffer
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: BytesBuffer, 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: BytesBuffer): 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,73 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.client
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.protocol.packet.configuration.*
import cn.rtast.libmc.protocol.packet.login.ClientboundDisconnectLoginPacket
import cn.rtast.libmc.protocol.packet.login.ClientboundLoginSuccessPacket
import cn.rtast.libmc.protocol.packet.login.ServerboundLoginAcknowledgedPacket
import cn.rtast.libmc.protocol.packet.play.*
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
internal class InternalPacketDispatcher(private val client: MinecraftClient) {
suspend fun dispatchEvent(packet: MinecraftPacket) = client.dispatch(packet)
suspend fun handleIncomingPackets(packet: MinecraftPacket) {
this.dispatchEvent(packet)
when (packet) {
is ClientboundLoginSuccessPacket -> {
client.networkChannel.sendPacket(ServerboundLoginAcknowledgedPacket)
client.stateMachine.transitionTo(ProtocolState.CONFIGURATION)
}
is ClientboundDisconnectLoginPacket -> {
println("Login denied: ${packet.reason}")
// close()
}
is ClientboundSelectKnownPacksPacket -> {
client.networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList())) // TODO empty resource packs list
}
is ClientboundPingPacket -> client.networkChannel.sendPacket(ServerboundPongPacket(packet.id))
is ClientboundKeepAliveConfigurationPacket -> {
client.networkChannel.sendPacket(ServerboundKeepAliveConfigurationPacket(packet.id))
}
is ClientboundFinishConfigurationPacket -> {
client.networkChannel.sendPacket(ServerboundAckFinishConfigurationPacket)
client.stateMachine.transitionTo(ProtocolState.PLAY)
}
is ClientboundDisconnectConfigurationPacket -> {
println("Configuration disconnected: ${packet.reason}")
// close()
}
is ClientboundLoginPlayPacket -> {
println("Successfully joined world! Entity ID: ${packet.entityId}")
}
is ClientboundKeepAlivePlayPacket -> {
client.networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(id = packet.id))
}
is ClientboundStartConfigurationPacket -> {
client.networkChannel.sendPacket(ServerboundConfigurationAcknowledgedPacket)
client.stateMachine.transitionTo(ProtocolState.CONFIGURATION)
}
is ClientboundDisconnectPlayPacket -> {
println("Disconnected from play session: ${packet.reason}")
// close()
}
}
}
}
@@ -7,60 +7,53 @@
package cn.rtast.libmc.protocol.client
import cn.rtast.libmc.common.LibMCContext
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.packet.UnknownPacket
import cn.rtast.libmc.protocol.event.PacketEventDispatcher
import cn.rtast.libmc.protocol.network.NetworkChannel
import cn.rtast.libmc.protocol.packet.configuration.*
import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket
import cn.rtast.libmc.protocol.packet.login.ClientboundDisconnectLoginPacket
import cn.rtast.libmc.protocol.packet.login.ClientboundLoginSuccessPacket
import cn.rtast.libmc.protocol.packet.login.ServerboundLoginAcknowledgedPacket
import cn.rtast.libmc.protocol.packet.login.ServerboundLoginStartPacket
import cn.rtast.libmc.protocol.packet.play.*
import cn.rtast.libmc.protocol.protocol.GameProtocols
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
import cn.rtast.libmc.protocol.util.generateOfflineUuid
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.uuid.Uuid
public class MinecraftClient(
public class MinecraftClient internal constructor(
private val host: String,
private val port: Int = 25565,
private val username: String,
private val uuid: Uuid = generateOfflineUuid(username),
private val context: LibMCContext = LibMCContext(),
) {
private val stateMachine = ClientStateMachine()
private val networkChannel = NetworkChannel(host, port, context, stateMachine)
private val listeners = mutableListOf<(MinecraftPacket) -> Unit>()
private val uuid: Uuid,
context: LibMCContext,
parentJob: Job?,
private val ioDispatcher: CoroutineDispatcher,
) : PacketEventDispatcher(), CoroutineScope {
internal val stateMachine = ClientStateMachine()
internal val networkChannel = NetworkChannel(host, port, context, stateMachine)
private val internalPacketDispatcher = InternalPacketDispatcher(this)
private val clientJob = SupervisorJob(parentJob)
private var listenJob: Job? = null
public suspend fun connect(protocolVersion: Int = 776) {
override val coroutineContext: CoroutineContext
get() = clientJob + ioDispatcher + CoroutineName("LibMC-MinecraftClient-$username")
public fun connect(protocolVersion: Int = 776) {
networkChannel.connect()
startListening()
networkChannel.sendPacket(
ServerboundHandshakePacket(
protocolVersion, host,
port.toUShort(),
HandshakeIntent.LOGIN
)
ServerboundHandshakePacket(protocolVersion, host, port.toUShort(), HandshakeIntent.LOGIN)
)
stateMachine.transitionTo(ProtocolState.LOGIN)
networkChannel.sendPacket(ServerboundLoginStartPacket(username, uuid))
listenJob?.join()
}
private fun startListening() {
listenJob = CoroutineScope(Dispatchers.IO).launch {
listenJob = launch {
try {
while (isActive) {
val packet = networkChannel.readNextPacket()
handleIncomingPackets(packet)
listeners.forEach { it.invoke(packet) }
}
while (isActive) internalPacketDispatcher.handleIncomingPackets(networkChannel.readNextPacket())
} catch (e: Exception) {
e.printStackTrace()
if (e is CancellationException) throw e
if (isActive) {
println("Network read loop exception: ${e.message}")
close()
@@ -69,65 +62,18 @@ public class MinecraftClient(
}
}
private fun handleIncomingPackets(packet: MinecraftPacket) {
when (packet) {
is ClientboundLoginSuccessPacket -> {
networkChannel.sendPacket(ServerboundLoginAcknowledgedPacket)
stateMachine.transitionTo(ProtocolState.CONFIGURATION)
}
is ClientboundDisconnectLoginPacket -> {
println("Login denied: ${packet.reason}")
close()
}
is ClientboundSelectKnownPacksPacket -> {
networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList())) // TODO empty resource packs list
}
is ClientboundPingPacket -> networkChannel.sendPacket(ServerboundPongPacket(packet.id))
is ClientboundKeepAliveConfigurationPacket -> {
networkChannel.sendPacket(ServerboundKeepAliveConfigurationPacket(packet.id))
}
is ClientboundFinishConfigurationPacket -> {
networkChannel.sendPacket(ServerboundAckFinishConfigurationPacket)
stateMachine.transitionTo(ProtocolState.PLAY)
}
is ClientboundDisconnectConfigurationPacket -> {
println("Configuration disconnected: ${packet.reason}")
close()
}
is ClientboundLoginPlayPacket -> {
println("Successfully joined world! Entity ID: ${packet.entityId}")
}
is ClientboundKeepAlivePlayPacket -> {
networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(id = packet.id))
}
is ClientboundStartConfigurationPacket -> {
networkChannel.sendPacket(ServerboundConfigurationAcknowledgedPacket)
stateMachine.transitionTo(ProtocolState.CONFIGURATION)
}
is ClientboundDisconnectPlayPacket -> {
println("Disconnected from play session: ${packet.reason}")
close()
}
// else -> println((packet as? UnknownPacket)?.data?.contentToString() ?: packet)
}
}
public fun onPacket(listener: (MinecraftPacket) -> Unit) {
listeners.add(listener)
}
public fun close() {
listenJob?.cancel()
networkChannel.close()
clientJob.cancel()
}
}
public fun createMinecraftClient(
host: String,
port: Int,
username: String,
uuid: Uuid = generateOfflineUuid(username),
context: LibMCContext = LibMCContext(),
parentJob: Job? = null,
ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
): MinecraftClient = MinecraftClient(host, port, username, uuid, context, parentJob, ioDispatcher)
@@ -0,0 +1,26 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.event
import cn.rtast.libmc.common.packet.PacketEvent
import kotlin.reflect.KClass
public open class PacketEventDispatcher {
@PublishedApi
internal val eventHandlers: MutableMap<KClass<out PacketEvent>, MutableList<suspend (PacketEvent) -> Unit>> =
mutableMapOf()
internal suspend fun dispatch(event: PacketEvent) {
eventHandlers[event::class]?.forEach { it.invoke(event) }
}
public inline fun <reified T : PacketEvent> on(crossinline block: suspend (T) -> Unit) {
val handlers = eventHandlers.getOrPut(T::class) { mutableListOf() }
handlers.add { event -> block(event as T) }
}
}
@@ -4,7 +4,6 @@
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.network
import cn.rtast.libmc.common.*
@@ -21,6 +20,7 @@ internal class NetworkChannel(
private var socket: Socket? = null
private var readChannel: ReadChannel? = null
private var writeChannel: WriteChannel? = null
private var threshold = -1
fun connect() {
val sk = Socket(host, port, context)
@@ -29,13 +29,25 @@ internal class NetworkChannel(
this.writeChannel = sk.openWriteChannel()
}
fun setCompression(threshold: Int) {
this.threshold = threshold
}
fun readNextPacket(): MinecraftPacket {
val channel = requireNotNull(readChannel) { "ReadChannel not connected" }
val length = channel.readVarInt()
val buf = channel.readBytes(length).wrap()
val packetLength = channel.readVarInt()
val rawFrameBytes = channel.readBytes(packetLength)
val payloadBuf = if (threshold < 0) rawFrameBytes.wrap() else {
val frameBuf = rawFrameBytes.wrap()
val dataLength = frameBuf.readVarInt()
if (dataLength == 0) {
frameBuf.readBytes(frameBuf.remaining.toInt()).wrap()
} else frameBuf.readBytes(frameBuf.remaining.toInt()).zlibDecompress(dataLength).wrap()
}
val currentState = stateMachine.currentState
val packetId = buf.readVarInt()
return GameProtocols.clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, buf)
val packetId = payloadBuf.readVarInt()
return GameProtocols.clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, payloadBuf)
}
fun sendPacket(packet: MinecraftPacket) {
@@ -43,8 +55,28 @@ internal class NetworkChannel(
val bodyBuffer = BytesBuffer()
GameProtocols.serverboundGameProtocols.getRegistry(stateMachine.currentState).encodePacket(bodyBuffer, packet)
val frameBuffer = BytesBuffer().apply {
writeVarInt(bodyBuffer.size)
writeBuffer(bodyBuffer)
if (threshold < 0) {
writeVarInt(bodyBuffer.size)
writeBuffer(bodyBuffer)
} else {
val uncompressedData = bodyBuffer.toByteArray()
if (uncompressedData.size < threshold) {
val contentBuf = BytesBuffer().apply {
writeVarInt(0)
writeBytes(uncompressedData)
}
writeVarInt(contentBuf.size)
writeBuffer(contentBuf)
} else {
val compressedData = uncompressedData.zlibCompress()
val contentBuf = BytesBuffer().apply {
writeVarInt(uncompressedData.size)
writeBytes(compressedData)
}
writeVarInt(contentBuf.size)
writeBuffer(contentBuf)
}
}
}
channel.writeFully(frameBuffer.toByteArray())
channel.flush()
@@ -0,0 +1,26 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.packet.login
import cn.rtast.libmc.common.BytesBuffer
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.common.readVarInt
import cn.rtast.libmc.common.writeVarInt
public data class ClientboundSetCompressionPacket(val threshold: Int) : MinecraftPacket {
public companion object Codec : PacketCodec<ClientboundSetCompressionPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundSetCompressionPacket) {
buffer.writeVarInt(value.threshold)
}
override fun decode(buffer: BytesBuffer): ClientboundSetCompressionPacket {
return ClientboundSetCompressionPacket(buffer.readVarInt())
}
}
}
@@ -9,8 +9,6 @@ package cn.rtast.libmc.protocol.packet.play
import cn.rtast.libmc.common.*
import cn.rtast.libmc.common.packet.MinecraftPacket
import cn.rtast.libmc.protocol.chat.ChatFilterType
import cn.rtast.libmc.protocol.chat.PreviousMessageEntry
import cn.rtast.libmc.protocol.util.writeMinimalTextNbt
import kotlin.uuid.Uuid
@@ -30,6 +28,53 @@ public data class ClientboundPlayerChatMessagePacket(
val senderName: String,
val targetName: String?,
) : MinecraftPacket {
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
}
}
public data class PreviousMessageEntry(
val messageId: Int,
val signature: ByteArray?,
) {
public companion object Codec : PacketCodec<PreviousMessageEntry> {
override fun encode(buffer: BytesBuffer, 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: BytesBuffer): 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
}
}
public companion object Codec : PacketCodec<ClientboundPlayerChatMessagePacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundPlayerChatMessagePacket) {
buffer.writeVarInt(value.globalIndex)
@@ -10,6 +10,7 @@ import cn.rtast.libmc.protocol.packet.configuration.*
import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket
import cn.rtast.libmc.protocol.packet.login.ClientboundDisconnectLoginPacket
import cn.rtast.libmc.protocol.packet.login.ClientboundLoginSuccessPacket
import cn.rtast.libmc.protocol.packet.login.ClientboundSetCompressionPacket
import cn.rtast.libmc.protocol.packet.login.ServerboundLoginAcknowledgedPacket
import cn.rtast.libmc.protocol.packet.login.ServerboundLoginStartPacket
import cn.rtast.libmc.protocol.packet.play.*
@@ -30,6 +31,7 @@ internal object GameProtocols {
register(ProtocolState.LOGIN) {
register(0x00, ClientboundDisconnectLoginPacket)
register(0x02, ClientboundLoginSuccessPacket)
register(0x03, ClientboundSetCompressionPacket)
}
register(ProtocolState.PLAY) {
register(0x2C, ClientboundKeepAlivePlayPacket)
@@ -7,7 +7,8 @@
package test
import cn.rtast.libmc.protocol.client.MinecraftClient
import cn.rtast.libmc.protocol.client.createMinecraftClient
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
@@ -15,7 +16,10 @@ class TestClient {
@Test
fun `test client`() = runTest {
val cli = MinecraftClient("127.0.0.1", 25565, "123")
cli.connect()
val cli = createMinecraftClient("127.0.0.1", 25565, "123")
cli.launch { cli.connect() }
while (true) {
}
}
}