Document Gradle and Paper workflow

This commit is contained in:
2026-09-26 23:56:21 -07:00
parent 726b178a53
commit a1f8fb3950
7 changed files with 237 additions and 84 deletions

View File

@@ -1,9 +1,24 @@
# Dev-server config for this project's tools/ scripts.
# Plain KEY=value only (no shell expansion). Environment variables take precedence.
# Configuration for tools/*.sh. Values are read as KEY=value data, not executed.
# Unquoted values may contain spaces; matching outer quotes are removed.
# Paper server
PAPER_VERSION=26.2
PAPER_BUILD=129
PAPER_USER_AGENT="majinnaibu-plugin-dev/1.0 (https://github.com/majinnaibu)"
JAVA_OPTS="-Xms2G -Xmx2G"
STOP_TIMEOUT=60
MINECRAFT_SERVER_JAR=paper-26.2-129.jar
# Relative paths are resolved from this directory; ~/ is expanded.
# Relative paths are resolved from the project root; ~/ is expanded.
MINECRAFT_SERVER_PATH=./server
MINECRAFT_BACKUP_PATH=./backups
# Modrinth publishing
# Release version is read from Gradle's Git-sensitive semantic version and checked against the jar's plugin.yml.
MODRINTH_TOKEN=
MODRINTH_PROJECT_ID=metropolis-builder
MODRINTH_CHANGELOG=
MODRINTH_GAME_VERSIONS=26.2
MODRINTH_LOADERS=paper
MODRINTH_VERSION_TYPE=
MODRINTH_DEPENDENCIES=
MODRINTH_FILE=

33
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,33 @@
# Contributing
## Development Setup
Use Java 25 or newer. The Gradle wrapper downloads Gradle 9.8; dependencies are declared in `build.gradle` and resolved from Maven Central, PaperMC, and EngineHub.
```sh
./gradlew build
```
The standard build runs JUnit 5 tests without starting Minecraft. Tests use Mockito for Bukkit and external-plugin boundaries. Keep domain and configuration behavior testable without a running server.
## Live Server Tests
Live tests are opt-in and run separately from `build`:
```sh
METROPOLIS_RCON_HOST=127.0.0.1 \
METROPOLIS_RCON_PORT=25575 \
METROPOLIS_RCON_PASSWORD=... \
METROPOLIS_SERVER_DIR=/path/to/server \
./gradlew integrationTest
```
Use a disposable or backed-up Paper server with compatible WorldGuard and WorldEdit plugin jars installed. Keep RCON local, use a temporary password, and restore the server after tests that modify regions or worlds.
## Pull Requests
- Keep changes focused and include tests for behavior changes.
- Run `./gradlew build` before submitting.
- Run `integrationTest` only when changing behavior that requires Paper or the external plugins; do not add it to the default build.
- Update `README.md` and project descriptions when user-facing setup, commands, or dependencies change.
- Keep generated build output, server data, credentials, and local `.env` files out of commits.

View File

@@ -1,7 +1,7 @@
{
"id": "metropolis",
"title": "Metropolis",
"description": "Legacy Bukkit plugin that automatically creates protected home plots and roads as players join a Minecraft server.",
"description": "Paper plugin that automatically creates protected home plots and connecting roads as players join a Minecraft server.",
"docId": "Metropolis",
"image": {
"url": "Pixel-art aerial view of a growing Minecraft city planned in a square grid, small player home plots connected by pale roads, a protected central spawn, blocky green landscape, subtle WorldGuard-inspired boundary lines, warm daylight, clean game-plugin thumbnail composition, no words or logos.",

View File

@@ -1,18 +1,12 @@
# Metropolis
Metropolis is a legacy Bukkit plugin that automatically expands a protected Minecraft city as players join the server. Each player receives a home plot surrounded by roads and positioned beside the existing city, with WorldGuard regions protecting the city and individual homes.
Metropolis is a Paper plugin that expands a protected Minecraft city as players join. It generates home plots and connecting roads, uses WorldGuard to protect the city and homes, and supports reserved plots and home teleportation.
## Details
## Platform
- Platform: Bukkit/Minecraft server plugin
- Language: Java
- Bukkit API: 1.7.9-R0.2
- Dependencies: WorldGuard 5.9 and WorldEdit 5.6.3
- Features: automatic home plots, road and plot generation, WorldGuard region management, reserved plots, home teleportation, and configurable plot and road materials
- Paper 26.2, Java 25+
- WorldGuard 7.0.19 and WorldEdit 7.4.5
- Built with Gradle 9.8 and Git-sensitive semantic versioning
- Configuration: `plugins/Metropolis/config.yml`
- Source: https://github.com/headhunter45/Metropolis
- License: GNU General Public License v3
## Image prompt
Pixel-art aerial view of a growing Minecraft city planned in a square grid, small player home plots connected by pale roads, a protected central spawn, blocky green landscape, subtle WorldGuard-inspired boundary lines, warm daylight, clean game-plugin thumbnail composition, no words or logos.
- Unit tests run without a server; live integration tests use the separate Gradle `integrationTest` task.
- License: GNU General Public License v3

114
README.md
View File

@@ -1,47 +1,67 @@
Metropolis generates a home for each user connecting to your world with roads between them. As new users connect the city expands.
WorldGuard regions are used for each home and for the city as a whole.
One region called "City" is created to protect the spawn and roads. As the assigned blocks expand this region is automatically updated.
Each user gets assigned a home region named "h_username" where username is replaced with their username. This region is surrounded by a road and located next to an existing region.
After creation these regions can be modified as normal via WorldGuard commands.
INSTALL:
To install just drop the jar into your plugins folder. All necessarry data will be created on first launch.
CONFIG:
Edit the config.yml file. You can set the height to clear above roads, the road width, the plot size (incluing half the road width on either side), the road material, and the level at which the road is generated.
Changelog:
v0.5
Added example docs
Added Commands to plugin.yml to move and evict homes
Added floor and road supports to config file and generation
v0.4.6
Added optional generation of a sign identifying owner in plots on creation
Added a Cuboid constructor that takes a worldedit selection
Added a debug mode that enables extra logging. MetropolisPlugin.DEBUG should be true for snapshots and false for release builds.
Added an occupied Plots list to MetropolisPlugin to keep track of both player homes and reserved plots.
Added the Plot class as a parent of PlayerHome and moved relevant code to it.
Added a command to reserve plots that aren't tied to a player. This can be used to setup a larger protected area around spawn
v0.4.5
v0.4.4
Added a welcome message telling players where their home is.
Switched from PlayerLoginEvent to PlayerJoinEvent
v0.4.3
Removed call to saveconfig on plugin unload.
v0.4.2
Made city region refresh on server restart.
v0.4.1
v0.4
v0.3
Added command to generate a home for a user that is not currently logged in.
v0.2.1
Made config file save on load and disable.
v0.2
Added configuration options.
v0.1
Initial Release
# Metropolis
Metropolis expands a protected Minecraft city as players join. Each player can receive a home plot connected by generated roads; WorldGuard protects the city, homes, and reserved plots.
## Requirements
- Paper 26.2, with Java 25 or newer
- WorldGuard 7.0.19
- WorldEdit 7.4.5
WorldGuard and WorldEdit are required plugin dependencies. Install them alongside Metropolis in the server's `plugins` directory.
## Build and Test
Build the plugin and run the server-free unit tests:
```sh
./gradlew clean build
```
The plugin jar is written to `build/libs/`. The Gradle wrapper uses Gradle 9.8, and Git-sensitive semantic versioning derives the artifact and `plugin.yml` versions from repository tags and commits.
Run the opt-in live-server tests against a running Paper server with WorldGuard, WorldEdit, and RCON enabled:
```sh
METROPOLIS_RCON_HOST=127.0.0.1 \
METROPOLIS_RCON_PORT=25575 \
METROPOLIS_RCON_PASSWORD=... \
METROPOLIS_SERVER_DIR=/path/to/server \
./gradlew integrationTest
```
`integrationTest` is separate from the normal `test` and `build` tasks. Keep RCON bound to a trusted interface and do not commit its password.
## Server Scripts
Copy `.env.example` to `.env` and configure the Paper version/build and server paths. The scripts read `.env` as key/value data.
- `tools/build-plugin.sh` builds and tests the plugin.
- `tools/deploy-plugin.sh` copies the Gradle-built jar to the configured server.
- `tools/start-server.sh` downloads the configured Paper build if needed and starts it.
- `tools/backup-server.sh` and `tools/restore-server.sh` back up or restore the configured server.
- `tools/publish-local.sh` publishes the artifact to the local Maven repository.
- `tools/publish-modrinth.sh` uploads a built jar after checking its version.
## Configuration
The default configuration is in `src/main/resources/config.yml` and is copied to `plugins/Metropolis/config.yml` on first startup. Materials use Bukkit names; the previous numeric IDs for stone, grass, cobblestone, and bedrock are still accepted when loading a configuration.
Player home ownership and selected-home data use UUIDs. Existing name-keyed `currentHomes.yml` entries and name-based WorldGuard home regions are recognized during migration.
## Commands
- `/metropolis` displays the plugin version.
- `/metropolis-home-acquire` acquires an available home plot.
- `/metropolis-home-generate <player>` generates a home for a known online or cached offline player.
- `/metropolis-home-list` lists Metropolis plots.
- `/metropolis-home-go [player]` teleports to a home.
- `/metropolis-home-move <home-number> [player]` changes a player's selected home.
- `/metropolis-home-evict <player>` removes a player's home ownership.
- `/metropolis-plot-reserve <name> <minX> <minY> <minZ> <maxX> <maxY> <maxZ>` reserves coordinates; a player may instead provide a WorldEdit selection.
- `/metropolis-plot-go <plot-name> [player]` teleports to a plot.
- `/metropolis-flag-reset` reapplies Metropolis protection flags.
## License
GNU General Public License v3.

View File

@@ -22,7 +22,7 @@
| MET-018 | Done | Build the plugin with Gradle and use the provided server scripts to smoke-test it on modern Paper with its required plugins installed. |
| MET-019 | Done | Address bugs and incompatibilities found by unit tests, integration tests, or modern Paper server testing. |
| MET-020 | Done | Adopt the git-sensitive semantic versioning Gradle plugin used by MobScores and ScoreKeeper, using the existing 0.5-SNAPSHOT version as the migration baseline; verify version/tag behavior and enable the Gradle configuration cache when compatible. |
| MET-021 | Ready | Update README.md, CONTRIBUTING.md, other documentation, and project scripts with the Gradle build, test, versioning, and modern Paper server workflow. |
| MET-021 | Done | Update README.md, CONTRIBUTING.md, other documentation, and project scripts with the Gradle build, test, versioning, and modern Paper server workflow. |
| MET-022 | Planning | (Optional) Add new features, quality-of-life improvements, or CI configuration after the modernization and documentation tasks are complete. |
| GIT-009 | Planning | i have three reserve plots and one just got overwritten when a new player joined. |
| GIT-008 | Ready | Implement multiple plot sizes by granting multiple plots around each other.

View File

@@ -4,30 +4,99 @@ set -euo pipefail
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 [[ ! -r "$PROJECT_ROOT/.env" ]]; then
echo "Error: $PROJECT_ROOT/.env is missing or unreadable. Copy .env.example to .env and set its values." >&2
exit 1
fi
load_project_env() {
local env_line env_key env_value first_char last_char
while IFS= read -r env_line || [[ -n "$env_line" ]]; do
env_line="${env_line%$'\r'}"
[[ "$env_line" =~ ^[[:space:]]*(#|$) ]] && continue
[[ "$env_line" == *=* ]] || continue
env_key="${env_line%%=*}"
env_value="${env_line#*=}"
env_key="${env_key#"${env_key%%[![:space:]]*}"}"
env_key="${env_key%"${env_key##*[![:space:]]}"}"
[[ "$env_key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
env_value="${env_value#"${env_value%%[![:space:]]*}"}"
env_value="${env_value%"${env_value##*[![:space:]]}"}"
if (( ${#env_value} >= 2 )); then
first_char="${env_value:0:1}"
last_char="${env_value:$((${#env_value} - 1)):1}"
if [[ "$first_char" == "$last_char" && ( "$first_char" == '"' || "$first_char" == "'" ) ]]; then
env_value="${env_value:1:$((${#env_value} - 2))}"
fi
fi
if [[ -z "${!env_key:-}" ]]; then
export "$env_key=$env_value"
fi
done < "$PROJECT_ROOT/.env"
}
load_project_env
MODRINTH_TOKEN="${MODRINTH_TOKEN:-}"
MODRINTH_PROJECT_ID="${MODRINTH_PROJECT_ID:-${MODRINTH_PROJECT_SLUG:-metropolis-builder}}"
MODRINTH_VERSION="${MODRINTH_VERSION:-${VERSION:-$(grep -E '<version>|<project>' pom.xml | head -n2 | tail -n1 | sed -E 's#.*<version>([^<]+)</version>.*#\1#')}}"
MODRINTH_PROJECT_ID="${MODRINTH_PROJECT_ID:-metropolis-builder}"
MODRINTH_VERSION="${MODRINTH_VERSION:-${VERSION:-}}"
if [[ -z "$MODRINTH_VERSION" ]]; then
MODRINTH_VERSION=$(cd "$PROJECT_ROOT" && ./gradlew -q properties --property version | sed -n 's/^version: //p' | head -n1)
fi
MODRINTH_CHANGELOG="${MODRINTH_CHANGELOG:-Auto release via publish-modrinth.sh}"
MODRINTH_GAME_VERSIONS="${MODRINTH_GAME_VERSIONS:-1.21,1.21.1,1.21.2,1.21.3,1.21.4,1.21.5,1.21.6,1.21.7}"
MODRINTH_GAME_VERSIONS="${MODRINTH_GAME_VERSIONS:-26.2}"
MODRINTH_LOADERS="${MODRINTH_LOADERS:-paper}"
MODRINTH_VERSION_TYPE="${MODRINTH_VERSION_TYPE:-}"
MODRINTH_FILE="${MODRINTH_FILE:-}"
MODRINTH_DEPENDENCIES="${MODRINTH_DEPENDENCIES:-}"
if [[ -z "$MODRINTH_VERSION" ]]; then
echo "Error: could not determine the project version from Gradle." >&2
exit 1
fi
if [[ -z "$MODRINTH_VERSION_TYPE" ]]; then
if [[ "$MODRINTH_VERSION" == *-* ]]; then
MODRINTH_VERSION_TYPE="beta"
else
MODRINTH_VERSION_TYPE="release"
fi
fi
case "$MODRINTH_VERSION_TYPE" in
release|beta|alpha) ;;
*) echo "Error: MODRINTH_VERSION_TYPE must be release, beta, or alpha." >&2; exit 1 ;;
esac
MODRINTH_VERSION_NAME="Metropolis $MODRINTH_VERSION"
if [[ -n "$MODRINTH_FILE" && "$MODRINTH_FILE" != /* ]]; then
MODRINTH_FILE="$PROJECT_ROOT/$MODRINTH_FILE"
fi
if [[ -z "$MODRINTH_TOKEN" ]]; then
echo "Error: MODRINTH_TOKEN is not set in $PROJECT_ROOT/.env" >&2
exit 1
fi
if [[ -z "$MODRINTH_FILE" ]]; then
MODRINTH_FILE=$(find "$PROJECT_ROOT/target" -maxdepth 1 -type f -name '*.jar' ! -name '*-sources.jar' ! -name '*-javadoc.jar' | head -n1 || true)
MODRINTH_FILE="$PROJECT_ROOT/build/libs/Metropolis-$MODRINTH_VERSION.jar"
fi
if [[ -z "$MODRINTH_FILE" || ! -f "$MODRINTH_FILE" ]]; then
echo "Error: no jar found for Modrinth upload. Build the project first or set MODRINTH_FILE." >&2
exit 1
fi
if ! command -v unzip >/dev/null; then
echo "Error: 'unzip' is required to verify the plugin version in the jar." >&2
exit 1
fi
JAR_VERSION=$(unzip -p "$MODRINTH_FILE" plugin.yml 2>/dev/null | sed -n 's/^version: //p' | tr -d "'\"" | head -n1 || true)
if [[ -z "$JAR_VERSION" ]]; then
echo "Error: could not read the plugin version from $MODRINTH_FILE." >&2
exit 1
fi
if [[ "$JAR_VERSION" != "$MODRINTH_VERSION" ]]; then
echo "Error: jar version $JAR_VERSION does not match Gradle version $MODRINTH_VERSION. Rebuild the plugin and retry." >&2
exit 1
fi
csv_to_json_array() {
jq -cn --arg csv "$1" '$csv | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))'
@@ -36,19 +105,41 @@ MODRINTH_GAME_VERSIONS_JSON=$(csv_to_json_array "$MODRINTH_GAME_VERSIONS")
MODRINTH_LOADERS_JSON=$(csv_to_json_array "$MODRINTH_LOADERS")
if [[ -n "$MODRINTH_DEPENDENCIES" ]]; then
MODRINTH_DEPENDENCIES_ARG=(--form "dependencies=$MODRINTH_DEPENDENCIES")
if ! MODRINTH_DEPENDENCIES_JSON=$(jq -ce 'if type == "array" then . else error("dependencies must be a JSON array") end' <<<"$MODRINTH_DEPENDENCIES"); then
echo "Error: MODRINTH_DEPENDENCIES must be a JSON array." >&2
exit 1
fi
else
MODRINTH_DEPENDENCIES_ARG=()
MODRINTH_DEPENDENCIES_JSON='[]'
fi
curl -fsSL \
-X POST "https://api.modrinth.com/v2/version" \
-H "Authorization: $MODRINTH_TOKEN" \
-F "project_id=$MODRINTH_PROJECT_ID" \
-F "version_number=$MODRINTH_VERSION" \
-F "changelog=$MODRINTH_CHANGELOG" \
-F "game_versions=${MODRINTH_GAME_VERSIONS_JSON}" \
-F "loaders=${MODRINTH_LOADERS_JSON}" \
-F "file=@$MODRINTH_FILE" \
"${MODRINTH_DEPENDENCIES_ARG[@]}" \
| jq .
MODRINTH_DATA=$(jq -cn \
--arg project_id "$MODRINTH_PROJECT_ID" \
--arg name "$MODRINTH_VERSION_NAME" \
--arg version_number "$MODRINTH_VERSION" \
--arg version_type "$MODRINTH_VERSION_TYPE" \
--arg changelog "$MODRINTH_CHANGELOG" \
--argjson featured false \
--argjson dependencies "$MODRINTH_DEPENDENCIES_JSON" \
--argjson game_versions "$MODRINTH_GAME_VERSIONS_JSON" \
--argjson loaders "$MODRINTH_LOADERS_JSON" \
'{project_id: $project_id, name: $name, version_number: $version_number, version_type: $version_type, featured: $featured, changelog: $changelog, dependencies: $dependencies, game_versions: $game_versions, loaders: $loaders, file_parts: ["file"], primary_file: "file"}')
CURL_ARGS=(
-X POST "https://api.modrinth.com/v2/version"
-H "Authorization: $MODRINTH_TOKEN"
-F "data=$MODRINTH_DATA;type=application/json"
-F "file=@$MODRINTH_FILE"
)
HTTP_RESPONSE=$(curl -sS -w $'\n%{http_code}' "${CURL_ARGS[@]}")
HTTP_STATUS="${HTTP_RESPONSE##*$'\n'}"
HTTP_BODY="${HTTP_RESPONSE%$'\n'*}"
if [[ "$HTTP_STATUS" != 2?? ]]; then
echo "Error: Modrinth API returned HTTP $HTTP_STATUS:" >&2
if ! printf '%s\n' "$HTTP_BODY" | jq -r '.description // .error // .' >&2; then
printf '%s\n' "$HTTP_BODY" >&2
fi
exit 1
fi
printf '%s\n' "$HTTP_BODY" | jq .