rcon client supports

This commit is contained in:
2026-09-04 01:27:18 +08:00
parent 350b900aaa
commit 817ec27813
27 files changed
+412 -93

No files matched your search

+35 -2
View File
@@ -1,6 +1,6 @@
# mcping # mcping
A lightweight motd ping library for kotlin multiplatform and java, A lightweight motd ping / rcon client library for kotlin multiplatform and java,
no dependencies in `jvm` target except `kotlin-stdlib` no dependencies in `jvm` target except `kotlin-stdlib`
native platforms required `ktor-network` and `kotlinx-io` native platforms required `ktor-network` and `kotlinx-io`
@@ -23,12 +23,15 @@ repositories {
} }
dependencies { dependencies {
implementation("cn.rtast.mcping:mcping:0.0.2") implementation("cn.rtast.mcping:mcping:0.0.3")
implementation("cn.rtast.mcping:rconlib:0.0.3")
} }
``` ```
> Get latest version at https://repo.rtast.cn/packages/-/cn.rtast.mcping:mcping/ > Get latest version at https://repo.rtast.cn/packages/-/cn.rtast.mcping:mcping/
# MC Ping
## Kotlin example ## Kotlin example
```kotlin ```kotlin
@@ -69,6 +72,36 @@ void main() {
> An example of java ping json response can be found at [ping-response-example](example/java-ping-response.json) (Formatted), > 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) > a raw response of bedrock ping response can be found at [bedrock-pinng-raw-response](example/bedrock-pinng-raw-response.txt)
# rcon client
```kotlin
fun main() {
val host = "127.0.0.1"
val port = 25575
val client = rconClient(host, port)
val authed = client.connect("123456")
if (authed) println(client.command("list")) else throw IllegalArgumentException("incorrect password")
}
```
> also supports java
```java
void main() {
String host = "127.0.0.1";
int port = 25575;
String password = "123456";
RCONClient rconClient = Rconlib.rconClient(host, port);
boolean authed = rconClient.connect(password);
if (authed) {
System.out.println(rconClient.command("list"));
} else {
throw new IllegalStateException("incorrect password");
}
}
```
# Open Source # Open Source
Open source under [Apache-2.0](LICENSE) Open source under [Apache-2.0](LICENSE)
@@ -13,15 +13,18 @@ public expect class _Buffer {
public constructor(bytes: ByteArray) public constructor(bytes: ByteArray)
public fun writeByte(value: Byte) public fun writeByte(value: Byte)
public fun writeShort(value: Short) public fun writeShort(value: Short, endian: ByteOrder = ByteOrder.BIG_ENDIAN)
public fun writeLong(value: Long) public fun writeInt(value: Int, endian: ByteOrder = ByteOrder.BIG_ENDIAN)
public fun writeLong(value: Long, endian: ByteOrder = ByteOrder.BIG_ENDIAN)
public fun writeBytes(bytes: ByteArray) public fun writeBytes(bytes: ByteArray)
public fun readByte(): Byte public fun readByte(): Byte
public fun readShort(): Short public fun readShort(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Short
public fun readLong(): Long public fun readInt(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Int
public fun readLong(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Long
public fun readBytes(length: Int): ByteArray public fun readBytes(length: Int): ByteArray
public fun toByteArray(): ByteArray public fun toByteArray(): ByteArray
public fun close()
public val size: Int public val size: Int
} }
@@ -0,0 +1,12 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.mcping.platform
public enum class ByteOrder {
BIG_ENDIAN, LITTLE_ENDIAN
}
@@ -10,9 +10,11 @@ package cn.rtast.mcping.platform
@Suppress("CLASSNAME") @Suppress("CLASSNAME")
public expect class _ReadChannel { public expect class _ReadChannel {
public fun readByte(): Byte public fun readByte(): Byte
public fun readShort(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Short
public fun readInt(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Int
public fun readLong(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Long
public fun readBytes(length: Int): ByteArray public fun readBytes(length: Int): ByteArray
public fun readFully(out: ByteArray, start: Int = 0, end: Int = out.size) public fun readFully(out: ByteArray, start: Int = 0, end: Int = out.size)
public fun readLong(): Long
} }
@Suppress("CLASSNAME") @Suppress("CLASSNAME")
@@ -11,4 +11,4 @@ package cn.rtast.mcping.platform
* An object class used to pass some parameters * An object class used to pass some parameters
* that exists only on native targets * that exists only on native targets
*/ */
public expect class PingContext() public expect class MCPingContext()
@@ -8,14 +8,14 @@
package cn.rtast.mcping.platform package cn.rtast.mcping.platform
@Suppress("CLASSNAME") @Suppress("CLASSNAME")
public expect class _Socket public constructor(host: String, port: Int, context: PingContext) { public expect class _Socket public constructor(host: String, port: Int, context: MCPingContext) {
public fun openReadChannel(): _ReadChannel public fun openReadChannel(): _ReadChannel
public fun openWriteChannel(): _WriteChannel public fun openWriteChannel(): _WriteChannel
public fun close() public fun close()
} }
@Suppress("CLASSNAME") @Suppress("CLASSNAME")
public expect class _UdpSocket public constructor(host: String, port: Int, context: PingContext) { public expect class _UdpSocket public constructor(host: String, port: Int, context: MCPingContext) {
public fun sendAndReceive(data: ByteArray): ByteArray public fun sendAndReceive(data: ByteArray): ByteArray
public fun close() public fun close()
} }
@@ -24,21 +24,37 @@ public actual class _Buffer {
outStream.write(value.toInt()) outStream.write(value.toInt())
} }
public actual fun writeShort(value: Short) { public actual fun writeShort(value: Short, endian: ByteOrder) {
val v = value.toInt() val v = value.toInt()
outStream.write(v shr 8) if (endian == ByteOrder.BIG_ENDIAN) {
outStream.write(v) outStream.write(v shr 8)
outStream.write(v)
} else {
outStream.write(v)
outStream.write(v shr 8)
}
} }
public actual fun writeLong(value: Long) { public actual fun writeInt(value: Int, endian: ByteOrder) {
outStream.write((value shr 56).toInt()) if (endian == ByteOrder.BIG_ENDIAN) {
outStream.write((value shr 48).toInt()) outStream.write(value shr 24)
outStream.write((value shr 40).toInt()) outStream.write(value shr 16)
outStream.write((value shr 32).toInt()) outStream.write(value shr 8)
outStream.write((value shr 24).toInt()) outStream.write(value)
outStream.write((value shr 16).toInt()) } else {
outStream.write((value shr 8).toInt()) outStream.write(value)
outStream.write(value.toInt()) outStream.write(value shr 8)
outStream.write(value shr 16)
outStream.write(value shr 24)
}
}
public actual fun writeLong(value: Long, endian: ByteOrder) {
if (endian == ByteOrder.BIG_ENDIAN) {
for (i in 56 downTo 0 step 8) outStream.write((value shr i).toInt())
} else {
for (i in 0..56 step 8) outStream.write((value shr i).toInt())
}
} }
public actual fun writeBytes(bytes: ByteArray) { public actual fun writeBytes(bytes: ByteArray) {
@@ -60,23 +76,9 @@ public actual class _Buffer {
return array[readOffset++] return array[readOffset++]
} }
public actual fun readShort(): Short { public actual fun readShort(endian: ByteOrder): Short = readBytes(2).toShort(endian)
val b1 = readByte().toInt() and 0xFF public actual fun readInt(endian: ByteOrder): Int = readBytes(4).toInt(endian)
val b2 = readByte().toInt() and 0xFF public actual fun readLong(endian: ByteOrder): Long = readBytes(8).toLong(endian)
return ((b1 shl 8) or b2).toShort()
}
public 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)
}
public actual fun readBytes(length: Int): ByteArray { public actual fun readBytes(length: Int): ByteArray {
val array = ensureReadArray() val array = ensureReadArray()
if (readOffset + length > array.size) throw IndexOutOfBoundsException("Buffer underflow") if (readOffset + length > array.size) throw IndexOutOfBoundsException("Buffer underflow")
@@ -87,6 +89,6 @@ public actual class _Buffer {
public actual fun toByteArray(): ByteArray = outStream.toByteArray() public actual fun toByteArray(): ByteArray = outStream.toByteArray()
public actual val size: Int public actual fun close(): Unit = outStream.close()
get() = outStream.size() public actual val size: Int get() = outStream.size()
} }
@@ -12,15 +12,11 @@ import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@Suppress("CLASSNAME") @Suppress("CLASSNAME")
public actual class _ReadChannel { public actual class _ReadChannel(private val _inputStream: InputStream) {
private val _inputStream: InputStream
public constructor(inputStream: InputStream) {
_inputStream = inputStream
}
public actual fun readByte(): Byte = _inputStream.read().toByte() public actual fun readByte(): Byte = _inputStream.read().toByte()
public actual fun readBytes(length: Int): ByteArray = _inputStream.readNBytes(length) public actual fun readBytes(length: Int): ByteArray = _inputStream.readNBytes(length)
public actual fun readFully(out: ByteArray, start: Int, end: Int) { public actual fun readFully(out: ByteArray, start: Int, end: Int) {
var bytesRead = 0 var bytesRead = 0
val length = end - start val length = end - start
@@ -31,23 +27,9 @@ public actual class _ReadChannel {
} }
} }
public actual fun readLong(): Long { public actual fun readShort(endian: ByteOrder): Short = readBytes(2).toShort(endian)
val bytes = ByteArray(8) public actual fun readInt(endian: ByteOrder): Int = readBytes(4).toInt(endian)
var read = 0 public actual fun readLong(endian: ByteOrder): Long = readBytes(8).toLong(endian)
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))
}
} }
@Suppress("CLASSNAME") @Suppress("CLASSNAME")
@@ -9,4 +9,4 @@ package cn.rtast.mcping.platform
/** /**
* No Context for jvm targets * No Context for jvm targets
*/ */
public actual class PingContext public actual class MCPingContext
@@ -0,0 +1,40 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.mcping.platform
internal fun ByteArray.toShort(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Short {
val b1 = this[0].toInt() and 0xFF
val b2 = this[1].toInt() and 0xFF
return if (endian == ByteOrder.BIG_ENDIAN) {
((b1 shl 8) or b2).toShort()
} else {
((b2 shl 8) or b1).toShort()
}
}
internal fun ByteArray.toInt(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Int {
val b1 = this[0].toInt() and 0xFF
val b2 = this[1].toInt() and 0xFF
val b3 = this[2].toInt() and 0xFF
val b4 = this[3].toInt() and 0xFF
return if (endian == ByteOrder.BIG_ENDIAN) {
(b1 shl 24) or (b2 shl 16) or (b3 shl 8) or b4
} else {
(b4 shl 24) or (b3 shl 16) or (b2 shl 8) or b1
}
}
internal fun ByteArray.toLong(endian: ByteOrder = ByteOrder.BIG_ENDIAN): Long {
var result = 0L
if (endian == ByteOrder.BIG_ENDIAN) {
for (b in this) result = (result shl 8) or (b.toLong() and 0xFF)
} else {
for (i in 7 downTo 0) result = (result shl 8) or (this[i].toLong() and 0xFF)
}
return result
}
@@ -12,7 +12,7 @@ import java.net.InetSocketAddress
import java.net.Socket as JvmSocket import java.net.Socket as JvmSocket
@Suppress("CLASSNAME") @Suppress("CLASSNAME")
public actual class _Socket public actual constructor(host: String, port: Int, context: PingContext) { public actual class _Socket public actual constructor(host: String, port: Int, context: MCPingContext) {
private val socket = JvmSocket(host, port) private val socket = JvmSocket(host, port)
public actual fun openReadChannel(): _ReadChannel = _ReadChannel(socket.getInputStream()) public actual fun openReadChannel(): _ReadChannel = _ReadChannel(socket.getInputStream())
@@ -21,7 +21,7 @@ public actual class _Socket public actual constructor(host: String, port: Int, c
} }
@Suppress("CLASSNAME") @Suppress("CLASSNAME")
public actual class _UdpSocket public actual constructor(host: String, port: Int, context: PingContext) { public actual class _UdpSocket public actual constructor(host: String, port: Int, context: MCPingContext) {
private val socket = DatagramSocket().apply { private val socket = DatagramSocket().apply {
soTimeout = 3000 soTimeout = 3000
connect(InetSocketAddress(host, port)) connect(InetSocketAddress(host, port))
@@ -6,6 +6,7 @@
package cn.rtast.mcping.platform package cn.rtast.mcping.platform
import io.ktor.utils.io.bits.*
import kotlinx.io.Buffer import kotlinx.io.Buffer
import kotlinx.io.readByteArray import kotlinx.io.readByteArray
@@ -22,16 +23,41 @@ public actual class _Buffer {
} }
public actual fun writeByte(value: Byte): Unit = _delegateBuf.writeByte(value) public actual fun writeByte(value: Byte): Unit = _delegateBuf.writeByte(value)
public actual fun writeShort(value: Short): Unit = _delegateBuf.writeShort(value) public actual fun writeShort(value: Short, endian: ByteOrder) {
public actual fun writeLong(value: Long): Unit = _delegateBuf.writeLong(value) if (endian == ByteOrder.BIG_ENDIAN) _delegateBuf.writeShort(value)
else _delegateBuf.writeShort(value.reverseByteOrder())
}
public actual fun writeInt(value: Int, endian: ByteOrder) {
if (endian == ByteOrder.BIG_ENDIAN) _delegateBuf.writeInt(value)
else _delegateBuf.writeInt(value.reverseByteOrder())
}
public actual fun writeLong(value: Long, endian: ByteOrder) {
if (endian == ByteOrder.BIG_ENDIAN) _delegateBuf.writeLong(value)
else _delegateBuf.writeLong(value.reverseByteOrder())
}
public actual fun writeBytes(bytes: ByteArray): Unit = _delegateBuf.write(bytes) public actual fun writeBytes(bytes: ByteArray): Unit = _delegateBuf.write(bytes)
public actual fun readByte(): Byte = _delegateBuf.readByte() public actual fun readByte(): Byte = _delegateBuf.readByte()
public actual fun readShort(): Short = _delegateBuf.readShort() public actual fun readShort(endian: ByteOrder): Short {
public actual fun readLong(): Long = _delegateBuf.readLong() val v = _delegateBuf.readShort()
public actual fun readBytes(length: Int): ByteArray = _delegateBuf.readByteArray(length) return if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder()
}
public actual fun readInt(endian: ByteOrder): Int {
val v = _delegateBuf.readInt()
return if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder()
}
public actual fun readLong(endian: ByteOrder): Long {
val v = _delegateBuf.readLong()
return if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder()
}
public actual fun readBytes(length: Int): ByteArray = _delegateBuf.readByteArray(length)
public actual fun toByteArray(): ByteArray = _delegateBuf.peek().readByteArray() public actual fun toByteArray(): ByteArray = _delegateBuf.peek().readByteArray()
public actual fun close(): Unit = _delegateBuf.close()
public actual val size: Int public actual val size: Int
get() = _delegateBuf.size.toInt() get() = _delegateBuf.size.toInt()
@@ -7,22 +7,31 @@
package cn.rtast.mcping.platform package cn.rtast.mcping.platform
import io.ktor.utils.io.* import io.ktor.utils.io.*
import io.ktor.utils.io.bits.reverseByteOrder
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@Suppress("CLASSNAME") @Suppress("CLASSNAME")
public actual class _ReadChannel { public actual class _ReadChannel(private val _readChannel: ByteReadChannel) {
private val _readChannel: ByteReadChannel
public constructor(readChannel: ByteReadChannel) {
_readChannel = readChannel
}
public actual fun readByte(): Byte = runBlocking { _readChannel.readByte() } public actual fun readByte(): Byte = runBlocking { _readChannel.readByte() }
public actual fun readBytes(length: Int): ByteArray = runBlocking { _readChannel.readByteArray(length) } public actual fun readBytes(length: Int): ByteArray = runBlocking { _readChannel.readByteArray(length) }
public actual fun readFully(out: ByteArray, start: Int, end: Int): Unit = public actual fun readFully(out: ByteArray, start: Int, end: Int): Unit =
runBlocking { _readChannel.readFully(out, start, end) } runBlocking { _readChannel.readFully(out, start, end) }
public actual fun readLong(): Long = runBlocking { _readChannel.readLong() } public actual fun readShort(endian: ByteOrder): Short = runBlocking {
val v = _readChannel.readShort()
if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder()
}
public actual fun readInt(endian: ByteOrder): Int = runBlocking {
val v = _readChannel.readInt()
if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder()
}
public actual fun readLong(endian: ByteOrder): Long = runBlocking {
val v = _readChannel.readLong()
if (endian == ByteOrder.BIG_ENDIAN) v else v.reverseByteOrder()
}
} }
@Suppress("CLASSNAME") @Suppress("CLASSNAME")
@@ -12,7 +12,7 @@ import io.ktor.network.selector.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
public actual class PingContext actual constructor() { public actual class MCPingContext actual constructor() {
internal var _selectorManager: SelectorManager = SelectorManager(Dispatchers.IO) internal var _selectorManager: SelectorManager = SelectorManager(Dispatchers.IO)
internal var _autoCloseSelectorManager: Boolean = false internal var _autoCloseSelectorManager: Boolean = false
@@ -12,7 +12,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.io.readByteArray import kotlinx.io.readByteArray
@Suppress("CLASSNAME") @Suppress("CLASSNAME")
public actual class _Socket public actual constructor(host: String, port: Int, context: PingContext) { public actual class _Socket public actual constructor(host: String, port: Int, context: MCPingContext) {
private val ctx = context private val ctx = context
private val socket = runBlocking { aSocket(ctx._selectorManager).tcp().connect(host, port) } private val socket = runBlocking { aSocket(ctx._selectorManager).tcp().connect(host, port) }
@@ -27,7 +27,7 @@ public actual class _Socket public actual constructor(host: String, port: Int, c
} }
@Suppress("CLASSNAME") @Suppress("CLASSNAME")
public actual class _UdpSocket public actual constructor(host: String, port: Int, context: PingContext) { public actual class _UdpSocket public actual constructor(host: String, port: Int, context: MCPingContext) {
private val ctx = context private val ctx = context
// use bind to create an unconnected socket // use bind to create an unconnected socket
+1 -1
View File
@@ -4,4 +4,4 @@ kotlin.native.enableKlibsCrossCompilation=true
kotlin.daemon.jvmargs=-Xmx2048M kotlin.daemon.jvmargs=-Xmx2048M
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
libVersion=0.0.2 libVersion=0.0.3
@@ -8,12 +8,12 @@
package cn.rtast.mcping.bedrock package cn.rtast.mcping.bedrock
import cn.rtast.mcping.PingResponse import cn.rtast.mcping.PingResponse
import cn.rtast.mcping.platform.PingContext import cn.rtast.mcping.platform.MCPingContext
import cn.rtast.mcping.platform._UdpSocket import cn.rtast.mcping.platform._UdpSocket
import cn.rtast.mcping.platform.wrap import cn.rtast.mcping.platform.wrap
import kotlin.time.Clock import kotlin.time.Clock
internal fun pingBedrockServer(host: String, port: Int, context: PingContext): PingResponse { internal fun pingBedrockServer(host: String, port: Int, context: MCPingContext): PingResponse {
val socket = _UdpSocket(host, port, context) val socket = _UdpSocket(host, port, context)
return try { return try {
val sendTime = Clock.System.now().toEpochMilliseconds() val sendTime = Clock.System.now().toEpochMilliseconds()
@@ -8,11 +8,11 @@
package cn.rtast.mcping.java package cn.rtast.mcping.java
import cn.rtast.mcping.PingResponse import cn.rtast.mcping.PingResponse
import cn.rtast.mcping.platform.PingContext import cn.rtast.mcping.platform.MCPingContext
import cn.rtast.mcping.platform._Socket import cn.rtast.mcping.platform._Socket
import kotlin.time.Clock import kotlin.time.Clock
internal fun pingJavaServer(host: String, port: Int, context: PingContext): PingResponse { internal fun pingJavaServer(host: String, port: Int, context: MCPingContext): PingResponse {
val socket = _Socket(host, port, context) val socket = _Socket(host, port, context)
val receiveChannel = socket.openReadChannel() val receiveChannel = socket.openReadChannel()
val sendChannel = socket.openWriteChannel() val sendChannel = socket.openWriteChannel()
@@ -11,7 +11,7 @@ package cn.rtast.mcping
import cn.rtast.mcping.bedrock.pingBedrockServer import cn.rtast.mcping.bedrock.pingBedrockServer
import cn.rtast.mcping.java.pingJavaServer import cn.rtast.mcping.java.pingJavaServer
import cn.rtast.mcping.platform.PingContext import cn.rtast.mcping.platform.MCPingContext
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmOverloads
@@ -20,7 +20,7 @@ public fun mcping(
host: String, host: String,
port: Int, port: Int,
type: ServerType = ServerType.Java, type: ServerType = ServerType.Java,
context: PingContext = PingContext(), context: MCPingContext = MCPingContext(),
): PingResponse = when (type) { ): PingResponse = when (type) {
ServerType.Java -> pingJavaServer(host, port, context) ServerType.Java -> pingJavaServer(host, port, context)
ServerType.Bedrock -> pingBedrockServer(host, port, context) ServerType.Bedrock -> pingBedrockServer(host, port, context)
@@ -9,7 +9,7 @@ package test
import cn.rtast.mcping.ServerType import cn.rtast.mcping.ServerType
import cn.rtast.mcping.mcping import cn.rtast.mcping.mcping
import cn.rtast.mcping.platform.PingContext import cn.rtast.mcping.platform.MCPingContext
import io.ktor.network.selector.* import io.ktor.network.selector.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
@@ -22,7 +22,7 @@ class TestMingwPing {
fun `test ping java on mingw with selectorManager`() = runTest { fun `test ping java on mingw with selectorManager`() = runTest {
val sm = SelectorManager(Dispatchers.IO) val sm = SelectorManager(Dispatchers.IO)
val response = val response =
mcping(host = "org.mc-complex.com", port = 25565, type = ServerType.Java, context = PingContext(sm)) mcping(host = "org.mc-complex.com", port = 25565, type = ServerType.Java, context = MCPingContext(sm))
println(response) println(response)
} }
@@ -30,7 +30,7 @@ class TestMingwPing {
fun `test ping bedrock on mingw with selectorManager`() = runTest { fun `test ping bedrock on mingw with selectorManager`() = runTest {
val sm = SelectorManager(Dispatchers.IO) val sm = SelectorManager(Dispatchers.IO)
val response = val response =
mcping(host = "play.wildnetwork.net", port = 19132, type = ServerType.Bedrock, context = PingContext(sm)) mcping(host = "play.wildnetwork.net", port = 19132, type = ServerType.Bedrock, context = MCPingContext(sm))
println(response) println(response)
println(response.toBedrockResponse()) println(response.toBedrockResponse())
} }
+4
View File
@@ -16,6 +16,10 @@ kotlin {
implementation(project(":common")) implementation(project(":common"))
} }
jvmMain.dependencies {
}
commonTest.dependencies { commonTest.dependencies {
implementation(kotlin("test")) implementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
@@ -0,0 +1,12 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.mcping.rconlib
import cn.rtast.mcping.platform._Buffer
internal fun _Buffer.writeNull() = writeByte(0x00)
@@ -0,0 +1,83 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.mcping.rconlib
import cn.rtast.mcping.platform.ByteOrder
import cn.rtast.mcping.platform._Buffer
import cn.rtast.mcping.platform._ReadChannel
import cn.rtast.mcping.platform._WriteChannel
internal abstract class Packet {
abstract val requestId: Int
abstract val type: Int
abstract val payload: String
protected val payloadBytes: ByteArray by lazy { payload.encodeToByteArray() }
val payloadLength: Int
get() = payloadBytes.size
open val length: Int
get() = 4 + 4 + payloadLength + 2
fun writePayload(buf: _Buffer) {
buf.writeInt(length, ByteOrder.LITTLE_ENDIAN)
buf.writeInt(requestId, ByteOrder.LITTLE_ENDIAN)
buf.writeInt(type, ByteOrder.LITTLE_ENDIAN)
buf.writeBytes(payloadBytes)
buf.writeNull()
buf.writeNull()
}
companion object Codec {
fun decode(channel: _ReadChannel): ResponsePacket {
val length = channel.readInt(ByteOrder.LITTLE_ENDIAN)
val requestId = channel.readInt(ByteOrder.LITTLE_ENDIAN)
val type = channel.readInt(ByteOrder.LITTLE_ENDIAN)
val payloadBuffer = _Buffer()
while (true) {
val b = channel.readByte()
if (b == 0x00.toByte()) break
payloadBuffer.writeByte(b)
}
channel.readByte() // consume last null byte
return ResponsePacket(length, requestId, type, payloadBuffer.toByteArray().decodeToString())
}
}
}
internal data class AuthPacket(
override val payload: String,
override val requestId: Int = 1,
) : Packet() {
override val type: Int = PacketType.AUTH
}
internal data class ExecCommandPacket(
override val payload: String,
override val requestId: Int,
) : Packet() {
override val type: Int = PacketType.EXEC_COMMAND
}
internal data class ResponsePacket(
override val length: Int,
override val requestId: Int,
override val type: Int,
override val payload: String,
) : Packet()
internal fun _WriteChannel.sendPacket(packet: Packet) {
val buf = _Buffer()
packet.writePayload(buf)
writeFully(buf.toByteArray())
flush()
}
internal fun _ReadChannel.readPacket(): ResponsePacket = Packet.decode(this)
@@ -0,0 +1,14 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package cn.rtast.mcping.rconlib
internal object PacketType {
const val AUTH = 3
const val EXEC_COMMAND = 2
const val RESPONSE = 1
}
@@ -4,6 +4,51 @@
* Date: 2026/9/4 * Date: 2026/9/4
*/ */
@file:JvmName("Rconlib")
package cn.rtast.mcping.rconlib package cn.rtast.mcping.rconlib
import cn.rtast.mcping.platform.MCPingContext
import cn.rtast.mcping.platform._ReadChannel
import cn.rtast.mcping.platform._Socket
import cn.rtast.mcping.platform._WriteChannel
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
public class RCONClient internal constructor(
private val host: String,
private val port: Int,
private val context: MCPingContext,
) : AutoCloseable {
private var socket: _Socket? = null
private var readChannel: _ReadChannel? = null
private var writeChannel: _WriteChannel? = null
private var currentRequestId = 1
public fun connect(password: String): Boolean {
socket = _Socket(host, port, context)
readChannel = socket!!.openReadChannel()
writeChannel = socket!!.openWriteChannel()
val authPacket = AuthPacket(password, currentRequestId)
writeChannel!!.sendPacket(authPacket)
return readChannel!!.readPacket().requestId != -1
}
public fun command(command: String): String {
val reqId = currentRequestId++
val commandPacket = ExecCommandPacket(command, reqId)
writeChannel!!.sendPacket(commandPacket)
return readChannel!!.readPacket().payload
}
public override fun close() {
socket?.close()
socket = null
readChannel = null
writeChannel = null
}
}
@JvmOverloads
public fun rconClient(host: String, port: Int, context: MCPingContext = MCPingContext()): RCONClient =
RCONClient(host, port, context)
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package test
import cn.rtast.mcping.rconlib.rconClient
import kotlin.test.Test
class TestRCON {
@Test
fun `test rcon client`() {
val host = "127.0.0.1"
val port = 25575
val client = rconClient(host, port)
val authed = client.connect("123456")
if (authed) println(client.command("list")) else throw IllegalArgumentException("incorrect password")
}
}
@@ -0,0 +1,29 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/4
*/
package test;
import cn.rtast.mcping.rconlib.RCONClient;
import cn.rtast.mcping.rconlib.Rconlib;
import org.junit.Test;
public class TestRconClientOnJava {
@Test
public void testRconClient() {
String host = "127.0.0.1";
int port = 25575;
String password = "123456";
RCONClient rconClient = Rconlib.rconClient(host, port);
boolean authed = rconClient.connect(password);
if (authed) {
System.out.println(rconClient.command("list"));
} else {
throw new IllegalStateException("incorrect password");
}
}
}