diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7fe0eda --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Dev-server config for this project's tools/ scripts. +# Plain KEY=value only (no shell expansion). Environment variables take precedence. + +PAPER_VERSION=26.2 +PAPER_BUILD=129 +MINECRAFT_SERVER_JAR=paper-26.2-129.jar +# Relative paths are resolved from this directory; ~/ is expanded. +MINECRAFT_SERVER_PATH=./server +MINECRAFT_BACKUP_PATH=./backups diff --git a/.gitignore b/.gitignore index 7428a22..38005d2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ build/ target/ .worktrees/ bin/ +/backups/ +/server/ +.env diff --git a/tools/backup-server.sh b/tools/backup-server.sh new file mode 100755 index 0000000..8ba750a --- /dev/null +++ b/tools/backup-server.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Backs up plugins, config, worlds, and settings from MINECRAFT_SERVER_PATH +# to MINECRAFT_BACKUP_PATH/backup-[-label].tar.bz2. +# +# Usage: backup-server.sh [-f|--force] [label] +# -f Back up even if the server is running (worlds may be inconsistent). + +set -euo pipefail + +TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=setup-env.sh +source "$TOOLS_DIR/setup-env.sh" + +FORCE=false +LABEL="" +for arg in "$@"; do + case "$arg" in + -f|--force) FORCE=true ;; + -h|--help) sed -n '2,7p' "$0"; exit 0 ;; + -*) echo "Unknown option: $arg" >&2; exit 1 ;; + *) LABEL="$arg" ;; + esac +done + +[[ -d "$MINECRAFT_SERVER_PATH" ]] || { echo "Error: $MINECRAFT_SERVER_PATH does not exist." >&2; exit 1; } + +if pgrep -f "java.*paper-.*\.jar" >/dev/null && ! $FORCE; then + echo "Error: the server is running. Stop it first (tools/stop-server.sh) or pass --force." >&2 + exit 1 +fi + +cd "$MINECRAFT_SERVER_PATH" +shopt -s nullglob + +ITEMS=() +[[ -d plugins ]] && ITEMS+=(plugins) +[[ -d config ]] && ITEMS+=(config) +for dir in */; do + dir="${dir%/}" + [[ -f "$dir/level.dat" ]] && ITEMS+=("$dir") +done +for f in *.yml *.yaml *.json *.properties eula.txt; do + ITEMS+=("$f") +done + +if (( ${#ITEMS[@]} == 0 )); then + echo "Error: nothing to back up in $MINECRAFT_SERVER_PATH." >&2 + exit 1 +fi + +# Keep labels filename-safe. +LABEL="${LABEL//[^A-Za-z0-9._-]/_}" +NAME="backup-$(date +%Y%m%d-%H%M%S)${LABEL:+-$LABEL}.tar.bz2" +mkdir -p "$MINECRAFT_BACKUP_PATH" +ARCHIVE="$MINECRAFT_BACKUP_PATH/$NAME" + +echo "Backing up: ${ITEMS[*]}" +trap 'rm -f "$ARCHIVE.part"' EXIT +tar --exclude 'plugins/.paper-remapped' --exclude '*/session.lock' \ + -cjf "$ARCHIVE.part" "${ITEMS[@]}" +mv "$ARCHIVE.part" "$ARCHIVE" +trap - EXIT + +echo "Created $ARCHIVE ($(du -h "$ARCHIVE" | awk '{print $1}'))" diff --git a/tools/bash/start-server.sh b/tools/bash/start-server.sh deleted file mode 100755 index db38831..0000000 --- a/tools/bash/start-server.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# Check required environment variables -if [[ -z "$MINECRAFT_SERVER_PATH" || -z "$MINECRAFT_SERVER_JAR" ]]; then - echo "Error: MINECRAFT_SERVER_PATH and MINECRAFT_SERVER_JAR must be set." - exit 1 -fi - -cd "$MINECRAFT_SERVER_PATH" - -# Find running server process (java with the server jar) -SERVER_PID=$(pgrep -f "java.*$MINECRAFT_SERVER_JAR" || true) - -if [[ -n "$SERVER_PID" ]]; then - echo "Minecraft server is running (PID: $SERVER_PID). Stopping it..." - kill "$SERVER_PID" - sleep 5 -else - echo "Minecraft server is not running." -fi - -# Start the server (in a detached screen session) -pushd "${MINECRAFT_SERVER_PATH}" -java -jar "${MINECRAFT_SERVER_JAR}" -popd diff --git a/tools/bash/stop-server.sh b/tools/bash/stop-server.sh deleted file mode 100755 index d95ed26..0000000 --- a/tools/bash/stop-server.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -set -e - -if [[ -z "$MINECRAFT_SERVER_PATH" || -z "$MINECRAFT_SERVER_JAR" ]]; then - echo "Error: MINECRAFT_SERVER_PATH and MINECRAFT_SERVER_JAR must be set." - exit 1 -fi - -cd "$MINECRAFT_SERVER_PATH" - -SERVER_PID=$(pgrep -f "java.*$MINECRAFT_SERVER_JAR" || true) - -if [[ -n "$SERVER_PID" ]]; then - echo "Minecraft server is running (PID: $SERVER_PID). Stopping it..." - kill "$SERVER_PID" - sleep 5 - echo "Minecraft server stopped." -else - echo "Minecraft server is not running." -fi diff --git a/tools/bash/build-plugin.sh b/tools/build-plugin.sh similarity index 54% rename from tools/bash/build-plugin.sh rename to tools/build-plugin.sh index 6b84304..4cc24bf 100755 --- a/tools/bash/build-plugin.sh +++ b/tools/build-plugin.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash set -e -# Find project root (two directories above this script) +# Find project root (one directory above this script) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$PROJECT_ROOT" diff --git a/tools/bash/deploy-plugin.sh b/tools/deploy-plugin.sh similarity index 88% rename from tools/bash/deploy-plugin.sh rename to tools/deploy-plugin.sh index e9472e2..403245c 100755 --- a/tools/bash/deploy-plugin.sh +++ b/tools/deploy-plugin.sh @@ -1,14 +1,17 @@ #!/usr/bin/env bash set -e +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# shellcheck source=setup-env.sh +source "$SCRIPT_DIR/setup-env.sh" + if [[ -z "$MINECRAFT_SERVER_PATH" ]]; then echo "Error: MINECRAFT_SERVER_PATH environment variable is not set." exit 1 fi -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - PLUGIN_JAR=$(ls -t "$PROJECT_ROOT"/build/libs/*.jar 2>/dev/null | head -n1) if [[ ! -f "$PLUGIN_JAR" ]]; then echo "Error: No plugin jar found in $PROJECT_ROOT/build/libs. Build the plugin first." diff --git a/tools/restore-server.sh b/tools/restore-server.sh new file mode 100755 index 0000000..63403bf --- /dev/null +++ b/tools/restore-server.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Restores a backup made by backup-server.sh into MINECRAFT_SERVER_PATH. +# Each top-level item in the archive (plugins, config, worlds, settings) is +# replaced; a "pre-restore" backup of the current state is taken first. +# +# Usage: restore-server.sh [-y|--yes] [--no-backup] [backup-file|name] +# Defaults to the newest backup in MINECRAFT_BACKUP_PATH. + +set -euo pipefail + +TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=setup-env.sh +source "$TOOLS_DIR/setup-env.sh" + +ASSUME_YES=false +SAFETY_BACKUP=true +ARCHIVE="" +for arg in "$@"; do + case "$arg" in + -y|--yes) ASSUME_YES=true ;; + --no-backup) SAFETY_BACKUP=false ;; + -h|--help) sed -n '2,8p' "$0"; exit 0 ;; + -*) echo "Unknown option: $arg" >&2; exit 1 ;; + *) ARCHIVE="$arg" ;; + esac +done + +if [[ -z "$ARCHIVE" ]]; then + ARCHIVE=$(ls -t "$MINECRAFT_BACKUP_PATH"/backup-*.tar.bz2 2>/dev/null | head -n1 || true) + [[ -n "$ARCHIVE" ]] || { echo "Error: no backups found in $MINECRAFT_BACKUP_PATH." >&2; exit 1; } +elif [[ ! -f "$ARCHIVE" && -f "$MINECRAFT_BACKUP_PATH/$ARCHIVE" ]]; then + ARCHIVE="$MINECRAFT_BACKUP_PATH/$ARCHIVE" +fi +[[ -f "$ARCHIVE" ]] || { echo "Error: backup not found: $ARCHIVE" >&2; exit 1; } +ARCHIVE="$(cd "$(dirname "$ARCHIVE")" && pwd)/$(basename "$ARCHIVE")" + +if pgrep -f "java.*paper-.*\.jar" >/dev/null; then + echo "Error: the server is running. Stop it first (tools/stop-server.sh)." >&2 + exit 1 +fi + +ENTRIES=$(tar -tjf "$ARCHIVE") +if grep -qE '^/|(^|/)\.\.(/|$)' <<<"$ENTRIES"; then + echo "Error: archive contains absolute or '..' paths; refusing to restore." >&2 + exit 1 +fi +TOP_LEVEL=$(sed -E 's#^\./##; s#/.*##' <<<"$ENTRIES" | { grep -vE '^\.?$' || true; } | sort -u) +[[ -n "$TOP_LEVEL" ]] || { echo "Error: backup is empty." >&2; exit 1; } + +echo "Restore $(basename "$ARCHIVE") into $MINECRAFT_SERVER_PATH" +echo "These items will be replaced:" +sed 's/^/ /' <<<"$TOP_LEVEL" +if ! $ASSUME_YES; then + read -r -p "Continue? [y/N] " reply + [[ "$reply" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 1; } +fi + +mkdir -p "$MINECRAFT_SERVER_PATH" +if $SAFETY_BACKUP && [[ -n "$(ls -A "$MINECRAFT_SERVER_PATH")" ]]; then + "$TOOLS_DIR/backup-server.sh" pre-restore +fi + +while IFS= read -r item; do + rm -rf "${MINECRAFT_SERVER_PATH:?}/$item" +done <<<"$TOP_LEVEL" + +tar -xjf "$ARCHIVE" -C "$MINECRAFT_SERVER_PATH" +echo "Restored $(basename "$ARCHIVE")." diff --git a/tools/setup-env.sh b/tools/setup-env.sh new file mode 100755 index 0000000..c0d4405 --- /dev/null +++ b/tools/setup-env.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Source (bash or zsh) to export the shared server config from ../.env +# (relative to this script): +# PAPER_VERSION, PAPER_BUILD, MINECRAFT_SERVER_PATH, MINECRAFT_SERVER_JAR, MINECRAFT_BACKUP_PATH + +_setup_env_self="${BASH_SOURCE[0]:-${(%):-%x}}" +_setup_env_root="$(cd "$(dirname "$_setup_env_self")/.." && pwd)" +_setup_env_file="${MINECRAFT_ENV_FILE:-$_setup_env_root/.env}" + +if [[ -f "$_setup_env_file" ]]; then + while IFS= read -r _line || [[ -n "$_line" ]]; do + _line="${_line%$'\r'}" + [[ "$_line" =~ ^[[:space:]]*(#|$) ]] && continue + _key="${_line%%=*}" + _value="${_line#*=}" + _key="${_key//[[:space:]]/}" + [[ "$_key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue + _value="${_value#\"}"; _value="${_value%\"}" + _value="${_value#\'}"; _value="${_value%\'}" + eval "_current=\"\${$_key:-}\"" + [[ -z "$_current" ]] && export "$_key=$_value" + done < "$_setup_env_file" +fi + +PAPER_VERSION="${PAPER_VERSION:-}" +PAPER_BUILD="${PAPER_BUILD:-}" +MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH:-MCServer}" +MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH/#\~/$HOME}" +[[ "$MINECRAFT_SERVER_PATH" == /* ]] || MINECRAFT_SERVER_PATH="$_setup_env_root/$MINECRAFT_SERVER_PATH" +MINECRAFT_BACKUP_PATH="${MINECRAFT_BACKUP_PATH:-backups}" +MINECRAFT_BACKUP_PATH="${MINECRAFT_BACKUP_PATH/#\~/$HOME}" +[[ "$MINECRAFT_BACKUP_PATH" == /* ]] || MINECRAFT_BACKUP_PATH="$_setup_env_root/$MINECRAFT_BACKUP_PATH" + +if [[ -z "${MINECRAFT_SERVER_JAR:-}" && -n "$PAPER_VERSION" && -n "$PAPER_BUILD" ]]; then + MINECRAFT_SERVER_JAR="paper-${PAPER_VERSION}-${PAPER_BUILD}.jar" +fi + +export PAPER_VERSION PAPER_BUILD MINECRAFT_SERVER_PATH MINECRAFT_SERVER_JAR MINECRAFT_BACKUP_PATH +unset _setup_env_self _setup_env_root _setup_env_file _line _key _value _current diff --git a/tools/start-server.sh b/tools/start-server.sh new file mode 100755 index 0000000..d4ddce5 --- /dev/null +++ b/tools/start-server.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Downloads (if missing) and runs a Paper server via the Fill v3 API. +# Server settings come from ../.env relative to this script (see setup-env.sh). +# +# Env vars: +# JAVA_OPTS Extra JVM args (default: -Xms2G -Xmx2G) + +set -euo pipefail + +TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=setup-env.sh +source "$TOOLS_DIR/setup-env.sh" + +[[ -n "$PAPER_VERSION" ]] || { echo "Error: PAPER_VERSION is not set (see .env)." >&2; exit 1; } + +PAPER_PROJECT="paper" +JAVA_OPTS="${JAVA_OPTS:--Xms2G -Xmx2G}" +FILL_API="https://fill.papermc.io/v3/projects/${PAPER_PROJECT}/versions/${PAPER_VERSION}" +# Fill requires a non-generic User-Agent with contact info. +USER_AGENT="${PAPER_USER_AGENT:-majinnaibu-plugin-dev/1.0 (https://github.com/majinnaibu)}" + +for cmd in curl jq java shasum; do + command -v "$cmd" >/dev/null || { echo "Error: '$cmd' is required." >&2; exit 1; } +done + +api_get() { + curl -fsSL -H "User-Agent: $USER_AGENT" "$1" +} + +mkdir -p "$MINECRAFT_SERVER_PATH" +cd "$MINECRAFT_SERVER_PATH" + +JAR_BUILD="${MINECRAFT_SERVER_JAR:-}" +JAR_BUILD="${JAR_BUILD#paper-"$PAPER_VERSION"-}"; JAR_BUILD="${JAR_BUILD%.jar}" +if [[ -z "$PAPER_BUILD" && "$JAR_BUILD" =~ ^[0-9]+$ ]]; then + PAPER_BUILD="$JAR_BUILD" +fi + +if [[ -z "$PAPER_BUILD" ]]; then + echo "Resolving latest stable build for Paper $PAPER_VERSION..." + PAPER_BUILD=$(api_get "$FILL_API/builds" \ + | jq -r 'map(select(.channel == "STABLE")) | max_by(.id) | .id // empty') || true + if [[ -z "$PAPER_BUILD" ]]; then + # Offline or no stable build: fall back to the newest local jar for this version. + for f in paper-"$PAPER_VERSION"-*.jar; do + b="${f#paper-"$PAPER_VERSION"-}"; b="${b%.jar}" + [[ "$b" =~ ^[0-9]+$ ]] && (( b > ${PAPER_BUILD:-0} )) && PAPER_BUILD="$b" + done + [[ -n "$PAPER_BUILD" ]] || { echo "Error: no stable build found for $PAPER_VERSION." >&2; exit 1; } + echo "Could not resolve from API; using local build $PAPER_BUILD." + fi +fi + +EXPECTED_JAR="paper-${PAPER_VERSION}-${PAPER_BUILD}.jar" +if [[ -n "${MINECRAFT_SERVER_JAR:-}" && "$MINECRAFT_SERVER_JAR" != "$EXPECTED_JAR" ]]; then + echo "Error: MINECRAFT_SERVER_JAR=$MINECRAFT_SERVER_JAR does not match PAPER_VERSION/PAPER_BUILD ($EXPECTED_JAR)." >&2 + exit 1 +fi +MINECRAFT_SERVER_JAR="$EXPECTED_JAR" + +if [[ ! -f "$MINECRAFT_SERVER_JAR" ]]; then + echo "Downloading $MINECRAFT_SERVER_JAR..." + BUILD_JSON=$(api_get "$FILL_API/builds/$PAPER_BUILD") + DOWNLOAD_URL=$(jq -r '.downloads."server:default".url // empty' <<<"$BUILD_JSON") + EXPECTED_SHA=$(jq -r '.downloads."server:default".checksums.sha256 // empty' <<<"$BUILD_JSON") + [[ -n "$DOWNLOAD_URL" ]] || { echo "Error: no download for $PAPER_VERSION build $PAPER_BUILD." >&2; exit 1; } + + TMP_JAR="$MINECRAFT_SERVER_JAR.part" + trap 'rm -f "$TMP_JAR"' EXIT + curl -fL --progress-bar -H "User-Agent: $USER_AGENT" -o "$TMP_JAR" "$DOWNLOAD_URL" + + ACTUAL_SHA=$(shasum -a 256 "$TMP_JAR" | awk '{print $1}') + if [[ -n "$EXPECTED_SHA" && "$ACTUAL_SHA" != "$EXPECTED_SHA" ]]; then + echo "Error: checksum mismatch (expected $EXPECTED_SHA, got $ACTUAL_SHA)." >&2 + exit 1 + fi + mv "$TMP_JAR" "$MINECRAFT_SERVER_JAR" + trap - EXIT +fi + +export MINECRAFT_SERVER_PATH MINECRAFT_SERVER_JAR + +"$TOOLS_DIR/stop-server.sh" + +if ! grep -qs '^eula=true' eula.txt; then + echo "Note: accept the EULA by setting eula=true in $MINECRAFT_SERVER_PATH/eula.txt (server will exit until then)." +fi + +echo "Starting $MINECRAFT_SERVER_JAR in $MINECRAFT_SERVER_PATH..." +# shellcheck disable=SC2086 +exec java $JAVA_OPTS -jar "$MINECRAFT_SERVER_JAR" --nogui + diff --git a/tools/stop-server.sh b/tools/stop-server.sh new file mode 100755 index 0000000..fb23a5e --- /dev/null +++ b/tools/stop-server.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Stops any running Paper server (java process running a paper-*.jar). + +set -euo pipefail + +STOP_TIMEOUT="${STOP_TIMEOUT:-60}" + +SERVER_PIDS=$(pgrep -f "java.*paper-.*\.jar" || true) + +if [[ -z "$SERVER_PIDS" ]]; then + echo "Minecraft server is not running." + exit 0 +fi + +for pid in $SERVER_PIDS; do + echo "Stopping Minecraft server (PID: $pid)..." + # SIGTERM lets Paper run its shutdown hook and save worlds. + kill "$pid" +done + +for ((i = 0; i < STOP_TIMEOUT; i++)); do + remaining="" + for pid in $SERVER_PIDS; do + kill -0 "$pid" 2>/dev/null && remaining="$remaining $pid" + done + if [[ -z "$remaining" ]]; then + echo "Minecraft server stopped." + exit 0 + fi + sleep 1 +done + +echo "Server did not stop within ${STOP_TIMEOUT}s; forcing:$remaining" +kill -9 $remaining