Support ping bedrock
This commit is contained in:
30 files changed
+736
-77
No files matched your search
@@ -23,19 +23,49 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("cn.rtast.mcping:mcping:0.0.1")
|
||||
implementation("cn.rtast.mcping:mcping:0.0.2")
|
||||
}
|
||||
```
|
||||
|
||||
## Kotlin example
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
// return a json respose
|
||||
val response: String = mcping("org.mc-complex.com", 25565)
|
||||
println(response)
|
||||
val sm = SelectorManager(Dispatchers.IO)
|
||||
// context is not required, if not passed, default context will be used
|
||||
// ping java server
|
||||
val response: PingResponse =
|
||||
mcping(host = "org.mc-complex.com", port = 25565, type = ServerType.Java, context = PingContext(sm))
|
||||
println(response.content)
|
||||
println(response.latency)
|
||||
|
||||
// ping bedrock
|
||||
val response = mcping("play.wildnetwork.net", 19132, ServerType.Bedrock)
|
||||
println(response.toBedrockResponse())
|
||||
}
|
||||
```
|
||||
|
||||
> An example json response can be found at [ping-response-example](example/java-ping-response.json) (Formatted)
|
||||
## Java example
|
||||
|
||||
```java
|
||||
void main() {
|
||||
// ping java server
|
||||
String testJavaHost = "org.mc-complex.com";
|
||||
PingResponse javaResponse = McPing.mcping(testJavaHost, 25565);
|
||||
|
||||
// ping bedrock server
|
||||
String testBedrockHost = "play.wildnetwork.net";
|
||||
PingResponse bedrockResponse = McPing.mcping(testBedrockHost, 19132, ServerType.Bedrock);
|
||||
System.out.println(bedrockResponse.toBedrockResponse());
|
||||
}
|
||||
```
|
||||
|
||||
> java consumers should not manually pass the context parameter
|
||||
|
||||
## Other resources
|
||||
|
||||
> An example of java ping json response can be found at [ping-response-example](example/java-ping-response.json) (Formatted),
|
||||
> a raw response of bedrock ping response can be found at [bedrock-pinng-raw-response](example/bedrock-pinng-raw-response.txt)
|
||||
|
||||
# Open Source
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ plugins {
|
||||
}
|
||||
|
||||
group = "cn.rtast.mcping"
|
||||
version = "0.0.1"
|
||||
version = providers.gradleProperty("libVersion").get()
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
MCPE;" &e&lWILD&f&lNETWORK &6&lS24 &8-&e discord.gg/WildPrison\n&a Release: &f531d 18h 19m 18s ago";2169;26.45;393;1500;11173189777763089377;Another Geyser server.;Survival;1;19132;0;
|
||||
@@ -3,3 +3,5 @@ kotlin.native.ignoreDisabledTargets=true
|
||||
kotlin.native.enableKlibsCrossCompilation=true
|
||||
kotlin.daemon.jvmargs=-Xmx2048M
|
||||
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
|
||||
|
||||
libVersion=0.0.2
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/3
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.mcping.bedrock
|
||||
|
||||
import cn.rtast.mcping.platform.PlatformBuffer
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Clock
|
||||
|
||||
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: PlatformBuffer)
|
||||
}
|
||||
|
||||
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: PlatformBuffer) {
|
||||
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: PlatformBuffer) {}
|
||||
|
||||
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,17 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/3
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.mcping.bedrock
|
||||
|
||||
import cn.rtast.mcping.platform.PlatformBuffer
|
||||
import cn.rtast.mcping.platform.UdpSocket
|
||||
|
||||
internal fun UdpSocket.sendPacket(packet: MinecraftBedrockPacket): ByteArray {
|
||||
val buf = PlatformBuffer()
|
||||
packet.writePayload(buf)
|
||||
return sendAndReceive(buf.toByteArray())
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/3
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.mcping.bedrock
|
||||
|
||||
import cn.rtast.mcping.PingResponse
|
||||
import cn.rtast.mcping.platform.PingContext
|
||||
import cn.rtast.mcping.platform.UdpSocket
|
||||
import cn.rtast.mcping.platform.wrap
|
||||
import kotlin.time.Clock
|
||||
|
||||
internal fun pingBedrockServer(host: String, port: Int, context: PingContext): 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.mcping.bedrock
|
||||
|
||||
import cn.rtast.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")
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -4,10 +4,10 @@
|
||||
* Date: 2026/9/3
|
||||
*/
|
||||
|
||||
package cn.rtast.mcping
|
||||
package cn.rtast.mcping.java
|
||||
|
||||
import cn.rtast.mcping.platform.PlatformBuffer
|
||||
import cn.rtast.mcping.platform.PlatformReadChannel
|
||||
import cn.rtast.mcping.platform.ReadChannel
|
||||
|
||||
|
||||
internal fun PlatformBuffer.writeVarInt(value: Int) {
|
||||
@@ -22,7 +22,7 @@ internal fun PlatformBuffer.writeVarInt(value: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PlatformReadChannel.readVarInt(): Int {
|
||||
internal fun ReadChannel.readVarInt(): Int {
|
||||
var value = 0
|
||||
var position = 0
|
||||
while (true) {
|
||||
@@ -41,7 +41,7 @@ internal fun PlatformBuffer.writeMcString(value: String) {
|
||||
this.writeBytes(bytes)
|
||||
}
|
||||
|
||||
internal fun PlatformReadChannel.readMcString(): String {
|
||||
internal fun ReadChannel.readMcString(): String {
|
||||
val length = this.readVarInt()
|
||||
val bytes = this.readBytes(length)
|
||||
return bytes.decodeToString()
|
||||
+6
-1
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.mcping
|
||||
package cn.rtast.mcping.java
|
||||
|
||||
import cn.rtast.mcping.platform.PlatformBuffer
|
||||
|
||||
@@ -41,3 +41,8 @@ internal data object StatusRequestPacket : MinecraftPacket {
|
||||
override val packetId: Int = 0x00
|
||||
override fun writePayload(buffer: PlatformBuffer) {}
|
||||
}
|
||||
|
||||
internal data class PingPacket(val currentTime: Long) : MinecraftPacket {
|
||||
override val packetId: Int = 0x01
|
||||
override fun writePayload(buffer: PlatformBuffer) = buffer.writeLong(currentTime)
|
||||
}
|
||||
+3
-3
@@ -5,12 +5,12 @@
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.mcping
|
||||
package cn.rtast.mcping.java
|
||||
|
||||
import cn.rtast.mcping.platform.PlatformBuffer
|
||||
import cn.rtast.mcping.platform.PlatformWriteChannel
|
||||
import cn.rtast.mcping.platform.WriteChannel
|
||||
|
||||
internal fun PlatformWriteChannel.sendPacket(packet: MinecraftPacket) {
|
||||
internal fun WriteChannel.sendPacket(packet: MinecraftPacket) {
|
||||
val bodyBuffer = PlatformBuffer()
|
||||
bodyBuffer.writeVarInt(packet.packetId)
|
||||
packet.writePayload(bodyBuffer)
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/3
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.mcping.java
|
||||
|
||||
import cn.rtast.mcping.PingResponse
|
||||
import cn.rtast.mcping.platform.PingContext
|
||||
import cn.rtast.mcping.platform.Socket
|
||||
import kotlin.time.Clock
|
||||
|
||||
internal fun pingJavaServer(host: String, port: Int, context: PingContext): 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()
|
||||
}
|
||||
}
|
||||
@@ -4,32 +4,24 @@
|
||||
* Date: 2026/9/3
|
||||
*/
|
||||
|
||||
@file:JvmName("McPing")
|
||||
|
||||
|
||||
package cn.rtast.mcping
|
||||
|
||||
import cn.rtast.mcping.platform.PlatformSocket
|
||||
import cn.rtast.mcping.bedrock.pingBedrockServer
|
||||
import cn.rtast.mcping.java.pingJavaServer
|
||||
import cn.rtast.mcping.platform.PingContext
|
||||
import kotlin.jvm.JvmName
|
||||
import kotlin.jvm.JvmOverloads
|
||||
|
||||
|
||||
public fun mcping(host: String, port: Int): String {
|
||||
val socket = PlatformSocket(host, port)
|
||||
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()
|
||||
if (packetId == StatusRequestPacket.packetId) receiveChannel.readMcString()
|
||||
else throw IllegalStateException("Server does not respond correct packet id, expected ${StatusRequestPacket.packetId} but got $packetId")
|
||||
} finally {
|
||||
socket.close()
|
||||
}
|
||||
@JvmOverloads
|
||||
public fun mcping(
|
||||
host: String,
|
||||
port: Int,
|
||||
type: ServerType = ServerType.Java,
|
||||
context: PingContext = PingContext(),
|
||||
): 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.mcping
|
||||
|
||||
import cn.rtast.mcping.bedrock.BedrockPingResponse
|
||||
import cn.rtast.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()
|
||||
}
|
||||
@@ -7,11 +7,21 @@
|
||||
|
||||
package cn.rtast.mcping.platform
|
||||
|
||||
internal expect class PlatformBuffer() {
|
||||
internal expect class PlatformBuffer {
|
||||
constructor()
|
||||
constructor(bytes: ByteArray)
|
||||
|
||||
fun writeByte(value: Byte)
|
||||
fun writeShort(value: Short)
|
||||
fun writeLong(value: Long)
|
||||
fun writeBytes(bytes: ByteArray)
|
||||
|
||||
fun readByte(): Byte
|
||||
fun readShort(): Short
|
||||
fun readLong(): Long
|
||||
fun readBytes(length: Int): ByteArray
|
||||
fun toByteArray(): ByteArray
|
||||
val size: Int
|
||||
}
|
||||
|
||||
internal fun ByteArray.wrap(): PlatformBuffer = PlatformBuffer(this)
|
||||
@@ -7,13 +7,14 @@
|
||||
|
||||
package cn.rtast.mcping.platform
|
||||
|
||||
internal expect class PlatformReadChannel {
|
||||
internal expect class ReadChannel {
|
||||
fun readByte(): Byte
|
||||
fun readBytes(length: Int): ByteArray
|
||||
fun readFully(out: ByteArray, start: Int = 0, end: Int = out.size)
|
||||
fun readLong(): Long
|
||||
}
|
||||
|
||||
internal expect class PlatformWriteChannel {
|
||||
internal expect class WriteChannel {
|
||||
fun writeFully(value: ByteArray, startIndex: Int = 0, endIndex: Int = value.size)
|
||||
fun flush()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/3
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.mcping.platform
|
||||
|
||||
/**
|
||||
* An object class used to pass some parameters
|
||||
* that exists only on native targets
|
||||
*/
|
||||
public expect class PingContext()
|
||||
@@ -7,8 +7,13 @@
|
||||
|
||||
package cn.rtast.mcping.platform
|
||||
|
||||
internal expect class PlatformSocket internal constructor(host: String, port: Int){
|
||||
fun openReadChannel(): PlatformReadChannel
|
||||
fun openWriteChannel(): PlatformWriteChannel
|
||||
internal expect class Socket internal constructor(host: String, port: Int, context: PingContext) {
|
||||
fun openReadChannel(): ReadChannel
|
||||
fun openWriteChannel(): WriteChannel
|
||||
fun close()
|
||||
}
|
||||
|
||||
internal expect class UdpSocket internal constructor(host: String, port: Int, context: PingContext) {
|
||||
fun sendAndReceive(data: ByteArray): ByteArray
|
||||
fun close()
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/3
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.mcping
|
||||
|
||||
public enum class ServerType {
|
||||
Java, Bedrock
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package test
|
||||
|
||||
import cn.rtast.mcping.ServerType
|
||||
import cn.rtast.mcping.mcping
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
@@ -18,4 +19,11 @@ class TestPing {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -8,28 +8,85 @@ package cn.rtast.mcping.platform
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
internal actual class PlatformBuffer actual constructor() {
|
||||
private val stream = ByteArrayOutputStream()
|
||||
internal actual class PlatformBuffer {
|
||||
private val outStream = ByteArrayOutputStream()
|
||||
private var readBuffer: ByteArray? = null
|
||||
private var readOffset = 0
|
||||
|
||||
actual fun writeByte(value: Byte) = stream.write(value.toInt())
|
||||
actual fun writeBytes(bytes: ByteArray) = stream.write(bytes)
|
||||
actual constructor()
|
||||
|
||||
actual constructor(bytes: ByteArray) {
|
||||
this.readBuffer = bytes
|
||||
outStream.write(bytes)
|
||||
}
|
||||
|
||||
actual fun writeByte(value: Byte) {
|
||||
outStream.write(value.toInt())
|
||||
}
|
||||
|
||||
actual fun writeShort(value: Short) {
|
||||
val v = value.toInt()
|
||||
outStream.write(v shr 8)
|
||||
outStream.write(v)
|
||||
}
|
||||
|
||||
actual fun writeLong(value: Long) {
|
||||
outStream.write((value shr 56).toInt())
|
||||
outStream.write((value shr 48).toInt())
|
||||
outStream.write((value shr 40).toInt())
|
||||
outStream.write((value shr 32).toInt())
|
||||
outStream.write((value shr 24).toInt())
|
||||
outStream.write((value shr 16).toInt())
|
||||
outStream.write((value shr 8).toInt())
|
||||
outStream.write(value.toInt())
|
||||
}
|
||||
|
||||
actual fun writeBytes(bytes: ByteArray) {
|
||||
outStream.write(bytes)
|
||||
}
|
||||
|
||||
private fun ensureReadArray(): ByteArray {
|
||||
var buf = readBuffer
|
||||
if (buf == null) {
|
||||
buf = outStream.toByteArray()
|
||||
readBuffer = buf
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
actual fun readByte(): Byte {
|
||||
val array = stream.toByteArray()
|
||||
val array = ensureReadArray()
|
||||
if (readOffset >= array.size) throw IndexOutOfBoundsException("Buffer underflow")
|
||||
return array[readOffset++]
|
||||
}
|
||||
|
||||
actual fun readShort(): Short {
|
||||
val b1 = readByte().toInt() and 0xFF
|
||||
val b2 = readByte().toInt() and 0xFF
|
||||
return ((b1 shl 8) or b2).toShort()
|
||||
}
|
||||
|
||||
actual fun readLong(): Long {
|
||||
return (readByte().toLong() and 0xFF shl 56) or
|
||||
(readByte().toLong() and 0xFF shl 48) or
|
||||
(readByte().toLong() and 0xFF shl 40) or
|
||||
(readByte().toLong() and 0xFF shl 32) or
|
||||
(readByte().toLong() and 0xFF shl 24) or
|
||||
(readByte().toLong() and 0xFF shl 16) or
|
||||
(readByte().toLong() and 0xFF shl 8) or
|
||||
(readByte().toLong() and 0xFF)
|
||||
}
|
||||
|
||||
actual fun readBytes(length: Int): ByteArray {
|
||||
val array = stream.toByteArray()
|
||||
val array = ensureReadArray()
|
||||
if (readOffset + length > array.size) throw IndexOutOfBoundsException("Buffer underflow")
|
||||
val result = array.copyOfRange(readOffset, readOffset + length)
|
||||
readOffset += length
|
||||
return result
|
||||
}
|
||||
|
||||
actual fun toByteArray(): ByteArray = stream.toByteArray()
|
||||
actual fun toByteArray(): ByteArray = outStream.toByteArray()
|
||||
|
||||
actual val size: Int
|
||||
get() = stream.size()
|
||||
get() = outStream.size()
|
||||
}
|
||||
@@ -7,10 +7,11 @@
|
||||
|
||||
package cn.rtast.mcping.platform
|
||||
|
||||
import java.io.EOFException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
internal actual class PlatformReadChannel {
|
||||
internal actual class ReadChannel {
|
||||
private val _inputStream: InputStream
|
||||
|
||||
constructor(inputStream: InputStream) {
|
||||
@@ -28,9 +29,27 @@ internal actual class PlatformReadChannel {
|
||||
bytesRead += read
|
||||
}
|
||||
}
|
||||
|
||||
actual fun readLong(): Long {
|
||||
val bytes = ByteArray(8)
|
||||
var read = 0
|
||||
while (read < 8) {
|
||||
val count = _inputStream.read(bytes, read, 8 - read)
|
||||
if (count == -1) throw EOFException()
|
||||
read += count
|
||||
}
|
||||
return ((bytes[0].toLong() and 0xFF shl 56) or
|
||||
(bytes[1].toLong() and 0xFF shl 48) or
|
||||
(bytes[2].toLong() and 0xFF shl 40) or
|
||||
(bytes[3].toLong() and 0xFF shl 32) or
|
||||
(bytes[4].toLong() and 0xFF shl 24) or
|
||||
(bytes[5].toLong() and 0xFF shl 16) or
|
||||
(bytes[6].toLong() and 0xFF shl 8) or
|
||||
(bytes[7].toLong() and 0xFF))
|
||||
}
|
||||
}
|
||||
|
||||
internal actual class PlatformWriteChannel {
|
||||
internal actual class WriteChannel {
|
||||
private val _outputStream: OutputStream
|
||||
|
||||
constructor(outputStream: OutputStream) {
|
||||
@@ -42,3 +61,14 @@ internal actual class PlatformWriteChannel {
|
||||
|
||||
actual fun flush() = _outputStream.flush()
|
||||
}
|
||||
|
||||
private fun InputStream.readNBytes(length: Int): ByteArray {
|
||||
val buffer = ByteArray(length)
|
||||
var totalRead = 0
|
||||
while (totalRead < length) {
|
||||
val read = this.read(buffer, totalRead, length - totalRead)
|
||||
if (read == -1) throw EOFException()
|
||||
totalRead += read
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/3
|
||||
*/
|
||||
|
||||
package cn.rtast.mcping.platform
|
||||
|
||||
/**
|
||||
* No Context for jvm targets
|
||||
*/
|
||||
public actual class PingContext
|
||||
@@ -6,12 +6,33 @@
|
||||
|
||||
package cn.rtast.mcping.platform
|
||||
|
||||
import java.net.Socket
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket as JvmSocket
|
||||
|
||||
internal actual class PlatformSocket internal actual constructor(host: String, port: Int) {
|
||||
private val socket = Socket(host, port)
|
||||
internal actual class Socket internal actual constructor(host: String, port: Int, context: PingContext) {
|
||||
private val socket = JvmSocket(host, port)
|
||||
|
||||
actual fun openReadChannel(): ReadChannel = ReadChannel(socket.getInputStream())
|
||||
actual fun openWriteChannel(): WriteChannel = WriteChannel(socket.getOutputStream())
|
||||
actual fun close() = socket.close()
|
||||
}
|
||||
|
||||
internal actual class UdpSocket internal actual constructor(host: String, port: Int, context: PingContext) {
|
||||
private val socket = DatagramSocket().apply {
|
||||
soTimeout = 3000
|
||||
connect(InetSocketAddress(host, port))
|
||||
}
|
||||
|
||||
actual fun sendAndReceive(data: ByteArray): ByteArray {
|
||||
socket.send(DatagramPacket(data, data.size))
|
||||
|
||||
val buf = ByteArray(2048)
|
||||
val receivePacket = DatagramPacket(buf, buf.size)
|
||||
socket.receive(receivePacket)
|
||||
return buf.copyOf(receivePacket.length)
|
||||
}
|
||||
|
||||
actual fun openReadChannel(): PlatformReadChannel = PlatformReadChannel(socket.getInputStream())
|
||||
actual fun openWriteChannel(): PlatformWriteChannel = PlatformWriteChannel(socket.getOutputStream())
|
||||
actual fun close() = socket.close()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/3
|
||||
*/
|
||||
|
||||
|
||||
package test;
|
||||
|
||||
import cn.rtast.mcping.McPing;
|
||||
import cn.rtast.mcping.PingResponse;
|
||||
import cn.rtast.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.mcping.ServerType
|
||||
import cn.rtast.mcping.mcping
|
||||
import cn.rtast.mcping.platform.PingContext
|
||||
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 = PingContext(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 = PingContext(sm))
|
||||
println(response)
|
||||
println(response.toBedrockResponse())
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,32 @@
|
||||
* Date: 2026/9/3
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.mcping.platform
|
||||
|
||||
import kotlinx.io.Buffer
|
||||
import kotlinx.io.readByteArray
|
||||
|
||||
internal actual class PlatformBuffer actual constructor() {
|
||||
private val _delegateBuf = Buffer()
|
||||
internal actual class PlatformBuffer {
|
||||
private val _delegateBuf: Buffer
|
||||
|
||||
actual constructor() {
|
||||
_delegateBuf = Buffer()
|
||||
}
|
||||
|
||||
actual constructor(bytes: ByteArray) {
|
||||
_delegateBuf = Buffer().apply { write(bytes) }
|
||||
}
|
||||
|
||||
actual fun writeByte(value: Byte) = _delegateBuf.writeByte(value)
|
||||
actual fun writeShort(value: Short) = _delegateBuf.writeShort(value)
|
||||
actual fun writeLong(value: Long) = _delegateBuf.writeLong(value)
|
||||
actual fun writeBytes(bytes: ByteArray) = _delegateBuf.write(bytes)
|
||||
|
||||
actual fun readByte(): Byte = _delegateBuf.readByte()
|
||||
actual fun readShort(): Short = _delegateBuf.readShort()
|
||||
actual fun readLong(): Long = _delegateBuf.readLong()
|
||||
actual fun readBytes(length: Int): ByteArray = _delegateBuf.readByteArray(length)
|
||||
|
||||
actual fun toByteArray(): ByteArray = _delegateBuf.peek().readByteArray()
|
||||
|
||||
actual val size: Int
|
||||
|
||||
@@ -9,7 +9,7 @@ package cn.rtast.mcping.platform
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
internal actual class PlatformReadChannel {
|
||||
internal actual class ReadChannel {
|
||||
private val _readChannel: ByteReadChannel
|
||||
|
||||
constructor(readChannel: ByteReadChannel) {
|
||||
@@ -19,9 +19,10 @@ internal actual class PlatformReadChannel {
|
||||
actual fun readByte(): Byte = runBlocking { _readChannel.readByte() }
|
||||
actual fun readBytes(length: Int): ByteArray = runBlocking { _readChannel.readByteArray(length) }
|
||||
actual fun readFully(out: ByteArray, start: Int, end: Int) = runBlocking { _readChannel.readFully(out, start, end) }
|
||||
actual fun readLong(): Long = runBlocking { _readChannel.readLong() }
|
||||
}
|
||||
|
||||
internal actual class PlatformWriteChannel {
|
||||
internal actual class WriteChannel {
|
||||
private val _writeChannel: ByteWriteChannel
|
||||
|
||||
constructor(writeChannel: ByteWriteChannel) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/3
|
||||
*/
|
||||
|
||||
@file:Suppress("PropertyName")
|
||||
|
||||
package cn.rtast.mcping.platform
|
||||
|
||||
import io.ktor.network.selector.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
|
||||
public actual class PingContext actual constructor() {
|
||||
internal var _selectorManager: SelectorManager = SelectorManager(Dispatchers.IO)
|
||||
internal var _autoCloseSelectorManager: Boolean = false
|
||||
|
||||
public constructor(selectorManager: SelectorManager, autoClose: Boolean = false) : this() {
|
||||
_selectorManager = selectorManager
|
||||
_autoCloseSelectorManager = autoClose
|
||||
}
|
||||
}
|
||||
@@ -6,23 +6,40 @@
|
||||
|
||||
package cn.rtast.mcping.platform
|
||||
|
||||
import io.ktor.network.selector.*
|
||||
import io.ktor.network.sockets.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import io.ktor.utils.io.core.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.io.readByteArray
|
||||
|
||||
internal actual class PlatformSocket internal actual constructor(host: String, port: Int) {
|
||||
internal actual class Socket internal actual constructor(host: String, port: Int, context: PingContext) {
|
||||
private val ctx = context
|
||||
private val socket = runBlocking { aSocket(ctx._selectorManager).tcp().connect(host, port) }
|
||||
|
||||
private val sm = SelectorManager(Dispatchers.IO)
|
||||
private val socket = runBlocking { aSocket(sm).tcp().connect(host, port) }
|
||||
|
||||
actual fun openReadChannel(): PlatformReadChannel = PlatformReadChannel(socket.openReadChannel())
|
||||
actual fun openWriteChannel(): PlatformWriteChannel =
|
||||
PlatformWriteChannel(socket.openWriteChannel(autoFlush = true))
|
||||
actual fun openReadChannel(): ReadChannel = ReadChannel(socket.openReadChannel())
|
||||
actual fun openWriteChannel(): WriteChannel =
|
||||
WriteChannel(socket.openWriteChannel(autoFlush = true))
|
||||
|
||||
actual fun close() {
|
||||
socket.close()
|
||||
sm.close()
|
||||
if (ctx._autoCloseSelectorManager) ctx._selectorManager.close()
|
||||
}
|
||||
}
|
||||
|
||||
internal actual class UdpSocket internal actual constructor(host: String, port: Int, context: PingContext) {
|
||||
private val ctx = context
|
||||
|
||||
// use bind to create an unconnected socket
|
||||
private val socket = runBlocking { aSocket(ctx._selectorManager).udp().bind() }
|
||||
private val remoteAddress = InetSocketAddress(host, port)
|
||||
|
||||
actual fun sendAndReceive(data: ByteArray): ByteArray = runBlocking {
|
||||
val packet = buildPacket { writeFully(data) }
|
||||
socket.send(Datagram(packet, remoteAddress))
|
||||
socket.receive().packet.readByteArray()
|
||||
}
|
||||
|
||||
actual fun close() {
|
||||
socket.close()
|
||||
if (ctx._autoCloseSelectorManager) ctx._selectorManager.close()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user