Introduce codec

This commit is contained in:
2026-09-04 16:55:56 +08:00
parent 1d6012ea23
commit 5e4f877a4a
22 files changed
+375 -101

No files matched your search

+2 -2
View File
@@ -1,6 +1,6 @@
plugins { plugins {
kotlin("multiplatform") version "2.4.10" apply false alias(libs.plugins.kotlin.multiplatform) apply false
id("maven-publish") alias(libs.plugins.maven.publish)
} }
allprojects { allprojects {
+14
View File
@@ -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" }
+3 -3
View File
@@ -17,13 +17,13 @@ kotlin {
} }
nativeMain.dependencies { nativeMain.dependencies {
implementation("io.ktor:ktor-network:3.5.2") implementation(libs.ktor.network)
implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.9.1") implementation(libs.kotlinx.io)
} }
commonTest.dependencies { commonTest.dependencies {
implementation(kotlin("test")) implementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") implementation(libs.kotlinx.coroutines.test)
} }
} }
@@ -27,6 +27,7 @@ public expect class _Buffer {
public fun hasRemaining(): Boolean public fun hasRemaining(): Boolean
public fun close() public fun close()
public val size: Int public val size: Int
public val remaining: Long
} }
public fun ByteArray.wrap(): _Buffer = _Buffer(this) public fun ByteArray.wrap(): _Buffer = _Buffer(this)
@@ -0,0 +1,38 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.common
public interface Encoder<in T> {
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<out T> {
public fun decode(buffer: _Buffer): T
public fun decodeFromByteArray(bytes: ByteArray): T = decode(bytes.wrap())
}
public interface PacketCodec<T> : Encoder<T>, Decoder<T>
@Suppress("NOTHING_TO_INLINE")
public inline fun <T> _Buffer.write(value: T, encoder: Encoder<T>): Unit = encoder.encode(this, value)
@Suppress("NOTHING_TO_INLINE")
public inline fun <T> _Buffer.read(decoder: Decoder<T>): 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)
}
@@ -0,0 +1,10 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.common
internal expect fun ByteArray.zlibDecompress(): ByteArray
@@ -94,4 +94,5 @@ public actual class _Buffer {
public actual fun close(): Unit = outStream.close() public actual fun close(): Unit = outStream.close()
public actual val size: Int get() = outStream.size() public actual val size: Int get() = outStream.size()
public actual val remaining: Long get() = (outStream.size() - readOffset).coerceAtLeast(0).toLong()
} }
@@ -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()
}
}
}
@@ -7,6 +7,7 @@
package cn.rtast.libmc.common package cn.rtast.libmc.common
import io.ktor.utils.io.bits.* import io.ktor.utils.io.bits.*
import io.ktor.utils.io.core.*
import kotlinx.io.Buffer import kotlinx.io.Buffer
import kotlinx.io.readByteArray import kotlinx.io.readByteArray
@@ -68,6 +69,6 @@ public actual class _Buffer {
public actual fun hasRemaining(): Boolean = !_delegateBuf.exhausted() public actual fun hasRemaining(): Boolean = !_delegateBuf.exhausted()
public actual fun close(): Unit = _delegateBuf.close() public actual fun close(): Unit = _delegateBuf.close()
public actual val size: Int public actual val size: Int get() = _delegateBuf.size.toInt()
get() = _delegateBuf.size.toInt() public actual val remaining: Long get() = _delegateBuf.remaining
} }
@@ -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<z_stream>()
stream.zalloc = null
stream.zfree = null
stream.opaque = null
val initResult = inflateInit2_(
stream.ptr,
ENABLE_ZLIB_GZIP_HEADER,
ZLIB_VERSION,
sizeOf<z_stream>().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<Byte>(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()
}
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ kotlin {
commonTest.dependencies { commonTest.dependencies {
implementation(kotlin("test")) implementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") implementation(libs.kotlinx.coroutines.test)
} }
} }
} }
@@ -7,10 +7,11 @@
package cn.rtast.libmc.mcping.bedrock package cn.rtast.libmc.mcping.bedrock
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer import cn.rtast.libmc.common._Buffer
import kotlin.random.Random import kotlin.random.Random
private val rakNetMagic = byteArrayOf( private val RAKNET_MAGIC = byteArrayOf(
0x00, 0xFF.toByte(), 0x00, 0xFF.toByte(),
0xFF.toByte(), 0x00, 0xFF.toByte(), 0x00,
0xFE.toByte(), 0xFE.toByte(), 0xFE.toByte(), 0xFE.toByte(),
@@ -22,24 +23,24 @@ private val rakNetMagic = byteArrayOf(
internal interface MinecraftBedrockPacket { internal interface MinecraftBedrockPacket {
val packetId: Byte val packetId: Byte
val time: Long
val magic: ByteArray
fun writePayload(buffer: _Buffer)
} }
internal data class BedrockRequestPacket( internal data class BedrockRequestPacket(
override val time: Long, val time: Long,
override val magic: ByteArray = rakNetMagic, val magic: ByteArray = RAKNET_MAGIC,
val guid: Long = Random.nextLong(), val guid: Long = Random.nextLong(),
) : MinecraftBedrockPacket { ) : MinecraftBedrockPacket {
override val packetId: Byte = 0x01 override val packetId: Byte = 0x01
override fun writePayload(buffer: _Buffer) { companion object Codec : PacketCodec<BedrockRequestPacket> {
buffer.writeByte(packetId) override fun encode(buffer: _Buffer, value: BedrockRequestPacket) {
buffer.writeLong(time) buffer.writeByte(value.packetId)
buffer.writeBytes(magic) buffer.writeLong(value.time)
buffer.writeLong(guid) buffer.writeBytes(value.magic)
buffer.writeLong(value.guid)
}
override fun decode(buffer: _Buffer): BedrockRequestPacket = throw UnsupportedOperationException()
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -64,13 +65,27 @@ internal data class BedrockRequestPacket(
internal data class BedrockResponsePacket( internal data class BedrockResponsePacket(
override val packetId: Byte, override val packetId: Byte,
override val time: Long, val time: Long,
val serverGuid: Long, val serverGuid: Long,
override val magic: ByteArray, val magic: ByteArray,
val stringLength: Short,
val payload: String, val payload: String,
) : MinecraftBedrockPacket { ) : MinecraftBedrockPacket {
override fun writePayload(buffer: _Buffer) {}
companion object Codec : PacketCodec<BedrockResponsePacket> {
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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@@ -79,10 +94,8 @@ internal data class BedrockResponsePacket(
if (packetId != other.packetId) return false if (packetId != other.packetId) return false
if (time != other.time) return false if (time != other.time) return false
if (serverGuid != other.serverGuid) return false if (serverGuid != other.serverGuid) return false
if (stringLength != other.stringLength) return false
if (!magic.contentEquals(other.magic)) return false if (!magic.contentEquals(other.magic)) return false
if (payload != other.payload) return false if (payload != other.payload) return false
return true return true
} }
@@ -90,7 +103,6 @@ internal data class BedrockResponsePacket(
var result = packetId.toInt() var result = packetId.toInt()
result = 31 * result + time.hashCode() result = 31 * result + time.hashCode()
result = 31 * result + serverGuid.hashCode() result = 31 * result + serverGuid.hashCode()
result = 31 * result + stringLength
result = 31 * result + magic.contentHashCode() result = 31 * result + magic.contentHashCode()
result = 31 * result + payload.hashCode() result = 31 * result + payload.hashCode()
return result return result
@@ -7,12 +7,13 @@
package cn.rtast.libmc.mcping.bedrock package cn.rtast.libmc.mcping.bedrock
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common._UdpSocket import cn.rtast.libmc.common._UdpSocket
internal fun _UdpSocket.sendPacket(packet: MinecraftBedrockPacket): ByteArray { internal fun <T : MinecraftBedrockPacket> _UdpSocket.sendPacket(packet: T, codec: PacketCodec<T>): ByteArray {
val buf = _Buffer() val buf = _Buffer()
packet.writePayload(buf) codec.encode(buf, packet)
return sendAndReceive(buf.toByteArray()) return sendAndReceive(buf.toByteArray())
} }
@@ -17,17 +17,12 @@ internal fun pingBedrockServer(host: String, port: Int, context: LibMCContext):
val socket = _UdpSocket(host, port, context) val socket = _UdpSocket(host, port, context)
return try { return try {
val sendTime = Clock.System.now().toEpochMilliseconds() val sendTime = Clock.System.now().toEpochMilliseconds()
val packet = BedrockRequestPacket(sendTime) val requestPacket = BedrockRequestPacket(sendTime)
val responseBytes = socket.sendPacket(packet) val responseBytes = socket.sendPacket(requestPacket, BedrockRequestPacket)
val receiveTime = Clock.System.now().toEpochMilliseconds() val receiveTime = Clock.System.now().toEpochMilliseconds()
val buf = responseBytes.wrap() val responsePacket = BedrockResponsePacket.decode(responseBytes.wrap())
buf.readByte() // packet id val latency = (receiveTime - sendTime).toInt()
buf.readLong() // time PingResponse(responsePacket.payload, latency)
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())
} finally { } finally {
socket.close() socket.close()
} }
@@ -6,43 +6,49 @@
package cn.rtast.libmc.mcping.java package cn.rtast.libmc.mcping.java
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common._ReadChannel
internal object VarIntCodec : PacketCodec<Int> {
internal fun _Buffer.writeVarInt(value: Int) { override fun encode(buffer: _Buffer, value: Int) {
var v = value var v = value
while (true) { while (true) {
if ((v and 0x7F.inv()) == 0) { if ((v and 0x7F.inv()) == 0) {
this.writeByte(v.toByte()) buffer.writeByte(v.toByte())
return 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 { internal object McStringCodec : PacketCodec<String> {
var value = 0 override fun encode(buffer: _Buffer, value: String) {
var position = 0 val bytes = value.encodeToByteArray()
while (true) { VarIntCodec.encode(buffer, bytes.size)
val currentByte = this.readByte().toInt() and 0xFF buffer.writeBytes(bytes)
value = value or ((currentByte and 0x7F) shl position) }
if ((currentByte and 0x80) == 0) break
position += 7 override fun decode(buffer: _Buffer): String {
if (position >= 35) throw IllegalArgumentException("VarInt too long") val length = VarIntCodec.decode(buffer)
val bytes = buffer.readBytes(length)
return bytes.decodeToString()
} }
return value
} }
internal fun _Buffer.writeMcString(value: String) { internal fun _Buffer.writeVarInt(value: Int) = VarIntCodec.encode(this, value)
val bytes = value.encodeToByteArray() internal fun _Buffer.readVarInt(): Int = VarIntCodec.decode(this)
this.writeVarInt(bytes.size)
this.writeBytes(bytes)
}
internal fun _ReadChannel.readMcString(): String {
val length = this.readVarInt()
val bytes = this.readBytes(length)
return bytes.decodeToString()
}
@@ -7,12 +7,11 @@
package cn.rtast.libmc.mcping.java package cn.rtast.libmc.mcping.java
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer import cn.rtast.libmc.common._Buffer
internal interface MinecraftPacket { internal interface MinecraftPacket {
val packetId: Int val packetId: Int
fun writePayload(buffer: _Buffer)
} }
// ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Handshake // ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Handshake
@@ -20,29 +19,41 @@ internal data class HandshakePacket(
val protocolVersion: Int, val protocolVersion: Int,
val serverAddress: String, val serverAddress: String,
val serverPort: UShort, val serverPort: UShort,
// 1 -> Status // 1 -> Status, 2 -> Login
val nextState: Int, val nextState: Int,
) : MinecraftPacket { ) : MinecraftPacket {
override val packetId: Int = 0x00 override val packetId: Int = 0x00
override fun writePayload(buffer: _Buffer) { companion object Codec : PacketCodec<HandshakePacket> {
buffer.writeVarInt(protocolVersion) override fun encode(buffer: _Buffer, value: HandshakePacket) {
buffer.writeMcString(serverAddress) VarIntCodec.encode(buffer, value.protocolVersion)
// write UShort McStringCodec.encode(buffer, value.serverAddress)
// write 2 bytes big endian buffer.writeShort(value.serverPort.toShort())
buffer.writeByte((serverPort.toInt() shr 8).toByte()) VarIntCodec.encode(buffer, value.nextState)
buffer.writeByte(serverPort.toByte()) }
buffer.writeVarInt(nextState)
override fun decode(buffer: _Buffer): HandshakePacket = throw UnsupportedOperationException()
} }
} }
// ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Status // ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Status
internal data object StatusRequestPacket : MinecraftPacket { internal data object StatusRequestPacket : MinecraftPacket, PacketCodec<StatusRequestPacket> {
override val packetId: Int = 0x00 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 { internal data class PingPacket(val currentTime: Long) : MinecraftPacket {
override val packetId: Int = 0x01 override val packetId: Int = 0x01
override fun writePayload(buffer: _Buffer) = buffer.writeLong(currentTime)
companion object : PacketCodec<PingPacket> {
override fun encode(buffer: _Buffer, value: PingPacket) {
buffer.writeLong(value.currentTime)
}
override fun decode(buffer: _Buffer): PingPacket {
return PingPacket(buffer.readLong())
}
}
} }
@@ -7,16 +7,20 @@
package cn.rtast.libmc.mcping.java package cn.rtast.libmc.mcping.java
import cn.rtast.libmc.common.PacketCodec
import cn.rtast.libmc.common._Buffer import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common._WriteChannel 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 <T : MinecraftPacket> _WriteChannel.sendPacket(packet: T, codec: PacketCodec<T>) {
val bodyBuffer = _Buffer() val bodyBuffer = _Buffer()
bodyBuffer.writeVarInt(packet.packetId) bodyBuffer.write(packet.packetId, VarIntCodec)
packet.writePayload(bodyBuffer) codec.encode(bodyBuffer, packet)
val frameBuffer = _Buffer() val frameBuffer = _Buffer()
frameBuffer.writeVarInt(bodyBuffer.size) frameBuffer.write(bodyBuffer.size, VarIntCodec)
frameBuffer.writeBytes(bodyBuffer.toByteArray()) frameBuffer.writeBuffer(bodyBuffer)
val bytes = frameBuffer.toByteArray() val bytes = frameBuffer.toByteArray()
this.writeFully(bytes, 0, bytes.size) this.writeFully(bytes, 0, bytes.size)
this.flush() this.flush()
@@ -8,6 +8,8 @@
package cn.rtast.libmc.mcping.java package cn.rtast.libmc.mcping.java
import cn.rtast.libmc.common.LibMCContext 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.common._Socket
import cn.rtast.libmc.mcping.PingResponse import cn.rtast.libmc.mcping.PingResponse
import kotlin.time.Clock import kotlin.time.Clock
@@ -24,22 +26,46 @@ internal fun pingJavaServer(host: String, port: Int, context: LibMCContext): Pin
serverPort = port.toUShort(), serverPort = port.toUShort(),
nextState = 1 nextState = 1
) )
sendChannel.sendPacket(handshakePacket) sendChannel.sendPacket(handshakePacket, HandshakePacket)
sendChannel.sendPacket(StatusRequestPacket) sendChannel.sendPacket(StatusRequestPacket, StatusRequestPacket)
receiveChannel.readVarInt() // consume a varint val statusFrameBuffer = receiveChannel.readPacketFrame()
val packetId = receiveChannel.readVarInt() val statusPacketId = VarIntCodec.decode(statusFrameBuffer)
val jsonResponse = if (packetId == StatusRequestPacket.packetId) receiveChannel.readMcString() if (statusPacketId != 0x00) {
else throw IllegalStateException("Server does not respond correct packet id, expected ${StatusRequestPacket.packetId} but got $packetId") throw IllegalStateException("Expected StatusResponse packet ID 0x00, got $statusPacketId")
}
val jsonResponse = McStringCodec.decode(statusFrameBuffer)
val sendTime = Clock.System.now().toEpochMilliseconds() val sendTime = Clock.System.now().toEpochMilliseconds()
val pingPacket = PingPacket(sendTime) val pingPacket = PingPacket(sendTime)
sendChannel.sendPacket(pingPacket) sendChannel.sendPacket(pingPacket, PingPacket)
receiveChannel.readVarInt() // consume a varint
receiveChannel.readVarInt() // packet id val pongFrameBuffer = receiveChannel.readPacketFrame()
receiveChannel.readLong() // pong packet payload val pongPacketId = VarIntCodec.decode(pongFrameBuffer)
PingResponse(jsonResponse, (Clock.System.now().toEpochMilliseconds() - sendTime).toInt()) if (pongPacketId != 0x01) {
throw IllegalStateException("Expected Pong packet ID 0x01, got $pongPacketId")
}
val latency = (Clock.System.now().toEpochMilliseconds() - sendTime).toInt()
PingResponse(jsonResponse, latency)
} finally { } finally {
socket.close() 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)
}
}
+32
View File
@@ -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)
}
}
}
@@ -0,0 +1,9 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.libmc.anvil
+1 -1
View File
@@ -22,7 +22,7 @@ kotlin {
commonTest.dependencies { commonTest.dependencies {
implementation(kotlin("test")) implementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") implementation(libs.kotlinx.coroutines.test)
} }
} }
} }
+1
View File
@@ -6,6 +6,7 @@ rootProject.name = "libmc"
includeSubModule(":common") includeSubModule(":common")
includeSubModule(":mcping") includeSubModule(":mcping")
includeSubModule(":rconlib") includeSubModule(":rconlib")
includeSubModule(":nbt")
fun includeSubModule(name: String) = include(name).also { fun includeSubModule(name: String) = include(name).also {
project(name).projectDir = file("libmc-${name.removePrefix(":")}") project(name).projectDir = file("libmc-${name.removePrefix(":")}")