22 Commits

Author SHA1 Message Date
a5649106b4 Reorganizes docs.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2026-09-28 00:19:14 -07:00
a7d828daeb Adds tests to github actions and a build badge.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2026-09-28 00:12:16 -07:00
db43411628 Updates spotless and adds a license header. 2026-09-27 02:29:43 -07:00
39998917ab Removes dependabot submissions.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2026-09-27 01:26:48 -07:00
cd3c58c942 Updates to docs and ci. 2026-09-27 01:14:52 -07:00
676407dbcb Fixes dependabot includes. 2026-09-27 00:25:52 -07:00
186afc7813 Adds modrinth dependency for ScoreKeeper. 2026-09-26 23:57:24 -07:00
c2abca419f Adds github actions to build and archive the jar. 2026-09-26 23:48:13 -07:00
9c69197e7f Adds tasks. 2026-09-26 23:47:13 -07:00
8096aca633 Adds tasks.
Some checks failed
Gradle Build / build (push) Has been cancelled
2026-09-26 22:01:38 -07:00
37020ecf9c Adds task. 2026-09-26 20:46:58 -07:00
12099685bf Updates to work with ScoreKeeper 0.2.2 buckets. 2026-09-26 20:29:13 -07:00
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
32 changed files with 1455 additions and 390 deletions

26
.env.example Normal file
View File

@@ -0,0 +1,26 @@
# 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 the project root; ~/ is expanded.
MINECRAFT_SERVER_PATH=./server
MINECRAFT_BACKUP_PATH=./backups
# Modrinth publishing
# Release version is read from Gradle and checked against the jar's plugin.yml.
MODRINTH_TOKEN=
MODRINTH_PROJECT_ID=mob-scores
# Optional Markdown override; empty generates notes from commits since the last tag.
MODRINTH_CHANGELOG=
MODRINTH_GAME_VERSIONS=26.2,26.3
MODRINTH_LOADERS=paper
MODRINTH_VERSION_TYPE=
MODRINTH_DEPENDENCIES='[{"project_id":"5NYwxysp","version_id":null,"file_name":null,"dependency_type":"required"}]'
MODRINTH_FILE=

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

@@ -0,0 +1,59 @@
name: Java CI with Gradle
on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Set up JDK 25
uses: actions/setup-java@v6
with:
java-version: '25'
distribution: 'temurin'
- name: Setup Gradle 9.8.0
uses: gradle/actions/setup-gradle@v5
with:
gradle-version: '9.8.0'
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Unit Tests
run: ./gradlew test
- name: Upload Test Report
if: always()
uses: actions/upload-artifact@v7
with:
name: MobScores-test-report
path: |
build/reports/tests/test
build/test-results/test
if-no-files-found: ignore
retention-days: 14
- name: Build with Gradle Wrapper
run: ./tools/build-plugin.sh
- name: Extract Project Version
id: project_info
run: |
VERSION=$(./gradlew properties --no-daemon -q | awk -F ': ' '$1 == "version" {print $2}')
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Upload Plugin Artifact
uses: actions/upload-artifact@v7
with:
name: MobScores-${{ steps.project_info.outputs.version }}
path: build/libs/*.jar
if-no-files-found: error
retention-days: 14

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.

View File

@@ -1,2 +0,0 @@
Manifest-Version: 1.0

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="WINDOWS-1252" standalone="no"?>
<jardesc>
<jar path="C:/Users/Tom/Desktop/Minecraft/Server (Bukkit) - Copy/plugins/MobScores.jar"/>
<options buildIfNeeded="true" compress="true" descriptionLocation="/MobScores/MobScores.jardesc" exportErrors="true" exportWarnings="true" includeDirectoryEntries="false" overwrite="false" saveDescription="true" storeRefactorings="false" useSourceFolders="false"/>
<storedRefactorings deprecationInfo="true" structuralOnly="false"/>
<selectedProjects/>
<manifest generateManifest="false" manifestLocation="/MobScores/Manifest.MF" manifestVersion="1.0" reuseManifest="true" saveManifest="true" usesManifest="true">
<sealing sealJar="false">
<packagesToSeal/>
<packagesToUnSeal/>
</sealing>
</manifest>
<selectedElements exportClassFiles="true" exportJavaFiles="false" exportOutputFolder="false">
<file path="/MobScores/.classpath"/>
<javaElement handleIdentifier="=MobScores/\/ScoreKeeper\/bin"/>
<javaElement handleIdentifier="=MobScores/src"/>
<file path="/MobScores/.project"/>
</selectedElements>
</jardesc>

View File

@@ -1,6 +1,32 @@
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
[![Java CI with Gradle](https://github.com/headhunter45/MobScores/actions/workflows/gradle.yml/badge.svg)](https://github.com/headhunter45/MobScores/actions/workflows/gradle.yml)
Edit plugins/MobScores/config.yml to set scores per mob.
MobScores awards ScoreKeeper points for configured mob kills. It is a Paper-only plugin for Paper 26.2 and 26.3, and requires the ScoreKeeper plugin to be installed and enabled first.
## Requirements
- Paper 26.2 or 26.3.
- Java 25 to build the plugin.
- ScoreKeeper 0.2.2 or newer.
Install both plugin JARs in the server's `plugins` directory. MobScores declares ScoreKeeper as a hard dependency, so it will not enable without it. Bukkit and Spigot servers are not supported.
## 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 Paper API `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. Entity type keys use the Paper API's `EntityType` names.
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 go into ScoreKeeper's dedicated `mob-scores` bucket and are tracked by player UUID. Use `/score-bucket mob-scores` before `/score-get` to view mob points; MobScores does not change the player's active bucket automatically.
## Build
Run `./gradlew test` and `./gradlew build` from the project root. See [CONTRIBUTING.md](CONTRIBUTING.md) for toolchain, local server, and helper-script details.

49
TODO.md
View File

@@ -1,24 +1,29 @@
# 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 | Planning | (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.
| MS-23 | Ready | Add JUnit 5 test infrastructure and keep the default Gradle `test` task server-free. Use an in-memory Paper test harness such as MockBukkit for plugin lifecycle and Bukkit/Paper behavior; use Mockito or test doubles for ScoreKeeper and narrow API boundaries where a harness is unnecessary. |
| MS-24 | Ready | Add fast unit tests for MobScores behavior: default and configured score-table loading, serialization and malformed or unknown entity entries; direct-player and projectile damage attribution; ignoring untracked entities and non-player damage; death awards, offline credited players, zero-value scores, and join-time score messages. |
| MS-25 | Ready | Add a separate Gradle `integrationTest` source set/task for behavior requiring a real Paper server with the built MobScores and ScoreKeeper jars installed. Verify plugin loading and ScoreKeeper compatibility, mob-death score awards and player-facing messages, and score-table persistence across a server restart. Run it only when explicitly requested; keep it out of the normal `test` and `build` tasks. |

View File

@@ -5,34 +5,68 @@
*/
plugins {
id 'com.diffplug.spotless' version '8.10.3'
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 {
mavenCentral()
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
// Modrinth Maven repository for ScoreKeeper
exclusiveContent {
forRepository {
maven {
name = 'Modrinth'
url = 'https://api.modrinth.com/maven'
}
}
filter {
includeGroup 'maven.modrinth'
}
}
mavenLocal()
}
dependencies {
compileOnly 'com.majinnaibu.minecraft.plugins:ScoreKeeper:0.2.1-SNAPSHOT'
compileOnly 'io.papermc.paper:paper-api:1.21.7-R0.1-SNAPSHOT'
compileOnly 'net.kyori:adventure-api:4.14.0'
// Group is always 'maven.modrinth', module is your project slug/id
compileOnly 'maven.modrinth:scorekeeper:0.2.2'
compileOnly 'io.papermc.paper:paper-api:26.2.build.129-stable'
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 +80,26 @@ 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)
}
}
spotless {
java {
googleJavaFormat('1.36.1')
importOrder 'java', 'javax', 'com', 'net', 'org', ''
licenseHeaderFile 'config/license-header.txt'
target 'src/**/*.java'
removeUnusedImports()
toggleOffOn()
}
}

View File

@@ -1,17 +1,17 @@
/*
This file is part of MobScores.
This file is part of Mob Scores.
MobScores is free software: you can redistribute it and/or modify
Mob Scores is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
MobScores is distributed in the hope that it will be useful,
Mob Scores is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with MobScores. If not, see <http://www.gnu.org/licenses/>.
along with Mob Scores. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*/

41
docs/human-tests.md Normal file
View File

@@ -0,0 +1,41 @@
# MobScores Human Testing
Use this checklist on a disposable Paper 26.2 or 26.3 server with Java 25+, MobScores, and ScoreKeeper 0.2.2 or newer installed. MobScores depends on ScoreKeeper for its `mob-scores` bucket, and its main behavior is driven by real combat and player-join events.
## Test Setup
- Back up the world and both plugin data folders. Configure a test server with the same plugin versions intended for production.
- Use at least two ordinary player accounts and an operator for setup. Keep the server log visible to catch dependency, config, and offline-award warnings.
- Save the generated `plugins/MobScores/config.yml` before changing it. Restart after config edits; MobScores reads the score table during plugin startup.
- Record player UUIDs, mob types, configured values, active score buckets, and scores before and after each kill.
## Human Checklist
### Plugin loading and configuration
- Start with both plugins installed. Confirm ScoreKeeper loads before MobScores, MobScores enables, and the `mob-scores` bucket exists with the expected labels and initial value.
- On a disposable server, test startup without ScoreKeeper and with a ScoreKeeper version older than 0.2.2. Paper should reject the missing hard dependency; MobScores should disable itself with a version warning for an outdated dependency. Then restore the supported version.
- On first startup, inspect the generated `ScoreTable`. Confirm the documented default entity values are present, including a zero-valued entry.
- Change a few values, restart, and verify they are loaded and preserved. Include a zero value and an unknown entity key; confirm zero-valued entities are omitted from join messages and invalid entries are logged without preventing valid entries from loading.
- Confirm the table uses Paper `EntityType` names and that malformed values or a missing score-table section are handled as intended.
### Kill credit and scoring
- As a player, kill configured entities using direct melee damage and player-fired projectiles. Confirm the expected points are added to that player's `mob-scores` score.
- Check the boundary cases: a non-player attacker, environmental death, an entity type absent from the table, and a configured zero-point entity. Confirm no unintended points are awarded.
- Have two players damage the same mob before it dies. The current implementation records the most recent direct or player-projectile attacker; verify that this credit policy matches the server's expectations.
- Damage a mob with a player, then let it die from fall, fire, or another environmental cause. Confirm whether retaining that player's credit is the intended behavior; the current implementation keeps the last recorded player attribution until the mob dies.
- Test a player who disconnects after damaging the mob but before it dies. The current implementation cannot award an offline player and logs a warning; confirm this is acceptable and that no other player's score changes.
- Check mob death without any previous qualifying player damage, and verify no stale or reused entity attribution grants points.
### Player messaging and ScoreKeeper workflows
- Join with a player account. Confirm the MobScores score table is sent once, includes configured nonzero entity types with the right values, and excludes zero-valued types.
- Run `/score-bucket mob-scores` followed by `/score-get`. Confirm the score reflects kills and the bucket switch message uses the expected singular/plural label. Verify MobScores does not change the player's active bucket automatically on join or kill.
- Give two players different kill totals and switch between `mob-scores` and another ScoreKeeper bucket. Confirm each bucket retains its own value and MobScores awards only to `mob-scores`.
- Restart the server and verify MobScores retains its configured table and ScoreKeeper retains the players' mob scores. Reconnect and confirm the join table still matches the active config.
- Exercise normal gameplay around claimed mobs: unload/reload chunks, kill several entities rapidly, and test multiplayer combat. Watch for missed, duplicated, or cross-player awards.
## Automated Coverage
There are currently no Java test sources in MobScores and no `integrationTest` source set/task. Although Gradle declares JUnit, `./gradlew test` currently has no MobScores behavior tests. The testing tasks marked “Ready” in `TODO.md` have not been implemented. Build success alone does not verify real event attribution or compatibility with the installed ScoreKeeper jar.

View File

@@ -0,0 +1,36 @@
**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 26.2 or 26.3.
- Java 25 or newer at runtime and for building.
- ScoreKeeper 0.2.2 or newer.
Install ScoreKeeper and MobScores in the server's `plugins` directory. ScoreKeeper is a required dependency and must be enabled for MobScores to load.
MobScores targets Paper only; Bukkit and Spigot servers are not supported.
## Configuration
MobScores creates `plugins/MobScores/config.yml` from its defaults. Edit `ScoreTable` to configure point values by Paper API `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 the dedicated `mob-scores` ScoreKeeper bucket. Use `/score-bucket mob-scores` to view mob points with ScoreKeeper commands. 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
docs/modrinth.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "Mob Scores",
"url": "https://modrinth.com/plugin/mob-scores",
"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": []
}
}

View File

@@ -0,0 +1,23 @@
{
"id": "mob-scores",
"title": "MobScores",
"description": "A Paper 26.2/26.3 integration plugin that attributes mob kills, including projectile kills, and awards configurable ScoreKeeper points by entity type.",
"docId": "MobScores",
"image": {
"url": "Retro Minecraft Paper 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 Paper 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": ""
}
}

View File

@@ -0,0 +1,12 @@
# MobScores
MobScores turns combat into a configurable scoring system. When a player damages a tracked mob, the plugin remembers the attribution and awards the matching ScoreKeeper bucket when that mob dies, including projectile kills.
It is a deliberately small Paper integration plugin with a clear boundary: Paper entity events on one side, ScoreKeeper's public scoring API on the other. The per-entity score table is configurable, zero-value entries can be disabled, and players see the active mob-score table when they join. It targets Paper 26.2 and 26.3 only, not Bukkit or Spigot.
## Engineering Notes
- Built for modern Paper with Java 25 and Gradle.
- Uses UUID-safe player attribution.
- Depends on ScoreKeeper 0.2.2 or newer.
- Tested through the standard server-free build workflow.

5
gradle.properties Normal file
View File

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

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

@@ -1,48 +1,52 @@
/*
This file is part of MobScores.
This file is part of Mob Scores.
MobScores is free software: you can redistribute it and/or modify
Mob Scores is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
MobScores is distributed in the hope that it will be useful,
Mob Scores is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with MobScores. If not, see <http://www.gnu.org/licenses/>.
along with Mob Scores. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*/
package com.majinnaibu.minecraft.plugins.mobscores;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.logging.Level;
import com.majinnaibu.minecraft.plugins.mobscores.listeners.MobDeathListener;
import com.majinnaibu.minecraft.plugins.mobscores.listeners.PlayerConnectListener;
import com.majinnaibu.minecraft.plugins.scorekeeper.ScoreKeeperPlugin;
import net.kyori.adventure.text.Component;
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;
import com.majinnaibu.minecraft.plugins.mobscores.listeners.MobDeathListener;
import com.majinnaibu.minecraft.plugins.mobscores.listeners.PlayerConnectListener;
import com.majinnaibu.minecraft.plugins.scorekeeper.ScoreKeeperPlugin;
public class MobScoresPlugin extends JavaPlugin {
private Map<Entity, Player> _claimedMobs = new HashMap<Entity, Player>();
private static final String SCORE_BUCKET_ID = "mob-scores";
private static final String MIN_SCORE_KEEPER_VERSION = "0.2.2";
private Map<UUID, UUID> _claimedMobs = new HashMap<UUID, UUID>();
private Map<EntityType, Integer> _scoreTable = new HashMap<EntityType, Integer>();
private ScoreKeeperPlugin _scoreKeeper = null;
private final String _logPrefix = "[MobScores] ";
private final Component _messagePrefix = Component.text("[")
private final Component _messagePrefix =
Component.text("[")
.append(Component.text("MobScores").color(NamedTextColor.AQUA))
.append(Component.text("] ").color(NamedTextColor.WHITE));
@@ -57,11 +61,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")) {
_scoreTable =
deserializeScoreTable(getConfig().getConfigurationSection("ScoreTable").getValues(false));
} else {
_scoreTable = getDefaultScoreTable();
@@ -70,17 +72,35 @@ public class MobScoresPlugin extends JavaPlugin {
}
PluginManager pm = getServer().getPluginManager();
_scoreKeeper = (ScoreKeeperPlugin)pm.getPlugin("ScoreKeeper");
ScoreKeeperPlugin scoreKeeper = (ScoreKeeperPlugin) pm.getPlugin("ScoreKeeper");
if(_scoreKeeper == null){
if (scoreKeeper == null) {
logWarning("Unable to find ScoreKeeper plugin.");
pm.disablePlugin(this);
return;
}
String scoreKeeperVersion = scoreKeeper.getPluginMeta().getVersion();
if (!isVersionAtLeast(scoreKeeperVersion, MIN_SCORE_KEEPER_VERSION)) {
logWarning(
"ScoreKeeper "
+ MIN_SCORE_KEEPER_VERSION
+ " or newer is required; found "
+ scoreKeeperVersion
+ ".");
pm.disablePlugin(this);
return;
}
_scoreKeeper = scoreKeeper;
if (_scoreKeeper.getBucket(SCORE_BUCKET_ID) == null) {
_scoreKeeper.createBucket(SCORE_BUCKET_ID, "mob point", "mob points", 0);
}
pm.registerEvents(new MobDeathListener(this), this);
pm.registerEvents(new PlayerConnectListener(this), this);
logInfo(getPluginMeta().getName() + " version " + getPluginMeta().getVersion() + " is enabled!");
logInfo(
getPluginMeta().getName() + " version " + getPluginMeta().getVersion() + " is enabled!");
}
private HashMap<EntityType, Integer> getDefaultScoreTable() {
@@ -112,19 +132,23 @@ 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);
_scoreKeeper.addScore(player, SCORE_BUCKET_ID, score);
} else {
logWarning("Unable to award score for {" + type.toString() + "}");
}
@@ -132,13 +156,16 @@ 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(Objects.requireNonNull(type.translationKey()))
.append(Component.text(" = "))
.append(
Component.text(Objects.requireNonNull(String.valueOf(entry.getValue())))
.color(NamedTextColor.GREEN));
sendPlayerMessage(player, message);
}
}
}
@@ -170,8 +197,12 @@ public class MobScoresPlugin extends JavaPlugin {
return serializable;
}
public void sendPlayerMessage(Player player, Component message) {
player.sendMessage(_messagePrefix.append(Objects.requireNonNull(message)));
}
public void sendPlayerMessage(Player player, String message) {
player.sendMessage(_messagePrefix.append(Component.text(message)));
player.sendMessage(_messagePrefix.append(Component.text(Objects.requireNonNull(message))));
}
public void logError(Exception ex) {
@@ -185,4 +216,53 @@ public class MobScoresPlugin extends JavaPlugin {
public void logWarning(String message) {
getLogger().warning(_logPrefix + message);
}
private boolean isVersionAtLeast(String version, String minimumVersion) {
int[] actual = parseVersion(version);
int[] minimum = parseVersion(minimumVersion);
if (actual == null || minimum == null) {
return false;
}
for (int index = 0; index < actual.length; index++) {
if (actual[index] != minimum[index]) {
return actual[index] > minimum[index];
}
}
return !isPrerelease(version);
}
private int[] parseVersion(String version) {
if (version == null) {
return null;
}
String coreVersion = version.replaceFirst("^[vV]", "");
int metadataIndex = coreVersion.indexOf('+');
if (metadataIndex >= 0) {
coreVersion = coreVersion.substring(0, metadataIndex);
}
int prereleaseIndex = coreVersion.indexOf('-');
if (prereleaseIndex >= 0) {
coreVersion = coreVersion.substring(0, prereleaseIndex);
}
String[] parts = coreVersion.split("\\.");
if (parts.length < 3) {
return null;
}
try {
return new int[] {
Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2])
};
} catch (NumberFormatException ex) {
return null;
}
}
private boolean isPrerelease(String version) {
String release = version.replaceFirst("^[vV]", "");
int metadataIndex = release.indexOf('+');
if (metadataIndex >= 0) {
release = release.substring(0, metadataIndex);
}
return release.indexOf('-') >= 0;
}
}

View File

@@ -1,32 +1,33 @@
/*
This file is part of MobScores.
This file is part of Mob Scores.
MobScores is free software: you can redistribute it and/or modify
Mob Scores is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
MobScores is distributed in the hope that it will be useful,
Mob Scores is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with MobScores. If not, see <http://www.gnu.org/licenses/>.
along with Mob Scores. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*/
package com.majinnaibu.minecraft.plugins.mobscores.listeners;
import com.majinnaibu.minecraft.plugins.mobscores.MobScoresPlugin;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import com.majinnaibu.minecraft.plugins.mobscores.MobScoresPlugin;
public class MobDeathListener implements Listener {
private MobScoresPlugin _plugin = null;
@@ -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);
}
}
@@ -52,7 +60,5 @@ public class MobDeathListener implements Listener {
}
@EventHandler
public void onEntityExplode(EntityExplodeEvent event) {
}
public void onEntityExplode(EntityExplodeEvent event) {}
}

View File

@@ -1,30 +1,30 @@
/*
This file is part of MobScores.
This file is part of Mob Scores.
MobScores is free software: you can redistribute it and/or modify
Mob Scores is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
MobScores is distributed in the hope that it will be useful,
Mob Scores is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with MobScores. If not, see <http://www.gnu.org/licenses/>.
along with Mob Scores. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*/
package com.majinnaibu.minecraft.plugins.mobscores.listeners;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.Listener;
import org.bukkit.event.EventHandler;
import com.majinnaibu.minecraft.plugins.mobscores.MobScoresPlugin;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
public class PlayerConnectListener implements Listener {
private MobScoresPlugin _plugin = null;
private MobScoresPlugin _plugin = null;
public PlayerConnectListener(MobScoresPlugin plugin) {
_plugin = plugin;

View File

@@ -2,4 +2,4 @@ name: MobScores
main: com.majinnaibu.minecraft.plugins.mobscores.MobScoresPlugin
depend: [ScoreKeeper]
version: ${version}
api-version: 1.21
api-version: '26.2'

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

@@ -0,0 +1,103 @@
#!/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)"
PROJECT_ROOT="$(cd "$TOOLS_DIR/.." && pwd)"
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
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH:-MCServer}"
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH/#\~/$HOME}"
[[ "$MINECRAFT_SERVER_PATH" == /* ]] || MINECRAFT_SERVER_PATH="$PROJECT_ROOT/$MINECRAFT_SERVER_PATH"
MINECRAFT_BACKUP_PATH="${MINECRAFT_BACKUP_PATH:-backups}"
MINECRAFT_BACKUP_PATH="${MINECRAFT_BACKUP_PATH/#\~/$HOME}"
[[ "$MINECRAFT_BACKUP_PATH" == /* ]] || MINECRAFT_BACKUP_PATH="$PROJECT_ROOT/$MINECRAFT_BACKUP_PATH"
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

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env bash
set -e
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."
exit 1
fi
# Optional: Warn if jar is older than any source file
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/"

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

63
tools/deploy-plugin.sh Executable file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
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
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH:-MCServer}"
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH/#\~/$HOME}"
[[ "$MINECRAFT_SERVER_PATH" == /* ]] || MINECRAFT_SERVER_PATH="$PROJECT_ROOT/$MINECRAFT_SERVER_PATH"
if [[ -z "$MINECRAFT_SERVER_PATH" ]]; then
echo "Error: MINECRAFT_SERVER_PATH environment variable is not set."
exit 1
fi
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
fi
# Optional: Warn if jar is older than any source file
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/"

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

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

@@ -0,0 +1,161 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
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:-mob-scores}"
generate_changelog() {
local current_tag previous_tag range commits
current_tag=$(git -C "$PROJECT_ROOT" describe --tags --exact-match HEAD 2>/dev/null || true)
if [[ -n "$current_tag" ]]; then
previous_tag=$(git -C "$PROJECT_ROOT" describe --tags --abbrev=0 "${current_tag}^" 2>/dev/null || true)
range="${previous_tag:+$previous_tag..}$current_tag"
else
previous_tag=$(git -C "$PROJECT_ROOT" describe --tags --abbrev=0 2>/dev/null || true)
range="${previous_tag:+$previous_tag..}HEAD"
fi
commits=$(git -C "$PROJECT_ROOT" log --no-merges --format='- %s (%h)' "$range" 2>/dev/null || true)
if [[ -z "$commits" ]]; then
commits='- No commit messages found for this release.'
fi
printf '%s\n\n%s\n' "## What's Changed" "$commits"
}
MODRINTH_CHANGELOG="${MODRINTH_CHANGELOG:-$(generate_changelog)}"
MODRINTH_GAME_VERSIONS="${MODRINTH_GAME_VERSIONS:-26.2,26.3}"
MODRINTH_LOADERS="${MODRINTH_LOADERS:-paper}"
MODRINTH_VERSION_TYPE="${MODRINTH_VERSION_TYPE:-}"
MODRINTH_FILE="${MODRINTH_FILE:-}"
MODRINTH_DEPENDENCIES="${MODRINTH_DEPENDENCIES:-}"
MODRINTH_VERSION=$(
"$PROJECT_ROOT/gradlew" -q -p "$PROJECT_ROOT" properties --property version \
| sed -n 's/^version: //p'
)
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="MobScores $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=$(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
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' | 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))'
}
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
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_JSON='[]'
fi
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 .

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

@@ -0,0 +1,107 @@
#!/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)"
PROJECT_ROOT="$(cd "$TOOLS_DIR/.." && pwd)"
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
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH:-MCServer}"
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH/#\~/$HOME}"
[[ "$MINECRAFT_SERVER_PATH" == /* ]] || MINECRAFT_SERVER_PATH="$PROJECT_ROOT/$MINECRAFT_SERVER_PATH"
MINECRAFT_BACKUP_PATH="${MINECRAFT_BACKUP_PATH:-backups}"
MINECRAFT_BACKUP_PATH="${MINECRAFT_BACKUP_PATH/#\~/$HOME}"
[[ "$MINECRAFT_BACKUP_PATH" == /* ]] || MINECRAFT_BACKUP_PATH="$PROJECT_ROOT/$MINECRAFT_BACKUP_PATH"
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")."

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

@@ -0,0 +1,131 @@
#!/usr/bin/env bash
# Downloads (if missing) and runs a Paper server via the Fill v3 API.
# Server settings come from the project .env file.
#
# Env vars:
# JAVA_OPTS Extra JVM args (default: -Xms2G -Xmx2G)
set -euo pipefail
TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$TOOLS_DIR/.." && pwd)"
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
PAPER_VERSION="${PAPER_VERSION:-}"
PAPER_BUILD="${PAPER_BUILD:-}"
MINECRAFT_SERVER_JAR="${MINECRAFT_SERVER_JAR:-}"
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH:-MCServer}"
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH/#\~/$HOME}"
[[ "$MINECRAFT_SERVER_PATH" == /* ]] || MINECRAFT_SERVER_PATH="$PROJECT_ROOT/$MINECRAFT_SERVER_PATH"
[[ -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