Reorganizes docs.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled

This commit is contained in:
2026-09-28 00:16:28 -07:00
parent 8a37231777
commit 4bc838cbe7
6 changed files with 0 additions and 0 deletions

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

@@ -0,0 +1,55 @@
# ScoreKeeper Human Testing
Use this checklist on a disposable Paper 26.2 or 26.3 server. ScoreKeeper stores data in its plugin folder and interacts with online players, command senders, and scheduled high-score runs; a passing build alone does not verify these in-game workflows.
## Test Setup
- Use Java 25 or newer and a backed-up test server. Keep the server log available throughout testing.
- Create at least two test players: one operator/admin and one ordinary player. Use a console or RCON session for console-sender cases.
- Before each run, preserve `plugins/ScoreKeeper/scores.yml` and `archives.yml`, or use a fresh test server. Archive, reset, and bucket-wide commands intentionally change scores.
- Record command sender, arguments, expected and observed messages, and score values before and after each operation.
## Human Checklist
### Startup and score commands
- Start Paper and confirm ScoreKeeper enables, registers every documented command, and loads existing data without errors.
- As a player, run `/score-get`, `/score-add`, `/score-subtract`, and `/score-reset`; check the displayed values and the configured initial value. Repeat with console/RCON, supplying a player name where required.
- Try zero, negative, very large, missing, and non-numeric amounts. Confirm invalid input is handled clearly and does not unexpectedly alter a score.
- Use two players to verify each player's score is independent. Change a player's Minecraft name if practical, then confirm their score remains associated with their UUID.
- Check singular/plural unit labels at values of 1, 0, and multiple units, and confirm messages show the correct bucket and value.
### Buckets and reporting
- Create a test bucket with a nonzero initial value and distinctive singular/plural labels. Verify invalid IDs, blank labels, and duplicate IDs are rejected cleanly.
- Switch one player to the bucket, add and subtract points, and confirm other players remain in their own active bucket. Switch back and verify the original score is unchanged.
- Change the server-wide default with `/score-bucket all <bucketId>`. Confirm individual assignments are cleared, online players receive the appropriate change notice, and new players use the new default.
- For each reporting policy (`none`, `player`, `admin`, `global`), change a score and verify exactly the intended audience receives the report. Test with an operator, a `scorekeeper.admin` player, an ordinary player, and the scored player.
- Exercise bucket creation, switching, and administration as authorized and unauthorized senders. The README says archive, bucket administration, and run management require `scorekeeper.admin`; verify the behavior on the target server.
- Note that the README says score get/add/subtract/reset currently have no permission checks and can target any online player. Confirm that exposure is acceptable for the server before enabling these commands publicly.
### Archives
- Archive one player's active-bucket score, then inspect `/score-archive-list` and `/score-archive-list <archiveId>`. Confirm the saved value and bucket are correct and the live score returns to that bucket's initial value.
- Repeat for `/score-archive bucket <bucketId>` and `/score-archive all`. Confirm only the selected bucket or all buckets are archived and reset as requested.
- Check archive IDs are listed newest first, then restart and confirm archives and their contents remain available.
- Try an unknown archive ID or bucket ID and verify a useful failure without unrelated scores being changed.
### High-score runs
- Start a target-score run and a timed run using a test bucket and at least three players. Confirm the run starts with the requested top-N and reset setting.
- Change scores for players inside and outside the current top-N. Verify individual score-change messages and global reporting for current top-N players match the run behavior.
- End runs by reaching the target, expiration, and `/score-run stop`. Also start an `until-disabled` run and stop it manually.
- Check final ranking order, ties, each online player's reported place, and the displayed top-N cutoff.
- With reset enabled, verify scores begin at the bucket initial value and pre-run scores are restored after completion. With reset disabled, confirm scores remain as earned.
- Restart while a run is active. Confirm the run and reset snapshot survive, timed runs expire at the intended time, and stopping or completing the run produces one final result rather than duplicate results.
### Persistence and upgrades
- Restart after changing scores, bucket definitions, reporting policies, the default bucket, and per-player bucket assignments. Verify all values and assignments persist.
- Test a clean data folder and an existing/older `scores.yml` or `archives.yml` on a backed-up server. Confirm startup handles defaults, known legacy data, and malformed or unknown entries without silently corrupting valid data.
- Check saved files after a clean shutdown and after a forced test restart; distinguish expected last-save behavior from data loss.
## Automated Coverage
There are currently no Java test sources or test dependencies in this project, and no integration-test source set. The Gradle `test` task therefore does not currently exercise ScoreKeeper behavior. All scenarios above should be treated as unverified until tested in-game; the “Ready” test tasks in `TODO.md` describe work that has not yet been added.

View File

@@ -0,0 +1,49 @@
**ScoreKeeper Plugin**
ScoreKeeper tracks player scores in configurable buckets. Use it for event points, competitions, or other server-side scoring systems, and integrate it with other plugins through the public API.
## Commands
Players can omit an optional player name to target themselves. Console and RCON must provide a player name for score get/add/subtract/reset commands.
* `/score-get [playerName]` - Displays the target player's score in their active bucket.
* `/score-add <amount>` - Adds an amount to your score in your active bucket.
* `/score-add <playerName> <amount>` - Adds an amount to the target player's score.
* `/score-subtract <amount>` - Subtracts an amount from your score in your active bucket.
* `/score-subtract <playerName> <amount>` - Subtracts an amount from the target player's score.
* `/score-reset [playerName]` - Resets the target player's active-bucket score to that bucket's initial value.
* `/score-archive` - Archives and resets your score in your active bucket.
* `/score-archive <playerName>` - Archives and resets the target player's score in their active bucket.
* `/score-archive all` - Archives and resets scores in every bucket.
* `/score-archive bucket <bucketId>` - Archives and resets every score in one bucket.
* `/score-archive-list` - Lists saved archive IDs, newest first.
* `/score-archive-list <archiveId>` - Displays the saved scores in one archive.
* `/score-bucket` - Displays your active bucket.
* `/score-bucket <bucketId>` - Switches your active bucket.
* `/score-bucket create <id> <singular> <plural> <initialValue> [reporting]` - Creates a bucket. Reporting can be `admin`, `global`, `player`, or `none` (default).
* `/score-bucket <playerName> <bucketId>` - Switches a player's active bucket.
* `/score-bucket all <bucketId>` - Changes the server's default bucket and clears individual bucket assignments.
* `/score-run start <bucketId> <topN> <reset:true|false> target <score>` - Starts a run that ends when a score reaches the target.
* `/score-run start <bucketId> <topN> <reset:true|false> duration <time>` - Starts a timed run. Use a positive number and lowercase `s`, `m`, `h`, `d`, or `w`, such as `4h`.
* `/score-run start <bucketId> <topN> <reset:true|false> until-disabled` - Starts a run with no automatic end.
* `/score-run stop <bucketId>` - Ends an active run.
## Permissions
The `scorekeeper.admin` permission (default: operators) is required for archive commands, bucket creation/administration, and high-score run management. Score get/add/subtract/reset and archive-list commands currently have no permission checks; add, subtract, and reset can target any online player.
## Features
* Create score buckets with custom singular/plural labels, initial values, and reporting policies.
* Reporting can notify operators/admins, everyone online, only the scored player, or nobody.
* Start target-score, timed, or until-disabled high-score runs with configurable top-N standings and optional score resets.
* Archive player, bucket, or all-bucket scores; archived scores reset to their bucket's initial value.
* Persist bucket definitions, assignments, scores, active runs, and archives in the plugin's data folder.
## API
Other plugins can create buckets with `createBucket(id, singular, plural, initialValue, reporting)`, use bucket-aware score methods, and start or stop high-score runs with `startTargetScoreRun`, `startTimedScoreRun`, `startUntilDisabledScoreRun`, and `stopHighScoreRun`.
## Compatibility
Built exclusively for Paper 26.2 and 26.3 with the Paper API and Adventure APIs. Bukkit and Spigot servers are not supported.

13
docs/modrinth.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "Score Keeper",
"url": "https://modrinth.com/plugin/scorekeeper",
"summary": "Track player scores in configurable buckets with custom reporting, high-score runs, and archives. Includes in-game commands and an API for other plugins.",
"visibility": "public",
"links": {
"issueTracker": "https://github.com/headhunter45/ScoreKeeper/issues",
"sourceCode": "https://github.com/headhunter45/ScoreKeeper",
"wikiPage": null,
"discordInvite": null,
"donationLinks": []
}
}

View File

@@ -0,0 +1,23 @@
{
"id": "scorekeeper",
"title": "ScoreKeeper",
"description": "A reusable Paper 26.2/26.3 scoring engine with named buckets, custom reporting, persistent archives, high-score runs, player commands, and a public API for companion plugins.",
"docId": "ScoreKeeper",
"image": {
"url": "https://image.pollinations.ai/prompt/pixel%20art%20Minecraft%20server%20scoreboard%20with%20a%20glowing%20diamond%20and%20number%20counters%2C%20clean%20square%20game%20plugin%20thumbnail%2C%20no%20text",
"alt": "Pixel art scoreboard with a diamond for the ScoreKeeper Minecraft plugin",
"prompt": "Pixel art Minecraft server scoreboard with a glowing diamond and number counters, clean square game plugin thumbnail, no text",
"width": 1024,
"height": 1024,
"backgroundClassName": "bg-surface"
},
"url": "/projects/scorekeeper",
"tags": [],
"source": {
"type": "",
"url": ""
},
"feedback": {
"url": ""
}
}

View File

@@ -0,0 +1,12 @@
# ScoreKeeper
ScoreKeeper is a reusable scoring engine for Paper 26.2 and 26.3. It gives events, competitions, and companion plugins a shared language for points: named buckets can have custom singular/plural labels, initial values, reporting policies, archives, and high-score runs.
The plugin is both a player-facing command system and an extension point. Administrators can start target-based, timed, or manually stopped score runs, while other plugins can create buckets, award points, archive results, and build richer game loops through the public API. Scores, assignments, runs, and archives persist across server restarts. Bukkit and Spigot servers are not supported.
## Engineering Notes
- Built with Gradle and Java 25 for modern Paper.
- Uses UUID-based player identity and YAML-backed persistence.
- Includes server-free JUnit coverage for domain rules, commands, reporting, archives, and lifecycle behavior.
- Designed as the scoring foundation used by companion plugins such as MobScores.

280
docs/project-reference.md Normal file
View File

@@ -0,0 +1,280 @@
# ScoreKeeper — Project Reference
> **For a developer joining the team cold.** Read top-to-bottom in ~8 minutes;
> you'll know where to look and what's real vs. planned on day one.
---
## 1. What is ScoreKeeper?
ScoreKeeper is a **Paper Minecraft plugin** (Java 25, Paper/Adventure APIs) that manages player
score tracking. It exposes five in-game/console commands (`/score-get`, `/score-add`,
`/score-subtract`, `/score-reset`, `/score-archive`) for manual score manipulation — no events,
no timers, no automation.
**State of the project:** live in-memory scores work today; the \"high-score table\" advertised
in the README name and `/score-archive` description is **not implemented**. Persistence, ranking,
and scoring formulas are all **future scope**. See §4 for the gap between *what exists* and
*what is planned*.
---
## 2. Tech Stack & Build
| Item | Value |
|------|-------|
| Language | Java 21 (Gradle auto-downloads toolchain) |
| Runtime target | Paper 26.2 and 26.3 |
| Build tool | Gradle 8.14.3 (`gradlew`) |
| Linting | Spotless (`googleJavaFormat()` + license header from `config/license-header.txt`) |
| Versioning | `org.danilopianini.git-sensitive-semantic-versioning`; minimum version `0.2.1`; reads `vX.Y.Z` tags |
| External deps | **None at runtime.** The Paper API is `compileOnly` only. |
| CI/release entry point | Run `./gradlew spotlessCheck build`, tag the release commit (for example, `git tag -a v0.2.2 -m "Release v0.2.2"`), then rebuild to produce the stable release artifact |
### Build quickly
```bash
./gradlew build # compiles, runs tests (none yet)
./gradlew spotlessApply # format to project style
./gradlew assemble # produces jar in build/libs/
```
Drop the resulting jar onto a Paper server's `plugins/` directory.
---
## 3. Architecture Overview
### Directory layout
```
├── src/main/java/com/majinnaibu/minecraft/plugins/scorekeeper/
│ ├── ScoreKeeperPlugin.java ← entry point, onEnable/onDisable, score CRUD core
│ └── commands/
│ ├── ScoreGetCommand.java → /score-get [player]
│ ├── ScoreAddCommand.java → /score-add [player] <amount>
│ ├── ScoreSubtractCommand.java → /score-subtract [player] <amount>
│ ├── ScoreResetCommand.java → /score-reset [player]
│ └── ScoreArchiveCommand.java → /score-archive [player] (stub)
├── src/main/resources/
│ └── plugin.yml ← command manifest + main-class declare
├── tools/bash/ & tools/powershell/ ← dev helper scripts
├── config/license-header.txt ← Spotless license header
├── build.gradle ← Gradle build config (above)
├── gradle.properties ← version, group coordinates
├── CONTRIBUTING.md ← dev env setup
└── README.md ← user-facing command reference + notes
```
### Data flow (text diagram)
```
ADMIN / CONSOLE / RCON ──▶ types a /score-* command
│
▼
┌──────────────────────┐
│ 5× Score*Command.java│ executors parse args,
│ resolves target │ resolvePlayerExact(name) (online-only)
└──────────┬───────────┘
│ calls
▼
┌──────────────────────────────┐
│ ScoreKeeperPlugin │
│ │
│ addScore / subtractScore │ read-modify-write
│ resetScore / setScore │
│ getScore (read) │
│ archiveScore (STUB — no-op) │
└──────────┬───────────────────┘
│
▼
HashMap<UUID, Integer> ← in RAM only
_playerScores ← ScoreKeeperPlugin:38
```
**Key architectural facts:**
- **Single class owns everything.** `ScoreKeeperPlugin` holds the scores map, score CRUD
methods, logging helpers (logWarning/logInfo/logError), and the Adventure component builder
for chat color. There is no service layer or boundary separation.
- **No event listeners.** Zero `@EventHandler`/`Listener` registrations across the codebase.
Game events (join, death, kill) do not affect scores. Only manual commands change state.
- **No scheduler or tick logic.** No periodic tasks, no countdowns, no automated scoring.
- **No permission nodes.** Every command is available to every sender (player, console, RCON).
### File: ScoreKeeperPlugin.java (`ScoreKeeperPlugin`)
- `main` class in `plugin.yml` (Paper plugin entry point)
- `HashMap<UUID, Integer> _playerScores` at line 38 — the sole score store
- `onEnable()` (line 52): wires five command executors, logs \"load not implemented\" warning
- `onDisable()` (line 46): logs \"save not implemented\" warning — scores lost on shutdown
- Score CRUD methods: `addScore`, `subtractScore`, `resetScore`, `setScore`, `getScore`,
`archiveScore` (lines 67–91, 76–86, 72–74)
- Private helpers: `getPlayerScore(Player)` (get-or-create at 0), `setPlayerScore(Player,int)`
---
## 4. Domain Model & Scoring Logic
### 4.1 What exists today (REAL — in code)
| Aspect | Detail | Source |
|--------|--------|--------|
| Score model | Single `int` per player, keyed by UUID | `ScoreKeeperPlugin:38` |
| Storage container | `HashMap<UUID, Integer>` on plugin instance | same file |
| Default value | `0` — lazy-created on first map access | `getPlayerScore:100-106` |
| Live only | **No persistence.** Scores erased on server restart | `onEnable:59`, `onDisable:47` |
| Scoring direction | Any integer (negatives allowed, no floor) | add/subtract are raw `+`/`-` |
| Recording method | Manual commands only — `/score-add`, `/score-subtract` | command executors |
| Auto-scoring | **None** — no events, no timers | proven by grep across src/ |
| Permissions | **None declared.** All commands open to all senders | `plugin.yml`, no permission guard in code |
### 4.2 Open Score Lifecycle (what exists + what is planned)
```
Stage 1. First access — getPlayerScore() auto-creates key at 0 [REAL]
2. Admin runs /score-add player N or /score-subtract [REAL]
3. Player accumulates points over the session [REAL]
4. Read via /score-get (read-only, lazily registers) [REAL]
5. Server restart — scores LOST on shutdown [REAL]
6. Intended: /score-archive freezes score → table [GAP ✓ not built]
7. Intended: high-score table display command [GAP ✓ not built]
```
### 4.3 The "high-score table" — status
| Feature | Status | Details |
|---------|--------|---------|
| `/score-archive` | **Stub** | Command prints \"archive command unimplemented\"; `archiveScore()` method only logs, never writes a table or resets the player's score |
| Persistence (save) | **Not built** | `onDisable()` is a TODO stub — map discarded at shutdown |
| Persistence (load) | **Not built** | `onEnable()` is a TODO stub — map always starts empty `{}` |
| Sorting / ranking | **Not coded** | No sort, no tie-breaking, no entry cap, no decay logic exists |
| Scoring formulas | **Not coded** | Scores are plain integer accumulators (`Σ(adds) − Σ(subtracts)`) |
**Design decisions to be made (none answered by code today):**
- Storage format for the table (YAML per Bukkit convention; JSON? SQLite?)
- What an entry looks like (name + score + timestamp? name is not stored with score today)
- Sort order and tie-breaking strategy
- Max entries / leaderboard cap
- Whether `/score-archive` also resets the live score (README says it does)
---
## 5. Plugin Integration & Reference Table
### 5.1 Command → Handler → Effect on score data
| Trigger | Usage | Handler | Effect |
|---------|-------|---------|--------|
| `/score-get [player]` | Self or other | `ScoreGetCommand.java:38-84`, delegated to `getScore → getPlayerScore` | **Read-only.** Returns integer. Lazily creates entry at `0` if unseen. |
| `/score-add [player] <N>` | Self (omit name) or target others | `ScoreAddCommand.java` | `score += N`. Amount must parse as int. No direction validation (negative N still adds). |
| `/score-subtract [player] <N>` | Same | `ScoreSubtractCommand.java` | `score -= N`. No minimum clamping; negatives freely produced. |
| `/score-reset [player]` | Same | `ScoreResetCommand.java` | `score = 0`. Key created at `0` if absent. |
| `/score-archive [player]` | Same | `ScoreArchiveCommand.java` | **No-op.** Prints \"archive command unimplemented\". Does NOT call the `archiveScore()` method. |
### 5.2 Shared behavior details
Every executor follows this pattern:
1. Parse arguments — if `split.length == 1` and sender is a player, target = self (RCON/console
requires an explicit `<playerName>` or prints usage).
2. Resolve target player via `server.getPlayerExact(name)` — **exact, case-sensitive, online-only.**
3. Call the corresponding `ScoreKeeperPlugin` method.
4. Echo color-coded result; return `true`.
### 5.3 Inter-plugin / public API surface
`ScoreKeeperPlugin` exposes these public methods that other plugins *could* call if they hold a reference
(but there is **no formal service registration**):
| Method | Visibility | Called by commands? | Notes |
|--------|------------|---------------------|-------|
| `addScore(Player, int)` | `public` | Yes (`/score-add`) | Read-modify-write on `_playerScores` |
| `subtractScore(Player, int)` | `public` | Yes (`/score-subtract`) | Same pattern |
| `getScore(Player)` | `public` | Yes (`/score-get`) | Wrapper around `getPlayerScore` |
| `setScore(Player, int)` | `public` | **No** | Internal write path only; not hooked to any command |
| `resetScore(Player)` | `public` | Yes (`/score-reset`) | Sets to `0` |
| `archiveScore(Player)` | `public` | **No** | Only logs a warning; never called by the archive command |
### 5.4 Concurrency note
The map is a plain `HashMap`. Safe because Paper dispatches commands on the server's single main
thread — but it is **not safe for off-thread use**. Any future event-driven scoring that runs
asynchronously could corrupt state via non-atomic read-modify-write.
---
## 6. Configuration Reference
| File | Format | Purpose |
|------|--------|---------|
| `plugin.yml` | YAML (Bukkit manifest) | Declares main class, api-version, five commands + descriptions/usage strings |
| `build.gradle` | Gradle Kotlin (Groovy DSL) | Dependencies, task config, release/spotless settings |
| `gradle.properties` | Properties | Project version and group coordinates (for Maven publishing) |
| **No `config.yml`** | — | ScoreKeeper has **no player-editable configuration**. |
| **No `permissions:` block** | — | No permission nodes declared; all commands are open. Plans say \"permissions coming after archive works.\" |
---
## 7. Running & Testing Locally
### Build
```bash
./gradlew assemble # produces ScoreKeeper.jar in build/libs/
```
### Run locally (Paper server)
1. Download Paper 26.2 or 26.3 from `https://papermc.io`
2. Copy the built jar into `plugins/`
3. Start the server, verify onEnable logs:
```
[ScoreKeeper] ScoreKeeper version X.Y.Z is enabled.
[ScoreKeeper] Unable to load scores from file. This feature is not implemented yet.
```
### Commands (in-game or via RCON/console)
| Test scenario | Command | Expected output |
|--------------|---------|-----------------|
| Check own score | `/score-get` | \"Your score is 0\" (creates entry at 0) |
| Add points to self | `/score-add 10` | \"You gained 10 points! Now have 10.\" |
| Subtract from self | `/score-subtract 5` | \"You lost 5 points! Now have 5.\" |
| Check another player | `/score-get PlayerName` | \"PlayerName's score is N.\" (must be online) |
| Reset own score | `/score-reset` | \"Your score has been reset to 0.\" |
| Archive (stub) | `/score-archive` | \"archive command unimplemented\" |
### Tests
- **No unit/integration tests exist yet.** `./gradlew test` runs an empty suite. Consider
adding tests for `ScoreKeeperPlugin`'s private `Player` score state mocking in a future task.
---
## 8. Where to Look First
| I want to understand… | Go to |
|----------------------|-------|
| The entire live data model | `ScoreKeeperPlugin:38` — one `HashMap<UUID, Integer>` field |
| How scores change (write path) | `addScore(/:67-70)`, `subtractScore(/:88-91)`, `setPlayerScore(/:108-110)` |
| How a player is looked up | `getPlayerExact(name)` online only — see any `*Command.java` line ~50 |
| First-time-player behavior | `getPlayerScore(/:100-106)` — auto-inserts `0` on first access |
| High-score / archive table | **Does not exist.** See `archiveScore(/:72-74)` stub; design decisions in §4.3 |
| Persistence (save/load) | `onDisable(/:47)`, `onEnable(/:59)` — both TODO, nothing writes to disk |
| What's declared/intended but not wired | `plugin.yml:18-20` (`/score-archive`), `README.md:11` and README notes |
| Command implementations | `commands/Score*Command.java` (all 5 handlers) |
| Build/runtime config | `build.gradle`, `plugin.yml`, `gradle.properties` |
| Dev env / CONTRIBUTING | `CONTRIBUTING.md` |
---
## Appendix: Risk Summary
| # | Issue | Impact | Section |
|---|-------|--------|---------|
| 1 | **Data loss on restart** — no save/load implemented | Every server boot wipes all scores (§4.1) | §4.1 |
| 2 | **No permissions** — any player can self-add points | Integrity of scoring is unenforceable today (§5.2) | §5.2 |
| 3 | **Race condition on add/subtract** — non-atomic read-modify-write on plain `HashMap` | Corrupt state if future event-driven scoring runs async (§5.4) | §5.4 |
| 4 | **No `int` overflow safety** — Java wrapping semantics apply | Undetectable score corruption near ±2.1 billion | §4.1 |
| 5 | **Name lookups online-only & case-sensitive** | Can't target offline players; \"Alice\" ≠ \"alice\" (§5.2) | §5.2 |