diff --git a/build.gradle.kts b/build.gradle.kts index a2ea04e..8f3c965 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - kotlin("multiplatform") version "2.4.10" apply false - id("maven-publish") + alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.maven.publish) } allprojects { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..f6c84cf --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,14 @@ +[versions] +kotlin = "2.4.10" +kotlinx-io = "0.9.1" +ktor-network = "3.5.2" +coroutines-test = "1.11.0" + +[libraries] +kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" } +ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor-network" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines-test" } + +[plugins] +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +maven-publish = { id = "maven-publish" } \ No newline at end of file diff --git a/libmc-common/build.gradle.kts b/libmc-common/build.gradle.kts index b9a9960..ecbc18f 100644 --- a/libmc-common/build.gradle.kts +++ b/libmc-common/build.gradle.kts @@ -17,13 +17,13 @@ kotlin { } nativeMain.dependencies { - implementation("io.ktor:ktor-network:3.5.2") - implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.9.1") + implementation(libs.ktor.network) + implementation(libs.kotlinx.io) } commonTest.dependencies { implementation(kotlin("test")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") + implementation(libs.kotlinx.coroutines.test) } } diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/buffer.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/buffer.kt index 0e9186d..3a1d9e1 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/buffer.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/buffer.kt @@ -27,6 +27,7 @@ public expect class _Buffer { public fun hasRemaining(): Boolean public fun close() public val size: Int + public val remaining: Long } public fun ByteArray.wrap(): _Buffer = _Buffer(this) \ No newline at end of file diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/codec.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/codec.kt new file mode 100644 index 0000000..66e2546 --- /dev/null +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/codec.kt @@ -0,0 +1,38 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.common + +public interface Encoder { + public fun encode(buffer: _Buffer, value: T) + + public fun encodeToByteArray(value: T): ByteArray { + val buf = _Buffer() + encode(buf, value) + return buf.toByteArray() + } +} + +public interface Decoder { + public fun decode(buffer: _Buffer): T + + public fun decodeFromByteArray(bytes: ByteArray): T = decode(bytes.wrap()) +} + +public interface PacketCodec : Encoder, Decoder + +@Suppress("NOTHING_TO_INLINE") +public inline fun _Buffer.write(value: T, encoder: Encoder): Unit = encoder.encode(this, value) + +@Suppress("NOTHING_TO_INLINE") +public inline fun _Buffer.read(decoder: Decoder): T = decoder.decode(this) + +public fun _Buffer.writeBuffer(source: _Buffer, length: Long = source.remaining) { + if (length <= 0) return + val bytes = source.readBytes(length.toInt()) + this.writeBytes(bytes) +} \ No newline at end of file diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/zlib.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/zlib.kt new file mode 100644 index 0000000..e36ca76 --- /dev/null +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/common/zlib.kt @@ -0,0 +1,10 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.common + +internal expect fun ByteArray.zlibDecompress(): ByteArray \ No newline at end of file diff --git a/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/buffer.jvm.kt b/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/buffer.jvm.kt index a94cf25..80529c4 100644 --- a/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/buffer.jvm.kt +++ b/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/buffer.jvm.kt @@ -94,4 +94,5 @@ public actual class _Buffer { public actual fun close(): Unit = outStream.close() public actual val size: Int get() = outStream.size() + public actual val remaining: Long get() = (outStream.size() - readOffset).coerceAtLeast(0).toLong() } \ No newline at end of file diff --git a/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/zlib.jvm.kt b/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/zlib.jvm.kt new file mode 100644 index 0000000..d97e719 --- /dev/null +++ b/libmc-common/src/jvmMain/kotlin/cn/rtast/libmc/common/zlib.jvm.kt @@ -0,0 +1,44 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + +package cn.rtast.libmc.common + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.util.zip.GZIPInputStream +import java.util.zip.Inflater + +public actual fun ByteArray.zlibDecompress(): ByteArray { + if (isEmpty()) return byteArrayOf() + val isGzip = size >= 2 && this[0] == 0x1F.toByte() && this[1] == 0x8B.toByte() + return if (isGzip) this.gzipDecompress() else { + val inflater = Inflater(false) + val outputStream = ByteArrayOutputStream(this.size) + val buffer = ByteArray(1024) + inflater.setInput(this) + while (!inflater.finished()) { + val length = inflater.inflate(buffer) + if (length > 0) outputStream.write(buffer, 0, length) + } + inflater.end() + outputStream.toByteArray() + } +} + +private fun ByteArray.gzipDecompress(): ByteArray { + if (isEmpty()) return byteArrayOf() + ByteArrayInputStream(this).use { bais -> + GZIPInputStream(bais).use { gzis -> + val outputStream = ByteArrayOutputStream(this.size * 2) + val buffer = ByteArray(1024) + var len: Int + while (gzis.read(buffer).also { len = it } != -1) { + outputStream.write(buffer, 0, len) + } + return outputStream.toByteArray() + } + } +} \ No newline at end of file diff --git a/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/buffer.native.kt b/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/buffer.native.kt index bca7cd2..7fd1346 100644 --- a/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/buffer.native.kt +++ b/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/buffer.native.kt @@ -7,6 +7,7 @@ package cn.rtast.libmc.common import io.ktor.utils.io.bits.* +import io.ktor.utils.io.core.* import kotlinx.io.Buffer import kotlinx.io.readByteArray @@ -68,6 +69,6 @@ public actual class _Buffer { public actual fun hasRemaining(): Boolean = !_delegateBuf.exhausted() public actual fun close(): Unit = _delegateBuf.close() - public actual val size: Int - get() = _delegateBuf.size.toInt() + public actual val size: Int get() = _delegateBuf.size.toInt() + public actual val remaining: Long get() = _delegateBuf.remaining } \ No newline at end of file diff --git a/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/zlib.native.kt b/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/zlib.native.kt new file mode 100644 index 0000000..6de10d8 --- /dev/null +++ b/libmc-common/src/nativeMain/kotlin/cn/rtast/libmc/common/zlib.native.kt @@ -0,0 +1,68 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + +@file:OptIn(ExperimentalForeignApi::class) + +package cn.rtast.libmc.common + +import kotlinx.cinterop.* +import platform.zlib.* + +private const val ENABLE_ZLIB_GZIP_HEADER = 15 + 32 + +public actual fun ByteArray.zlibDecompress(): ByteArray { + if (isEmpty()) return ByteArray(0) + return memScoped { + val stream = alloc() + stream.zalloc = null + stream.zfree = null + stream.opaque = null + + val initResult = inflateInit2_( + stream.ptr, + ENABLE_ZLIB_GZIP_HEADER, + ZLIB_VERSION, + sizeOf().toInt() + ) + check(initResult == Z_OK) { "inflateInit2_ failed with code: $initResult" } + + val inputPinned = this@zlibDecompress.pin() + try { + stream.next_in = inputPinned.addressOf(0).reinterpret() + stream.avail_in = this@zlibDecompress.size.toUInt() + + val bufferSize = 4096 + val tempBuffer = ByteArray(bufferSize) + val tempPinned = tempBuffer.pin() + val output = ArrayList(this@zlibDecompress.size * 3) + + try { + var result: Int + do { + stream.next_out = tempPinned.addressOf(0).reinterpret() + stream.avail_out = bufferSize.toUInt() + + result = inflate(stream.ptr, Z_NO_FLUSH) + check(result == Z_OK || result == Z_STREAM_END) { "inflate error: $result" } + + val bytesDecompressed = bufferSize - stream.avail_out.toInt() + for (i in 0 until bytesDecompressed) { + output.add(tempBuffer[i]) + } + + if (result == Z_STREAM_END) break + } while (stream.avail_in > 0u || stream.avail_out == 0u) + } finally { + tempPinned.unpin() + } + + inflateEnd(stream.ptr) + return@memScoped output.toByteArray() + } finally { + inputPinned.unpin() + } + } +} \ No newline at end of file diff --git a/libmc-mcping/build.gradle.kts b/libmc-mcping/build.gradle.kts index d88aef9..1dabf62 100644 --- a/libmc-mcping/build.gradle.kts +++ b/libmc-mcping/build.gradle.kts @@ -18,7 +18,7 @@ kotlin { commonTest.dependencies { implementation(kotlin("test")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") + implementation(libs.kotlinx.coroutines.test) } } } \ No newline at end of file diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/bedrock/packet.kt b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/bedrock/packet.kt index 287419d..ccf186a 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/bedrock/packet.kt +++ b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/bedrock/packet.kt @@ -7,10 +7,11 @@ package cn.rtast.libmc.mcping.bedrock +import cn.rtast.libmc.common.PacketCodec import cn.rtast.libmc.common._Buffer import kotlin.random.Random -private val rakNetMagic = byteArrayOf( +private val RAKNET_MAGIC = byteArrayOf( 0x00, 0xFF.toByte(), 0xFF.toByte(), 0x00, 0xFE.toByte(), 0xFE.toByte(), @@ -22,24 +23,24 @@ private val rakNetMagic = byteArrayOf( internal interface MinecraftBedrockPacket { val packetId: Byte - val time: Long - val magic: ByteArray - - fun writePayload(buffer: _Buffer) } internal data class BedrockRequestPacket( - override val time: Long, - override val magic: ByteArray = rakNetMagic, + val time: Long, + val magic: ByteArray = RAKNET_MAGIC, val guid: Long = Random.nextLong(), ) : MinecraftBedrockPacket { override val packetId: Byte = 0x01 - override fun writePayload(buffer: _Buffer) { - buffer.writeByte(packetId) - buffer.writeLong(time) - buffer.writeBytes(magic) - buffer.writeLong(guid) + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: BedrockRequestPacket) { + buffer.writeByte(value.packetId) + buffer.writeLong(value.time) + buffer.writeBytes(value.magic) + buffer.writeLong(value.guid) + } + + override fun decode(buffer: _Buffer): BedrockRequestPacket = throw UnsupportedOperationException() } override fun equals(other: Any?): Boolean { @@ -64,13 +65,27 @@ internal data class BedrockRequestPacket( internal data class BedrockResponsePacket( override val packetId: Byte, - override val time: Long, + val time: Long, val serverGuid: Long, - override val magic: ByteArray, - val stringLength: Short, + val magic: ByteArray, val payload: String, ) : MinecraftBedrockPacket { - override fun writePayload(buffer: _Buffer) {} + + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: BedrockResponsePacket) = throw UnsupportedOperationException() + override fun decode(buffer: _Buffer): BedrockResponsePacket { + val packetId = buffer.readByte() + if (packetId != 0x1C.toByte()) throw IllegalStateException("Expected pong id 0x1C, got $packetId") + val time = buffer.readLong() + val serverGuid = buffer.readLong() + val magic = buffer.readBytes(16) + if (!magic.contentEquals(RAKNET_MAGIC)) throw IllegalStateException("Invalid magic in response") + val payloadLength = buffer.readShort().toInt() and 0xFFFF + val payloadBytes = buffer.readBytes(payloadLength) + val payload = payloadBytes.decodeToString() + return BedrockResponsePacket(packetId, time, serverGuid, magic, payload) + } + } override fun equals(other: Any?): Boolean { if (this === other) return true @@ -79,10 +94,8 @@ internal data class BedrockResponsePacket( if (packetId != other.packetId) return false if (time != other.time) return false if (serverGuid != other.serverGuid) return false - if (stringLength != other.stringLength) return false if (!magic.contentEquals(other.magic)) return false if (payload != other.payload) return false - return true } @@ -90,7 +103,6 @@ internal data class BedrockResponsePacket( var result = packetId.toInt() result = 31 * result + time.hashCode() result = 31 * result + serverGuid.hashCode() - result = 31 * result + stringLength result = 31 * result + magic.contentHashCode() result = 31 * result + payload.hashCode() return result diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/bedrock/packet_writer.kt b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/bedrock/packet_writer.kt index 94c70c8..224bde5 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/bedrock/packet_writer.kt +++ b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/bedrock/packet_writer.kt @@ -7,12 +7,13 @@ package cn.rtast.libmc.mcping.bedrock +import cn.rtast.libmc.common.PacketCodec import cn.rtast.libmc.common._Buffer import cn.rtast.libmc.common._UdpSocket -internal fun _UdpSocket.sendPacket(packet: MinecraftBedrockPacket): ByteArray { +internal fun _UdpSocket.sendPacket(packet: T, codec: PacketCodec): ByteArray { val buf = _Buffer() - packet.writePayload(buf) + codec.encode(buf, packet) return sendAndReceive(buf.toByteArray()) } \ No newline at end of file diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/bedrock/ping_bedrock.kt b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/bedrock/ping_bedrock.kt index f38487b..e20748e 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/bedrock/ping_bedrock.kt +++ b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/bedrock/ping_bedrock.kt @@ -17,17 +17,12 @@ internal fun pingBedrockServer(host: String, port: Int, context: LibMCContext): val socket = _UdpSocket(host, port, context) return try { val sendTime = Clock.System.now().toEpochMilliseconds() - val packet = BedrockRequestPacket(sendTime) - val responseBytes = socket.sendPacket(packet) + val requestPacket = BedrockRequestPacket(sendTime) + val responseBytes = socket.sendPacket(requestPacket, BedrockRequestPacket) val receiveTime = Clock.System.now().toEpochMilliseconds() - val buf = responseBytes.wrap() - buf.readByte() // packet id - buf.readLong() // time - buf.readLong() // server guid - buf.readBytes(16) // magic refer to `rakNetMagic` - val payloadLength = buf.readShort() - val payload = buf.readBytes(payloadLength.toInt()) - PingResponse(payload.decodeToString(), (receiveTime - sendTime).toInt()) + val responsePacket = BedrockResponsePacket.decode(responseBytes.wrap()) + val latency = (receiveTime - sendTime).toInt() + PingResponse(responsePacket.payload, latency) } finally { socket.close() } diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/mc_primitives.kt b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/mc_primitives.kt index f9ba094..d9e4216 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/mc_primitives.kt +++ b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/mc_primitives.kt @@ -6,43 +6,49 @@ package cn.rtast.libmc.mcping.java +import cn.rtast.libmc.common.PacketCodec import cn.rtast.libmc.common._Buffer -import cn.rtast.libmc.common._ReadChannel - -internal fun _Buffer.writeVarInt(value: Int) { - var v = value - while (true) { - if ((v and 0x7F.inv()) == 0) { - this.writeByte(v.toByte()) - return +internal object VarIntCodec : PacketCodec { + override fun encode(buffer: _Buffer, value: Int) { + var v = value + while (true) { + if ((v and 0x7F.inv()) == 0) { + buffer.writeByte(v.toByte()) + return + } + buffer.writeByte(((v and 0x7F) or 0x80).toByte()) + v = v ushr 7 } - this.writeByte(((v and 0x7F) or 0x80).toByte()) - v = v ushr 7 + } + + override fun decode(buffer: _Buffer): Int { + var value = 0 + var position = 0 + while (true) { + val currentByte = buffer.readByte().toInt() and 0xFF + value = value or ((currentByte and 0x7F) shl position) + if ((currentByte and 0x80) == 0) break + position += 7 + if (position >= 35) throw IllegalArgumentException("VarInt too long") + } + return value } } -internal fun _ReadChannel.readVarInt(): Int { - var value = 0 - var position = 0 - while (true) { - val currentByte = this.readByte().toInt() and 0xFF - value = value or ((currentByte and 0x7F) shl position) - if ((currentByte and 0x80) == 0) break - position += 7 - if (position >= 35) throw IllegalArgumentException("VarInt too long") +internal object McStringCodec : PacketCodec { + override fun encode(buffer: _Buffer, value: String) { + val bytes = value.encodeToByteArray() + VarIntCodec.encode(buffer, bytes.size) + buffer.writeBytes(bytes) + } + + override fun decode(buffer: _Buffer): String { + val length = VarIntCodec.decode(buffer) + val bytes = buffer.readBytes(length) + return bytes.decodeToString() } - return value } -internal fun _Buffer.writeMcString(value: String) { - val bytes = value.encodeToByteArray() - this.writeVarInt(bytes.size) - this.writeBytes(bytes) -} - -internal fun _ReadChannel.readMcString(): String { - val length = this.readVarInt() - val bytes = this.readBytes(length) - return bytes.decodeToString() -} \ No newline at end of file +internal fun _Buffer.writeVarInt(value: Int) = VarIntCodec.encode(this, value) +internal fun _Buffer.readVarInt(): Int = VarIntCodec.decode(this) \ No newline at end of file diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet.kt b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet.kt index 0c78f8f..25fc2a8 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet.kt +++ b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet.kt @@ -7,12 +7,11 @@ package cn.rtast.libmc.mcping.java +import cn.rtast.libmc.common.PacketCodec import cn.rtast.libmc.common._Buffer internal interface MinecraftPacket { val packetId: Int - - fun writePayload(buffer: _Buffer) } // ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Handshake @@ -20,29 +19,41 @@ internal data class HandshakePacket( val protocolVersion: Int, val serverAddress: String, val serverPort: UShort, - // 1 -> Status + // 1 -> Status, 2 -> Login val nextState: Int, ) : MinecraftPacket { override val packetId: Int = 0x00 - override fun writePayload(buffer: _Buffer) { - buffer.writeVarInt(protocolVersion) - buffer.writeMcString(serverAddress) - // write UShort - // write 2 bytes big endian - buffer.writeByte((serverPort.toInt() shr 8).toByte()) - buffer.writeByte(serverPort.toByte()) - buffer.writeVarInt(nextState) + companion object Codec : PacketCodec { + override fun encode(buffer: _Buffer, value: HandshakePacket) { + VarIntCodec.encode(buffer, value.protocolVersion) + McStringCodec.encode(buffer, value.serverAddress) + buffer.writeShort(value.serverPort.toShort()) + VarIntCodec.encode(buffer, value.nextState) + } + + override fun decode(buffer: _Buffer): HandshakePacket = throw UnsupportedOperationException() } } // ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Status -internal data object StatusRequestPacket : MinecraftPacket { +internal data object StatusRequestPacket : MinecraftPacket, PacketCodec { override val packetId: Int = 0x00 - override fun writePayload(buffer: _Buffer) {} + + override fun encode(buffer: _Buffer, value: StatusRequestPacket) {} + override fun decode(buffer: _Buffer): StatusRequestPacket = throw UnsupportedOperationException() } internal data class PingPacket(val currentTime: Long) : MinecraftPacket { override val packetId: Int = 0x01 - override fun writePayload(buffer: _Buffer) = buffer.writeLong(currentTime) + + companion object : PacketCodec { + override fun encode(buffer: _Buffer, value: PingPacket) { + buffer.writeLong(value.currentTime) + } + + override fun decode(buffer: _Buffer): PingPacket { + return PingPacket(buffer.readLong()) + } + } } \ No newline at end of file diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet_writer.kt b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet_writer.kt index f4b12a7..7999991 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet_writer.kt +++ b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/packet_writer.kt @@ -7,16 +7,20 @@ package cn.rtast.libmc.mcping.java +import cn.rtast.libmc.common.PacketCodec import cn.rtast.libmc.common._Buffer import cn.rtast.libmc.common._WriteChannel +import cn.rtast.libmc.common.write +import cn.rtast.libmc.common.writeBuffer -internal fun _WriteChannel.sendPacket(packet: MinecraftPacket) { + +internal fun _WriteChannel.sendPacket(packet: T, codec: PacketCodec) { val bodyBuffer = _Buffer() - bodyBuffer.writeVarInt(packet.packetId) - packet.writePayload(bodyBuffer) + bodyBuffer.write(packet.packetId, VarIntCodec) + codec.encode(bodyBuffer, packet) val frameBuffer = _Buffer() - frameBuffer.writeVarInt(bodyBuffer.size) - frameBuffer.writeBytes(bodyBuffer.toByteArray()) + frameBuffer.write(bodyBuffer.size, VarIntCodec) + frameBuffer.writeBuffer(bodyBuffer) val bytes = frameBuffer.toByteArray() this.writeFully(bytes, 0, bytes.size) this.flush() diff --git a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/ping_java.kt b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/ping_java.kt index 8966ab2..0b5742b 100644 --- a/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/ping_java.kt +++ b/libmc-mcping/src/commonMain/kotlin/cn/rtast/libmc/mcping/java/ping_java.kt @@ -8,6 +8,8 @@ package cn.rtast.libmc.mcping.java import cn.rtast.libmc.common.LibMCContext +import cn.rtast.libmc.common._Buffer +import cn.rtast.libmc.common._ReadChannel import cn.rtast.libmc.common._Socket import cn.rtast.libmc.mcping.PingResponse import kotlin.time.Clock @@ -24,22 +26,46 @@ internal fun pingJavaServer(host: String, port: Int, context: LibMCContext): Pin serverPort = port.toUShort(), nextState = 1 ) - sendChannel.sendPacket(handshakePacket) - sendChannel.sendPacket(StatusRequestPacket) + sendChannel.sendPacket(handshakePacket, HandshakePacket) + sendChannel.sendPacket(StatusRequestPacket, StatusRequestPacket) - receiveChannel.readVarInt() // consume a varint - val packetId = receiveChannel.readVarInt() - val jsonResponse = if (packetId == StatusRequestPacket.packetId) receiveChannel.readMcString() - else throw IllegalStateException("Server does not respond correct packet id, expected ${StatusRequestPacket.packetId} but got $packetId") + val statusFrameBuffer = receiveChannel.readPacketFrame() + val statusPacketId = VarIntCodec.decode(statusFrameBuffer) + if (statusPacketId != 0x00) { + throw IllegalStateException("Expected StatusResponse packet ID 0x00, got $statusPacketId") + } + val jsonResponse = McStringCodec.decode(statusFrameBuffer) val sendTime = Clock.System.now().toEpochMilliseconds() val pingPacket = PingPacket(sendTime) - sendChannel.sendPacket(pingPacket) - receiveChannel.readVarInt() // consume a varint - receiveChannel.readVarInt() // packet id - receiveChannel.readLong() // pong packet payload - PingResponse(jsonResponse, (Clock.System.now().toEpochMilliseconds() - sendTime).toInt()) + sendChannel.sendPacket(pingPacket, PingPacket) + + val pongFrameBuffer = receiveChannel.readPacketFrame() + val pongPacketId = VarIntCodec.decode(pongFrameBuffer) + if (pongPacketId != 0x01) { + throw IllegalStateException("Expected Pong packet ID 0x01, got $pongPacketId") + } + val latency = (Clock.System.now().toEpochMilliseconds() - sendTime).toInt() + PingResponse(jsonResponse, latency) } finally { socket.close() } +} + +private fun _ReadChannel.readVarIntWithCodec(): Int { + val tempBuffer = _Buffer() + while (true) { + val byte = this.readByte() + tempBuffer.writeByte(byte) + if ((byte.toInt() and 0x80) == 0) break + } + return VarIntCodec.decode(tempBuffer) +} + +private fun _ReadChannel.readPacketFrame(): _Buffer { + val length = this.readVarIntWithCodec() + val frameBytes = this.readBytes(length) + return _Buffer().apply { + writeBytes(frameBytes) + } } \ No newline at end of file diff --git a/libmc-nbt/build.gradle.kts b/libmc-nbt/build.gradle.kts new file mode 100644 index 0000000..c154c51 --- /dev/null +++ b/libmc-nbt/build.gradle.kts @@ -0,0 +1,32 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +repositories { + maven("https://repo.rtast.cn/packages/") +} + +kotlin { + explicitApi() + + linuxX64() + linuxArm64() + macosArm64() + mingwX64() + iosArm64() + iosSimulatorArm64() + jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } + + sourceSets { + commonMain.dependencies { + implementation(project(":common")) + } + + jvmMain.dependencies { + + } + + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + } + } +} \ No newline at end of file diff --git a/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/anvil/anvil.kt b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/anvil/anvil.kt new file mode 100644 index 0000000..214d011 --- /dev/null +++ b/libmc-nbt/src/commonMain/kotlin/cn/rtast/libmc/anvil/anvil.kt @@ -0,0 +1,9 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/4 + */ + + +package cn.rtast.libmc.anvil + diff --git a/libmc-rconlib/build.gradle.kts b/libmc-rconlib/build.gradle.kts index 0f8eae9..039da51 100644 --- a/libmc-rconlib/build.gradle.kts +++ b/libmc-rconlib/build.gradle.kts @@ -22,7 +22,7 @@ kotlin { commonTest.dependencies { implementation(kotlin("test")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") + implementation(libs.kotlinx.coroutines.test) } } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 8062980..6517e83 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,6 +6,7 @@ rootProject.name = "libmc" includeSubModule(":common") includeSubModule(":mcping") includeSubModule(":rconlib") +includeSubModule(":nbt") fun includeSubModule(name: String) = include(name).also { project(name).projectDir = file("libmc-${name.removePrefix(":")}")