Introduce codec
This commit is contained in:
22 files changed
+375
-101
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user