Update submodule name
This commit is contained in:
38 files changed
+7
-3
No files matched your search
@@ -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()
|
||||
}
|
||||
}
|
||||
+116
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user