10 Commits

Author SHA1 Message Date
ea18bb2f5c Adds versioning plugin.
Some checks failed
Gradle Build / build (push) Has been cancelled
2026-09-26 20:02:01 -07:00
7a404aa27f Adds modrinth description. 2026-09-26 19:58:24 -07:00
1ede40cb28 Test MobScores ScoreKeeper integration 2026-09-26 19:41:12 -07:00
648e706c86 Fix score attribution and preserve custom scores 2026-09-26 18:46:22 -07:00
11c5c94839 Document MobScores setup and development 2026-09-26 18:42:42 -07:00
d95c4cf418 Key mob claimant tracking by UUID 2026-09-26 18:41:01 -07:00
af81796866 Adds tasks. 2026-09-26 18:37:27 -07:00
221438e60e Fixes a messaging error and updates build scripts. 2026-09-26 17:46:10 -07:00
f2d89b6314 Script cleanup. 2026-09-26 13:46:36 -07:00
80ca82b2fd Adds project description files. 2026-09-24 19:44:46 -07:00
26 changed files with 746 additions and 76 deletions

9
.env.example Normal file
View File

@@ -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

23
.github/workflows/gradle.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Gradle Build
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: headhunter45/ScoreKeeper
path: ScoreKeeper
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
- uses: gradle/actions/setup-gradle@v4
- run: ./gradlew publishToMavenLocal
working-directory: ScoreKeeper
- run: ./gradlew test build

5
.gitignore vendored
View File

@@ -1,3 +1,8 @@
.gradle/
build/
target/
.worktrees/
bin/
/backups/
/server/
.env

View File

@@ -0,0 +1,45 @@
# Contributing
## Requirements
- Java 25. The Gradle toolchain can download it automatically; `tools/build-plugin.sh` also provisions Temurin 25 under the project Gradle user home.
- A recent Paper server for runtime testing.
- The ScoreKeeper plugin, which is a required runtime dependency.
## Build and Test
From the project root:
```sh
./gradlew test
./gradlew build
```
The distributable plugin JAR is written to `build/libs/`. To use the repository helper, run:
```sh
tools/build-plugin.sh
```
## Local Paper Testing
Set `PAPER_VERSION` and `MINECRAFT_SERVER_PATH` in a local `.env` file at the MobScores project root, or export them in your shell. Set `MINECRAFT_ENV_FILE` if the helper scripts should load a shared environment file instead. Build ScoreKeeper and install it into the server first, then build and deploy MobScores:
```sh
tools/build-plugin.sh
tools/deploy-plugin.sh
```
Start the server with `tools/start-server.sh`. The script downloads a stable Paper build when one is not already present. Review the server log for dependency and plugin-load errors, then test score-table configuration and mob-kill scoring in game. Back up the server before testing against a world with data you care about.
## Configuration
The default resource is `src/main/resources/config.yml`; Paper copies it to `plugins/MobScores/config.yml` on first startup. `ScoreTable` maps Bukkit `EntityType` names to integer point values. To change values on a test server, edit the generated config and restart the server.
## Helper Scripts
- `tools/build-plugin.sh` builds with Java 25.
- `tools/deploy-plugin.sh` copies the latest non-`-all` JAR into `$MINECRAFT_SERVER_PATH/plugins/`.
- `tools/start-server.sh` downloads/resolves and launches Paper using `.env` settings.
- `tools/backup-server.sh` and `tools/restore-server.sh` manage server backups.
- `tools/publish-local.sh` publishes the artifact to Maven Local for dependent plugin builds.

34
Modrinth-Description.md Normal file
View File

@@ -0,0 +1,34 @@
**MobScores**
MobScores awards ScoreKeeper points for configured mob kills. Set a score for each supported entity type and let ScoreKeeper track the player's total by UUID.
## Requirements
- Paper 1.21.7 or a compatible newer server.
- Java 25 or newer at runtime and for building.
- ScoreKeeper 0.2.2 or a compatible release.
Install ScoreKeeper and MobScores in the server's `plugins` directory. ScoreKeeper is a required dependency and must be enabled for MobScores to load.
## Configuration
MobScores creates `plugins/MobScores/config.yml` from its defaults. Edit `ScoreTable` to configure point values by Bukkit `EntityType` name:
```yaml
ScoreTable:
ZOMBIE: 25
CREEPER: 50
SILVERFISH: 0
```
Set an entity's value to `0` to disable its points and hide it from the join-time score table. Restart the server after changing the configuration.
## Scoring
MobScores records credit for configured entity types damaged directly by a player or by a projectile fired by a player. When a credited player remains online when the entity dies, its configured points are added to that player's active ScoreKeeper bucket. Other mob attribution is not currently handled.
MobScores has no commands of its own. Players see the nonzero score table when they join.
## Build
Run `./gradlew test` and `./gradlew build` from the project root. See [CONTRIBUTING.md](CONTRIBUTING.md) for development and local Paper testing instructions.

13
Modrinth.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "MobScores",
"url": "https://modrinth.com/plugin/mobscores",
"summary": "Award ScoreKeeper points for configured mob kills with a customizable per-entity score table.",
"visibility": "public",
"links": {
"issueTracker": "https://github.com/headhunter45/MobScores/issues",
"sourceCode": "https://github.com/headhunter45/MobScores",
"wikiPage": null,
"discordInvite": null,
"donationLinks": []
}
}

23
Project-Description.json Normal file
View File

@@ -0,0 +1,23 @@
{
"id": "mob-scores",
"title": "MobScores",
"description": "Legacy Bukkit plugin that awards configurable scores for killing mobs.",
"docId": "MobScores",
"image": {
"url": "Retro Minecraft Bukkit server plugin artwork showing a blocky player defeating a hostile zombie, floating score numbers, a simple scoreboard motif, pixel-art style, dark forest-green and charcoal palette, no words or logos.",
"alt": "Pixel-art scene of a Minecraft player defeating a zombie with score numbers floating nearby",
"prompt": "Retro Minecraft Bukkit server plugin artwork showing a blocky player defeating a hostile zombie, floating score numbers, a simple scoreboard motif, pixel-art style, dark forest-green and charcoal palette, no words or logos.",
"width": 0,
"height": 0,
"backgroundClassName": "bg-surface"
},
"url": "/projects/mob-scores",
"tags": ["open-source"],
"source": {
"type": "",
"url": ""
},
"feedback": {
"url": ""
}
}

16
Project-Description.md Normal file
View File

@@ -0,0 +1,16 @@
# MobScores
MobScores is a legacy Bukkit plugin that awards configurable scores to players for killing mobs. It works with the `ScoreKeeper` plugin, records the player who last damaged a mob, and awards the configured value when that mob dies.
## Details
- Platform: Bukkit/Minecraft server plugin
- Language: Java
- Dependency: ScoreKeeper
- Configuration: `plugins/MobScores/config.yml`
- Commands: None
- License: GNU Affero General Public License v3
## Image prompt
Retro Minecraft Bukkit server plugin artwork showing a blocky player defeating a hostile zombie, floating score numbers, a simple scoreboard motif, pixel-art style, dark forest-green and charcoal palette, no words or logos.

View File

@@ -1,6 +1,30 @@
This mod is copyright (c) 2011 Tom Hicks and released under the AGPL v3. See the file agpl-3.0.txt for the specifics of the license.
# MobScores
No Commands
MobScores awards ScoreKeeper points for configured mob kills. It is a Paper plugin and requires the ScoreKeeper plugin to be installed and enabled first.
Edit plugins/MobScores/config.yml to set scores per mob.
## Requirements
- Paper 1.21.7 or a compatible newer server.
- Java 25 to build the plugin.
- ScoreKeeper 0.2.2 or the version declared by `build.gradle`.
Install both plugin JARs in the server's `plugins` directory. MobScores declares ScoreKeeper as a hard dependency, so it will not enable without it.
## Score Table
On first startup, MobScores creates `plugins/MobScores/config.yml` with default scores for supported entity types. Edit the `ScoreTable` mapping to change points; keys are Bukkit `EntityType` names and values are integer points:
```yaml
ScoreTable:
ZOMBIE: 25
CREEPER: 50
SILVERFISH: 0
```
Restart the server after editing the configuration. A configured value of `0` suppresses that entity from the score table shown to players when they join.
MobScores has no commands of its own. It records credit for configured entity types damaged directly by a player or by a projectile fired by a player. Other mob attribution is not currently handled. Points are awarded through ScoreKeeper's existing player API and follow the player's UUID-backed score.
## Build
Run `./gradlew test` and `./gradlew build` from the project root. See [CONTRIBUTING.md](CONTRIBUTING.md) for toolchain, local server, and helper-script details.

46
TODO.md
View File

@@ -1,24 +1,26 @@
# Modernize MobScores Plugin: Implementation Tasks
- [x] Analyze all Java source files for deprecated or removed Bukkit API usage, including logger, event registration, listener classes, configuration API, and entity/player mapping.
- [x] Identify and document any hardcoded Bukkit references or legacy patterns (e.g., Player as HashMap key, old event registration, CraftBukkit class names).
- [ ] Review and list any custom scripts or resources (e.g., bash scripts in tools/bash/) that may need migration or updates.
- [x] Initialize Gradle in the project root and create a build.gradle file with project metadata, PaperMC API dependency, JUnit, Java toolchain, repository, resource handling, and plugins as needed.
- [x] Remove Maven-specific files (pom.xml, .mvn/ directory, Maven wrapper scripts) and Eclipse-specific files (.classpath, .project, .settings/) if present.
- [x] Update .gitignore to add Gradle-specific ignores and remove Maven/Eclipse-specific ignores.
- [x] Ensure plugin.yml is present in src/main/resources and update for PaperMC compatibility (api-version, commands, required fields).
- [x] Refactor all logger usage to use getLogger() from JavaPlugin.
- [x] Refactor all event listeners to use the modern event system (Listener interface, @EventHandler, registerEvents).
- [x] Replace use of org.bukkit.util.config.Configuration with the modern configuration API (getConfig(), saveConfig(), etc.).
- [x] Update score table to use Bukkit entity types or enums instead of CraftBukkit class names.
- [ ] If storing player scores, refactor to use UUID as the key instead of Player or String.
- [x] Build the plugin with Gradle (./gradlew build) and test on a modern Paper server using the provided bash scripts.
- [x] Address any bugs or incompatibilities found during testing on a modern server.
- [ ] Update README.md and CONTRIBUTING.md with new build, usage, and development instructions. _(Depends on: bugfixes)_
- [ ] (Optional) Add new features, quality-of-life improvements, automated tests, or CI configuration. _(Depends on: docs update)_
- [x] Update MobScores to depend on the latest version of the ScoreKeeper plugin (update dependency in build.gradle and plugin.yml as needed).
- [ ] Test MobScores with the latest ScoreKeeper to ensure score tracking, awarding, and all integration points work as expected (including with players who have changed names).
- [x] Refactor MobDeathListener and PlayerConnectListener to implement Listener interface and use @EventHandler annotations instead of extending EntityListener/PlayerListener.
- [x] Update event registration in MobScoresPlugin to use getServer().getPluginManager().registerEvents(...).
- [x] Replace all usage of org.bukkit.util.config.Configuration with the modern Bukkit configuration API (getConfig(), saveConfig(), reloadConfig(), etc.).
- [x] Remove or refactor any code using deprecated or removed Bukkit/Spigot/Paper APIs that are not available in the modern Paper API.
| ID | Status | Title |
|:-----:|:-----:|:------|
| MS-01 | Done | Analyze all Java source files for deprecated or removed Bukkit API usage, including logger, event registration, listener classes, configuration API, and entity/player mapping. |
| MS-02 | Done | Identify and document any hardcoded Bukkit references or legacy patterns (e.g., Player as HashMap key, old event registration, CraftBukkit class names). |
| MS-03 | Done | Review and list any custom scripts or resources (e.g., bash scripts in tools/bash/) that may need migration or updates.|
| MS-04 | Done | Initialize Gradle in the project root and create a build.gradle file with project metadata, PaperMC API dependency, JUnit, Java toolchain, repository, resource handling, and plugins as needed. |
| MS-05 | Done | Remove Maven-specific files (pom.xml, .mvn/ directory, Maven wrapper scripts) and Eclipse-specific files (.classpath, .project, .settings/) if present. |
| MS-06 | Done | Update .gitignore to add Gradle-specific ignores and remove Maven/Eclipse-specific ignores. |
| MS-07 | Done | Ensure plugin.yml is present in src/main/resources and update for PaperMC compatibility (api-version, commands, required fields). |
| MS-08 | Done | Refactor all logger usage to use getLogger() from JavaPlugin. |
| MS-09 | Done | Refactor all event listeners to use the modern event system (Listener interface, @EventHandler, registerEvents).
| MS-10 | Done | Replace use of org.bukkit.util.config.Configuration with the modern configuration API (getConfig(), saveConfig(), etc.).
| MS-11 | Done | Update score table to use Bukkit entity types or enums instead of CraftBukkit class names.
| MS-12 | Done | If storing player scores, refactor to use UUID as the key instead of Player or String.
| MS-13 | Done | Build the plugin with Gradle (./gradlew build) and test on a modern Paper server using the provided bash scripts.
| MS-14 | Done | Address any bugs or incompatibilities found during testing on a modern server.
| MS-15 | Done | Update README.md and CONTRIBUTING.md with new build, usage, and development instructions. _(Depends on: bugfixes)_
| MS-16 | Done | (Optional) Add new features, quality-of-life improvements, automated tests, or CI configuration. _(Depends on: docs update)_
| MS-17 | Done | Update MobScores to depend on the latest version of the ScoreKeeper plugin (update dependency in build.gradle and plugin.yml as needed).
| MS-18 | Done | Test MobScores with the latest ScoreKeeper to ensure score tracking, awarding, and all integration points work as expected (including with players who have changed names).
| MS-19 | Done | Refactor MobDeathListener and PlayerConnectListener to implement Listener interface and use @EventHandler annotations instead of extending EntityListener/PlayerListener.
| MS-20 | Done | Update event registration in MobScoresPlugin to use getServer().getPluginManager().registerEvents(...).
| MS-21 | Done | Replace all usage of org.bukkit.util.config.Configuration with the modern Bukkit configuration API (getConfig(), saveConfig(), reloadConfig(), etc.).
| MS-22 | Done | Remove or refactor any code using deprecated or removed Bukkit/Spigot/Paper APIs that are not available in the modern Paper API.

View File

@@ -7,6 +7,12 @@
plugins {
id 'java-library'
id 'maven-publish'
id 'org.danilopianini.git-sensitive-semantic-versioning' version '7.0.24'
}
gitSemVer {
minimumVersion.set('0.2.1')
versionPrefix.set('v')
}
repositories {
@@ -16,23 +22,36 @@ repositories {
}
dependencies {
compileOnly 'com.majinnaibu.minecraft.plugins:ScoreKeeper:0.2.1-SNAPSHOT'
compileOnly 'com.majinnaibu.minecraft.plugins:ScoreKeeper:0.2.2-SNAPSHOT'
compileOnly 'io.papermc.paper:paper-api:1.21.7-R0.1-SNAPSHOT'
compileOnly 'net.kyori:adventure-api:4.14.0'
testImplementation libs.junit.junit
}
group = 'com.majinnaibu.minecraft.plugins'
version = '0.2.1-SNAPSHOT'
description = 'MobScores'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
// Paper 26.1+ requires Java 25.
languageVersion = JavaLanguageVersion.of(25)
}
}
tasks.named('wrapper') {
gradleVersion = '9.8.0'
distributionType = Wrapper.DistributionType.BIN
distributionBase = Wrapper.PathBase.PROJECT
distributionPath = '.gradle/wrapper/dists'
archiveBase = Wrapper.PathBase.PROJECT
archivePath = '.gradle/wrapper/dists'
}
tasks.named('updateDaemonJvm') {
languageVersion = JavaLanguageVersion.of(25)
}
publishing {
publications {
maven(MavenPublication) {
@@ -46,13 +65,15 @@ tasks.withType(JavaCompile).configureEach {
// options.compilerArgs << "-Xlint:deprecation"
}
tasks.withType(Javadoc) {
tasks.withType(Javadoc).configureEach {
options.encoding = 'UTF-8'
}
def pluginVersion = project.version.toString()
processResources {
inputs.property "version", project.version
inputs.property "version", pluginVersion
filesMatching('plugin.yml') {
expand(project.properties)
expand(version: pluginVersion)
}
}

5
gradle.properties Normal file
View File

@@ -0,0 +1,5 @@
org.gradle.java.installations.auto-download=true
org.gradle.java.installations.auto-detect=false
org.gradle.java.installations.fromEnv=JAVA_HOME
org.gradle.configuration-cache=true
org.gradle.daemon=true

View File

@@ -1,7 +1,8 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionBase=PROJECT
distributionPath=.gradle/wrapper/dists
distributionSha256Sum=bafd5ce9cfaea0fbccfdc8439a1ac42fbd4cd9c89dc9a988228d8a2639a58e6c
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStoreBase=PROJECT
zipStorePath=.gradle/wrapper/dists

View File

@@ -1 +1,5 @@
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'MobScores'

View File

@@ -18,8 +18,8 @@ along with MobScores. If not, see <http://www.gnu.org/licenses/>.
package com.majinnaibu.minecraft.plugins.mobscores;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import net.kyori.adventure.text.Component;
@@ -28,7 +28,6 @@ import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Zombie;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
@@ -37,7 +36,7 @@ import com.majinnaibu.minecraft.plugins.mobscores.listeners.PlayerConnectListene
import com.majinnaibu.minecraft.plugins.scorekeeper.ScoreKeeperPlugin;
public class MobScoresPlugin extends JavaPlugin {
private Map<Entity, Player> _claimedMobs = new HashMap<Entity, Player>();
private Map<UUID, UUID> _claimedMobs = new HashMap<UUID, UUID>();
private Map<EntityType, Integer> _scoreTable = new HashMap<EntityType, Integer>();
private ScoreKeeperPlugin _scoreKeeper = null;
@@ -57,12 +56,9 @@ public class MobScoresPlugin extends JavaPlugin {
// Create the default config if it doesn't exist.
saveDefaultConfig();
getConfig().set("ScoreTable", serializeScoreTable(getDefaultScoreTable()));
saveConfig();
// Load our score table from config or set defaults.
if (getConfig().isConfigurationSection("ScoreTable")) {
deserializeScoreTable(getConfig().getConfigurationSection("ScoreTable").getValues(false));
_scoreTable = deserializeScoreTable(getConfig().getConfigurationSection("ScoreTable").getValues(false));
} else {
_scoreTable = getDefaultScoreTable();
getConfig().set("ScoreTable", serializeScoreTable(_scoreTable));
@@ -112,17 +108,21 @@ public class MobScoresPlugin extends JavaPlugin {
}
public void claimMob(Entity entity, Player damager) {
if(entity instanceof Zombie){
_claimedMobs.put((Zombie)entity, damager);
if (_scoreTable.containsKey(entity.getType())) {
_claimedMobs.put(entity.getUniqueId(), damager.getUniqueId());
}
}
public void awardScore(Entity entity) {
if(_claimedMobs.containsKey(entity)){
UUID playerId = _claimedMobs.remove(entity.getUniqueId());
if(playerId != null){
EntityType type = entity.getType();
if (_scoreTable.containsKey(type)) {
Player player = _claimedMobs.get(entity);
Player player = getServer().getPlayer(playerId);
if (player == null) {
logWarning("Unable to award score because the credited player is offline.");
return;
}
int score = _scoreTable.get(type);
_scoreKeeper.addScore(player, score);
} else {
@@ -132,13 +132,14 @@ public class MobScoresPlugin extends JavaPlugin {
}
public void sendPlayerScoreTable(Player player) {
Iterator<Map.Entry<EntityType, Integer>> i = _scoreTable.entrySet().iterator();
Map.Entry<EntityType, Integer> pair = null;
for(pair = i.next(); i.hasNext(); pair = i.next()){
if(pair.getValue() != 0){
EntityType type = pair.getKey();
Component entityName = Component.translatable(type.translationKey());
sendPlayerMessage(player, entityName + " = " + pair.getValue().toString());
for (Map.Entry<EntityType, Integer> entry : _scoreTable.entrySet()) {
if (entry.getValue() != 0) {
EntityType type = entry.getKey();
Component message = Component.translatable(type.translationKey())
.append(Component.text(" = "))
.append(Component.text(String.valueOf(entry.getValue()))
.color(NamedTextColor.GREEN));
sendPlayerMessage(player, message);
}
}
}
@@ -170,6 +171,10 @@ public class MobScoresPlugin extends JavaPlugin {
return serializable;
}
public void sendPlayerMessage(Player player, Component message) {
player.sendMessage(_messagePrefix.append(message));
}
public void sendPlayerMessage(Player player, String message) {
player.sendMessage(_messagePrefix.append(Component.text(message)));
}

View File

@@ -19,6 +19,7 @@ package com.majinnaibu.minecraft.plugins.mobscores.listeners;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
@@ -37,12 +38,19 @@ public class MobDeathListener implements Listener {
@EventHandler
public void onEntityDamage(EntityDamageEvent event) {
if(event instanceof EntityDamageByEntityEvent){
EntityDamageByEntityEvent ev = (EntityDamageByEntityEvent) event;
Entity damager = ev.getDamager();
if(damager instanceof Player){
_plugin.claimMob(ev.getEntity(), (Player)damager);
}
if (!(event instanceof EntityDamageByEntityEvent damageEvent)) {
return;
}
Entity damager = damageEvent.getDamager();
Player creditedPlayer = null;
if (damager instanceof Player directPlayer) {
creditedPlayer = directPlayer;
} else if (damager instanceof Projectile projectile
&& projectile.getShooter() instanceof Player projectileShooter) {
creditedPlayer = projectileShooter;
}
if (creditedPlayer != null) {
_plugin.claimMob(event.getEntity(), creditedPlayer);
}
}

64
tools/backup-server.sh Executable file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Backs up plugins, config, worlds, and settings from MINECRAFT_SERVER_PATH
# to MINECRAFT_BACKUP_PATH/backup-<timestamp>[-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}'))"

View File

@@ -1,10 +0,0 @@
#!/usr/bin/env bash
set -e
# Find project root (two directories above this script)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
./gradlew build

44
tools/build-plugin.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
export GRADLE_USER_HOME="${GRADLE_USER_HOME:-$PROJECT_ROOT/.gradle/user-home}"
mkdir -p "$GRADLE_USER_HOME/jdks"
ensure_java_25() {
local version="25"
local os arch archive java_bin actual_home
case "$(uname -s)" in
Darwin) os="mac" ;;
Linux) os="linux" ;;
*) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;;
esac
case "$(uname -m)" in
arm64|aarch64) arch="aarch64" ;;
x86_64|amd64) arch="x64" ;;
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac
java_bin=$(find "$GRADLE_USER_HOME/jdks" -path '*/bin/java' -type f 2>/dev/null | head -n 1 || true)
if [[ -z "$java_bin" ]]; then
echo "Downloading Temurin JDK $version into $GRADLE_USER_HOME/jdks..."
archive="$GRADLE_USER_HOME/jdks/temurin-$version.tar.gz"
curl -fsSL "https://api.adoptium.net/v3/binary/latest/$version/ga/${os}/${arch}/jdk/hotspot/normal/eclipse" -o "$archive"
tar -xzf "$archive" -C "$GRADLE_USER_HOME/jdks"
rm -f "$archive"
java_bin=$(find "$GRADLE_USER_HOME/jdks" -path '*/bin/java' -type f 2>/dev/null | head -n 1 || true)
fi
[[ -n "$java_bin" ]] || { echo "Error: could not locate installed JDK 25." >&2; exit 1; }
actual_home="$(dirname "$(dirname "$java_bin")")"
export JAVA_HOME="$actual_home"
export PATH="$JAVA_HOME/bin:$PATH"
}
ensure_java_25
cd "$PROJECT_ROOT"
./gradlew build "$@"

View File

@@ -1,15 +1,18 @@
#!/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)
PLUGIN_JAR=$(ls -t "$PROJECT_ROOT"/build/libs/*.jar 2>/dev/null | grep -v -- '-all\.jar$' | head -n1)
if [[ ! -f "$PLUGIN_JAR" ]]; then
echo "Error: No plugin jar found in $PROJECT_ROOT/build/libs. Build the plugin first."
exit 1

44
tools/publish-local.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
export GRADLE_USER_HOME="${GRADLE_USER_HOME:-$PROJECT_ROOT/.gradle/user-home}"
mkdir -p "$GRADLE_USER_HOME/jdks"
ensure_java_25() {
local version="25"
local os arch archive java_bin actual_home
case "$(uname -s)" in
Darwin) os="mac" ;;
Linux) os="linux" ;;
*) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;;
esac
case "$(uname -m)" in
arm64|aarch64) arch="aarch64" ;;
x86_64|amd64) arch="x64" ;;
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac
java_bin=$(find "$GRADLE_USER_HOME/jdks" -path '*/bin/java' -type f 2>/dev/null | head -n 1 || true)
if [[ -z "$java_bin" ]]; then
echo "Downloading Temurin JDK $version into $GRADLE_USER_HOME/jdks..."
archive="$GRADLE_USER_HOME/jdks/temurin-$version.tar.gz"
curl -fsSL "https://api.adoptium.net/v3/binary/latest/$version/ga/${os}/${arch}/jdk/hotspot/normal/eclipse" -o "$archive"
tar -xzf "$archive" -C "$GRADLE_USER_HOME/jdks"
rm -f "$archive"
java_bin=$(find "$GRADLE_USER_HOME/jdks" -path '*/bin/java' -type f 2>/dev/null | head -n 1 || true)
fi
[[ -n "$java_bin" ]] || { echo "Error: could not locate installed JDK 25." >&2; exit 1; }
actual_home="$(dirname "$(dirname "$java_bin")")"
export JAVA_HOME="$actual_home"
export PATH="$JAVA_HOME/bin:$PATH"
}
ensure_java_25
cd "$PROJECT_ROOT"
./gradlew publishToMavenLocal "$@"

54
tools/publish-modrinth.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env bash
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"
MODRINTH_TOKEN="${MODRINTH_TOKEN:-}"
MODRINTH_PROJECT_ID="${MODRINTH_PROJECT_ID:-${MODRINTH_PROJECT_SLUG:-mob-scores}}"
MODRINTH_VERSION="${MODRINTH_VERSION:-${VERSION:-$(grep -E '^version=' gradle.properties | head -n1 | cut -d= -f2-)}}"
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_LOADERS="${MODRINTH_LOADERS:-paper}"
MODRINTH_FILE="${MODRINTH_FILE:-}"
MODRINTH_DEPENDENCIES="${MODRINTH_DEPENDENCIES:-}"
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=$(ls -t "$PROJECT_ROOT"/build/libs/*.jar 2>/dev/null | grep -v -- '-all\.jar$' | head -n1 || true)
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
csv_to_json_array() {
jq -cn --arg csv "$1" '$csv | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))'
}
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")
else
MODRINTH_DEPENDENCIES_ARG=()
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 .

68
tools/restore-server.sh Executable file
View File

@@ -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")."

39
tools/setup-env.sh Executable file
View File

@@ -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

92
tools/start-server.sh Executable file
View File

@@ -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

34
tools/stop-server.sh Executable file
View File

@@ -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