Update submodule name

This commit is contained in:
2026-09-04 15:51:16 +08:00
parent ddc5e2aa17
commit 1d6012ea23
38 files changed
+7 -3

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
kotlin {
explicitApi()
linuxX64()
linuxArm64()
macosArm64()
mingwX64()
iosArm64()
iosSimulatorArm64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets {
commonMain.dependencies {
implementation(project(":common"))
}
commonTest.dependencies {
implementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
}
}
}
@@ -0,0 +1,98 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package cn.rtast.libmc.mcping.bedrock
import cn.rtast.libmc.common._Buffer
import kotlin.random.Random
private val rakNetMagic = byteArrayOf(
0x00, 0xFF.toByte(),
0xFF.toByte(), 0x00,
0xFE.toByte(), 0xFE.toByte(),
0xFE.toByte(), 0xFE.toByte(),
0xFD.toByte(), 0xFD.toByte(),
0xFD.toByte(), 0xFD.toByte(),
0x12, 0x34, 0x56, 0x78
)
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 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)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as BedrockRequestPacket
if (time != other.time) return false
if (guid != other.guid) return false
if (packetId != other.packetId) return false
if (!magic.contentEquals(other.magic)) return false
return true
}
override fun hashCode(): Int {
var result = time.hashCode()
result = 31 * result + guid.hashCode()
result = 31 * result + packetId
result = 31 * result + magic.contentHashCode()
return result
}
}
internal data class BedrockResponsePacket(
override val packetId: Byte,
override val time: Long,
val serverGuid: Long,
override val magic: ByteArray,
val stringLength: Short,
val payload: String,
) : MinecraftBedrockPacket {
override fun writePayload(buffer: _Buffer) {}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as 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
}
override fun hashCode(): Int {
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
}
}
@@ -0,0 +1,18 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package cn.rtast.libmc.mcping.bedrock
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common._UdpSocket
internal fun _UdpSocket.sendPacket(packet: MinecraftBedrockPacket): ByteArray {
val buf = _Buffer()
packet.writePayload(buf)
return sendAndReceive(buf.toByteArray())
}
@@ -0,0 +1,34 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package cn.rtast.libmc.mcping.bedrock
import cn.rtast.libmc.common.LibMCContext
import cn.rtast.libmc.common._UdpSocket
import cn.rtast.libmc.common.wrap
import cn.rtast.libmc.mcping.PingResponse
import kotlin.time.Clock
internal fun pingBedrockServer(host: String, port: Int, context: LibMCContext): PingResponse {
val socket = _UdpSocket(host, port, context)
return try {
val sendTime = Clock.System.now().toEpochMilliseconds()
val packet = BedrockRequestPacket(sendTime)
val responseBytes = socket.sendPacket(packet)
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())
} finally {
socket.close()
}
}
@@ -0,0 +1,116 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package cn.rtast.libmc.mcping.bedrock
import cn.rtast.libmc.mcping.PingResponse
public enum class BedrockGameMode(public val gameMode: String) {
Survival("Survival"),
Creative("Creative"),
Adventure("Adventure"),
Spectator("Spectator"), // reserved?
Hardcore("Hardcore"),
Unknown(""); // reserved
public companion object {
public fun parse(value: String?): BedrockGameMode {
return entries.firstOrNull {
it.name.equals(value, ignoreCase = true)
} ?: Unknown
}
}
}
public data class BedrockPingResponse(
/**
* always be `MCPE`
* index 0
*/
val protocolHeader: String,
/**
* MOTD line 1
* index 1
*/
val motdLine1: String,
/**
* protocol version
* index 2
*/
val protocolVersion: Int,
/**
* game version
* index 3
*/
val gameVersion: String,
/**
* online players
* index 4
*/
val onlinePlayers: Int,
/**
* maximum players
* index 5
*/
val maximumPlayers: Int,
/**
* server GUID
* parsed as String -> origin bytes count is 8
* index 6
*/
val serverGUID: String,
/**
* MOTD line 2
* index 7
*/
val motdLine2: String,
/**
* game mode
* index 8
*/
val gameMode: BedrockGameMode,
/**
* Nintendo Limited
* Nintendo Switch online restriction flag (1 indicates restriction enabled/processed)
* index 9
*/
val nintendoLimited: Boolean,
/**
* ipv4 port
* if disabled or unconfigured, it will be 0
* index 10
*/
val ipv4Port: Int,
/**
* ipv6 port
* if disabled or unconfigured, it will be 0
* index 11
*/
val ipv6Port: Int,
/**
* latency ms
* reserved not exists in response packet
*/
val latency: Int
)
internal fun PingResponse.parseBedrockPingResponse(): BedrockPingResponse {
try {
val fields = content.split(";")
return BedrockPingResponse(
fields[0], fields[1], fields[2].toInt(),
fields[3], fields[4].toInt(),
fields[5].toInt(), fields[6],
fields[7], BedrockGameMode.parse(fields[8]),
fields.getOrNull(9) == "1", fields[10].toInt(),
fields[11].toInt(), this.latency // pass through PingResponse.latency
)
} catch (e: Exception) {
e.printStackTrace()
throw IllegalStateException("The server respond incorrect response: Missing fields or type mismatch")
}
}
@@ -0,0 +1,48 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package cn.rtast.libmc.mcping.java
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
}
this.writeByte(((v and 0x7F) or 0x80).toByte())
v = v ushr 7
}
}
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")
}
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()
}
@@ -0,0 +1,48 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package cn.rtast.libmc.mcping.java
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
internal data class HandshakePacket(
val protocolVersion: Int,
val serverAddress: String,
val serverPort: UShort,
// 1 -> Status
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)
}
}
// ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Status
internal data object StatusRequestPacket : MinecraftPacket {
override val packetId: Int = 0x00
override fun writePayload(buffer: _Buffer) {}
}
internal data class PingPacket(val currentTime: Long) : MinecraftPacket {
override val packetId: Int = 0x01
override fun writePayload(buffer: _Buffer) = buffer.writeLong(currentTime)
}
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package cn.rtast.libmc.mcping.java
import cn.rtast.libmc.common._Buffer
import cn.rtast.libmc.common._WriteChannel
internal fun _WriteChannel.sendPacket(packet: MinecraftPacket) {
val bodyBuffer = _Buffer()
bodyBuffer.writeVarInt(packet.packetId)
packet.writePayload(bodyBuffer)
val frameBuffer = _Buffer()
frameBuffer.writeVarInt(bodyBuffer.size)
frameBuffer.writeBytes(bodyBuffer.toByteArray())
val bytes = frameBuffer.toByteArray()
this.writeFully(bytes, 0, bytes.size)
this.flush()
}
@@ -0,0 +1,45 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package cn.rtast.libmc.mcping.java
import cn.rtast.libmc.common.LibMCContext
import cn.rtast.libmc.common._Socket
import cn.rtast.libmc.mcping.PingResponse
import kotlin.time.Clock
internal fun pingJavaServer(host: String, port: Int, context: LibMCContext): PingResponse {
val socket = _Socket(host, port, context)
val receiveChannel = socket.openReadChannel()
val sendChannel = socket.openWriteChannel()
return try {
val handshakePacket = HandshakePacket(
protocolVersion = -1,
serverAddress = host,
serverPort = port.toUShort(),
nextState = 1
)
sendChannel.sendPacket(handshakePacket)
sendChannel.sendPacket(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 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())
} finally {
socket.close()
}
}
@@ -0,0 +1,27 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
@file:JvmName("McPing")
package cn.rtast.libmc.mcping
import cn.rtast.libmc.common.LibMCContext
import cn.rtast.libmc.mcping.bedrock.pingBedrockServer
import cn.rtast.libmc.mcping.java.pingJavaServer
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
@JvmOverloads
public fun mcping(
host: String,
port: Int,
type: ServerType = ServerType.Java,
context: LibMCContext = LibMCContext(),
): PingResponse = when (type) {
ServerType.Java -> pingJavaServer(host, port, context)
ServerType.Bedrock -> pingBedrockServer(host, port, context)
}
@@ -0,0 +1,24 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package cn.rtast.libmc.mcping
import cn.rtast.libmc.mcping.bedrock.BedrockPingResponse
import cn.rtast.libmc.mcping.bedrock.parseBedrockPingResponse
public data class PingResponse(
/**
* raw response content
*/
public val content: String,
/**
* latency ms
*/
public val latency: Int,
) {
public fun toBedrockResponse(): BedrockPingResponse = parseBedrockPingResponse()
}
@@ -0,0 +1,12 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package cn.rtast.libmc.mcping
public enum class ServerType {
Java, Bedrock
}
@@ -0,0 +1,29 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package test
import cn.rtast.libmc.mcping.ServerType
import cn.rtast.libmc.mcping.mcping
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
class TestPing {
@Test
fun `test ping java server`() = runTest {
val response = mcping("org.mc-complex.com", 25565)
println(response)
}
@Test
fun `test ping bedrock server`() = runTest {
val response = mcping("play.wildnetwork.net", 19132, ServerType.Bedrock)
println(response)
println(response.toBedrockResponse())
}
}
@@ -0,0 +1,33 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package test;
import cn.rtast.libmc.mcping.McPing;
import cn.rtast.libmc.mcping.PingResponse;
import cn.rtast.libmc.mcping.ServerType;
import org.junit.Test;
public class TestPingInJava {
@Test
public void testPingJava() {
String testJavaHost = "org.mc-complex.com";
PingResponse resp = McPing.mcping(testJavaHost, 25565);
System.out.println(resp);
}
@Test
public void testPingBedrock() {
String testBedrockHost = "play.wildnetwork.net";
PingResponse resp = McPing.mcping(testBedrockHost, 19132, ServerType.Bedrock);
System.out.println(resp.getContent());
System.out.println(resp.getLatency());
System.out.println(resp.toBedrockResponse());
}
}
@@ -0,0 +1,37 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/3
*/
package test
import cn.rtast.libmc.common.LibMCContext
import cn.rtast.libmc.mcping.ServerType
import cn.rtast.libmc.mcping.mcping
import io.ktor.network.selector.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
class TestMingwPing {
@Test
fun `test ping java on mingw with selectorManager`() = runTest {
val sm = SelectorManager(Dispatchers.IO)
val response =
mcping(host = "org.mc-complex.com", port = 25565, type = ServerType.Java, context = LibMCContext(sm))
println(response)
}
@Test
fun `test ping bedrock on mingw with selectorManager`() = runTest {
val sm = SelectorManager(Dispatchers.IO)
val response =
mcping(host = "play.wildnetwork.net", port = 19132, type = ServerType.Bedrock, context = LibMCContext(sm))
println(response)
println(response.toBedrockResponse())
}
}