9 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
21 changed files with 595 additions and 491 deletions

View File

@@ -8,6 +8,7 @@ PAPER_USER_AGENT="majinnaibu-plugin-dev/1.0 (https://github.com/majinnaibu)"
JAVA_OPTS="-Xms2G -Xmx2G" JAVA_OPTS="-Xms2G -Xmx2G"
STOP_TIMEOUT=60 STOP_TIMEOUT=60
MINECRAFT_SERVER_JAR=paper-26.2-129.jar MINECRAFT_SERVER_JAR=paper-26.2-129.jar
# Relative paths are resolved from the project root; ~/ is expanded. # Relative paths are resolved from the project root; ~/ is expanded.
MINECRAFT_SERVER_PATH=./server MINECRAFT_SERVER_PATH=./server
MINECRAFT_BACKUP_PATH=./backups MINECRAFT_BACKUP_PATH=./backups
@@ -16,9 +17,10 @@ MINECRAFT_BACKUP_PATH=./backups
# Release version is read from Gradle and checked against the jar's plugin.yml. # Release version is read from Gradle and checked against the jar's plugin.yml.
MODRINTH_TOKEN= MODRINTH_TOKEN=
MODRINTH_PROJECT_ID=mob-scores MODRINTH_PROJECT_ID=mob-scores
# Optional Markdown override; empty generates notes from commits since the last tag.
MODRINTH_CHANGELOG= MODRINTH_CHANGELOG=
MODRINTH_GAME_VERSIONS=1.21,1.21.1,1.21.2,1.21.3,1.21.4,1.21.5,1.21.6,1.21.7 MODRINTH_GAME_VERSIONS=26.2,26.3
MODRINTH_LOADERS=paper MODRINTH_LOADERS=paper
MODRINTH_VERSION_TYPE= MODRINTH_VERSION_TYPE=
MODRINTH_DEPENDENCIES= MODRINTH_DEPENDENCIES='[{"project_id":"5NYwxysp","version_id":null,"file_name":null,"dependency_type":"required"}]'
MODRINTH_FILE= MODRINTH_FILE=

View File

@@ -1,23 +1,59 @@
name: Gradle Build name: Java CI with Gradle
on: on:
push: push:
branches: [ "develop" ]
pull_request: pull_request:
branches: [ "develop" ]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v6
with: with:
repository: headhunter45/ScoreKeeper
path: ScoreKeeper
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25' java-version: '25'
- uses: gradle/actions/setup-gradle@v4 distribution: 'temurin'
- run: ./gradlew publishToMavenLocal
working-directory: ScoreKeeper - name: Setup Gradle 9.8.0
- run: ./gradlew test build 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

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,23 +0,0 @@
{
"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": ""
}
}

View File

@@ -1,16 +0,0 @@
# 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,30 +1,32 @@
# MobScores # MobScores
MobScores awards ScoreKeeper points for configured mob kills. It is a Paper plugin and requires the ScoreKeeper plugin to be installed and enabled first. [![Java CI with Gradle](https://github.com/headhunter45/MobScores/actions/workflows/gradle.yml/badge.svg)](https://github.com/headhunter45/MobScores/actions/workflows/gradle.yml)
## Requirements 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.
- Paper 1.21.7 or a compatible newer server. ## Requirements
- Java 25 to build the plugin.
- ScoreKeeper 0.2.2 or newer. - Paper 26.2 or 26.3.
- Java 25 to build the plugin.
Install both plugin JARs in the server's `plugins` directory. MobScores declares ScoreKeeper as a hard dependency, so it will not enable without it. - ScoreKeeper 0.2.2 or newer.
## Score Table 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.
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: ## Score Table
```yaml 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:
ScoreTable:
ZOMBIE: 25 ```yaml
CREEPER: 50 ScoreTable:
SILVERFISH: 0 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 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. 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.
## Build 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.
Run `./gradlew test` and `./gradlew build` from the project root. See [CONTRIBUTING.md](CONTRIBUTING.md) for toolchain, local server, and helper-script details. ## Build
Run `./gradlew test` and `./gradlew build` from the project root. See [CONTRIBUTING.md](CONTRIBUTING.md) for toolchain, local server, and helper-script details.

View File

@@ -24,6 +24,6 @@
| MS-20 | Done | Update event registration in MobScoresPlugin to use getServer().getPluginManager().registerEvents(...). | 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-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-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-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-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. | | 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,6 +5,7 @@
*/ */
plugins { plugins {
id 'com.diffplug.spotless' version '8.10.3'
id 'java-library' id 'java-library'
id 'maven-publish' id 'maven-publish'
id 'org.danilopianini.git-sensitive-semantic-versioning' version '7.0.24' id 'org.danilopianini.git-sensitive-semantic-versioning' version '7.0.24'
@@ -18,17 +19,31 @@ gitSemVer {
repositories { repositories {
mavenCentral() mavenCentral()
maven { url = 'https://repo.papermc.io/repository/maven-public/' } 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() mavenLocal()
} }
dependencies { dependencies {
compileOnly 'com.majinnaibu.minecraft.plugins:ScoreKeeper:0.2.2' // Group is always 'maven.modrinth', module is your project slug/id
compileOnly 'io.papermc.paper:paper-api:1.21.7-R0.1-SNAPSHOT' compileOnly 'maven.modrinth:scorekeeper:0.2.2'
compileOnly 'io.papermc.paper:paper-api:26.2.build.129-stable'
testImplementation libs.junit.junit testImplementation libs.junit.junit
} }
group = 'com.majinnaibu.minecraft.plugins' group = 'com.majinnaibu.minecraft.plugins'
description = 'MobScores' description = 'MobScores'
@@ -77,3 +92,14 @@ processResources {
expand(version: pluginVersion) 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 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 the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (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 but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details. GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License 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

@@ -4,15 +4,17 @@ MobScores awards ScoreKeeper points for configured mob kills. Set a score for ea
## Requirements ## Requirements
- Paper 1.21.7 or a compatible newer server. - Paper 26.2 or 26.3.
- Java 25 or newer at runtime and for building. - Java 25 or newer at runtime and for building.
- ScoreKeeper 0.2.2 or newer. - 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. 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 ## Configuration
MobScores creates `plugins/MobScores/config.yml` from its defaults. Edit `ScoreTable` to configure point values by Bukkit `EntityType` name: MobScores creates `plugins/MobScores/config.yml` from its defaults. Edit `ScoreTable` to configure point values by Paper API `EntityType` name:
```yaml ```yaml
ScoreTable: ScoreTable:

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.

View File

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

View File

@@ -1,262 +1,268 @@
/* /*
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 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 the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (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 but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details. GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License 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; package com.majinnaibu.minecraft.plugins.mobscores;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.Objects;
import java.util.logging.Level; import java.util.UUID;
import java.util.logging.Level;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import com.majinnaibu.minecraft.plugins.mobscores.listeners.MobDeathListener;
import com.majinnaibu.minecraft.plugins.mobscores.listeners.PlayerConnectListener;
import org.bukkit.entity.Entity; import com.majinnaibu.minecraft.plugins.scorekeeper.ScoreKeeperPlugin;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player; import net.kyori.adventure.text.Component;
import org.bukkit.plugin.PluginManager; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.entity.Entity;
import com.majinnaibu.minecraft.plugins.mobscores.listeners.MobDeathListener; import org.bukkit.entity.EntityType;
import com.majinnaibu.minecraft.plugins.mobscores.listeners.PlayerConnectListener; import org.bukkit.entity.Player;
import com.majinnaibu.minecraft.plugins.scorekeeper.ScoreKeeperPlugin; import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
public class MobScoresPlugin extends JavaPlugin {
private static final String SCORE_BUCKET_ID = "mob-scores"; public class MobScoresPlugin extends JavaPlugin {
private static final String MIN_SCORE_KEEPER_VERSION = "0.2.2"; 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 Map<UUID, UUID> _claimedMobs = new HashMap<UUID, UUID>();
private ScoreKeeperPlugin _scoreKeeper = null; 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 String _logPrefix = "[MobScores] ";
.append(Component.text("MobScores").color(NamedTextColor.AQUA)) private final Component _messagePrefix =
.append(Component.text("] ").color(NamedTextColor.WHITE)); Component.text("[")
.append(Component.text("MobScores").color(NamedTextColor.AQUA))
@Override .append(Component.text("] ").color(NamedTextColor.WHITE));
public void onDisable() {
getConfig().set("ScoreTable", serializeScoreTable(_scoreTable)); @Override
saveConfig(); public void onDisable() {
} getConfig().set("ScoreTable", serializeScoreTable(_scoreTable));
saveConfig();
@Override }
public void onEnable() {
// Create the default config if it doesn't exist. @Override
saveDefaultConfig(); public void onEnable() {
// Create the default config if it doesn't exist.
// Load our score table from config or set defaults. saveDefaultConfig();
if (getConfig().isConfigurationSection("ScoreTable")) {
_scoreTable = deserializeScoreTable(getConfig().getConfigurationSection("ScoreTable").getValues(false)); // Load our score table from config or set defaults.
} else { if (getConfig().isConfigurationSection("ScoreTable")) {
_scoreTable = getDefaultScoreTable(); _scoreTable =
getConfig().set("ScoreTable", serializeScoreTable(_scoreTable)); deserializeScoreTable(getConfig().getConfigurationSection("ScoreTable").getValues(false));
saveConfig(); } else {
} _scoreTable = getDefaultScoreTable();
getConfig().set("ScoreTable", serializeScoreTable(_scoreTable));
PluginManager pm = getServer().getPluginManager(); saveConfig();
ScoreKeeperPlugin scoreKeeper = (ScoreKeeperPlugin)pm.getPlugin("ScoreKeeper"); }
if (scoreKeeper == null) { PluginManager pm = getServer().getPluginManager();
logWarning("Unable to find ScoreKeeper plugin."); ScoreKeeperPlugin scoreKeeper = (ScoreKeeperPlugin) pm.getPlugin("ScoreKeeper");
pm.disablePlugin(this);
return; if (scoreKeeper == null) {
} logWarning("Unable to find ScoreKeeper plugin.");
pm.disablePlugin(this);
String scoreKeeperVersion = scoreKeeper.getPluginMeta().getVersion(); return;
if (!isVersionAtLeast(scoreKeeperVersion, MIN_SCORE_KEEPER_VERSION)) { }
logWarning(
"ScoreKeeper " String scoreKeeperVersion = scoreKeeper.getPluginMeta().getVersion();
+ MIN_SCORE_KEEPER_VERSION if (!isVersionAtLeast(scoreKeeperVersion, MIN_SCORE_KEEPER_VERSION)) {
+ " or newer is required; found " logWarning(
+ scoreKeeperVersion "ScoreKeeper "
+ "."); + MIN_SCORE_KEEPER_VERSION
pm.disablePlugin(this); + " or newer is required; found "
return; + scoreKeeperVersion
} + ".");
_scoreKeeper = scoreKeeper; pm.disablePlugin(this);
if (_scoreKeeper.getBucket(SCORE_BUCKET_ID) == null) { return;
_scoreKeeper.createBucket(SCORE_BUCKET_ID, "mob point", "mob points", 0); }
} _scoreKeeper = scoreKeeper;
if (_scoreKeeper.getBucket(SCORE_BUCKET_ID) == null) {
pm.registerEvents(new MobDeathListener(this), this); _scoreKeeper.createBucket(SCORE_BUCKET_ID, "mob point", "mob points", 0);
pm.registerEvents(new PlayerConnectListener(this), this); }
logInfo(getPluginMeta().getName() + " version " + getPluginMeta().getVersion() + " is enabled!"); pm.registerEvents(new MobDeathListener(this), this);
} pm.registerEvents(new PlayerConnectListener(this), this);
private HashMap<EntityType, Integer> getDefaultScoreTable() { logInfo(
HashMap<EntityType, Integer> scores = new HashMap<EntityType, Integer>(); getPluginMeta().getName() + " version " + getPluginMeta().getVersion() + " is enabled!");
}
scores.put(EntityType.BLAZE, 100);
scores.put(EntityType.CAVE_SPIDER, 100); private HashMap<EntityType, Integer> getDefaultScoreTable() {
scores.put(EntityType.CREEPER, 50); HashMap<EntityType, Integer> scores = new HashMap<EntityType, Integer>();
scores.put(EntityType.DROWNED, 25);
scores.put(EntityType.ELDER_GUARDIAN, 250); scores.put(EntityType.BLAZE, 100);
scores.put(EntityType.ENDERMAN, 100); scores.put(EntityType.CAVE_SPIDER, 100);
scores.put(EntityType.ENDERMITE, 1000); scores.put(EntityType.CREEPER, 50);
scores.put(EntityType.END_CRYSTAL, 250); scores.put(EntityType.DROWNED, 25);
scores.put(EntityType.GHAST, 250); scores.put(EntityType.ELDER_GUARDIAN, 250);
scores.put(EntityType.GIANT, 250); scores.put(EntityType.ENDERMAN, 100);
scores.put(EntityType.GUARDIAN, 250); scores.put(EntityType.ENDERMITE, 1000);
scores.put(EntityType.HUSK, 25); scores.put(EntityType.END_CRYSTAL, 250);
scores.put(EntityType.PILLAGER, 25); scores.put(EntityType.GHAST, 250);
scores.put(EntityType.RAVAGER, 50); scores.put(EntityType.GIANT, 250);
scores.put(EntityType.SHULKER, 50); scores.put(EntityType.GUARDIAN, 250);
scores.put(EntityType.SILVERFISH, 0); scores.put(EntityType.HUSK, 25);
scores.put(EntityType.SKELETON, 50); scores.put(EntityType.PILLAGER, 25);
scores.put(EntityType.WITHER_SKELETON, 100); scores.put(EntityType.RAVAGER, 50);
scores.put(EntityType.ZOMBIE, 25); scores.put(EntityType.SHULKER, 50);
scores.put(EntityType.SLIME, 100); scores.put(EntityType.SILVERFISH, 0);
scores.put(EntityType.SPIDER, 50); scores.put(EntityType.SKELETON, 50);
scores.put(EntityType.WITHER_SKELETON, 100);
return scores; scores.put(EntityType.ZOMBIE, 25);
} scores.put(EntityType.SLIME, 100);
scores.put(EntityType.SPIDER, 50);
public void claimMob(Entity entity, Player damager) {
if (_scoreTable.containsKey(entity.getType())) { return scores;
_claimedMobs.put(entity.getUniqueId(), damager.getUniqueId()); }
}
} public void claimMob(Entity entity, Player damager) {
if (_scoreTable.containsKey(entity.getType())) {
public void awardScore(Entity entity) { _claimedMobs.put(entity.getUniqueId(), damager.getUniqueId());
UUID playerId = _claimedMobs.remove(entity.getUniqueId()); }
if(playerId != null){ }
EntityType type = entity.getType();
if (_scoreTable.containsKey(type)) { public void awardScore(Entity entity) {
Player player = getServer().getPlayer(playerId); UUID playerId = _claimedMobs.remove(entity.getUniqueId());
if (player == null) { if (playerId != null) {
logWarning("Unable to award score because the credited player is offline."); EntityType type = entity.getType();
return; if (_scoreTable.containsKey(type)) {
} Player player = getServer().getPlayer(playerId);
int score = _scoreTable.get(type); if (player == null) {
_scoreKeeper.addScore(player, SCORE_BUCKET_ID, score); logWarning("Unable to award score because the credited player is offline.");
} else { return;
logWarning("Unable to award score for {" + type.toString() + "}"); }
} int score = _scoreTable.get(type);
} _scoreKeeper.addScore(player, SCORE_BUCKET_ID, score);
} } else {
logWarning("Unable to award score for {" + type.toString() + "}");
public void sendPlayerScoreTable(Player player) { }
for (Map.Entry<EntityType, Integer> entry : _scoreTable.entrySet()) { }
if (entry.getValue() != 0) { }
EntityType type = entry.getKey();
Component message = Component.translatable(type.translationKey()) public void sendPlayerScoreTable(Player player) {
.append(Component.text(" = ")) for (Map.Entry<EntityType, Integer> entry : _scoreTable.entrySet()) {
.append(Component.text(String.valueOf(entry.getValue())) if (entry.getValue() != 0) {
.color(NamedTextColor.GREEN)); EntityType type = entry.getKey();
sendPlayerMessage(player, message); Component message =
} Component.translatable(Objects.requireNonNull(type.translationKey()))
} .append(Component.text(" = "))
} .append(
Component.text(Objects.requireNonNull(String.valueOf(entry.getValue())))
private Map<EntityType, Integer> deserializeScoreTable(Map<String, Object> rawValue) { .color(NamedTextColor.GREEN));
HashMap<EntityType, Integer> scoreTable = new HashMap<>(); sendPlayerMessage(player, message);
if (rawValue instanceof Map) { }
scoreTable = new HashMap<>(); }
for (Map.Entry<String, Object> entry : rawValue.entrySet()) { }
try {
EntityType type = EntityType.valueOf(entry.getKey()); private Map<EntityType, Integer> deserializeScoreTable(Map<String, Object> rawValue) {
Integer value = ((Number) entry.getValue()).intValue(); HashMap<EntityType, Integer> scoreTable = new HashMap<>();
scoreTable.put(type, value); if (rawValue instanceof Map) {
} catch (Exception ex) { scoreTable = new HashMap<>();
logError(ex); for (Map.Entry<String, Object> entry : rawValue.entrySet()) {
} try {
} EntityType type = EntityType.valueOf(entry.getKey());
return scoreTable; Integer value = ((Number) entry.getValue()).intValue();
} else { scoreTable.put(type, value);
return getDefaultScoreTable(); } catch (Exception ex) {
} logError(ex);
} }
}
private Map<String, Integer> serializeScoreTable(Map<EntityType, Integer> scoreTable) { return scoreTable;
Map<String, Integer> serializable = new HashMap<>(); } else {
for (Map.Entry<EntityType, Integer> entry : scoreTable.entrySet()) { return getDefaultScoreTable();
serializable.put(entry.getKey().name(), entry.getValue()); }
} }
return serializable;
} private Map<String, Integer> serializeScoreTable(Map<EntityType, Integer> scoreTable) {
Map<String, Integer> serializable = new HashMap<>();
public void sendPlayerMessage(Player player, Component message) { for (Map.Entry<EntityType, Integer> entry : scoreTable.entrySet()) {
player.sendMessage(_messagePrefix.append(message)); serializable.put(entry.getKey().name(), entry.getValue());
} }
return serializable;
public void sendPlayerMessage(Player player, String message) { }
player.sendMessage(_messagePrefix.append(Component.text(message)));
} public void sendPlayerMessage(Player player, Component message) {
player.sendMessage(_messagePrefix.append(Objects.requireNonNull(message)));
public void logError(Exception ex) { }
getLogger().log(Level.SEVERE, _logPrefix + ex.toString());
} public void sendPlayerMessage(Player player, String message) {
player.sendMessage(_messagePrefix.append(Component.text(Objects.requireNonNull(message))));
public void logInfo(String messag) { }
getLogger().info(_logPrefix + messag);
} public void logError(Exception ex) {
getLogger().log(Level.SEVERE, _logPrefix + ex.toString());
public void logWarning(String message) { }
getLogger().warning(_logPrefix + message);
} public void logInfo(String messag) {
getLogger().info(_logPrefix + messag);
private boolean isVersionAtLeast(String version, String minimumVersion) { }
int[] actual = parseVersion(version);
int[] minimum = parseVersion(minimumVersion); public void logWarning(String message) {
if (actual == null || minimum == null) { getLogger().warning(_logPrefix + message);
return false; }
}
for (int index = 0; index < actual.length; index++) { private boolean isVersionAtLeast(String version, String minimumVersion) {
if (actual[index] != minimum[index]) { int[] actual = parseVersion(version);
return actual[index] > minimum[index]; int[] minimum = parseVersion(minimumVersion);
} if (actual == null || minimum == null) {
} return false;
return !isPrerelease(version); }
} for (int index = 0; index < actual.length; index++) {
if (actual[index] != minimum[index]) {
private int[] parseVersion(String version) { return actual[index] > minimum[index];
if (version == null) { }
return null; }
} return !isPrerelease(version);
String coreVersion = version.replaceFirst("^[vV]", ""); }
int metadataIndex = coreVersion.indexOf('+');
if (metadataIndex >= 0) { private int[] parseVersion(String version) {
coreVersion = coreVersion.substring(0, metadataIndex); if (version == null) {
} return null;
int prereleaseIndex = coreVersion.indexOf('-'); }
if (prereleaseIndex >= 0) { String coreVersion = version.replaceFirst("^[vV]", "");
coreVersion = coreVersion.substring(0, prereleaseIndex); int metadataIndex = coreVersion.indexOf('+');
} if (metadataIndex >= 0) {
String[] parts = coreVersion.split("\\."); coreVersion = coreVersion.substring(0, metadataIndex);
if (parts.length < 3) { }
return null; int prereleaseIndex = coreVersion.indexOf('-');
} if (prereleaseIndex >= 0) {
try { coreVersion = coreVersion.substring(0, prereleaseIndex);
return new int[] { }
Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2]) String[] parts = coreVersion.split("\\.");
}; if (parts.length < 3) {
} catch (NumberFormatException ex) { return null;
return null; }
} try {
} return new int[] {
Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2])
private boolean isPrerelease(String version) { };
String release = version.replaceFirst("^[vV]", ""); } catch (NumberFormatException ex) {
int metadataIndex = release.indexOf('+'); return null;
if (metadataIndex >= 0) { }
release = release.substring(0, metadataIndex); }
}
return release.indexOf('-') >= 0; 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,66 +1,64 @@
/* /*
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 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 the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (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 but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details. GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License 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; package com.majinnaibu.minecraft.plugins.mobscores.listeners;
import org.bukkit.entity.Entity; import com.majinnaibu.minecraft.plugins.mobscores.MobScoresPlugin;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile; import org.bukkit.entity.Entity;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.entity.Projectile;
import org.bukkit.event.entity.EntityDeathEvent; import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityExplodeEvent; import org.bukkit.event.Listener;
import org.bukkit.event.EventHandler; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import com.majinnaibu.minecraft.plugins.mobscores.MobScoresPlugin; import org.bukkit.event.entity.EntityExplodeEvent;
public class MobDeathListener implements Listener { public class MobDeathListener implements Listener {
private MobScoresPlugin _plugin = null; private MobScoresPlugin _plugin = null;
public MobDeathListener(MobScoresPlugin plugin) { public MobDeathListener(MobScoresPlugin plugin) {
_plugin = plugin; _plugin = plugin;
} }
@EventHandler @EventHandler
public void onEntityDamage(EntityDamageEvent event) { public void onEntityDamage(EntityDamageEvent event) {
if (!(event instanceof EntityDamageByEntityEvent damageEvent)) { if (!(event instanceof EntityDamageByEntityEvent damageEvent)) {
return; return;
} }
Entity damager = damageEvent.getDamager(); Entity damager = damageEvent.getDamager();
Player creditedPlayer = null; Player creditedPlayer = null;
if (damager instanceof Player directPlayer) { if (damager instanceof Player directPlayer) {
creditedPlayer = directPlayer; creditedPlayer = directPlayer;
} else if (damager instanceof Projectile projectile } else if (damager instanceof Projectile projectile
&& projectile.getShooter() instanceof Player projectileShooter) { && projectile.getShooter() instanceof Player projectileShooter) {
creditedPlayer = projectileShooter; creditedPlayer = projectileShooter;
} }
if (creditedPlayer != null) { if (creditedPlayer != null) {
_plugin.claimMob(event.getEntity(), creditedPlayer); _plugin.claimMob(event.getEntity(), creditedPlayer);
} }
} }
@EventHandler @EventHandler
public void onEntityDeath(EntityDeathEvent event) { public void onEntityDeath(EntityDeathEvent event) {
_plugin.awardScore(event.getEntity()); _plugin.awardScore(event.getEntity());
} }
@EventHandler @EventHandler
public void onEntityExplode(EntityExplodeEvent event) { public void onEntityExplode(EntityExplodeEvent event) {}
} }
}

View File

@@ -1,37 +1,37 @@
/* /*
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 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 the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (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 but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details. GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License 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; package com.majinnaibu.minecraft.plugins.mobscores.listeners;
import org.bukkit.event.player.PlayerJoinEvent; import com.majinnaibu.minecraft.plugins.mobscores.MobScoresPlugin;
import org.bukkit.event.Listener;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import com.majinnaibu.minecraft.plugins.mobscores.MobScoresPlugin; import org.bukkit.event.player.PlayerJoinEvent;
public class PlayerConnectListener implements Listener { public class PlayerConnectListener implements Listener {
private MobScoresPlugin _plugin = null; private MobScoresPlugin _plugin = null;
public PlayerConnectListener(MobScoresPlugin plugin) { public PlayerConnectListener(MobScoresPlugin plugin) {
_plugin = plugin; _plugin = plugin;
} }
@EventHandler @EventHandler
public void onPlayerJoin(PlayerJoinEvent event) { public void onPlayerJoin(PlayerJoinEvent event) {
_plugin.sendPlayerScoreTable(event.getPlayer()); _plugin.sendPlayerScoreTable(event.getPlayer());
} }
} }

View File

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

View File

@@ -40,8 +40,24 @@ load_project_env
MODRINTH_TOKEN="${MODRINTH_TOKEN:-}" MODRINTH_TOKEN="${MODRINTH_TOKEN:-}"
MODRINTH_PROJECT_ID="${MODRINTH_PROJECT_ID:-mob-scores}" MODRINTH_PROJECT_ID="${MODRINTH_PROJECT_ID:-mob-scores}"
MODRINTH_CHANGELOG="${MODRINTH_CHANGELOG:-Auto release via publish-modrinth.sh}" generate_changelog() {
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}" 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_LOADERS="${MODRINTH_LOADERS:-paper}"
MODRINTH_VERSION_TYPE="${MODRINTH_VERSION_TYPE:-}" MODRINTH_VERSION_TYPE="${MODRINTH_VERSION_TYPE:-}"
MODRINTH_FILE="${MODRINTH_FILE:-}" MODRINTH_FILE="${MODRINTH_FILE:-}"