Files
ScoreKeeper/Project.md

14 KiB
Raw Permalink Blame History

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 21, Bukkit/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 1.21.7 (Bukkit + Adventure APIs)
Build tool Gradle 8.14.3 (gradlew)
Linting Spotless (googleJavaFormat() + license header from config/license-header.txt)
Release net.researchgate.release plugin; tags v$version; rejects snapshot deps except paper-api
External deps None at runtime. paper-api:1.21.7-R0.1-SNAPSHOT is compileOnly only.
CI/release entry point ./gradlew spotlessCheck build then ./gradlew release

Build quickly

./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 (Bukkit 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

./gradlew assemble           # produces ScoreKeeper.jar in build/libs/

Run locally (Paper server)

  1. Download Paper 1.21.7 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