# 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 ```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] │ ├── ScoreSubtractCommand.java → /score-subtract [player] │ ├── 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 ← 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 _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` 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] ` | 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] ` | 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 `` 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 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` 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 |