commit b3583ba48d6ae59405c558053d57270fa5c2ba57 Author: RTAkland Date: Thu Sep 3 19:32:46 2026 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6420b18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Kotlin ### +.kotlin + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +.idea/ +.kotlin/ +**/build/* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..143b6be --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 RTAkland + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..aeca516 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# mcping + +A lightweight motd ping library for kotlin multiplatform and java, +no dependencies in `jvm` target except `kotlin-stdlib` + +native platforms required `ktor-network` and `kotlinx-io` + +# Supported targets + +- jvm 1.8 +- mingwX64 +- linuxArm64 +- liunxX64 +- iosArm64 +- iosSimulatorArm64 +- macosArm64 + +# Get started + +```kotlin +repositories { + maven("https://repo.rtast.cn/packages") +} + +dependencies { + implementation("cn.rtast.mcping:mcping:0.0.1") +} +``` + +```kotlin +fun main() { + // return a json respose + val response: String = mcping("org.mc-complex.com", 25565) + println(response) +} +``` + +> An example json response can be found at [ping-response-example](example/java-ping-response.json) (Formatted) + +# Open Source + +Open source under [Apache-2.0](LICENSE) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..98f1409 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,57 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("multiplatform") version "2.4.10" + id("maven-publish") +} + +group = "cn.rtast.mcping" +version = "0.0.1" + +repositories { + mavenCentral() +} + +kotlin { + explicitApi() + + linuxX64() + linuxArm64() + macosArm64() + mingwX64() + iosArm64() + iosSimulatorArm64() + jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } + + sourceSets { + jvmMain.dependencies { + // no dependencies needed + } + + nativeMain.dependencies { + implementation("io.ktor:ktor-network:3.5.2") + implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.9.1") + } + + commonTest.dependencies { + implementation(kotlin("test")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") + } + } + + compilerOptions.freeCompilerArgs.addAll("-Xexpect-actual-classes") +} + +publishing { + repositories { + maven("https://repo.rtast.cn/packages") { + credentials(HttpHeaderCredentials::class) { + name = "Authorization" + value = "Bearer ${System.getenv("PUBLISH_TOKEN")}" + } + authentication { + create("header") + } + } + } +} \ No newline at end of file diff --git a/example/java-ping-response.json b/example/java-ping-response.json new file mode 100644 index 0000000..b82f495 --- /dev/null +++ b/example/java-ping-response.json @@ -0,0 +1,321 @@ +{ + "version": { + "protocol": 776, + "name": "Velocity 1.7.2-26.2" + }, + "players": { + "online": 5901, + "max": 5951, + "sample": [] + }, + "description": { + "extra": [ + { + "extra": [ + { + "color": "white", + "text": "sᴍᴘ" + }, + " ", + { + "extra": [ + { + "extra": [ + { + "color": "#CFDFE3", + "text": "◁" + }, + { + "color": "#AFD1D9", + "text": "═" + }, + { + "color": "#8EC3CF", + "text": "═" + } + ], + "text": "" + } + ], + "text": "" + }, + { + "color": "dark_gray", + "text": "[‐" + }, + " ", + { + "extra": [ + { + "bold": true, + "extra": [ + { + "color": "#8EC3CF", + "text": "C" + }, + { + "color": "#97C7D2", + "text": "O" + }, + { + "color": "#A1CBD5", + "text": "M" + }, + { + "color": "#AACFD8", + "text": "P" + }, + { + "color": "#B3D3DA", + "text": "L" + }, + { + "color": "#BCD7DD", + "text": "E" + }, + { + "color": "#C6DBE0", + "text": "X" + }, + { + "color": "#CFDFE3", + "text": " " + }, + { + "color": "#C6DBE0", + "text": " " + }, + { + "color": "#BCD7DD", + "text": "G" + }, + { + "color": "#B3D3DA", + "text": "A" + }, + { + "color": "#AACFD8", + "text": "M" + }, + { + "color": "#A1CBD5", + "text": "I" + }, + { + "color": "#97C7D2", + "text": "N" + }, + { + "color": "#8EC3CF", + "text": "G" + } + ], + "text": "" + } + ], + "text": "" + }, + " ", + { + "color": "dark_gray", + "text": "‐]" + }, + { + "extra": [ + { + "extra": [ + { + "color": "#8EC3CF", + "text": "═" + }, + { + "color": "#AFD1D9", + "text": "═" + }, + { + "color": "#CFDFE3", + "text": "▷" + } + ], + "text": "" + } + ], + "text": "" + }, + " ", + { + "color": "white", + "text": "ғᴀᴄᴛɪᴏɴs" + } + ], + "text": "" + }, + "\n", + { + "extra": [ + { + "color": "white", + "text": "sᴋʏʙʟᴏᴄᴋ" + }, + " ", + { + "color": "#8EC3CF", + "extra": [ + { + "bold": true, + "obfuscated": true, + "extra": [ + { + "color": "#F1AEFF", + "extra": [ + { + "color": "#8EC3CF", + "text": "i" + } + ], + "text": "i" + } + ], + "text": "i" + }, + " ", + { + "extra": [ + { + "extra": [ + { + "color": "#F1AEFF", + "text": "#" + }, + { + "color": "#F2B5FF", + "text": "1" + }, + { + "color": "#F3BCFF", + "text": " " + }, + { + "color": "#F5C3FF", + "text": "1" + }, + { + "color": "#F6CAFF", + "text": "." + }, + { + "color": "#F7D1FF", + "text": "2" + }, + { + "color": "#F8D8FF", + "text": "1" + }, + { + "color": "#F9DFFF", + "text": " " + }, + { + "color": "#FAE6FF", + "text": "ᴠ" + }, + { + "color": "#FCEDFF", + "text": "ᴀ" + }, + { + "color": "#FDF4FF", + "text": "ɴ" + }, + { + "color": "#FEFBFF", + "text": "ɪ" + }, + { + "color": "#FDF4FF", + "text": "ʟ" + }, + { + "color": "#FCEDFF", + "text": "ʟ" + }, + { + "color": "#FAE6FF", + "text": "ᴀ" + }, + { + "color": "#F9DFFF", + "text": " " + }, + { + "color": "#F8D8FF", + "text": "ɴ" + }, + { + "color": "#F7D1FF", + "text": "ᴇ" + }, + { + "color": "#F6CAFF", + "text": "ᴛ" + }, + { + "color": "#F5C3FF", + "text": "ᴡ" + }, + { + "color": "#F3BCFF", + "text": "ᴏ" + }, + { + "color": "#F2B5FF", + "text": "ʀ" + }, + { + "color": "#F1AEFF", + "text": "ᴋ" + } + ], + "text": "" + } + ], + "text": "" + }, + { + "extra": [ + { + "bold": true, + "obfuscated": true, + "extra": [ + { + "color": "#F1AEFF", + "extra": [ + { + "color": "#8EC3CF", + "text": "i" + } + ], + "text": "i" + } + ], + "text": "i" + }, + " ", + { + "color": "white", + "text": "ʟɪғᴇsᴛᴇᴀʟ" + } + ], + "text": " " + } + ], + "text": "" + } + ], + "text": "" + } + ], + "text": "" + }, + "favicon": "data:image/png;base64,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\u003d\u003d" +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..804fa97 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +kotlin.code.style=official +kotlin.native.ignoreDisabledTargets=true +kotlin.native.enableKlibsCrossCompilation=true +kotlin.daemon.jvmargs=-Xmx2048M +org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f5fa0eb --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Sep 03 17:39:51 CST 2026 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..2d798d4 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,4 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} +rootProject.name = "mcping" \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/mcping/mc_primitives.kt b/src/commonMain/kotlin/cn/rtast/mcping/mc_primitives.kt new file mode 100644 index 0000000..0a60740 --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/mcping/mc_primitives.kt @@ -0,0 +1,48 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + +package cn.rtast.mcping + +import cn.rtast.mcping.platform.PlatformBuffer +import cn.rtast.mcping.platform.PlatformReadChannel + + +internal fun PlatformBuffer.writeVarInt(value: Int) { + var v = value + while (true) { + if ((v and 0x7F.inv()) == 0) { + this.writeByte(v.toByte()) + return + } + this.writeByte(((v and 0x7F) or 0x80).toByte()) + v = v ushr 7 + } +} + +internal fun PlatformReadChannel.readVarInt(): Int { + var value = 0 + var position = 0 + while (true) { + val currentByte = this.readByte().toInt() and 0xFF + value = value or ((currentByte and 0x7F) shl position) + if ((currentByte and 0x80) == 0) break + position += 7 + if (position >= 35) throw IllegalArgumentException("VarInt too long") + } + return value +} + +internal fun PlatformBuffer.writeMcString(value: String) { + val bytes = value.encodeToByteArray() + this.writeVarInt(bytes.size) + this.writeBytes(bytes) +} + +internal fun PlatformReadChannel.readMcString(): String { + val length = this.readVarInt() + val bytes = this.readBytes(length) + return bytes.decodeToString() +} \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/mcping/mcping.kt b/src/commonMain/kotlin/cn/rtast/mcping/mcping.kt new file mode 100644 index 0000000..e1e0025 --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/mcping/mcping.kt @@ -0,0 +1,35 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + + +package cn.rtast.mcping + +import cn.rtast.mcping.platform.PlatformSocket + + +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() + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/mcping/packet.kt b/src/commonMain/kotlin/cn/rtast/mcping/packet.kt new file mode 100644 index 0000000..63a2bfd --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/mcping/packet.kt @@ -0,0 +1,43 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + + +package cn.rtast.mcping + +import cn.rtast.mcping.platform.PlatformBuffer + +internal interface MinecraftPacket { + val packetId: Int + + fun writePayload(buffer: PlatformBuffer) +} + +// ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Handshake +internal data class HandshakePacket( + val protocolVersion: Int, + val serverAddress: String, + val serverPort: UShort, + // 1 -> Status + val nextState: Int, +) : MinecraftPacket { + override val packetId: Int = 0x00 + + override fun writePayload(buffer: PlatformBuffer) { + buffer.writeVarInt(protocolVersion) + buffer.writeMcString(serverAddress) + // write UShort + // write 2 bytes big endian + buffer.writeByte((serverPort.toInt() shr 8).toByte()) + buffer.writeByte(serverPort.toByte()) + buffer.writeVarInt(nextState) + } +} + +// ref https://minecraft.wiki/w/Java_Edition_protocol/Packets#Status +internal data object StatusRequestPacket : MinecraftPacket { + override val packetId: Int = 0x00 + override fun writePayload(buffer: PlatformBuffer) {} +} \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/mcping/packet_writer.kt b/src/commonMain/kotlin/cn/rtast/mcping/packet_writer.kt new file mode 100644 index 0000000..fd1014c --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/mcping/packet_writer.kt @@ -0,0 +1,23 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + + +package cn.rtast.mcping + +import cn.rtast.mcping.platform.PlatformBuffer +import cn.rtast.mcping.platform.PlatformWriteChannel + +internal fun PlatformWriteChannel.sendPacket(packet: MinecraftPacket) { + val bodyBuffer = PlatformBuffer() + bodyBuffer.writeVarInt(packet.packetId) + packet.writePayload(bodyBuffer) + val frameBuffer = PlatformBuffer() + frameBuffer.writeVarInt(bodyBuffer.size) + frameBuffer.writeBytes(bodyBuffer.toByteArray()) + val bytes = frameBuffer.toByteArray() + this.writeFully(bytes, 0, bytes.size) + this.flush() +} \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/mcping/platform/buffer.kt b/src/commonMain/kotlin/cn/rtast/mcping/platform/buffer.kt new file mode 100644 index 0000000..77245ea --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/mcping/platform/buffer.kt @@ -0,0 +1,17 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + + +package cn.rtast.mcping.platform + +internal expect class PlatformBuffer() { + fun writeByte(value: Byte) + fun writeBytes(bytes: ByteArray) + fun readByte(): Byte + fun readBytes(length: Int): ByteArray + fun toByteArray(): ByteArray + val size: Int +} \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/mcping/platform/channels.kt b/src/commonMain/kotlin/cn/rtast/mcping/platform/channels.kt new file mode 100644 index 0000000..62ef0c2 --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/mcping/platform/channels.kt @@ -0,0 +1,19 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + + +package cn.rtast.mcping.platform + +internal expect class PlatformReadChannel { + fun readByte(): Byte + fun readBytes(length: Int): ByteArray + fun readFully(out: ByteArray, start: Int = 0, end: Int = out.size) +} + +internal expect class PlatformWriteChannel { + fun writeFully(value: ByteArray, startIndex: Int = 0, endIndex: Int = value.size) + fun flush() +} \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/mcping/platform/socket.kt b/src/commonMain/kotlin/cn/rtast/mcping/platform/socket.kt new file mode 100644 index 0000000..df9c3c7 --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/mcping/platform/socket.kt @@ -0,0 +1,14 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + + +package cn.rtast.mcping.platform + +internal expect class PlatformSocket internal constructor(host: String, port: Int){ + fun openReadChannel(): PlatformReadChannel + fun openWriteChannel(): PlatformWriteChannel + fun close() +} \ No newline at end of file diff --git a/src/commonTest/kotlin/test/TestPing.kt b/src/commonTest/kotlin/test/TestPing.kt new file mode 100644 index 0000000..09b5096 --- /dev/null +++ b/src/commonTest/kotlin/test/TestPing.kt @@ -0,0 +1,21 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + + +package test + +import cn.rtast.mcping.mcping +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class TestPing { + + @Test + fun `test ping java server`() = runTest { + val response = mcping("org.mc-complex.com", 25565) + println(response) + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/cn/rtast/mcping/platform/buffer.jvm.kt b/src/jvmMain/kotlin/cn/rtast/mcping/platform/buffer.jvm.kt new file mode 100644 index 0000000..721d965 --- /dev/null +++ b/src/jvmMain/kotlin/cn/rtast/mcping/platform/buffer.jvm.kt @@ -0,0 +1,35 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + +package cn.rtast.mcping.platform + +import java.io.ByteArrayOutputStream + +internal actual class PlatformBuffer actual constructor() { + private val stream = ByteArrayOutputStream() + private var readOffset = 0 + + actual fun writeByte(value: Byte) = stream.write(value.toInt()) + actual fun writeBytes(bytes: ByteArray) = stream.write(bytes) + actual fun readByte(): Byte { + val array = stream.toByteArray() + if (readOffset >= array.size) throw IndexOutOfBoundsException("Buffer underflow") + return array[readOffset++] + } + + actual fun readBytes(length: Int): ByteArray { + val array = stream.toByteArray() + 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 val size: Int + get() = stream.size() +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/cn/rtast/mcping/platform/channels.jvm.kt b/src/jvmMain/kotlin/cn/rtast/mcping/platform/channels.jvm.kt new file mode 100644 index 0000000..0d9a67a --- /dev/null +++ b/src/jvmMain/kotlin/cn/rtast/mcping/platform/channels.jvm.kt @@ -0,0 +1,44 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + + +package cn.rtast.mcping.platform + +import java.io.InputStream +import java.io.OutputStream + +internal actual class PlatformReadChannel { + private val _inputStream: InputStream + + constructor(inputStream: InputStream) { + _inputStream = inputStream + } + + actual fun readByte(): Byte = _inputStream.read().toByte() + actual fun readBytes(length: Int): ByteArray = _inputStream.readNBytes(length) + actual fun readFully(out: ByteArray, start: Int, end: Int) { + var bytesRead = 0 + val length = end - start + while (bytesRead < length) { + val read = _inputStream.read(out, start + bytesRead, length - bytesRead) + if (read == -1) throw IllegalStateException("End of stream") + bytesRead += read + } + } +} + +internal actual class PlatformWriteChannel { + private val _outputStream: OutputStream + + constructor(outputStream: OutputStream) { + _outputStream = outputStream + } + + actual fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) = + _outputStream.write(value, startIndex, endIndex - startIndex) + + actual fun flush() = _outputStream.flush() +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/cn/rtast/mcping/platform/socket.jvm.kt b/src/jvmMain/kotlin/cn/rtast/mcping/platform/socket.jvm.kt new file mode 100644 index 0000000..43f16ad --- /dev/null +++ b/src/jvmMain/kotlin/cn/rtast/mcping/platform/socket.jvm.kt @@ -0,0 +1,17 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + +package cn.rtast.mcping.platform + +import java.net.Socket + +internal actual class PlatformSocket internal actual constructor(host: String, port: Int) { + private val socket = Socket(host, port) + + actual fun openReadChannel(): PlatformReadChannel = PlatformReadChannel(socket.getInputStream()) + actual fun openWriteChannel(): PlatformWriteChannel = PlatformWriteChannel(socket.getOutputStream()) + actual fun close() = socket.close() +} \ No newline at end of file diff --git a/src/nativeMain/kotlin/cn/rtast/mcping/platform/buffer.native.kt b/src/nativeMain/kotlin/cn/rtast/mcping/platform/buffer.native.kt new file mode 100644 index 0000000..61ae21d --- /dev/null +++ b/src/nativeMain/kotlin/cn/rtast/mcping/platform/buffer.native.kt @@ -0,0 +1,24 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * 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() + + actual fun writeByte(value: Byte) = _delegateBuf.writeByte(value) + actual fun writeBytes(bytes: ByteArray) = _delegateBuf.write(bytes) + actual fun readByte(): Byte = _delegateBuf.readByte() + actual fun readBytes(length: Int): ByteArray = _delegateBuf.readByteArray(length) + actual fun toByteArray(): ByteArray = _delegateBuf.peek().readByteArray() + + actual val size: Int + get() = _delegateBuf.size.toInt() +} \ No newline at end of file diff --git a/src/nativeMain/kotlin/cn/rtast/mcping/platform/channels.native.kt b/src/nativeMain/kotlin/cn/rtast/mcping/platform/channels.native.kt new file mode 100644 index 0000000..d1a1f6e --- /dev/null +++ b/src/nativeMain/kotlin/cn/rtast/mcping/platform/channels.native.kt @@ -0,0 +1,35 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + +package cn.rtast.mcping.platform + +import io.ktor.utils.io.* +import kotlinx.coroutines.runBlocking + +internal actual class PlatformReadChannel { + private val _readChannel: ByteReadChannel + + constructor(readChannel: ByteReadChannel) { + _readChannel = readChannel + } + + 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) } +} + +internal actual class PlatformWriteChannel { + private val _writeChannel: ByteWriteChannel + + constructor(writeChannel: ByteWriteChannel) { + _writeChannel = writeChannel + } + + actual fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) = + runBlocking { _writeChannel.writeFully(value, startIndex, endIndex) } + + actual fun flush() = runBlocking { _writeChannel.flush() } +} \ No newline at end of file diff --git a/src/nativeMain/kotlin/cn/rtast/mcping/platform/socket.native.kt b/src/nativeMain/kotlin/cn/rtast/mcping/platform/socket.native.kt new file mode 100644 index 0000000..086aff0 --- /dev/null +++ b/src/nativeMain/kotlin/cn/rtast/mcping/platform/socket.native.kt @@ -0,0 +1,28 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/3 + */ + +package cn.rtast.mcping.platform + +import io.ktor.network.selector.* +import io.ktor.network.sockets.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.runBlocking + +internal actual class PlatformSocket internal actual constructor(host: String, port: Int) { + + 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 close() { + socket.close() + sm.close() + } +} \ No newline at end of file