diff --git a/README.md b/README.md index b18d263..50f582a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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` native platforms required `ktor-network` and `kotlinx-io` @@ -23,12 +23,15 @@ repositories { } 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/ +# MC Ping + ## Kotlin example ```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), > 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 under [Apache-2.0](LICENSE) \ No newline at end of file diff --git a/common/src/commonMain/kotlin/cn/rtast/mcping/platform/buffer.kt b/common/src/commonMain/kotlin/cn/rtast/mcping/platform/buffer.kt index c63aa4a..5b03016 100644 --- a/common/src/commonMain/kotlin/cn/rtast/mcping/platform/buffer.kt +++ b/common/src/commonMain/kotlin/cn/rtast/mcping/platform/buffer.kt @@ -13,15 +13,18 @@ public expect class _Buffer { public constructor(bytes: ByteArray) public fun writeByte(value: Byte) - public fun writeShort(value: Short) - public fun writeLong(value: Long) + public fun writeShort(value: Short, endian: ByteOrder = ByteOrder.BIG_ENDIAN) + 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 readByte(): Byte - public fun readShort(): Short - public fun readLong(): Long + 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 toByteArray(): ByteArray + public fun close() public val size: Int } diff --git a/common/src/commonMain/kotlin/cn/rtast/mcping/platform/byte_order.kt b/common/src/commonMain/kotlin/cn/rtast/mcping/platform/byte_order.kt new file mode 100644 index 0000000..89182d3 --- /dev/null +++ b/common/src/commonMain/kotlin/cn/rtast/mcping/platform/byte_order.kt @@ -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 +} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/cn/rtast/mcping/platform/channels.kt b/common/src/commonMain/kotlin/cn/rtast/mcping/platform/channels.kt index 0bcc120..b808d87 100644 --- a/common/src/commonMain/kotlin/cn/rtast/mcping/platform/channels.kt +++ b/common/src/commonMain/kotlin/cn/rtast/mcping/platform/channels.kt @@ -10,9 +10,11 @@ package cn.rtast.mcping.platform @Suppress("CLASSNAME") public expect class _ReadChannel { 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 readFully(out: ByteArray, start: Int = 0, end: Int = out.size) - public fun readLong(): Long } @Suppress("CLASSNAME") diff --git a/common/src/commonMain/kotlin/cn/rtast/mcping/platform/context.kt b/common/src/commonMain/kotlin/cn/rtast/mcping/platform/context.kt index 4473aa3..0f17788 100644 --- a/common/src/commonMain/kotlin/cn/rtast/mcping/platform/context.kt +++ b/common/src/commonMain/kotlin/cn/rtast/mcping/platform/context.kt @@ -11,4 +11,4 @@ package cn.rtast.mcping.platform * An object class used to pass some parameters * that exists only on native targets */ -public expect class PingContext() \ No newline at end of file +public expect class MCPingContext() \ No newline at end of file diff --git a/common/src/commonMain/kotlin/cn/rtast/mcping/platform/socket.kt b/common/src/commonMain/kotlin/cn/rtast/mcping/platform/socket.kt index c65cdd6..8e28496 100644 --- a/common/src/commonMain/kotlin/cn/rtast/mcping/platform/socket.kt +++ b/common/src/commonMain/kotlin/cn/rtast/mcping/platform/socket.kt @@ -8,14 +8,14 @@ package cn.rtast.mcping.platform @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 openWriteChannel(): _WriteChannel public fun close() } @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 close() } \ No newline at end of file diff --git a/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/buffer.jvm.kt b/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/buffer.jvm.kt index 205a5eb..4fa9762 100644 --- a/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/buffer.jvm.kt +++ b/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/buffer.jvm.kt @@ -24,21 +24,37 @@ public actual class _Buffer { outStream.write(value.toInt()) } - public actual fun writeShort(value: Short) { + public actual fun writeShort(value: Short, endian: ByteOrder) { val v = value.toInt() - outStream.write(v shr 8) - outStream.write(v) + if (endian == ByteOrder.BIG_ENDIAN) { + outStream.write(v shr 8) + outStream.write(v) + } else { + outStream.write(v) + outStream.write(v shr 8) + } } - public 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()) + public actual fun writeInt(value: Int, endian: ByteOrder) { + if (endian == ByteOrder.BIG_ENDIAN) { + outStream.write(value shr 24) + outStream.write(value shr 16) + outStream.write(value shr 8) + outStream.write(value) + } else { + outStream.write(value) + 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) { @@ -60,23 +76,9 @@ public actual class _Buffer { return array[readOffset++] } - public actual fun readShort(): Short { - val b1 = readByte().toInt() and 0xFF - val b2 = readByte().toInt() and 0xFF - 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 readShort(endian: ByteOrder): Short = readBytes(2).toShort(endian) + public actual fun readInt(endian: ByteOrder): Int = readBytes(4).toInt(endian) + public actual fun readLong(endian: ByteOrder): Long = readBytes(8).toLong(endian) public actual fun readBytes(length: Int): ByteArray { val array = ensureReadArray() 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 val size: Int - get() = outStream.size() + public actual fun close(): Unit = outStream.close() + public actual val size: Int get() = outStream.size() } \ No newline at end of file diff --git a/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/channels.jvm.kt b/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/channels.jvm.kt index d4fd7a0..f89bf52 100644 --- a/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/channels.jvm.kt +++ b/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/channels.jvm.kt @@ -12,15 +12,11 @@ import java.io.InputStream import java.io.OutputStream @Suppress("CLASSNAME") -public actual class _ReadChannel { - private val _inputStream: InputStream - - public constructor(inputStream: InputStream) { - _inputStream = inputStream - } +public actual class _ReadChannel(private val _inputStream: InputStream) { public actual fun readByte(): Byte = _inputStream.read().toByte() public actual fun readBytes(length: Int): ByteArray = _inputStream.readNBytes(length) + public actual fun readFully(out: ByteArray, start: Int, end: Int) { var bytesRead = 0 val length = end - start @@ -31,23 +27,9 @@ public actual class _ReadChannel { } } - public 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)) - } + public actual fun readShort(endian: ByteOrder): Short = readBytes(2).toShort(endian) + public actual fun readInt(endian: ByteOrder): Int = readBytes(4).toInt(endian) + public actual fun readLong(endian: ByteOrder): Long = readBytes(8).toLong(endian) } @Suppress("CLASSNAME") diff --git a/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/context.jvm.kt b/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/context.jvm.kt index bb135cc..93857f9 100644 --- a/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/context.jvm.kt +++ b/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/context.jvm.kt @@ -9,4 +9,4 @@ package cn.rtast.mcping.platform /** * No Context for jvm targets */ -public actual class PingContext \ No newline at end of file +public actual class MCPingContext \ No newline at end of file diff --git a/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/order_convert.kt b/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/order_convert.kt new file mode 100644 index 0000000..22a57ff --- /dev/null +++ b/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/order_convert.kt @@ -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 +} \ No newline at end of file diff --git a/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/socket.jvm.kt b/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/socket.jvm.kt index 1cdbecb..c725bd8 100644 --- a/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/socket.jvm.kt +++ b/common/src/jvmMain/kotlin/cn/rtast/mcping/platform/socket.jvm.kt @@ -12,7 +12,7 @@ import java.net.InetSocketAddress import java.net.Socket as JvmSocket @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) 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") -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 { soTimeout = 3000 connect(InetSocketAddress(host, port)) diff --git a/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/buffer.native.kt b/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/buffer.native.kt index 8ac2e2d..39738fd 100644 --- a/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/buffer.native.kt +++ b/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/buffer.native.kt @@ -6,6 +6,7 @@ package cn.rtast.mcping.platform +import io.ktor.utils.io.bits.* import kotlinx.io.Buffer 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 writeShort(value: Short): Unit = _delegateBuf.writeShort(value) - public actual fun writeLong(value: Long): Unit = _delegateBuf.writeLong(value) + public actual fun writeShort(value: Short, endian: ByteOrder) { + 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 readByte(): Byte = _delegateBuf.readByte() - public actual fun readShort(): Short = _delegateBuf.readShort() - public actual fun readLong(): Long = _delegateBuf.readLong() - public actual fun readBytes(length: Int): ByteArray = _delegateBuf.readByteArray(length) + public actual fun readShort(endian: ByteOrder): Short { + val v = _delegateBuf.readShort() + 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 close(): Unit = _delegateBuf.close() public actual val size: Int get() = _delegateBuf.size.toInt() diff --git a/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/channels.native.kt b/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/channels.native.kt index 23c4391..a9a4c02 100644 --- a/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/channels.native.kt +++ b/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/channels.native.kt @@ -7,22 +7,31 @@ package cn.rtast.mcping.platform import io.ktor.utils.io.* +import io.ktor.utils.io.bits.reverseByteOrder import kotlinx.coroutines.runBlocking @Suppress("CLASSNAME") -public actual class _ReadChannel { - private val _readChannel: ByteReadChannel - - public constructor(readChannel: ByteReadChannel) { - _readChannel = readChannel - } +public actual class _ReadChannel(private val _readChannel: ByteReadChannel) { public actual fun readByte(): Byte = runBlocking { _readChannel.readByte() } public actual fun readBytes(length: Int): ByteArray = runBlocking { _readChannel.readByteArray(length) } public actual fun readFully(out: ByteArray, start: Int, end: Int): Unit = 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") diff --git a/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/context.native.kt b/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/context.native.kt index dce97ef..e808136 100644 --- a/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/context.native.kt +++ b/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/context.native.kt @@ -12,7 +12,7 @@ import io.ktor.network.selector.* import kotlinx.coroutines.Dispatchers 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 _autoCloseSelectorManager: Boolean = false diff --git a/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/socket.native.kt b/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/socket.native.kt index aa0035e..b706bfe 100644 --- a/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/socket.native.kt +++ b/common/src/nativeMain/kotlin/cn/rtast/mcping/platform/socket.native.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.io.readByteArray @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 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") -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 // use bind to create an unconnected socket diff --git a/gradle.properties b/gradle.properties index 028f884..9aa8522 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,4 +4,4 @@ kotlin.native.enableKlibsCrossCompilation=true kotlin.daemon.jvmargs=-Xmx2048M org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 -libVersion=0.0.2 \ No newline at end of file +libVersion=0.0.3 \ No newline at end of file diff --git a/mcping/src/commonMain/kotlin/cn/rtast/mcping/bedrock/ping_bedrock.kt b/mcping/src/commonMain/kotlin/cn/rtast/mcping/bedrock/ping_bedrock.kt index d5c29b5..d5c5ed5 100644 --- a/mcping/src/commonMain/kotlin/cn/rtast/mcping/bedrock/ping_bedrock.kt +++ b/mcping/src/commonMain/kotlin/cn/rtast/mcping/bedrock/ping_bedrock.kt @@ -8,12 +8,12 @@ package cn.rtast.mcping.bedrock 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.wrap 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) return try { val sendTime = Clock.System.now().toEpochMilliseconds() diff --git a/mcping/src/commonMain/kotlin/cn/rtast/mcping/java/ping_java.kt b/mcping/src/commonMain/kotlin/cn/rtast/mcping/java/ping_java.kt index d4caad5..4d5ab12 100644 --- a/mcping/src/commonMain/kotlin/cn/rtast/mcping/java/ping_java.kt +++ b/mcping/src/commonMain/kotlin/cn/rtast/mcping/java/ping_java.kt @@ -8,11 +8,11 @@ package cn.rtast.mcping.java import cn.rtast.mcping.PingResponse -import cn.rtast.mcping.platform.PingContext +import cn.rtast.mcping.platform.MCPingContext import cn.rtast.mcping.platform._Socket 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 receiveChannel = socket.openReadChannel() val sendChannel = socket.openWriteChannel() diff --git a/mcping/src/commonMain/kotlin/cn/rtast/mcping/mcping.kt b/mcping/src/commonMain/kotlin/cn/rtast/mcping/mcping.kt index 563628e..be9dc86 100644 --- a/mcping/src/commonMain/kotlin/cn/rtast/mcping/mcping.kt +++ b/mcping/src/commonMain/kotlin/cn/rtast/mcping/mcping.kt @@ -11,7 +11,7 @@ package cn.rtast.mcping import cn.rtast.mcping.bedrock.pingBedrockServer 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.JvmOverloads @@ -20,7 +20,7 @@ public fun mcping( host: String, port: Int, type: ServerType = ServerType.Java, - context: PingContext = PingContext(), + context: MCPingContext = MCPingContext(), ): PingResponse = when (type) { ServerType.Java -> pingJavaServer(host, port, context) ServerType.Bedrock -> pingBedrockServer(host, port, context) diff --git a/mcping/src/mingwTest/kotlin/test/TestMingwPing.kt b/mcping/src/mingwTest/kotlin/test/TestMingwPing.kt index 7a08ca1..9189d3b 100644 --- a/mcping/src/mingwTest/kotlin/test/TestMingwPing.kt +++ b/mcping/src/mingwTest/kotlin/test/TestMingwPing.kt @@ -9,7 +9,7 @@ package test import cn.rtast.mcping.ServerType import cn.rtast.mcping.mcping -import cn.rtast.mcping.platform.PingContext +import cn.rtast.mcping.platform.MCPingContext import io.ktor.network.selector.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -22,7 +22,7 @@ class TestMingwPing { 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)) + mcping(host = "org.mc-complex.com", port = 25565, type = ServerType.Java, context = MCPingContext(sm)) println(response) } @@ -30,7 +30,7 @@ class TestMingwPing { 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)) + mcping(host = "play.wildnetwork.net", port = 19132, type = ServerType.Bedrock, context = MCPingContext(sm)) println(response) println(response.toBedrockResponse()) } diff --git a/rconlib/build.gradle.kts b/rconlib/build.gradle.kts index d88aef9..0f8eae9 100644 --- a/rconlib/build.gradle.kts +++ b/rconlib/build.gradle.kts @@ -16,6 +16,10 @@ kotlin { implementation(project(":common")) } + jvmMain.dependencies { + + } + commonTest.dependencies { implementation(kotlin("test")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") diff --git a/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/buffer_ext.kt b/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/buffer_ext.kt new file mode 100644 index 0000000..3a35587 --- /dev/null +++ b/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/buffer_ext.kt @@ -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) \ No newline at end of file diff --git a/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/packet.kt b/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/packet.kt new file mode 100644 index 0000000..871c889 --- /dev/null +++ b/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/packet.kt @@ -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) \ No newline at end of file diff --git a/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/packet_type.kt b/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/packet_type.kt new file mode 100644 index 0000000..a1fcceb --- /dev/null +++ b/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/packet_type.kt @@ -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 +} \ No newline at end of file diff --git a/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/rconlib.kt b/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/rconlib.kt index ebb68e1..7b0744b 100644 --- a/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/rconlib.kt +++ b/rconlib/src/commonMain/kotlin/cn/rtast/mcping/rconlib/rconlib.kt @@ -4,6 +4,51 @@ * Date: 2026/9/4 */ +@file:JvmName("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) \ No newline at end of file diff --git a/rconlib/src/commonTest/kotlin/test/TestRCON.kt b/rconlib/src/commonTest/kotlin/test/TestRCON.kt new file mode 100644 index 0000000..9f81460 --- /dev/null +++ b/rconlib/src/commonTest/kotlin/test/TestRCON.kt @@ -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") + } +} \ No newline at end of file diff --git a/rconlib/src/jvmTest/java/test/TestRconClientOnJava.java b/rconlib/src/jvmTest/java/test/TestRconClientOnJava.java new file mode 100644 index 0000000..88aadce --- /dev/null +++ b/rconlib/src/jvmTest/java/test/TestRconClientOnJava.java @@ -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"); + } + } +}