diff --git a/.classpath b/.classpath
index 8fe0193..1dec708 100644
--- a/.classpath
+++ b/.classpath
@@ -1,8 +1,57 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 3633933..32a7f3a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,15 @@
+.gradle/
+build/
+target/
+.worktrees/
+bin/
+/backups/
+/server/
+.env
+
+# Old entries double check these later.
/server
/Plugin.jardesc
/Plugin-Release.jardesc
/lib
/target
-.worktrees/
diff --git a/.project b/.project
index 9a2c887..7d50a9c 100644
--- a/.project
+++ b/.project
@@ -20,4 +20,15 @@
org.eclipse.m2e.core.maven2Nature
org.eclipse.jdt.core.javanature
+
+
+ 1790453742830
+
+ 30
+
+ org.eclipse.core.resources.regexFilterMatcher
+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+
+
+
diff --git a/.settings/org.eclipse.jdt.apt.core.prefs b/.settings/org.eclipse.jdt.apt.core.prefs
new file mode 100644
index 0000000..d4313d4
--- /dev/null
+++ b/.settings/org.eclipse.jdt.apt.core.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.apt.aptEnabled=false
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index f6b2e38..c79b505 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -1,13 +1,16 @@
-#Sat Mar 03 21:02:20 PST 2012
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
-org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
-org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
+org.eclipse.jdt.core.compiler.processAnnotations=disabled
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8
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/build-plugin.sh b/tools/build-plugin.sh
new file mode 100755
index 0000000..6136d0d
--- /dev/null
+++ b/tools/build-plugin.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -e
+
+# Find project root (one directory above this script)
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+cd "$PROJECT_ROOT"
+
+if [[ -x ./mvnw ]]; then
+ ./mvnw package
+else
+ mvn package
+fi
diff --git a/tools/deploy-plugin.sh b/tools/deploy-plugin.sh
new file mode 100755
index 0000000..87b738d
--- /dev/null
+++ b/tools/deploy-plugin.sh
@@ -0,0 +1,29 @@
+#!/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
+
+# Skip Maven side artifacts (shade originals, sources, javadoc).
+PLUGIN_JAR=$(ls -t "$PROJECT_ROOT"/target/*.jar 2>/dev/null \
+ | grep -vE '/original-[^/]*\.jar$|-(sources|javadoc|tests)\.jar$' | head -n1 || true)
+if [[ ! -f "$PLUGIN_JAR" ]]; then
+ echo "Error: No plugin jar found in $PROJECT_ROOT/target. Build the plugin first."
+ exit 1
+fi
+
+if find "$PROJECT_ROOT/src/main/java" "$PROJECT_ROOT/src/main/resources" -type f -newer "$PLUGIN_JAR" | grep -q .; then
+ echo "Warning: The built plugin jar is older than some source files. Consider rebuilding."
+fi
+
+mkdir -p "$MINECRAFT_SERVER_PATH/plugins"
+cp "$PLUGIN_JAR" "$MINECRAFT_SERVER_PATH/plugins/"
+echo "Deployed $PLUGIN_JAR to $MINECRAFT_SERVER_PATH/plugins/"
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