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

+1 -1
View File
@@ -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)
}
}