Add event dispatcher
This commit is contained in:
18 files changed
+353
-318
No files matched your search
@@ -7,4 +7,4 @@
|
|||||||
|
|
||||||
package cn.rtast.libmc.common.packet
|
package cn.rtast.libmc.common.packet
|
||||||
|
|
||||||
public interface MinecraftPacket
|
public interface MinecraftPacket : PacketEvent
|
||||||
+2
-1
@@ -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
|
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.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.util.zip.Deflater
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import java.util.zip.Inflater
|
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
|
* Date: 2026/9/4
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalForeignApi::class)
|
@file:OptIn(ExperimentalForeignApi::class, UnsafeNumber::class)
|
||||||
|
|
||||||
package cn.rtast.libmc.common
|
package cn.rtast.libmc.common
|
||||||
|
|
||||||
import kotlinx.cinterop.*
|
import kotlinx.cinterop.*
|
||||||
|
import platform.posix.u_longVar
|
||||||
import platform.zlib.*
|
import platform.zlib.*
|
||||||
|
|
||||||
private const val ENABLE_ZLIB_GZIP_HEADER = 15 + 32
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ kotlin {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(project(":common"))
|
api(project(":common"))
|
||||||
api(libs.kotlinx.serialization.core)
|
api(libs.kotlinx.serialization.core)
|
||||||
api(libs.kotlinx.serialization.json)
|
api(libs.kotlinx.serialization.json)
|
||||||
api(libs.kotlinx.coroutines)
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-48
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+73
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-87
@@ -7,60 +7,53 @@
|
|||||||
package cn.rtast.libmc.protocol.client
|
package cn.rtast.libmc.protocol.client
|
||||||
|
|
||||||
import cn.rtast.libmc.common.LibMCContext
|
import cn.rtast.libmc.common.LibMCContext
|
||||||
import cn.rtast.libmc.common.packet.MinecraftPacket
|
import cn.rtast.libmc.protocol.event.PacketEventDispatcher
|
||||||
import cn.rtast.libmc.common.packet.UnknownPacket
|
|
||||||
import cn.rtast.libmc.protocol.network.NetworkChannel
|
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.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.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.HandshakeIntent
|
||||||
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
|
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
|
||||||
import cn.rtast.libmc.protocol.util.generateOfflineUuid
|
import cn.rtast.libmc.protocol.util.generateOfflineUuid
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
public class MinecraftClient(
|
public class MinecraftClient internal constructor(
|
||||||
private val host: String,
|
private val host: String,
|
||||||
private val port: Int = 25565,
|
private val port: Int = 25565,
|
||||||
private val username: String,
|
private val username: String,
|
||||||
private val uuid: Uuid = generateOfflineUuid(username),
|
private val uuid: Uuid,
|
||||||
private val context: LibMCContext = LibMCContext(),
|
context: LibMCContext,
|
||||||
) {
|
parentJob: Job?,
|
||||||
private val stateMachine = ClientStateMachine()
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
private val networkChannel = NetworkChannel(host, port, context, stateMachine)
|
) : PacketEventDispatcher(), CoroutineScope {
|
||||||
private val listeners = mutableListOf<(MinecraftPacket) -> Unit>()
|
|
||||||
|
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
|
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()
|
networkChannel.connect()
|
||||||
startListening()
|
startListening()
|
||||||
networkChannel.sendPacket(
|
networkChannel.sendPacket(
|
||||||
ServerboundHandshakePacket(
|
ServerboundHandshakePacket(protocolVersion, host, port.toUShort(), HandshakeIntent.LOGIN)
|
||||||
protocolVersion, host,
|
|
||||||
port.toUShort(),
|
|
||||||
HandshakeIntent.LOGIN
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
stateMachine.transitionTo(ProtocolState.LOGIN)
|
stateMachine.transitionTo(ProtocolState.LOGIN)
|
||||||
networkChannel.sendPacket(ServerboundLoginStartPacket(username, uuid))
|
networkChannel.sendPacket(ServerboundLoginStartPacket(username, uuid))
|
||||||
listenJob?.join()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startListening() {
|
private fun startListening() {
|
||||||
listenJob = CoroutineScope(Dispatchers.IO).launch {
|
listenJob = launch {
|
||||||
try {
|
try {
|
||||||
while (isActive) {
|
while (isActive) internalPacketDispatcher.handleIncomingPackets(networkChannel.readNextPacket())
|
||||||
val packet = networkChannel.readNextPacket()
|
|
||||||
handleIncomingPackets(packet)
|
|
||||||
listeners.forEach { it.invoke(packet) }
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
if (e is CancellationException) throw e
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
println("Network read loop exception: ${e.message}")
|
println("Network read loop exception: ${e.message}")
|
||||||
close()
|
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() {
|
public fun close() {
|
||||||
listenJob?.cancel()
|
|
||||||
networkChannel.close()
|
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)
|
||||||
+26
@@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-5
@@ -4,7 +4,6 @@
|
|||||||
* Date: 2026/9/5
|
* Date: 2026/9/5
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package cn.rtast.libmc.protocol.network
|
package cn.rtast.libmc.protocol.network
|
||||||
|
|
||||||
import cn.rtast.libmc.common.*
|
import cn.rtast.libmc.common.*
|
||||||
@@ -21,6 +20,7 @@ internal class NetworkChannel(
|
|||||||
private var socket: Socket? = null
|
private var socket: Socket? = null
|
||||||
private var readChannel: ReadChannel? = null
|
private var readChannel: ReadChannel? = null
|
||||||
private var writeChannel: WriteChannel? = null
|
private var writeChannel: WriteChannel? = null
|
||||||
|
private var threshold = -1
|
||||||
|
|
||||||
fun connect() {
|
fun connect() {
|
||||||
val sk = Socket(host, port, context)
|
val sk = Socket(host, port, context)
|
||||||
@@ -29,13 +29,25 @@ internal class NetworkChannel(
|
|||||||
this.writeChannel = sk.openWriteChannel()
|
this.writeChannel = sk.openWriteChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setCompression(threshold: Int) {
|
||||||
|
this.threshold = threshold
|
||||||
|
}
|
||||||
|
|
||||||
fun readNextPacket(): MinecraftPacket {
|
fun readNextPacket(): MinecraftPacket {
|
||||||
val channel = requireNotNull(readChannel) { "ReadChannel not connected" }
|
val channel = requireNotNull(readChannel) { "ReadChannel not connected" }
|
||||||
val length = channel.readVarInt()
|
val packetLength = channel.readVarInt()
|
||||||
val buf = channel.readBytes(length).wrap()
|
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 currentState = stateMachine.currentState
|
||||||
val packetId = buf.readVarInt()
|
val packetId = payloadBuf.readVarInt()
|
||||||
return GameProtocols.clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, buf)
|
return GameProtocols.clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, payloadBuf)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendPacket(packet: MinecraftPacket) {
|
fun sendPacket(packet: MinecraftPacket) {
|
||||||
@@ -43,8 +55,28 @@ internal class NetworkChannel(
|
|||||||
val bodyBuffer = BytesBuffer()
|
val bodyBuffer = BytesBuffer()
|
||||||
GameProtocols.serverboundGameProtocols.getRegistry(stateMachine.currentState).encodePacket(bodyBuffer, packet)
|
GameProtocols.serverboundGameProtocols.getRegistry(stateMachine.currentState).encodePacket(bodyBuffer, packet)
|
||||||
val frameBuffer = BytesBuffer().apply {
|
val frameBuffer = BytesBuffer().apply {
|
||||||
|
if (threshold < 0) {
|
||||||
writeVarInt(bodyBuffer.size)
|
writeVarInt(bodyBuffer.size)
|
||||||
writeBuffer(bodyBuffer)
|
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.writeFully(frameBuffer.toByteArray())
|
||||||
channel.flush()
|
channel.flush()
|
||||||
|
|||||||
+26
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
-2
@@ -9,8 +9,6 @@ package cn.rtast.libmc.protocol.packet.play
|
|||||||
|
|
||||||
import cn.rtast.libmc.common.*
|
import cn.rtast.libmc.common.*
|
||||||
import cn.rtast.libmc.common.packet.MinecraftPacket
|
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 cn.rtast.libmc.protocol.util.writeMinimalTextNbt
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
@@ -30,6 +28,53 @@ public data class ClientboundPlayerChatMessagePacket(
|
|||||||
val senderName: String,
|
val senderName: String,
|
||||||
val targetName: String?,
|
val targetName: String?,
|
||||||
) : MinecraftPacket {
|
) : 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> {
|
public companion object Codec : PacketCodec<ClientboundPlayerChatMessagePacket> {
|
||||||
override fun encode(buffer: BytesBuffer, value: ClientboundPlayerChatMessagePacket) {
|
override fun encode(buffer: BytesBuffer, value: ClientboundPlayerChatMessagePacket) {
|
||||||
buffer.writeVarInt(value.globalIndex)
|
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.handshake.ServerboundHandshakePacket
|
||||||
import cn.rtast.libmc.protocol.packet.login.ClientboundDisconnectLoginPacket
|
import cn.rtast.libmc.protocol.packet.login.ClientboundDisconnectLoginPacket
|
||||||
import cn.rtast.libmc.protocol.packet.login.ClientboundLoginSuccessPacket
|
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.ServerboundLoginAcknowledgedPacket
|
||||||
import cn.rtast.libmc.protocol.packet.login.ServerboundLoginStartPacket
|
import cn.rtast.libmc.protocol.packet.login.ServerboundLoginStartPacket
|
||||||
import cn.rtast.libmc.protocol.packet.play.*
|
import cn.rtast.libmc.protocol.packet.play.*
|
||||||
@@ -30,6 +31,7 @@ internal object GameProtocols {
|
|||||||
register(ProtocolState.LOGIN) {
|
register(ProtocolState.LOGIN) {
|
||||||
register(0x00, ClientboundDisconnectLoginPacket)
|
register(0x00, ClientboundDisconnectLoginPacket)
|
||||||
register(0x02, ClientboundLoginSuccessPacket)
|
register(0x02, ClientboundLoginSuccessPacket)
|
||||||
|
register(0x03, ClientboundSetCompressionPacket)
|
||||||
}
|
}
|
||||||
register(ProtocolState.PLAY) {
|
register(ProtocolState.PLAY) {
|
||||||
register(0x2C, ClientboundKeepAlivePlayPacket)
|
register(0x2C, ClientboundKeepAlivePlayPacket)
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
package test
|
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 kotlinx.coroutines.test.runTest
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
@@ -15,7 +16,10 @@ class TestClient {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test client`() = runTest {
|
fun `test client`() = runTest {
|
||||||
val cli = MinecraftClient("127.0.0.1", 25565, "123")
|
val cli = createMinecraftClient("127.0.0.1", 25565, "123")
|
||||||
cli.connect()
|
cli.launch { cli.connect() }
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user