diff --git a/.github/workflows/fetch-mc-forge.yml b/.github/workflows/fetch-mc-forge.yml index e29750f8..d6c6c14e 100644 --- a/.github/workflows/fetch-mc-forge.yml +++ b/.github/workflows/fetch-mc-forge.yml @@ -30,6 +30,15 @@ jobs: 1.21.5, 1.21.6, 1.21.7, + 1.21.8, + 1.21.9, + 1.21.10, + 1.21.11, + 26.1, + 26.1.1, + 26.1.2, + 26.2, + 1.21.8, ] runs-on: ubuntu-latest steps: diff --git a/.github/workflows/fetch-papermc.yml b/.github/workflows/fetch-papermc.yml index ed7d7d08..779c8caf 100644 --- a/.github/workflows/fetch-papermc.yml +++ b/.github/workflows/fetch-papermc.yml @@ -29,6 +29,14 @@ jobs: 1.21.5, 1.21.6, 1.21.7, + 1.21.8, + 1.21.9, + 1.21.10, + 1.21.11, + 26.1.1, + 26.1.2, + 26.2, + 1.21.8, ] runs-on: ubuntu-latest steps: diff --git a/.github/workflows/fetch-spigot.yml b/.github/workflows/fetch-spigot.yml index 3c4422a9..2fd9e8ba 100644 --- a/.github/workflows/fetch-spigot.yml +++ b/.github/workflows/fetch-spigot.yml @@ -29,6 +29,13 @@ jobs: { java-version: "21", spigot-version: "1.21.5" }, { java-version: "21", spigot-version: "1.21.6" }, { java-version: "21", spigot-version: "1.21.8" }, + { java-version: "21", spigot-version: "1.21.9" }, + { java-version: "21", spigot-version: "1.21.10" }, + { java-version: "21", spigot-version: "1.21.11" }, + { java-version: "25", spigot-version: "26.1" }, + { java-version: "25", spigot-version: "26.1.1" }, + { java-version: "25", spigot-version: "26.1.2" }, + { java-version: "25", spigot-version: "26.2" }, ] runs-on: ubuntu-latest steps: diff --git a/scripts/push.sh b/scripts/push.sh old mode 100755 new mode 100644 index 3d5045f8..c9339eb8 --- a/scripts/push.sh +++ b/scripts/push.sh @@ -113,6 +113,13 @@ push_release_artifacts() { run druid push artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.5 ./scrolls/minecraft/minecraft-spigot/1.21.5 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.6 ./scrolls/minecraft/minecraft-spigot/1.21.6 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.8 ./scrolls/minecraft/minecraft-spigot/1.21.8 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.9 ./scrolls/minecraft/minecraft-spigot/1.21.9 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.10 ./scrolls/minecraft/minecraft-spigot/1.21.10 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.11 ./scrolls/minecraft/minecraft-spigot/1.21.11 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-spigot:26.1 ./scrolls/minecraft/minecraft-spigot/26.1 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-spigot:26.1.1 ./scrolls/minecraft/minecraft-spigot/26.1.1 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-spigot:26.1.2 ./scrolls/minecraft/minecraft-spigot/26.1.2 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-spigot:26.2 ./scrolls/minecraft/minecraft-spigot/26.2 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.17 ./scrolls/minecraft/minecraft-vanilla/1.17 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.17.1 ./scrolls/minecraft/minecraft-vanilla/1.17.1 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.18 ./scrolls/minecraft/minecraft-vanilla/1.18 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft @@ -133,6 +140,14 @@ push_release_artifacts() { run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.5 ./scrolls/minecraft/minecraft-vanilla/1.21.5 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.6 ./scrolls/minecraft/minecraft-vanilla/1.21.6 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.7 ./scrolls/minecraft/minecraft-vanilla/1.21.7 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.8 ./scrolls/minecraft/minecraft-vanilla/1.21.8 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.9 ./scrolls/minecraft/minecraft-vanilla/1.21.9 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.10 ./scrolls/minecraft/minecraft-vanilla/1.21.10 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.11 ./scrolls/minecraft/minecraft-vanilla/1.21.11 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:26.1 ./scrolls/minecraft/minecraft-vanilla/26.1 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:26.1.1 ./scrolls/minecraft/minecraft-vanilla/26.1.1 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:26.1.2 ./scrolls/minecraft/minecraft-vanilla/26.1.2 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:26.2 ./scrolls/minecraft/minecraft-vanilla/26.2 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.17 ./scrolls/minecraft/papermc/1.17 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.17.1 ./scrolls/minecraft/papermc/1.17.1 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.18.1 ./scrolls/minecraft/papermc/1.18.1 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft @@ -152,6 +167,13 @@ push_release_artifacts() { run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.5 ./scrolls/minecraft/papermc/1.21.5 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.6 ./scrolls/minecraft/papermc/1.21.6 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.7 ./scrolls/minecraft/papermc/1.21.7 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.8 ./scrolls/minecraft/papermc/1.21.8 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.9 ./scrolls/minecraft/papermc/1.21.9 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.10 ./scrolls/minecraft/papermc/1.21.10 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.11 ./scrolls/minecraft/papermc/1.21.11 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:26.1.1 ./scrolls/minecraft/papermc/26.1.1 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:26.1.2 ./scrolls/minecraft/papermc/26.1.2 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-paper:26.2 ./scrolls/minecraft/papermc/26.2 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.17.1 ./scrolls/minecraft/forge/1.17.1 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.18 ./scrolls/minecraft/forge/1.18 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.18.1 ./scrolls/minecraft/forge/1.18.1 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft @@ -173,6 +195,14 @@ push_release_artifacts() { run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.5 ./scrolls/minecraft/forge/1.21.5 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.6 ./scrolls/minecraft/forge/1.21.6 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.7 ./scrolls/minecraft/forge/1.21.7 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.8 ./scrolls/minecraft/forge/1.21.8 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.9 ./scrolls/minecraft/forge/1.21.9 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.10 ./scrolls/minecraft/forge/1.21.10 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.11 ./scrolls/minecraft/forge/1.21.11 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:26.1 ./scrolls/minecraft/forge/26.1 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:26.1.1 ./scrolls/minecraft/forge/26.1.1 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:26.1.2 ./scrolls/minecraft/forge/26.1.2 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft + run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:26.2 ./scrolls/minecraft/forge/26.2 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-cuberite:latest ./scrolls/minecraft/cuberite/latest -p main=25565 -p webpanel=8080 -i artifacts.druid.gg/druid-team/druid:v0.1.257 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-rust-oxide:latest ./scrolls/rust/rust-oxide/latest -p main=/udp -p query=/udp -p rcon -p rustplus -i artifacts.druid.gg/druid-team/druid:v0.1.257-steamcmd --min-disk 10Gi --min-ram 6Gi --min-cpu 1 --smart --category rust run druid push artifacts.druid.gg/druid-team/scroll-rust-vanilla:latest ./scrolls/rust/rust-vanilla/latest -p main=/udp -p query=/udp -p rcon -p rustplus -i artifacts.druid.gg/druid-team/druid:v0.1.257-steamcmd --min-disk 10Gi --min-ram 6Gi --min-cpu 1 --smart --category rust diff --git a/scrolls/minecraft/forge/.build/artifacts.json b/scrolls/minecraft/forge/.build/artifacts.json index 73cc9a8e..06551b36 100644 --- a/scrolls/minecraft/forge/.build/artifacts.json +++ b/scrolls/minecraft/forge/.build/artifacts.json @@ -19,5 +19,13 @@ "1.21.4": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.4", "1.21.5": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.5", "1.21.6": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.6", - "1.21.7": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.7" + "1.21.7": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.7", + "1.21.8": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.8", + "1.21.9": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.9", + "1.21.10": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.10", + "1.21.11": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.11", + "26.1": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:26.1", + "26.1.1": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:26.1.1", + "26.1.2": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:26.1.2", + "26.2": "artifacts.druid.gg/druid-team/scroll-minecraft-forge:26.2" } diff --git a/scrolls/minecraft/forge/.build/vars.json b/scrolls/minecraft/forge/.build/vars.json index ed69406b..a617c5a7 100644 --- a/scrolls/minecraft/forge/.build/vars.json +++ b/scrolls/minecraft/forge/.build/vars.json @@ -58,5 +58,29 @@ }, "1.21.7": { "jdkVersion": "21" + }, + "1.21.8": { + "jdkVersion": "21" + }, + "1.21.9": { + "jdkVersion": "21" + }, + "1.21.10": { + "jdkVersion": "21" + }, + "1.21.11": { + "jdkVersion": "21" + }, + "26.1": { + "jdkVersion": "25" + }, + "26.1.1": { + "jdkVersion": "25" + }, + "26.1.2": { + "jdkVersion": "25" + }, + "26.2": { + "jdkVersion": "25" } } diff --git a/scrolls/minecraft/forge/1.21.10/data/run.sh b/scrolls/minecraft/forge/1.21.10/data/run.sh new file mode 100644 index 00000000..dbce7d86 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.10/data/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar forge-*-shim.jar nogui diff --git a/scrolls/minecraft/forge/1.21.10/data/server.properties.default b/scrolls/minecraft/forge/1.21.10/data/server.properties.default new file mode 100644 index 00000000..e9385571 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.10/data/server.properties.default @@ -0,0 +1,56 @@ +allow-flight=false +allow-nether=true +broadcast-console-to-ops=true +broadcast-rcon-to-ops=true +difficulty=easy +enable-command-block=false +enable-jmx-monitoring=false +enable-query=false +enable-rcon=true +enable-status=true +enforce-secure-profile=true +enforce-whitelist=false +entity-broadcast-range-percentage=100 +force-gamemode=false +function-permission-level=2 +gamemode=survival +generate-structures=true +generator-settings={} +hardcore=false +hide-online-players=false +initial-disabled-packs= +initial-enabled-packs=vanilla +level-name=world +level-seed= +level-type=minecraft\:normal +max-chained-neighbor-updates=1000000 +max-players=20 +max-tick-time=60000 +max-world-size=29999984 +motd=A Minecraft Server +network-compression-threshold=256 +online-mode=true +op-permission-level=4 +player-idle-timeout=0 +prevent-proxy-connections=false +pvp=true +query.port=__DRUID_PORT_MAIN__ +rate-limit=0 +rcon.password=__DRUID_RCON_PASSWORD__ +rcon.port=__DRUID_PORT_RCON__ +require-resource-pack=false +resource-pack= +resource-pack-prompt= +resource-pack-sha1= +server-ip= +server-port=__DRUID_PORT_MAIN__ +simulation-distance=10 +spawn-animals=true +spawn-monsters=true +spawn-npcs=true +spawn-protection=16 +sync-chunk-writes=true +text-filtering-config= +use-native-transport=true +view-distance=10 +white-list=false diff --git a/scrolls/minecraft/forge/1.21.10/data/update.sh b/scrolls/minecraft/forge/1.21.10/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/forge/1.21.10/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/forge/1.21.10/data/update_user_args.sh b/scrolls/minecraft/forge/1.21.10/data/update_user_args.sh new file mode 100644 index 00000000..0f6686bf --- /dev/null +++ b/scrolls/minecraft/forge/1.21.10/data/update_user_args.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + + +echo -Xmx$MAX > user_jvm_args.txt \ No newline at end of file diff --git a/scrolls/minecraft/forge/1.21.10/packet_handler/json.lua b/scrolls/minecraft/forge/1.21.10/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.10/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/forge/1.21.10/packet_handler/minecraft.lua b/scrolls/minecraft/forge/1.21.10/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/forge/1.21.10/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/forge/1.21.10/scroll.yaml b/scrolls/minecraft/forge/1.21.10/scroll.yaml new file mode 100644 index 00000000..593ecce5 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.10/scroll.yaml @@ -0,0 +1,114 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-forge +desc: Minecraft Forge +version: 0.0.1 +app_version: "1.21.10" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./update_user_args.sh" + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./run.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - forge-installer.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/forge/forge-1.21.10.jar + - image: eclipse-temurin:21-jre + mounts: + - path: "/server" + working_dir: "/server" + command: + - java + - "-jar" + - forge-installer.jar + - "--installServer" + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - rm + - forge-installer.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/forge/1.21.10/update/.gitkeep b/scrolls/minecraft/forge/1.21.10/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/forge/1.21.11/data/run.sh b/scrolls/minecraft/forge/1.21.11/data/run.sh new file mode 100644 index 00000000..dbce7d86 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.11/data/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar forge-*-shim.jar nogui diff --git a/scrolls/minecraft/forge/1.21.11/data/server.properties.default b/scrolls/minecraft/forge/1.21.11/data/server.properties.default new file mode 100644 index 00000000..e9385571 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.11/data/server.properties.default @@ -0,0 +1,56 @@ +allow-flight=false +allow-nether=true +broadcast-console-to-ops=true +broadcast-rcon-to-ops=true +difficulty=easy +enable-command-block=false +enable-jmx-monitoring=false +enable-query=false +enable-rcon=true +enable-status=true +enforce-secure-profile=true +enforce-whitelist=false +entity-broadcast-range-percentage=100 +force-gamemode=false +function-permission-level=2 +gamemode=survival +generate-structures=true +generator-settings={} +hardcore=false +hide-online-players=false +initial-disabled-packs= +initial-enabled-packs=vanilla +level-name=world +level-seed= +level-type=minecraft\:normal +max-chained-neighbor-updates=1000000 +max-players=20 +max-tick-time=60000 +max-world-size=29999984 +motd=A Minecraft Server +network-compression-threshold=256 +online-mode=true +op-permission-level=4 +player-idle-timeout=0 +prevent-proxy-connections=false +pvp=true +query.port=__DRUID_PORT_MAIN__ +rate-limit=0 +rcon.password=__DRUID_RCON_PASSWORD__ +rcon.port=__DRUID_PORT_RCON__ +require-resource-pack=false +resource-pack= +resource-pack-prompt= +resource-pack-sha1= +server-ip= +server-port=__DRUID_PORT_MAIN__ +simulation-distance=10 +spawn-animals=true +spawn-monsters=true +spawn-npcs=true +spawn-protection=16 +sync-chunk-writes=true +text-filtering-config= +use-native-transport=true +view-distance=10 +white-list=false diff --git a/scrolls/minecraft/forge/1.21.11/data/update.sh b/scrolls/minecraft/forge/1.21.11/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/forge/1.21.11/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/forge/1.21.11/data/update_user_args.sh b/scrolls/minecraft/forge/1.21.11/data/update_user_args.sh new file mode 100644 index 00000000..0f6686bf --- /dev/null +++ b/scrolls/minecraft/forge/1.21.11/data/update_user_args.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + + +echo -Xmx$MAX > user_jvm_args.txt \ No newline at end of file diff --git a/scrolls/minecraft/forge/1.21.11/packet_handler/json.lua b/scrolls/minecraft/forge/1.21.11/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.11/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/forge/1.21.11/packet_handler/minecraft.lua b/scrolls/minecraft/forge/1.21.11/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/forge/1.21.11/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/forge/1.21.11/scroll.yaml b/scrolls/minecraft/forge/1.21.11/scroll.yaml new file mode 100644 index 00000000..c87a6b1f --- /dev/null +++ b/scrolls/minecraft/forge/1.21.11/scroll.yaml @@ -0,0 +1,114 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-forge +desc: Minecraft Forge +version: 0.0.1 +app_version: "1.21.11" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./update_user_args.sh" + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./run.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - forge-installer.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/forge/forge-1.21.11.jar + - image: eclipse-temurin:21-jre + mounts: + - path: "/server" + working_dir: "/server" + command: + - java + - "-jar" + - forge-installer.jar + - "--installServer" + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - rm + - forge-installer.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/forge/1.21.11/update/.gitkeep b/scrolls/minecraft/forge/1.21.11/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/forge/1.21.8/data/run.sh b/scrolls/minecraft/forge/1.21.8/data/run.sh new file mode 100644 index 00000000..dbce7d86 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.8/data/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar forge-*-shim.jar nogui diff --git a/scrolls/minecraft/forge/1.21.8/data/server.properties.default b/scrolls/minecraft/forge/1.21.8/data/server.properties.default new file mode 100644 index 00000000..e9385571 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.8/data/server.properties.default @@ -0,0 +1,56 @@ +allow-flight=false +allow-nether=true +broadcast-console-to-ops=true +broadcast-rcon-to-ops=true +difficulty=easy +enable-command-block=false +enable-jmx-monitoring=false +enable-query=false +enable-rcon=true +enable-status=true +enforce-secure-profile=true +enforce-whitelist=false +entity-broadcast-range-percentage=100 +force-gamemode=false +function-permission-level=2 +gamemode=survival +generate-structures=true +generator-settings={} +hardcore=false +hide-online-players=false +initial-disabled-packs= +initial-enabled-packs=vanilla +level-name=world +level-seed= +level-type=minecraft\:normal +max-chained-neighbor-updates=1000000 +max-players=20 +max-tick-time=60000 +max-world-size=29999984 +motd=A Minecraft Server +network-compression-threshold=256 +online-mode=true +op-permission-level=4 +player-idle-timeout=0 +prevent-proxy-connections=false +pvp=true +query.port=__DRUID_PORT_MAIN__ +rate-limit=0 +rcon.password=__DRUID_RCON_PASSWORD__ +rcon.port=__DRUID_PORT_RCON__ +require-resource-pack=false +resource-pack= +resource-pack-prompt= +resource-pack-sha1= +server-ip= +server-port=__DRUID_PORT_MAIN__ +simulation-distance=10 +spawn-animals=true +spawn-monsters=true +spawn-npcs=true +spawn-protection=16 +sync-chunk-writes=true +text-filtering-config= +use-native-transport=true +view-distance=10 +white-list=false diff --git a/scrolls/minecraft/forge/1.21.8/data/update.sh b/scrolls/minecraft/forge/1.21.8/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/forge/1.21.8/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/forge/1.21.8/data/update_user_args.sh b/scrolls/minecraft/forge/1.21.8/data/update_user_args.sh new file mode 100644 index 00000000..0f6686bf --- /dev/null +++ b/scrolls/minecraft/forge/1.21.8/data/update_user_args.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + + +echo -Xmx$MAX > user_jvm_args.txt \ No newline at end of file diff --git a/scrolls/minecraft/forge/1.21.8/packet_handler/json.lua b/scrolls/minecraft/forge/1.21.8/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.8/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/forge/1.21.8/packet_handler/minecraft.lua b/scrolls/minecraft/forge/1.21.8/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/forge/1.21.8/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/forge/1.21.8/scroll.yaml b/scrolls/minecraft/forge/1.21.8/scroll.yaml new file mode 100644 index 00000000..fb4d724c --- /dev/null +++ b/scrolls/minecraft/forge/1.21.8/scroll.yaml @@ -0,0 +1,114 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-forge +desc: Minecraft Forge +version: 0.0.1 +app_version: "1.21.8" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./update_user_args.sh" + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./run.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - forge-installer.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/forge/forge-1.21.8.jar + - image: eclipse-temurin:21-jre + mounts: + - path: "/server" + working_dir: "/server" + command: + - java + - "-jar" + - forge-installer.jar + - "--installServer" + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - rm + - forge-installer.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/forge/1.21.8/update/.gitkeep b/scrolls/minecraft/forge/1.21.8/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/forge/1.21.9/data/run.sh b/scrolls/minecraft/forge/1.21.9/data/run.sh new file mode 100644 index 00000000..dbce7d86 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.9/data/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar forge-*-shim.jar nogui diff --git a/scrolls/minecraft/forge/1.21.9/data/server.properties.default b/scrolls/minecraft/forge/1.21.9/data/server.properties.default new file mode 100644 index 00000000..e9385571 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.9/data/server.properties.default @@ -0,0 +1,56 @@ +allow-flight=false +allow-nether=true +broadcast-console-to-ops=true +broadcast-rcon-to-ops=true +difficulty=easy +enable-command-block=false +enable-jmx-monitoring=false +enable-query=false +enable-rcon=true +enable-status=true +enforce-secure-profile=true +enforce-whitelist=false +entity-broadcast-range-percentage=100 +force-gamemode=false +function-permission-level=2 +gamemode=survival +generate-structures=true +generator-settings={} +hardcore=false +hide-online-players=false +initial-disabled-packs= +initial-enabled-packs=vanilla +level-name=world +level-seed= +level-type=minecraft\:normal +max-chained-neighbor-updates=1000000 +max-players=20 +max-tick-time=60000 +max-world-size=29999984 +motd=A Minecraft Server +network-compression-threshold=256 +online-mode=true +op-permission-level=4 +player-idle-timeout=0 +prevent-proxy-connections=false +pvp=true +query.port=__DRUID_PORT_MAIN__ +rate-limit=0 +rcon.password=__DRUID_RCON_PASSWORD__ +rcon.port=__DRUID_PORT_RCON__ +require-resource-pack=false +resource-pack= +resource-pack-prompt= +resource-pack-sha1= +server-ip= +server-port=__DRUID_PORT_MAIN__ +simulation-distance=10 +spawn-animals=true +spawn-monsters=true +spawn-npcs=true +spawn-protection=16 +sync-chunk-writes=true +text-filtering-config= +use-native-transport=true +view-distance=10 +white-list=false diff --git a/scrolls/minecraft/forge/1.21.9/data/update.sh b/scrolls/minecraft/forge/1.21.9/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/forge/1.21.9/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/forge/1.21.9/data/update_user_args.sh b/scrolls/minecraft/forge/1.21.9/data/update_user_args.sh new file mode 100644 index 00000000..0f6686bf --- /dev/null +++ b/scrolls/minecraft/forge/1.21.9/data/update_user_args.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + + +echo -Xmx$MAX > user_jvm_args.txt \ No newline at end of file diff --git a/scrolls/minecraft/forge/1.21.9/packet_handler/json.lua b/scrolls/minecraft/forge/1.21.9/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.9/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/forge/1.21.9/packet_handler/minecraft.lua b/scrolls/minecraft/forge/1.21.9/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/forge/1.21.9/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/forge/1.21.9/scroll.yaml b/scrolls/minecraft/forge/1.21.9/scroll.yaml new file mode 100644 index 00000000..51211064 --- /dev/null +++ b/scrolls/minecraft/forge/1.21.9/scroll.yaml @@ -0,0 +1,114 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-forge +desc: Minecraft Forge +version: 0.0.1 +app_version: "1.21.9" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./update_user_args.sh" + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./run.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - forge-installer.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/forge/forge-1.21.9.jar + - image: eclipse-temurin:21-jre + mounts: + - path: "/server" + working_dir: "/server" + command: + - java + - "-jar" + - forge-installer.jar + - "--installServer" + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - rm + - forge-installer.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/forge/1.21.9/update/.gitkeep b/scrolls/minecraft/forge/1.21.9/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/forge/26.1.1/data/run.sh b/scrolls/minecraft/forge/26.1.1/data/run.sh new file mode 100644 index 00000000..dbce7d86 --- /dev/null +++ b/scrolls/minecraft/forge/26.1.1/data/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar forge-*-shim.jar nogui diff --git a/scrolls/minecraft/forge/26.1.1/data/server.properties.default b/scrolls/minecraft/forge/26.1.1/data/server.properties.default new file mode 100644 index 00000000..e9385571 --- /dev/null +++ b/scrolls/minecraft/forge/26.1.1/data/server.properties.default @@ -0,0 +1,56 @@ +allow-flight=false +allow-nether=true +broadcast-console-to-ops=true +broadcast-rcon-to-ops=true +difficulty=easy +enable-command-block=false +enable-jmx-monitoring=false +enable-query=false +enable-rcon=true +enable-status=true +enforce-secure-profile=true +enforce-whitelist=false +entity-broadcast-range-percentage=100 +force-gamemode=false +function-permission-level=2 +gamemode=survival +generate-structures=true +generator-settings={} +hardcore=false +hide-online-players=false +initial-disabled-packs= +initial-enabled-packs=vanilla +level-name=world +level-seed= +level-type=minecraft\:normal +max-chained-neighbor-updates=1000000 +max-players=20 +max-tick-time=60000 +max-world-size=29999984 +motd=A Minecraft Server +network-compression-threshold=256 +online-mode=true +op-permission-level=4 +player-idle-timeout=0 +prevent-proxy-connections=false +pvp=true +query.port=__DRUID_PORT_MAIN__ +rate-limit=0 +rcon.password=__DRUID_RCON_PASSWORD__ +rcon.port=__DRUID_PORT_RCON__ +require-resource-pack=false +resource-pack= +resource-pack-prompt= +resource-pack-sha1= +server-ip= +server-port=__DRUID_PORT_MAIN__ +simulation-distance=10 +spawn-animals=true +spawn-monsters=true +spawn-npcs=true +spawn-protection=16 +sync-chunk-writes=true +text-filtering-config= +use-native-transport=true +view-distance=10 +white-list=false diff --git a/scrolls/minecraft/forge/26.1.1/data/update.sh b/scrolls/minecraft/forge/26.1.1/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/forge/26.1.1/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/forge/26.1.1/data/update_user_args.sh b/scrolls/minecraft/forge/26.1.1/data/update_user_args.sh new file mode 100644 index 00000000..0f6686bf --- /dev/null +++ b/scrolls/minecraft/forge/26.1.1/data/update_user_args.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + + +echo -Xmx$MAX > user_jvm_args.txt \ No newline at end of file diff --git a/scrolls/minecraft/forge/26.1.1/packet_handler/json.lua b/scrolls/minecraft/forge/26.1.1/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/forge/26.1.1/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/forge/26.1.1/packet_handler/minecraft.lua b/scrolls/minecraft/forge/26.1.1/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/forge/26.1.1/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/forge/26.1.1/scroll.yaml b/scrolls/minecraft/forge/26.1.1/scroll.yaml new file mode 100644 index 00000000..3870ea67 --- /dev/null +++ b/scrolls/minecraft/forge/26.1.1/scroll.yaml @@ -0,0 +1,114 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-forge +desc: Minecraft Forge +version: 0.0.1 +app_version: "26.1.1" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./update_user_args.sh" + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./run.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - forge-installer.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/forge/forge-26.1.1.jar + - image: eclipse-temurin:25-jre + mounts: + - path: "/server" + working_dir: "/server" + command: + - java + - "-jar" + - forge-installer.jar + - "--installServer" + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - rm + - forge-installer.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/forge/26.1.1/update/.gitkeep b/scrolls/minecraft/forge/26.1.1/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/forge/26.1.2/data/run.sh b/scrolls/minecraft/forge/26.1.2/data/run.sh new file mode 100644 index 00000000..dbce7d86 --- /dev/null +++ b/scrolls/minecraft/forge/26.1.2/data/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar forge-*-shim.jar nogui diff --git a/scrolls/minecraft/forge/26.1.2/data/server.properties.default b/scrolls/minecraft/forge/26.1.2/data/server.properties.default new file mode 100644 index 00000000..e9385571 --- /dev/null +++ b/scrolls/minecraft/forge/26.1.2/data/server.properties.default @@ -0,0 +1,56 @@ +allow-flight=false +allow-nether=true +broadcast-console-to-ops=true +broadcast-rcon-to-ops=true +difficulty=easy +enable-command-block=false +enable-jmx-monitoring=false +enable-query=false +enable-rcon=true +enable-status=true +enforce-secure-profile=true +enforce-whitelist=false +entity-broadcast-range-percentage=100 +force-gamemode=false +function-permission-level=2 +gamemode=survival +generate-structures=true +generator-settings={} +hardcore=false +hide-online-players=false +initial-disabled-packs= +initial-enabled-packs=vanilla +level-name=world +level-seed= +level-type=minecraft\:normal +max-chained-neighbor-updates=1000000 +max-players=20 +max-tick-time=60000 +max-world-size=29999984 +motd=A Minecraft Server +network-compression-threshold=256 +online-mode=true +op-permission-level=4 +player-idle-timeout=0 +prevent-proxy-connections=false +pvp=true +query.port=__DRUID_PORT_MAIN__ +rate-limit=0 +rcon.password=__DRUID_RCON_PASSWORD__ +rcon.port=__DRUID_PORT_RCON__ +require-resource-pack=false +resource-pack= +resource-pack-prompt= +resource-pack-sha1= +server-ip= +server-port=__DRUID_PORT_MAIN__ +simulation-distance=10 +spawn-animals=true +spawn-monsters=true +spawn-npcs=true +spawn-protection=16 +sync-chunk-writes=true +text-filtering-config= +use-native-transport=true +view-distance=10 +white-list=false diff --git a/scrolls/minecraft/forge/26.1.2/data/update.sh b/scrolls/minecraft/forge/26.1.2/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/forge/26.1.2/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/forge/26.1.2/data/update_user_args.sh b/scrolls/minecraft/forge/26.1.2/data/update_user_args.sh new file mode 100644 index 00000000..0f6686bf --- /dev/null +++ b/scrolls/minecraft/forge/26.1.2/data/update_user_args.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + + +echo -Xmx$MAX > user_jvm_args.txt \ No newline at end of file diff --git a/scrolls/minecraft/forge/26.1.2/packet_handler/json.lua b/scrolls/minecraft/forge/26.1.2/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/forge/26.1.2/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/forge/26.1.2/packet_handler/minecraft.lua b/scrolls/minecraft/forge/26.1.2/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/forge/26.1.2/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/forge/26.1.2/scroll.yaml b/scrolls/minecraft/forge/26.1.2/scroll.yaml new file mode 100644 index 00000000..b9392cf1 --- /dev/null +++ b/scrolls/minecraft/forge/26.1.2/scroll.yaml @@ -0,0 +1,114 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-forge +desc: Minecraft Forge +version: 0.0.1 +app_version: "26.1.2" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./update_user_args.sh" + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./run.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - forge-installer.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/forge/forge-26.1.2.jar + - image: eclipse-temurin:25-jre + mounts: + - path: "/server" + working_dir: "/server" + command: + - java + - "-jar" + - forge-installer.jar + - "--installServer" + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - rm + - forge-installer.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/forge/26.1.2/update/.gitkeep b/scrolls/minecraft/forge/26.1.2/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/forge/26.1/data/run.sh b/scrolls/minecraft/forge/26.1/data/run.sh new file mode 100644 index 00000000..dbce7d86 --- /dev/null +++ b/scrolls/minecraft/forge/26.1/data/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar forge-*-shim.jar nogui diff --git a/scrolls/minecraft/forge/26.1/data/server.properties.default b/scrolls/minecraft/forge/26.1/data/server.properties.default new file mode 100644 index 00000000..e9385571 --- /dev/null +++ b/scrolls/minecraft/forge/26.1/data/server.properties.default @@ -0,0 +1,56 @@ +allow-flight=false +allow-nether=true +broadcast-console-to-ops=true +broadcast-rcon-to-ops=true +difficulty=easy +enable-command-block=false +enable-jmx-monitoring=false +enable-query=false +enable-rcon=true +enable-status=true +enforce-secure-profile=true +enforce-whitelist=false +entity-broadcast-range-percentage=100 +force-gamemode=false +function-permission-level=2 +gamemode=survival +generate-structures=true +generator-settings={} +hardcore=false +hide-online-players=false +initial-disabled-packs= +initial-enabled-packs=vanilla +level-name=world +level-seed= +level-type=minecraft\:normal +max-chained-neighbor-updates=1000000 +max-players=20 +max-tick-time=60000 +max-world-size=29999984 +motd=A Minecraft Server +network-compression-threshold=256 +online-mode=true +op-permission-level=4 +player-idle-timeout=0 +prevent-proxy-connections=false +pvp=true +query.port=__DRUID_PORT_MAIN__ +rate-limit=0 +rcon.password=__DRUID_RCON_PASSWORD__ +rcon.port=__DRUID_PORT_RCON__ +require-resource-pack=false +resource-pack= +resource-pack-prompt= +resource-pack-sha1= +server-ip= +server-port=__DRUID_PORT_MAIN__ +simulation-distance=10 +spawn-animals=true +spawn-monsters=true +spawn-npcs=true +spawn-protection=16 +sync-chunk-writes=true +text-filtering-config= +use-native-transport=true +view-distance=10 +white-list=false diff --git a/scrolls/minecraft/forge/26.1/data/update.sh b/scrolls/minecraft/forge/26.1/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/forge/26.1/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/forge/26.1/data/update_user_args.sh b/scrolls/minecraft/forge/26.1/data/update_user_args.sh new file mode 100644 index 00000000..0f6686bf --- /dev/null +++ b/scrolls/minecraft/forge/26.1/data/update_user_args.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + + +echo -Xmx$MAX > user_jvm_args.txt \ No newline at end of file diff --git a/scrolls/minecraft/forge/26.1/packet_handler/json.lua b/scrolls/minecraft/forge/26.1/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/forge/26.1/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/forge/26.1/packet_handler/minecraft.lua b/scrolls/minecraft/forge/26.1/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/forge/26.1/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/forge/26.1/scroll.yaml b/scrolls/minecraft/forge/26.1/scroll.yaml new file mode 100644 index 00000000..b42f9d43 --- /dev/null +++ b/scrolls/minecraft/forge/26.1/scroll.yaml @@ -0,0 +1,114 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-forge +desc: Minecraft Forge +version: 0.0.1 +app_version: "26.1" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./update_user_args.sh" + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./run.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - forge-installer.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/forge/forge-26.1.jar + - image: eclipse-temurin:25-jre + mounts: + - path: "/server" + working_dir: "/server" + command: + - java + - "-jar" + - forge-installer.jar + - "--installServer" + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - rm + - forge-installer.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/forge/26.1/update/.gitkeep b/scrolls/minecraft/forge/26.1/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/forge/26.2/data/run.sh b/scrolls/minecraft/forge/26.2/data/run.sh new file mode 100644 index 00000000..dbce7d86 --- /dev/null +++ b/scrolls/minecraft/forge/26.2/data/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar forge-*-shim.jar nogui diff --git a/scrolls/minecraft/forge/26.2/data/server.properties.default b/scrolls/minecraft/forge/26.2/data/server.properties.default new file mode 100644 index 00000000..e9385571 --- /dev/null +++ b/scrolls/minecraft/forge/26.2/data/server.properties.default @@ -0,0 +1,56 @@ +allow-flight=false +allow-nether=true +broadcast-console-to-ops=true +broadcast-rcon-to-ops=true +difficulty=easy +enable-command-block=false +enable-jmx-monitoring=false +enable-query=false +enable-rcon=true +enable-status=true +enforce-secure-profile=true +enforce-whitelist=false +entity-broadcast-range-percentage=100 +force-gamemode=false +function-permission-level=2 +gamemode=survival +generate-structures=true +generator-settings={} +hardcore=false +hide-online-players=false +initial-disabled-packs= +initial-enabled-packs=vanilla +level-name=world +level-seed= +level-type=minecraft\:normal +max-chained-neighbor-updates=1000000 +max-players=20 +max-tick-time=60000 +max-world-size=29999984 +motd=A Minecraft Server +network-compression-threshold=256 +online-mode=true +op-permission-level=4 +player-idle-timeout=0 +prevent-proxy-connections=false +pvp=true +query.port=__DRUID_PORT_MAIN__ +rate-limit=0 +rcon.password=__DRUID_RCON_PASSWORD__ +rcon.port=__DRUID_PORT_RCON__ +require-resource-pack=false +resource-pack= +resource-pack-prompt= +resource-pack-sha1= +server-ip= +server-port=__DRUID_PORT_MAIN__ +simulation-distance=10 +spawn-animals=true +spawn-monsters=true +spawn-npcs=true +spawn-protection=16 +sync-chunk-writes=true +text-filtering-config= +use-native-transport=true +view-distance=10 +white-list=false diff --git a/scrolls/minecraft/forge/26.2/data/update.sh b/scrolls/minecraft/forge/26.2/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/forge/26.2/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/forge/26.2/data/update_user_args.sh b/scrolls/minecraft/forge/26.2/data/update_user_args.sh new file mode 100644 index 00000000..0f6686bf --- /dev/null +++ b/scrolls/minecraft/forge/26.2/data/update_user_args.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + + +echo -Xmx$MAX > user_jvm_args.txt \ No newline at end of file diff --git a/scrolls/minecraft/forge/26.2/packet_handler/json.lua b/scrolls/minecraft/forge/26.2/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/forge/26.2/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/forge/26.2/packet_handler/minecraft.lua b/scrolls/minecraft/forge/26.2/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/forge/26.2/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/forge/26.2/scroll.yaml b/scrolls/minecraft/forge/26.2/scroll.yaml new file mode 100644 index 00000000..8faee051 --- /dev/null +++ b/scrolls/minecraft/forge/26.2/scroll.yaml @@ -0,0 +1,114 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-forge +desc: Minecraft Forge +version: 0.0.1 +app_version: "26.2" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./update_user_args.sh" + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./run.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - forge-installer.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/forge/forge-26.2.jar + - image: eclipse-temurin:25-jre + mounts: + - path: "/server" + working_dir: "/server" + command: + - java + - "-jar" + - forge-installer.jar + - "--installServer" + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - rm + - forge-installer.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/forge/26.2/update/.gitkeep b/scrolls/minecraft/forge/26.2/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-spigot/.build/artifacts.json b/scrolls/minecraft/minecraft-spigot/.build/artifacts.json index 83e93a76..bac6fbd3 100644 --- a/scrolls/minecraft/minecraft-spigot/.build/artifacts.json +++ b/scrolls/minecraft/minecraft-spigot/.build/artifacts.json @@ -18,5 +18,12 @@ "1.21.4": "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.4", "1.21.5": "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.5", "1.21.6": "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.6", - "1.21.8": "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.8" + "1.21.8": "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.8", + "1.21.9": "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.9", + "1.21.10": "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.10", + "1.21.11": "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.11", + "26.1": "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:26.1", + "26.1.1": "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:26.1.1", + "26.1.2": "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:26.1.2", + "26.2": "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:26.2" } diff --git a/scrolls/minecraft/minecraft-spigot/.build/vars.json b/scrolls/minecraft/minecraft-spigot/.build/vars.json index 4d5170c6..8c6f96cc 100644 --- a/scrolls/minecraft/minecraft-spigot/.build/vars.json +++ b/scrolls/minecraft/minecraft-spigot/.build/vars.json @@ -61,5 +61,26 @@ }, "1.21.8": { "jdkVersion": "21" + }, + "1.21.9": { + "jdkVersion": "21" + }, + "1.21.10": { + "jdkVersion": "21" + }, + "1.21.11": { + "jdkVersion": "21" + }, + "26.1": { + "jdkVersion": "25" + }, + "26.1.1": { + "jdkVersion": "25" + }, + "26.1.2": { + "jdkVersion": "25" + }, + "26.2": { + "jdkVersion": "25" } } diff --git a/scrolls/minecraft/minecraft-spigot/1.21.10/data/server.properties.default b/scrolls/minecraft/minecraft-spigot/1.21.10/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.10/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-spigot/1.21.10/data/start.sh b/scrolls/minecraft/minecraft-spigot/1.21.10/data/start.sh new file mode 100644 index 00000000..7dca6b34 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.10/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar spigot.jar nogui diff --git a/scrolls/minecraft/minecraft-spigot/1.21.10/data/update.sh b/scrolls/minecraft/minecraft-spigot/1.21.10/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.10/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/1.21.10/packet_handler/json.lua b/scrolls/minecraft/minecraft-spigot/1.21.10/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.10/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/1.21.10/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-spigot/1.21.10/packet_handler/minecraft.lua new file mode 100644 index 00000000..6281f201 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.10/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Extracting snapshot..." + else + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Restoring Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up..." + else + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Backing up Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-spigot/1.21.10/scroll.yaml b/scrolls/minecraft/minecraft-spigot/1.21.10/scroll.yaml new file mode 100644 index 00000000..d5549918 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.10/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-spigot +desc: Minecraft Spigot +version: 0.0.1 +app_version: "1.21.10" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - spigot.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/spigot/spigot-1.21.10.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-spigot/1.21.10/update/.gitkeep b/scrolls/minecraft/minecraft-spigot/1.21.10/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-spigot/1.21.11/data/server.properties.default b/scrolls/minecraft/minecraft-spigot/1.21.11/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.11/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-spigot/1.21.11/data/start.sh b/scrolls/minecraft/minecraft-spigot/1.21.11/data/start.sh new file mode 100644 index 00000000..7dca6b34 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.11/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar spigot.jar nogui diff --git a/scrolls/minecraft/minecraft-spigot/1.21.11/data/update.sh b/scrolls/minecraft/minecraft-spigot/1.21.11/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.11/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/1.21.11/packet_handler/json.lua b/scrolls/minecraft/minecraft-spigot/1.21.11/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.11/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/1.21.11/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-spigot/1.21.11/packet_handler/minecraft.lua new file mode 100644 index 00000000..6281f201 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.11/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Extracting snapshot..." + else + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Restoring Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up..." + else + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Backing up Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-spigot/1.21.11/scroll.yaml b/scrolls/minecraft/minecraft-spigot/1.21.11/scroll.yaml new file mode 100644 index 00000000..707794b0 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.11/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-spigot +desc: Minecraft Spigot +version: 0.0.1 +app_version: "1.21.11" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - spigot.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/spigot/spigot-1.21.11.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-spigot/1.21.11/update/.gitkeep b/scrolls/minecraft/minecraft-spigot/1.21.11/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-spigot/1.21.9/data/server.properties.default b/scrolls/minecraft/minecraft-spigot/1.21.9/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.9/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-spigot/1.21.9/data/start.sh b/scrolls/minecraft/minecraft-spigot/1.21.9/data/start.sh new file mode 100644 index 00000000..7dca6b34 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.9/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar spigot.jar nogui diff --git a/scrolls/minecraft/minecraft-spigot/1.21.9/data/update.sh b/scrolls/minecraft/minecraft-spigot/1.21.9/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.9/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/1.21.9/packet_handler/json.lua b/scrolls/minecraft/minecraft-spigot/1.21.9/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.9/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/1.21.9/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-spigot/1.21.9/packet_handler/minecraft.lua new file mode 100644 index 00000000..6281f201 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.9/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Extracting snapshot..." + else + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Restoring Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up..." + else + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Backing up Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-spigot/1.21.9/scroll.yaml b/scrolls/minecraft/minecraft-spigot/1.21.9/scroll.yaml new file mode 100644 index 00000000..4f94e179 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/1.21.9/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-spigot +desc: Minecraft Spigot +version: 0.0.1 +app_version: "1.21.9" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - spigot.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/spigot/spigot-1.21.9.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-spigot/1.21.9/update/.gitkeep b/scrolls/minecraft/minecraft-spigot/1.21.9/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-spigot/26.1.1/data/server.properties.default b/scrolls/minecraft/minecraft-spigot/26.1.1/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1.1/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-spigot/26.1.1/data/start.sh b/scrolls/minecraft/minecraft-spigot/26.1.1/data/start.sh new file mode 100644 index 00000000..7dca6b34 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1.1/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar spigot.jar nogui diff --git a/scrolls/minecraft/minecraft-spigot/26.1.1/data/update.sh b/scrolls/minecraft/minecraft-spigot/26.1.1/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1.1/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/26.1.1/packet_handler/json.lua b/scrolls/minecraft/minecraft-spigot/26.1.1/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1.1/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/26.1.1/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-spigot/26.1.1/packet_handler/minecraft.lua new file mode 100644 index 00000000..6281f201 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1.1/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Extracting snapshot..." + else + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Restoring Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up..." + else + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Backing up Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-spigot/26.1.1/scroll.yaml b/scrolls/minecraft/minecraft-spigot/26.1.1/scroll.yaml new file mode 100644 index 00000000..a5f7d789 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1.1/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-spigot +desc: Minecraft Spigot +version: 0.0.1 +app_version: "26.1.1" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - spigot.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/spigot/spigot-26.1.1.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-spigot/26.1.1/update/.gitkeep b/scrolls/minecraft/minecraft-spigot/26.1.1/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-spigot/26.1.2/data/server.properties.default b/scrolls/minecraft/minecraft-spigot/26.1.2/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1.2/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-spigot/26.1.2/data/start.sh b/scrolls/minecraft/minecraft-spigot/26.1.2/data/start.sh new file mode 100644 index 00000000..7dca6b34 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1.2/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar spigot.jar nogui diff --git a/scrolls/minecraft/minecraft-spigot/26.1.2/data/update.sh b/scrolls/minecraft/minecraft-spigot/26.1.2/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1.2/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/26.1.2/packet_handler/json.lua b/scrolls/minecraft/minecraft-spigot/26.1.2/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1.2/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/26.1.2/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-spigot/26.1.2/packet_handler/minecraft.lua new file mode 100644 index 00000000..6281f201 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1.2/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Extracting snapshot..." + else + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Restoring Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up..." + else + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Backing up Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-spigot/26.1.2/scroll.yaml b/scrolls/minecraft/minecraft-spigot/26.1.2/scroll.yaml new file mode 100644 index 00000000..28ee76db --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1.2/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-spigot +desc: Minecraft Spigot +version: 0.0.1 +app_version: "26.1.2" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - spigot.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/spigot/spigot-26.1.2.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-spigot/26.1.2/update/.gitkeep b/scrolls/minecraft/minecraft-spigot/26.1.2/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-spigot/26.1/data/server.properties.default b/scrolls/minecraft/minecraft-spigot/26.1/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-spigot/26.1/data/start.sh b/scrolls/minecraft/minecraft-spigot/26.1/data/start.sh new file mode 100644 index 00000000..7dca6b34 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar spigot.jar nogui diff --git a/scrolls/minecraft/minecraft-spigot/26.1/data/update.sh b/scrolls/minecraft/minecraft-spigot/26.1/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/26.1/packet_handler/json.lua b/scrolls/minecraft/minecraft-spigot/26.1/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/26.1/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-spigot/26.1/packet_handler/minecraft.lua new file mode 100644 index 00000000..6281f201 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Extracting snapshot..." + else + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Restoring Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up..." + else + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Backing up Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-spigot/26.1/scroll.yaml b/scrolls/minecraft/minecraft-spigot/26.1/scroll.yaml new file mode 100644 index 00000000..8d73466b --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.1/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-spigot +desc: Minecraft Spigot +version: 0.0.1 +app_version: "26.1" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - spigot.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/spigot/spigot-26.1.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-spigot/26.1/update/.gitkeep b/scrolls/minecraft/minecraft-spigot/26.1/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-spigot/26.2/data/server.properties.default b/scrolls/minecraft/minecraft-spigot/26.2/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.2/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-spigot/26.2/data/start.sh b/scrolls/minecraft/minecraft-spigot/26.2/data/start.sh new file mode 100644 index 00000000..7dca6b34 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.2/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar spigot.jar nogui diff --git a/scrolls/minecraft/minecraft-spigot/26.2/data/update.sh b/scrolls/minecraft/minecraft-spigot/26.2/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.2/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/26.2/packet_handler/json.lua b/scrolls/minecraft/minecraft-spigot/26.2/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.2/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-spigot/26.2/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-spigot/26.2/packet_handler/minecraft.lua new file mode 100644 index 00000000..6281f201 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.2/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Extracting snapshot..." + else + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Restoring Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up..." + else + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + end + obj.description = "Backing up Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-spigot/26.2/scroll.yaml b/scrolls/minecraft/minecraft-spigot/26.2/scroll.yaml new file mode 100644 index 00000000..5f77fee9 --- /dev/null +++ b/scrolls/minecraft/minecraft-spigot/26.2/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-spigot +desc: Minecraft Spigot +version: 0.0.1 +app_version: "26.2" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - spigot.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/spigot/spigot-26.2.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-spigot/26.2/update/.gitkeep b/scrolls/minecraft/minecraft-spigot/26.2/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-vanilla/.build/artifacts.json b/scrolls/minecraft/minecraft-vanilla/.build/artifacts.json index a0110274..697981fd 100644 --- a/scrolls/minecraft/minecraft-vanilla/.build/artifacts.json +++ b/scrolls/minecraft/minecraft-vanilla/.build/artifacts.json @@ -18,5 +18,13 @@ "1.21.4": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.4", "1.21.5": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.5", "1.21.6": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.6", - "1.21.7": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.7" + "1.21.7": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.7", + "1.21.8": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.8", + "1.21.9": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.9", + "1.21.10": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.10", + "1.21.11": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:1.21.11", + "26.1": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:26.1", + "26.1.1": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:26.1.1", + "26.1.2": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:26.1.2", + "26.2": "artifacts.druid.gg/druid-team/scroll-minecraft-vanilla:26.2" } diff --git a/scrolls/minecraft/minecraft-vanilla/.build/vars.json b/scrolls/minecraft/minecraft-vanilla/.build/vars.json index f606da9b..f43d6418 100644 --- a/scrolls/minecraft/minecraft-vanilla/.build/vars.json +++ b/scrolls/minecraft/minecraft-vanilla/.build/vars.json @@ -82,5 +82,37 @@ "1.21.7": { "jarUrl": "https://piston-data.mojang.com/v1/objects/05e4b48fbc01f0385adb74bcff9751d34552486c/server.jar", "jdkVersion": "21" + }, + "1.21.8": { + "jarUrl": "https://piston-data.mojang.com/v1/objects/6bce4ef400e4efaa63a13d5e6f6b500be969ef81/server.jar", + "jdkVersion": "21" + }, + "1.21.9": { + "jarUrl": "https://piston-data.mojang.com/v1/objects/11e54c2081420a4d49db3007e66c80a22579ff2a/server.jar", + "jdkVersion": "21" + }, + "1.21.10": { + "jarUrl": "https://piston-data.mojang.com/v1/objects/95495a7f485eedd84ce928cef5e223b757d2f764/server.jar", + "jdkVersion": "21" + }, + "1.21.11": { + "jarUrl": "https://piston-data.mojang.com/v1/objects/64bb6d763bed0a9f1d632ec347938594144943ed/server.jar", + "jdkVersion": "21" + }, + "26.1": { + "jarUrl": "https://piston-data.mojang.com/v1/objects/3872a7f07a1a595e651aef8b058dfc2bb3772f46/server.jar", + "jdkVersion": "25" + }, + "26.1.1": { + "jarUrl": "https://piston-data.mojang.com/v1/objects/49c8195703ad0ba4f0a4efbccfd85a4a8ca57431/server.jar", + "jdkVersion": "25" + }, + "26.1.2": { + "jarUrl": "https://piston-data.mojang.com/v1/objects/97ccd4c0ed3f81bbb7bfacddd1090b0c56f9bc51/server.jar", + "jdkVersion": "25" + }, + "26.2": { + "jarUrl": "https://piston-data.mojang.com/v1/objects/823e2250d24b3ddac457a60c92a6a941943fcd6a/server.jar", + "jdkVersion": "25" } } diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.10/data/server.properties.default b/scrolls/minecraft/minecraft-vanilla/1.21.10/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.10/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.10/data/start.sh b/scrolls/minecraft/minecraft-vanilla/1.21.10/data/start.sh new file mode 100644 index 00000000..73044c6b --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.10/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar server.jar nogui diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.10/data/update.sh b/scrolls/minecraft/minecraft-vanilla/1.21.10/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.10/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.10/packet_handler/json.lua b/scrolls/minecraft/minecraft-vanilla/1.21.10/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.10/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.10/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-vanilla/1.21.10/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.10/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.10/scroll.yaml b/scrolls/minecraft/minecraft-vanilla/1.21.10/scroll.yaml new file mode 100644 index 00000000..6fd96c34 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.10/scroll.yaml @@ -0,0 +1,76 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-vanilla +desc: Minecraft Vanilla +version: 0.0.1 +app_version: "1.21.10" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - >- + apk add --no-cache ca-certificates wget + && wget -q -O server.jar https://piston-data.mojang.com/v1/objects/95495a7f485eedd84ce928cef5e223b757d2f764/server.jar + && echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh && echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.10/update/.gitkeep b/scrolls/minecraft/minecraft-vanilla/1.21.10/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.11/data/server.properties.default b/scrolls/minecraft/minecraft-vanilla/1.21.11/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.11/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.11/data/start.sh b/scrolls/minecraft/minecraft-vanilla/1.21.11/data/start.sh new file mode 100644 index 00000000..73044c6b --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.11/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar server.jar nogui diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.11/data/update.sh b/scrolls/minecraft/minecraft-vanilla/1.21.11/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.11/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.11/packet_handler/json.lua b/scrolls/minecraft/minecraft-vanilla/1.21.11/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.11/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.11/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-vanilla/1.21.11/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.11/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.11/scroll.yaml b/scrolls/minecraft/minecraft-vanilla/1.21.11/scroll.yaml new file mode 100644 index 00000000..c0c0dd89 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.11/scroll.yaml @@ -0,0 +1,76 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-vanilla +desc: Minecraft Vanilla +version: 0.0.1 +app_version: "1.21.11" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - >- + apk add --no-cache ca-certificates wget + && wget -q -O server.jar https://piston-data.mojang.com/v1/objects/64bb6d763bed0a9f1d632ec347938594144943ed/server.jar + && echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh && echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.11/update/.gitkeep b/scrolls/minecraft/minecraft-vanilla/1.21.11/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.8/data/server.properties.default b/scrolls/minecraft/minecraft-vanilla/1.21.8/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.8/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.8/data/start.sh b/scrolls/minecraft/minecraft-vanilla/1.21.8/data/start.sh new file mode 100644 index 00000000..73044c6b --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.8/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar server.jar nogui diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.8/data/update.sh b/scrolls/minecraft/minecraft-vanilla/1.21.8/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.8/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.8/packet_handler/json.lua b/scrolls/minecraft/minecraft-vanilla/1.21.8/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.8/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.8/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-vanilla/1.21.8/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.8/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.8/scroll.yaml b/scrolls/minecraft/minecraft-vanilla/1.21.8/scroll.yaml new file mode 100644 index 00000000..3ba753ae --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.8/scroll.yaml @@ -0,0 +1,76 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-vanilla +desc: Minecraft Vanilla +version: 0.0.1 +app_version: "1.21.8" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - >- + apk add --no-cache ca-certificates wget + && wget -q -O server.jar https://piston-data.mojang.com/v1/objects/6bce4ef400e4efaa63a13d5e6f6b500be969ef81/server.jar + && echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh && echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.8/update/.gitkeep b/scrolls/minecraft/minecraft-vanilla/1.21.8/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.9/data/server.properties.default b/scrolls/minecraft/minecraft-vanilla/1.21.9/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.9/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.9/data/start.sh b/scrolls/minecraft/minecraft-vanilla/1.21.9/data/start.sh new file mode 100644 index 00000000..73044c6b --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.9/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar server.jar nogui diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.9/data/update.sh b/scrolls/minecraft/minecraft-vanilla/1.21.9/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.9/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.9/packet_handler/json.lua b/scrolls/minecraft/minecraft-vanilla/1.21.9/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.9/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.9/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-vanilla/1.21.9/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.9/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.9/scroll.yaml b/scrolls/minecraft/minecraft-vanilla/1.21.9/scroll.yaml new file mode 100644 index 00000000..7d526781 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/1.21.9/scroll.yaml @@ -0,0 +1,76 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-vanilla +desc: Minecraft Vanilla +version: 0.0.1 +app_version: "1.21.9" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - >- + apk add --no-cache ca-certificates wget + && wget -q -O server.jar https://piston-data.mojang.com/v1/objects/11e54c2081420a4d49db3007e66c80a22579ff2a/server.jar + && echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh && echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-vanilla/1.21.9/update/.gitkeep b/scrolls/minecraft/minecraft-vanilla/1.21.9/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.1/data/server.properties.default b/scrolls/minecraft/minecraft-vanilla/26.1.1/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1.1/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.1/data/start.sh b/scrolls/minecraft/minecraft-vanilla/26.1.1/data/start.sh new file mode 100644 index 00000000..73044c6b --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1.1/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar server.jar nogui diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.1/data/update.sh b/scrolls/minecraft/minecraft-vanilla/26.1.1/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1.1/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.1/packet_handler/json.lua b/scrolls/minecraft/minecraft-vanilla/26.1.1/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1.1/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.1/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-vanilla/26.1.1/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1.1/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.1/scroll.yaml b/scrolls/minecraft/minecraft-vanilla/26.1.1/scroll.yaml new file mode 100644 index 00000000..cefad590 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1.1/scroll.yaml @@ -0,0 +1,76 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-vanilla +desc: Minecraft Vanilla +version: 0.0.1 +app_version: "26.1.1" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - >- + apk add --no-cache ca-certificates wget + && wget -q -O server.jar https://piston-data.mojang.com/v1/objects/49c8195703ad0ba4f0a4efbccfd85a4a8ca57431/server.jar + && echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh && echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.1/update/.gitkeep b/scrolls/minecraft/minecraft-vanilla/26.1.1/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.2/data/server.properties.default b/scrolls/minecraft/minecraft-vanilla/26.1.2/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1.2/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.2/data/start.sh b/scrolls/minecraft/minecraft-vanilla/26.1.2/data/start.sh new file mode 100644 index 00000000..73044c6b --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1.2/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar server.jar nogui diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.2/data/update.sh b/scrolls/minecraft/minecraft-vanilla/26.1.2/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1.2/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.2/packet_handler/json.lua b/scrolls/minecraft/minecraft-vanilla/26.1.2/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1.2/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.2/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-vanilla/26.1.2/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1.2/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.2/scroll.yaml b/scrolls/minecraft/minecraft-vanilla/26.1.2/scroll.yaml new file mode 100644 index 00000000..53235ee4 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1.2/scroll.yaml @@ -0,0 +1,76 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-vanilla +desc: Minecraft Vanilla +version: 0.0.1 +app_version: "26.1.2" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - >- + apk add --no-cache ca-certificates wget + && wget -q -O server.jar https://piston-data.mojang.com/v1/objects/97ccd4c0ed3f81bbb7bfacddd1090b0c56f9bc51/server.jar + && echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh && echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-vanilla/26.1.2/update/.gitkeep b/scrolls/minecraft/minecraft-vanilla/26.1.2/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-vanilla/26.1/data/server.properties.default b/scrolls/minecraft/minecraft-vanilla/26.1/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-vanilla/26.1/data/start.sh b/scrolls/minecraft/minecraft-vanilla/26.1/data/start.sh new file mode 100644 index 00000000..73044c6b --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar server.jar nogui diff --git a/scrolls/minecraft/minecraft-vanilla/26.1/data/update.sh b/scrolls/minecraft/minecraft-vanilla/26.1/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/26.1/packet_handler/json.lua b/scrolls/minecraft/minecraft-vanilla/26.1/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/26.1/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-vanilla/26.1/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-vanilla/26.1/scroll.yaml b/scrolls/minecraft/minecraft-vanilla/26.1/scroll.yaml new file mode 100644 index 00000000..dae56246 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.1/scroll.yaml @@ -0,0 +1,76 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-vanilla +desc: Minecraft Vanilla +version: 0.0.1 +app_version: "26.1" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - >- + apk add --no-cache ca-certificates wget + && wget -q -O server.jar https://piston-data.mojang.com/v1/objects/3872a7f07a1a595e651aef8b058dfc2bb3772f46/server.jar + && echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh && echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-vanilla/26.1/update/.gitkeep b/scrolls/minecraft/minecraft-vanilla/26.1/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/minecraft-vanilla/26.2/data/server.properties.default b/scrolls/minecraft/minecraft-vanilla/26.2/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.2/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/minecraft-vanilla/26.2/data/start.sh b/scrolls/minecraft/minecraft-vanilla/26.2/data/start.sh new file mode 100644 index 00000000..73044c6b --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.2/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar server.jar nogui diff --git a/scrolls/minecraft/minecraft-vanilla/26.2/data/update.sh b/scrolls/minecraft/minecraft-vanilla/26.2/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.2/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/26.2/packet_handler/json.lua b/scrolls/minecraft/minecraft-vanilla/26.2/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.2/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/minecraft-vanilla/26.2/packet_handler/minecraft.lua b/scrolls/minecraft/minecraft-vanilla/26.2/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.2/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/minecraft-vanilla/26.2/scroll.yaml b/scrolls/minecraft/minecraft-vanilla/26.2/scroll.yaml new file mode 100644 index 00000000..83ad3142 --- /dev/null +++ b/scrolls/minecraft/minecraft-vanilla/26.2/scroll.yaml @@ -0,0 +1,76 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-vanilla +desc: Minecraft Vanilla +version: 0.0.1 +app_version: "26.2" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - >- + apk add --no-cache ca-certificates wget + && wget -q -O server.jar https://piston-data.mojang.com/v1/objects/823e2250d24b3ddac457a60c92a6a941943fcd6a/server.jar + && echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh && echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/minecraft-vanilla/26.2/update/.gitkeep b/scrolls/minecraft/minecraft-vanilla/26.2/update/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scrolls/minecraft/papermc/.build/artifacts.json b/scrolls/minecraft/papermc/.build/artifacts.json index f0430c88..d89c7d04 100644 --- a/scrolls/minecraft/papermc/.build/artifacts.json +++ b/scrolls/minecraft/papermc/.build/artifacts.json @@ -17,5 +17,12 @@ "1.21.4": "artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.4", "1.21.5": "artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.5", "1.21.6": "artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.6", - "1.21.7": "artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.7" + "1.21.7": "artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.7", + "1.21.8": "artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.8", + "1.21.9": "artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.9", + "1.21.10": "artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.10", + "1.21.11": "artifacts.druid.gg/druid-team/scroll-minecraft-paper:1.21.11", + "26.1.1": "artifacts.druid.gg/druid-team/scroll-minecraft-paper:26.1.1", + "26.1.2": "artifacts.druid.gg/druid-team/scroll-minecraft-paper:26.1.2", + "26.2": "artifacts.druid.gg/druid-team/scroll-minecraft-paper:26.2" } diff --git a/scrolls/minecraft/papermc/.build/vars.json b/scrolls/minecraft/papermc/.build/vars.json index 194621df..5eeabc46 100644 --- a/scrolls/minecraft/papermc/.build/vars.json +++ b/scrolls/minecraft/papermc/.build/vars.json @@ -61,5 +61,26 @@ }, "1.21.7": { "jdkVersion": "21" + }, + "1.21.8": { + "jdkVersion": "21" + }, + "1.21.9": { + "jdkVersion": "21" + }, + "1.21.10": { + "jdkVersion": "21" + }, + "1.21.11": { + "jdkVersion": "21" + }, + "26.1.1": { + "jdkVersion": "25" + }, + "26.1.2": { + "jdkVersion": "25" + }, + "26.2": { + "jdkVersion": "25" } } diff --git a/scrolls/minecraft/papermc/1.21.10/data/server.properties.default b/scrolls/minecraft/papermc/1.21.10/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.10/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/papermc/1.21.10/data/start.sh b/scrolls/minecraft/papermc/1.21.10/data/start.sh new file mode 100644 index 00000000..fe0c9d56 --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.10/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar paper.jar nogui diff --git a/scrolls/minecraft/papermc/1.21.10/data/update.sh b/scrolls/minecraft/papermc/1.21.10/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.10/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/1.21.10/packet_handler/json.lua b/scrolls/minecraft/papermc/1.21.10/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.10/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/1.21.10/packet_handler/minecraft.lua b/scrolls/minecraft/papermc/1.21.10/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.10/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/papermc/1.21.10/scroll.yaml b/scrolls/minecraft/papermc/1.21.10/scroll.yaml new file mode 100644 index 00000000..1ccc653c --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.10/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-paper +desc: PaperMC +version: 0.0.1 +app_version: "1.21.10" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - paper.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/papermc/paper-1.21.10.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/papermc/1.21.11/data/server.properties.default b/scrolls/minecraft/papermc/1.21.11/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.11/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/papermc/1.21.11/data/start.sh b/scrolls/minecraft/papermc/1.21.11/data/start.sh new file mode 100644 index 00000000..fe0c9d56 --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.11/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar paper.jar nogui diff --git a/scrolls/minecraft/papermc/1.21.11/data/update.sh b/scrolls/minecraft/papermc/1.21.11/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.11/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/1.21.11/packet_handler/json.lua b/scrolls/minecraft/papermc/1.21.11/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.11/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/1.21.11/packet_handler/minecraft.lua b/scrolls/minecraft/papermc/1.21.11/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.11/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/papermc/1.21.11/scroll.yaml b/scrolls/minecraft/papermc/1.21.11/scroll.yaml new file mode 100644 index 00000000..ad2799bd --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.11/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-paper +desc: PaperMC +version: 0.0.1 +app_version: "1.21.11" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - paper.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/papermc/paper-1.21.11.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/papermc/1.21.8/data/server.properties.default b/scrolls/minecraft/papermc/1.21.8/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.8/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/papermc/1.21.8/data/start.sh b/scrolls/minecraft/papermc/1.21.8/data/start.sh new file mode 100644 index 00000000..fe0c9d56 --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.8/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar paper.jar nogui diff --git a/scrolls/minecraft/papermc/1.21.8/data/update.sh b/scrolls/minecraft/papermc/1.21.8/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.8/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/1.21.8/packet_handler/json.lua b/scrolls/minecraft/papermc/1.21.8/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.8/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/1.21.8/packet_handler/minecraft.lua b/scrolls/minecraft/papermc/1.21.8/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.8/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/papermc/1.21.8/scroll.yaml b/scrolls/minecraft/papermc/1.21.8/scroll.yaml new file mode 100644 index 00000000..1880170d --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.8/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-paper +desc: PaperMC +version: 0.0.1 +app_version: "1.21.8" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - paper.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/papermc/paper-1.21.8.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/papermc/1.21.9/data/server.properties.default b/scrolls/minecraft/papermc/1.21.9/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.9/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/papermc/1.21.9/data/start.sh b/scrolls/minecraft/papermc/1.21.9/data/start.sh new file mode 100644 index 00000000..fe0c9d56 --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.9/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar paper.jar nogui diff --git a/scrolls/minecraft/papermc/1.21.9/data/update.sh b/scrolls/minecraft/papermc/1.21.9/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.9/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/1.21.9/packet_handler/json.lua b/scrolls/minecraft/papermc/1.21.9/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.9/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/1.21.9/packet_handler/minecraft.lua b/scrolls/minecraft/papermc/1.21.9/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.9/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/papermc/1.21.9/scroll.yaml b/scrolls/minecraft/papermc/1.21.9/scroll.yaml new file mode 100644 index 00000000..c44c95fb --- /dev/null +++ b/scrolls/minecraft/papermc/1.21.9/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-paper +desc: PaperMC +version: 0.0.1 +app_version: "1.21.9" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:21-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - paper.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/papermc/paper-1.21.9.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/papermc/26.1.1/data/server.properties.default b/scrolls/minecraft/papermc/26.1.1/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/papermc/26.1.1/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/papermc/26.1.1/data/start.sh b/scrolls/minecraft/papermc/26.1.1/data/start.sh new file mode 100644 index 00000000..fe0c9d56 --- /dev/null +++ b/scrolls/minecraft/papermc/26.1.1/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar paper.jar nogui diff --git a/scrolls/minecraft/papermc/26.1.1/data/update.sh b/scrolls/minecraft/papermc/26.1.1/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/papermc/26.1.1/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/26.1.1/packet_handler/json.lua b/scrolls/minecraft/papermc/26.1.1/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/papermc/26.1.1/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/26.1.1/packet_handler/minecraft.lua b/scrolls/minecraft/papermc/26.1.1/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/papermc/26.1.1/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/papermc/26.1.1/scroll.yaml b/scrolls/minecraft/papermc/26.1.1/scroll.yaml new file mode 100644 index 00000000..7984dbd9 --- /dev/null +++ b/scrolls/minecraft/papermc/26.1.1/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-paper +desc: PaperMC +version: 0.0.1 +app_version: "26.1.1" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - paper.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/papermc/paper-26.1.1.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/papermc/26.1.2/data/server.properties.default b/scrolls/minecraft/papermc/26.1.2/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/papermc/26.1.2/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/papermc/26.1.2/data/start.sh b/scrolls/minecraft/papermc/26.1.2/data/start.sh new file mode 100644 index 00000000..fe0c9d56 --- /dev/null +++ b/scrolls/minecraft/papermc/26.1.2/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar paper.jar nogui diff --git a/scrolls/minecraft/papermc/26.1.2/data/update.sh b/scrolls/minecraft/papermc/26.1.2/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/papermc/26.1.2/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/26.1.2/packet_handler/json.lua b/scrolls/minecraft/papermc/26.1.2/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/papermc/26.1.2/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/26.1.2/packet_handler/minecraft.lua b/scrolls/minecraft/papermc/26.1.2/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/papermc/26.1.2/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/papermc/26.1.2/scroll.yaml b/scrolls/minecraft/papermc/26.1.2/scroll.yaml new file mode 100644 index 00000000..13aac793 --- /dev/null +++ b/scrolls/minecraft/papermc/26.1.2/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-paper +desc: PaperMC +version: 0.0.1 +app_version: "26.1.2" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - paper.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/papermc/paper-26.1.2.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start diff --git a/scrolls/minecraft/papermc/26.2/data/server.properties.default b/scrolls/minecraft/papermc/26.2/data/server.properties.default new file mode 100644 index 00000000..6372e191 --- /dev/null +++ b/scrolls/minecraft/papermc/26.2/data/server.properties.default @@ -0,0 +1,50 @@ +#Minecraft server properties +#Mon Apr 11 00:50:50 CEST 2022 +enable-jmx-monitoring=false +rcon.port=__DRUID_PORT_RCON__ +gamemode=survival +enable-command-block=false +enable-query=false +level-name=world +motd=A Minecraft Server +query.port=__DRUID_PORT_MAIN__ +pvp=true +difficulty=easy +network-compression-threshold=256 +require-resource-pack=false +max-tick-time=60000 +use-native-transport=true +max-players=20 +online-mode=true +enable-status=true +allow-flight=false +broadcast-rcon-to-ops=true +view-distance=10 +server-ip= +resource-pack-prompt= +allow-nether=true +server-port=__DRUID_PORT_MAIN__ +enable-rcon=true +sync-chunk-writes=true +op-permission-level=4 +prevent-proxy-connections=false +resource-pack= +entity-broadcast-range-percentage=100 +rcon.password=__DRUID_RCON_PASSWORD__ +player-idle-timeout=0 +debug=false +force-gamemode=false +rate-limit=0 +hardcore=false +white-list=false +broadcast-console-to-ops=true +spawn-npcs=true +spawn-animals=true +snooper-enabled=true +function-permission-level=2 +text-filtering-config= +spawn-monsters=true +enforce-whitelist=false +resource-pack-sha1= +spawn-protection=16 +max-world-size=29999984 diff --git a/scrolls/minecraft/papermc/26.2/data/start.sh b/scrolls/minecraft/papermc/26.2/data/start.sh new file mode 100644 index 00000000..fe0c9d56 --- /dev/null +++ b/scrolls/minecraft/papermc/26.2/data/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +MAX=${DRUID_MAX_MEMORY:-} +MAX=${MAX%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + +if [ ! -f server.properties ] && [ -f server.properties.default ]; then + if [ -f .druid-rcon-password ]; then + RCON_PASSWORD=$(cat .druid-rcon-password) + else + umask 077 + RCON_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 || true) + if [ "${#RCON_PASSWORD}" -ne 50 ]; then + echo "Failed to generate RCON password" >&2 + exit 1 + fi + printf '%s\n' "$RCON_PASSWORD" > .druid-rcon-password + fi + sed \ + -e "s|__DRUID_RCON_PASSWORD__|$RCON_PASSWORD|g" \ + -e "s|__DRUID_PORT_MAIN__|${DRUID_PORT_MAIN_1:-25565}|g" \ + -e "s|__DRUID_PORT_RCON__|${DRUID_PORT_RCON_1:-25575}|g" \ + server.properties.default > server.properties +fi + +java -Xmx$MAX -Xms1024M -jar paper.jar nogui diff --git a/scrolls/minecraft/papermc/26.2/data/update.sh b/scrolls/minecraft/papermc/26.2/data/update.sh new file mode 100644 index 00000000..966812fe --- /dev/null +++ b/scrolls/minecraft/papermc/26.2/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/26.2/packet_handler/json.lua b/scrolls/minecraft/papermc/26.2/packet_handler/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/scrolls/minecraft/papermc/26.2/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/scrolls/minecraft/papermc/26.2/packet_handler/minecraft.lua b/scrolls/minecraft/papermc/26.2/packet_handler/minecraft.lua new file mode 100644 index 00000000..cb36f45f --- /dev/null +++ b/scrolls/minecraft/papermc/26.2/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " .. string.format("%.2f", snapshotPercentage) .. "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/scrolls/minecraft/papermc/26.2/scroll.yaml b/scrolls/minecraft/papermc/26.2/scroll.yaml new file mode 100644 index 00000000..9f935d41 --- /dev/null +++ b/scrolls/minecraft/papermc/26.2/scroll.yaml @@ -0,0 +1,90 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-paper +desc: PaperMC +version: 0.0.1 +app_version: "26.2" +ports: +- name: main + protocol: tcp + port: 25565 +- name: rcon + protocol: tcp + port: 25575 +commands: + start: + needs: + - install + run: restart + procedures: + - id: coldstart + image: artifacts.druid.gg/druid-team/druid:v0.1.256 + expectedPorts: + - name: main + - name: rcon + mounts: + - path: "/runtime" + sub_path: "." + working_dir: "/runtime" + env: + DRUID_ROOT: "/runtime" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/minecraft.lua" + DRUID_PORT_RCON_COLDSTARTER: "generic" + command: + - druid-coldstarter + - id: start + image: eclipse-temurin:25-jre + expectedPorts: + - name: main + keepAliveTraffic: 1mb/5m + - name: rcon + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "./start.sh" + stop: + run: always + procedures: + - type: signal + target: start + signal: SIGTERM + install: + run: once + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - wget + - "-q" + - "-O" + - paper.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/papermc/paper-26.2.jar + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt + update: + procedures: + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - apk add --no-cache jq yq moreutils && ./update.sh + - image: alpine:3.20 + mounts: + - path: "/server" + working_dir: "/server" + command: + - sh + - "-c" + - echo eula=true > eula.txt +serve: start