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
|
||||
|
||||
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
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
-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
|
||||
|
||||
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)
|
||||
+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
|
||||
*/
|
||||
|
||||
|
||||
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 {
|
||||
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()
|
||||
|
||||
+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.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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user