Introduce codec
This commit is contained in:
22 files changed
+375
-101
No files matched your search
+2
-2
@@ -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 {
|
||||
|
||||
@@ -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" }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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 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
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<BedrockRequestPacket> {
|
||||
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<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 {
|
||||
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
|
||||
|
||||
@@ -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 <T : MinecraftBedrockPacket> _UdpSocket.sendPacket(packet: T, codec: PacketCodec<T>): ByteArray {
|
||||
val buf = _Buffer()
|
||||
packet.writePayload(buf)
|
||||
codec.encode(buf, packet)
|
||||
return sendAndReceive(buf.toByteArray())
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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<Int> {
|
||||
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<String> {
|
||||
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()
|
||||
}
|
||||
internal fun _Buffer.writeVarInt(value: Int) = VarIntCodec.encode(this, value)
|
||||
internal fun _Buffer.readVarInt(): Int = VarIntCodec.decode(this)
|
||||
@@ -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<HandshakePacket> {
|
||||
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<StatusRequestPacket> {
|
||||
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<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
|
||||
|
||||
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 <T : MinecraftPacket> _WriteChannel.sendPacket(packet: T, codec: PacketCodec<T>) {
|
||||
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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(":")}")
|
||||
|
||||
Reference in New Issue
Block a user