From 16be0055957b6c2737e7c15910863cec35278357 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Wed, 23 Sep 2026 19:57:17 -0700 Subject: [PATCH 1/7] Adds .worktrees to gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6ea6a0a..2793e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .gradle/ build/ target/ +.worktrees From 6d4cffed16952ff7d008a19906df02424ef734e2 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Wed, 23 Sep 2026 21:36:27 -0700 Subject: [PATCH 2/7] Adds project description docs. --- .gitignore | 3 ++- .vscode/settings.json | 15 +++++++++------ ProjectDescription.json | 23 +++++++++++++++++++++++ ProjectDescription.md | 16 ++++++++++++++++ 4 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 ProjectDescription.json create mode 100644 ProjectDescription.md diff --git a/.gitignore b/.gitignore index 2793e0a..7428a22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .gradle/ build/ target/ -.worktrees +.worktrees/ +bin/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 90d61f8..e9fad8d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,10 @@ { - "[java]": { - "editor.formatOnSave": true - }, - "java.configuration.updateBuildConfiguration": "interactive", - "java.compile.nullAnalysis.mode": "automatic" -} \ No newline at end of file + "[java]": { + "editor.formatOnSave": true + }, + "java.configuration.updateBuildConfiguration": "interactive", + "java.compile.nullAnalysis.mode": "automatic", + "chat.tools.terminal.autoApprove": { + "printf": true + } +} diff --git a/ProjectDescription.json b/ProjectDescription.json new file mode 100644 index 0000000..b8f4d3a --- /dev/null +++ b/ProjectDescription.json @@ -0,0 +1,23 @@ +{ + "id": "scorekeeper", + "title": "ScoreKeeper", + "description": "Paper Minecraft plugin for tracking and managing player scores.", + "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": "" + } +} diff --git a/ProjectDescription.md b/ProjectDescription.md new file mode 100644 index 0000000..c38bff5 --- /dev/null +++ b/ProjectDescription.md @@ -0,0 +1,16 @@ +# ScoreKeeper + +ScoreKeeper is a Paper Minecraft plugin for tracking player scores during server activities, events, or games. It keeps scores per player in memory and exposes commands for viewing, changing, resetting, and archiving scores. + +## Details + +- Built for Paper API 1.21.7 with Java 21 and Gradle. +- Registers `/score-get`, `/score-add`, `/score-subtract`, `/score-reset`, and `/score-archive`. +- Commands use the executing player when a player name is omitted. +- Scores are currently held in memory and are not persisted when the server stops. +- Score archiving is planned, but the current implementation reports that archiving is unavailable. +- The project is packaged as a Java plugin with a `plugin.yml` descriptor. + +## Image + +No project image asset is included in the repository. The JSON description includes a prompt-backed image URL for a future project thumbnail. From fefa4a5ecca424a2159b88afcdbfca5ed089d7bb Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Wed, 16 Jul 2025 05:01:34 -0700 Subject: [PATCH 3/7] Adds spotlessCheck and build to the release process. --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9ef68f2..46b4d4c 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,8 @@ publishing { } release { - buildTasks.addAll(['spotlessCheck', 'build']) +// buildTasks.addAll(['spotlessCheck', 'build']) + buildTasks = ['spotlessCheck', 'build'] failOnSnapshotDependencies = true ignoredSnapshotDependencies = [ "io.papermc.paper:paper-api" From 1e19b4cf4dcc421b82823e158b4accd548f7a831 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Thu, 24 Sep 2026 13:24:13 -0700 Subject: [PATCH 4/7] Document plugin integration and event/command wiring (t_f5d60eac) --- docs/plugin-integration-and-event-wiring.md | 166 ++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 docs/plugin-integration-and-event-wiring.md diff --git a/docs/plugin-integration-and-event-wiring.md b/docs/plugin-integration-and-event-wiring.md new file mode 100644 index 0000000..f7501a6 --- /dev/null +++ b/docs/plugin-integration-and-event-wiring.md @@ -0,0 +1,166 @@ +# ScoreKeeper — Plugin Integration & Event/Command Reference + +> Analysis for the ScoreKeeper codebase. Scope: how the plugin hooks into the +> Minecraft (Paper) server runtime — event listeners, command registrations, +> scheduled/tick logic, and inter-plugin/API surface — with a table mapping each +> trigger → handler class → effect on score data. +> +> Platform: **Paper** (`io.papermc.paper:paper-api:1.21.7-R0.1-SNAPSHOT`), Java 21, +> Gradle. Main class: `com.majinnaibu.minecraft.plugins.scorekeeper.ScoreKeeperPlugin`. + +--- + +## 1. TL;DR for a new developer + +ScoreKeeper is almost entirely **command-driven**. It registers **no Bukkit event +listeners**, declares **no `@EventHandler` methods, no `Listener` implementations, +and calls `getPluginManager().registerEvents(...)` nowhere**. It also schedules +**no timed tasks** (no `BukkitScheduler`/`BukkitRunnable`/tick logic). + +The only things that touch score state are **five console/chat commands** +(`score-get`, `score-add`, `score-subtract`, `score-reset`, `score-archive`). +Score state lives in an **in-memory `HashMap`** and is **not +persisted** — `onEnable`/`onDisable` have `TODO` stubs, so data is lost on restart. + +There is a **`highscore` / high-score table in the project name and the task +prompt, but no such command or logic exists yet** — it is unimplemented. + +--- + +## 2. Event listeners (Bukkit/Adventure) + +**None.** The plugin listens to no server events. Confirmed by absence of any of: +`@EventHandler`, `implements Listener`, `registerEvents(`, `PlayerJoinEvent`, +`PlayerQuitEvent`, `PlayerDeathEvent`, or any other event import/registration. + +| Server event | Handler | Effect on score data | +|---|---|---| +| PlayerJoin | *none* | none | +| PlayerQuit | *none* | none | +| PlayerDeath | *none* | none | +| (all other Bukkit events) | *none* | none | +| Custom/plugin-injected events | *none* | none | + +**Implication:** nothing about a player's score changes as a result of gameplay +events (joining, quitting, dying, scoring points in-game, etc.). The only way +score data changes is through the commands in §3. + +--- + +## 3. Command registrations & permissions + +Commands are declared in `src/main/resources/plugin.yml` (no `aliases`, no +`permissions:` block, and no per-command `permission:` — so **every command is +available to every sender with no permission gating**). Each is wired to an +executor in `ScoreKeeperPlugin.onEnable()`. + +Argument-resolution idiom shared by every handler: +`boolean rcon = !(sender instanceof Player);` — an RCON caller has no self +context, so the "target = self" shortcut is unavailable and a `` +must be supplied. + +`` is resolved with `server.getPlayerExact(name)` — an **exact, +case-sensitive lookup of currently-online players only**. Score operations +therefore apply to **online players**, and an unknown/offline name yields a +"Can't find a player with that name" error (except where noted). + +### Reference table — command → handler class → effect on score data + +| Trigger (command) | Usage | Handler class | Delegates to | Effect on score data | +|---|---|---|---|---| +| `score-get` | `/score-get [player]` | `ScoreGetCommand` | `ScoreKeeperPlugin.getScore(Player)` → `getPlayerScore` | **Read only.** Returns the player's current score. Side effect: lazily registers the player at `0` if unseen. No persistent change. | +| `score-add` | `/score-add [player] ` | `ScoreAddCommand` | `ScoreKeeperPlugin.addScore(Player,int)` | `score = old + amount` (integer `amount` in `[1..]`). | +| `score-subtract` | `/score-subtract [player] ` | `ScoreSubtractCommand` | `ScoreKeeperPlugin.subtractScore(Player,int)` | `score = old - amount` (can go negative). | +| `score-reset` | `/score-reset [player]` | `ScoreResetCommand` | `ScoreKeeperPlugin.resetScore(Player)` | `score = 0` (writes 0; key created if absent). | +| `score-archive` | `/score-archive [player]` | `ScoreArchiveCommand` | *nothing* | **No state change.** Handler prints `"archive command unimplemented"`. It does **not** call `ScoreKeeperPlugin.archiveScore(...)` — see §5. | + +### Command behaviour details + +- **Sender vs. target.** Single-arg form means "operate on the invoking player" + (`split.length == 1` → `targetPlayer = sender`). Two-arg form means + ` ` (add/subtract) or `` (get/reset). For an + RCON sender, the single-arg form is treated as missing a target and prints + usage instead. +- **Amount parsing.** `score-add`/`score-subtract` require an integer + `amount`; a non-integer prints `"amount must be an integer"`. +- **Error/usage messaging.** `echoError` (red) and `echoUsage` (colour-coded) + differ between player and RCON call sites (`/score-add` vs `score-add`). + All handlers `return true` (command handled). +- **Permissions.** None declared → no operator/permission requirement; any + player or RCON can run them. + +--- + +## 4. Scheduled tasks / tick-based logic + +**None.** Confirmed by absence of `getScheduler()`, `runTask`, +`BukkitRunnable`, `BukkitTask`, `scheduleSync*`, or any periodic/repeating +logic. ScoreKeeper runs no background or tick-driven work. + +--- + +## 5. Inter-plugin dependencies & API exposure + +- **Declared dependencies:** plugin.yml has no `depend`, `softdepend`, `load`, + or `load-before` → **no declared inter-plugin coupling**. +- **Build dependency:** the only third-party dependency is + `io.papermc.paper:paper-api` (marked `compileOnly`, and explicitly ignored as a + snapshot by the release plugin). No other plugin/API is referenced. +- **Public API surface.** `ScoreKeeperPlugin` is a plain `JavaPlugin` exposing + `public` methods that other plugins *could* call if they hold a reference: + - `void addScore(Player, int)` + - `void subtractScore(Player, int)` + - `int getScore(Player)` + - `void setScore(Player, int)` — **public but used by no command** (internal + write path; the only writer not reachable via a command). + - `void resetScore(Player)` + - `void archiveScore(Player)` — **public but called by nothing**; it only logs + a warning. The `/score-archive` command does **not** invoke it. + - `void sendMessage(CommandSender, Component)`, `logInfo/logWarning/logError` +- **No formal service registration.** There is no `registerService`/`asService` + (ServiceLoader), no dedicated API artifact, and no `api:` block in plugin.yml. + Exposure is by **public method surface only** — informal and not discoverable + by other plugins. +- **Persistence (unimplemented).** `onEnable` logs a warning that load-from-file + is unimplemented; `onDisable` logs that save-to-file is unimplemented. The + `HashMap` is the sole state store and is **forgotten on restart**. + +--- + +## 6. Lifecycle + +| Hook | What it does | +|---|---| +| `onEnable()` | Wires the five `score-*` command executors; logs the "load not implemented" warning and an enable log line. | +| `onDisable()` | Logs the "save not implemented" warning. **All in-memory scores are lost on shutdown.** | + +--- + +## 7. State model (quick reference) + +- **Store:** `private HashMap _playerScores` on `ScoreKeeperPlugin` + (one map field for the whole plugin). +- **Keying:** by `Player.getUniqueId()` (UUID), so scores are per-player and + survive name changes *within a single server run*; lost on restart. +- **Lazy init:** any read (including `get`/`score-get`) auto-creates the key at `0`. +- **Writers:** `addScore`, `subtractScore`, `resetScore`, `setScore`, all funneled + through `setPlayerScore(player, value)`. +- **Concurrency:** plain `HashMap`; safe today only because command dispatch + runs on the server's main thread. Not thread-safe for off-thread use. + +--- + +## 8. "Which server trigger causes which state change" — summary + +| Trigger | State change | +|---|---| +| `/score-add [player] ` | `_playerScores[uuid] += amount` | +| `/score-subtract [player] ` | `_playerScores[uuid] -= amount` | +| `/score-reset [player]` | `_playerScores[uuid] = 0` | +| `/score-get [player]` | none (reads; lazily registers at 0) | +| `/score-archive [player]` | none (not implemented) | +| Any Minecraft/Bukkit event (join/quit/death/etc.) | **none — no listeners exist** | +| Server tick / scheduled task | **none — no scheduler exists** | +| Server enable | no data change (registration + log only) | +| Server disable | in-memory scores discarded (no persistence) | +| `addScore`/`subtractScore`/`setScore`/`archiveScore` via the public API | same table where called; only reachable by code holding a plugin reference, not by in-game events or commands (except add/subtract/reset which do have commands) | From 03232e90f6db7bc76c7368a991415dabb489eef5 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Thu, 24 Sep 2026 13:46:12 -0700 Subject: [PATCH 5/7] docs: add domain analysis for score tracking & high-score table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Source-grounded write-up (docs/domain-score-tracking.md) covering the score-entry data model, recording mechanism, the (unimplemented) high-score table, and scoring formulas, with full file:line citations and edge-case walk-through. Key finding: live in-memory HashMap only; no persistence, no high-score table, no formula — the title's 'high-score table' is an aspirational stub (/score-archive unimplemented). --- docs/domain-score-tracking.md | 365 ++++++++++++++++++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 docs/domain-score-tracking.md diff --git a/docs/domain-score-tracking.md b/docs/domain-score-tracking.md new file mode 100644 index 0000000..1fa4ad8 --- /dev/null +++ b/docs/domain-score-tracking.md @@ -0,0 +1,365 @@ +# ScoreKeeper — Score-Tracking & "High-Score Table" Domain Analysis + +> **Scope of this document:** the domain logic that tracks player scores and (intended) the +> high-score table. Written for a new team member who must understand the scoring system +> *without opening the source*. Every claim below is traced to a file/line so it can be +> re-verified. +> +> **Bottom line up front (read this first):** ScoreKeeper currently tracks **live scores +> only, in memory, for the running server session**. It has **no persistence** and **no +> high-score table**. The high-score table is *described as a planned feature* in the +> README and is where the `/score-archive` command is *intended* to land scores, but +> **that code path is an unimplemented stub today.** There is no sorting, no tie-breaking, +> no entry cap, no decay, and no scoring formula/weighting anywhere in the codebase. +> This is a greenfield/early-stage domain — the write-up documents the *real* state and +> flags the gap, because documenting features that don't exist as if they did would mislead +> the next engineer. + +--- + +## 0. How to read the rest of this doc against the task's four questions + +The task asks for (1) the score-entry data model + persistence format, (2) how scores are +recorded, (3) the high-score-table algorithm, and (4) scoring formulas/weighting. +For each question the answer is: **the mechanism that exists vs. the mechanism the project +intends**, so readers never conflate the two. + +| # | Question | Short answer | +|---|----------|--------------| +| 1 | Score-entry data model & persistence | Live score = `UUID -> int` in a `HashMap`. **No persistence** (load/save are `TODO` stubs). | +| 2 | How scores are recorded | **Manual, event-free:** the *administrator* runs a command. No game events, no timers. | +| 3 | High-score-table algorithm | **Not implemented.** No sort/tie-break/cap/decay. Only an aspirational `/score-archive`. | +| 4 | Scoring formulas / weighting | **None.** Scores are plain integer accumulators; no weighting, no decay, no bonuses. | + +--- + +## 1. The score-entry data model & persistence *(task Q1)* + +### 1.1 What a "score entry" actually is (current reality) + +The entire live-score domain is **one field** in the plugin's entry-point class: + +- **File:** `src/main/java/com/majinnaibu/minecraft/plugins/scorekeeper/ScoreKeeperPlugin.java:38` +- **Declaration:** `private final HashMap _playerScores = new HashMap();` + +That is the whole model: + +| Aspect | Value | +|--------|-------| +| Record shape | A single `Integer` (the score), keyed by a player's `UUID`. | +| Key type | `java.util.UUID` — the player's `UniqueId`, *not* the name string. | +| Value type | `int` (Java `Integer`), autoboxed into the map. | +| Container | one `java.util.HashMap` on the plugin instance. | +| Uniqueness | one entry per `UUID`. A player can have exactly one live score at a time. | +| Default for a new player | `0` — see §3.1 ("first-time player" edge case). | + +There is **no `Score`/`ScoreEntry`/`PlayerStats` value class**, no timestamp, no +name stored alongside the score (name is looked up from the live server only — see §2.3), +and no secondary index. + +### 1.2 Persistence format: **none exists** + +Persistence is *intended* but *not built*. Three concrete stubs prove this: + +1. **On disable (server stop):** `ScoreKeeperPlugin.java:46-49` `onDisable()` logs + *"Unable to save scores to file. This feature is not implemented yet."* — the map + is **discarded** on shutdown; nothing is written. +2. **On enable (server start):** `ScoreKeeperPlugin.java:59-60` `onEnable()` logs + *"Unable to load scores from file. This feature is not implemented yet."* — the map + always starts **empty** `{}` every server start. +3. **Archive:** `ScoreKeeperPlugin.java:72-74` `archiveScore(Player)` logs + *"Unable to archive score for ."* and returns — the "save to high-score table" + action is a no-op. + +So there is **no SQLite, no JSON, no YAML, no flat file, no config.yml**. The only file on +disk that concerns scores is `plugin.yml` (the command manifest — it lists the commands but +stores no scores). A grep of the whole tree for `sql / json / yaml / saveResource / +YamlConfiguration / File / Files. / Gson / Jackson` finds **zero hits** other than the +TODO/log strings. + +**Persistence format today = "in RAM only, lost on server restart."** + +> Implication for a new engineer: any "who's the top player" question answered *across +> restarts* is impossible today. Scores reset to the map being empty on every server boot. + +--- + +## 2. How scores are recorded *(task Q2)* + +### 2.1 The recording model is **manual and command-driven — no events, no timers** + +Scores move only when an administrator (or console/RCON) **types a command**. There is: + +- **No** `implements Listener`, no `@EventHandler`, no `registerEvents(...)`, no + `getServer().getPluginManager().registerEvents(...)`. +- **No** scheduler: no `getScheduler()`, no `runTask`, no `Timer`. +- **No** automatic hook to game events (e.g. a player killing a mob or reaching a goal + awards points automatically — *nothing like that exists*). + +A grep for `EventListener|@EventHandler|Listener|Scheduler|runTask` across `src/` returns +**zero**. + +So "scoring" happens **on demand**, by a person, via one of the four mutating commands: + +| Command | Effect on the map | Source | +|---------|-------------------|--------| +| `/score-add [player] ` | `score += amount` | `ScoreKeeperPlugin.addScore` `:67-70` | +| `/score-subtract [player] ` | `score -= amount` | `ScoreKeeperPlugin.subtractScore` `:88-91` | +| `/score-reset [player]` | `score = 0` | `ScoreKeeperPlugin.resetScore` `:80-82` | +| `/score-archive [player]` | **intended** to snapshot to a high-score table and reset to 0; **actually a no-op** | `ScoreKeeperPlugin.archiveScore` `:72-74` | + +`/score-get [player]` is read-only (returns the score, §2.4). + +### 2.2 The add/subtract primitives (the only real "write" path) + +Both funnel through one private setter: + +``` +addScore(player, n) -> old = getPlayerScore(player); setPlayerScore(player, old + n) :67-70 +subtractScore(player,n)-> old = getPlayerScore(player); setPlayerScore(player, old - n) :88-91 +setScore(player, s) -> setPlayerScore(player, s) :84-86 +resetScore(player) -> setPlayerScore(player, 0) :80-82 +setPlayerScore(p, v) -> _playerScores.put(p.getUniqueId(), v) :108-110 +``` + +Notes a new engineer must internalize: + +- **Read-modify-write.** `addScore`/`subtractScore` read the current value, compute, write + back in two map operations. (There is no concurrency control — see §5 risks.) +- **`int` math.** Values are Java `int`; a score can go **negative** (there is no floor at + 0 — `/score-subtract 50` on a 10-score yields `-40`), and can overflow `int` only after + `~2.1e9` points, which is not a practical concern. +- **No validation of the direction.** The only validation is *syntactic*: the amount must + parse as an integer (see §2.3). + +### 2.3 Command surface (who/what is allowed to record a score) + +Every command executor lives in `src/main/java/.../scorekeeper/commands/` and shares one +shape: parse args, resolve the target player, call the `ScoreKeeperPlugin` method, return +`true` (consumed). Two behavioral details matter: + +- **Self-target default:** if a *player* runs a command and omits the name, they target + *themselves*: `targetPlayer = (Player) sender`. (e.g. `ScoreGetCommand.java:44`, + `ScoreAddCommand.java:48`, `ScoreResetCommand.java:47-49`.) +- **RCON/console path:** if the sender is **not** a player (console/RCON), a player name + is **required** — omitting it prints usage instead of acting + (`ScoreGetCommand.java:46-48`, `ScoreAddCommand.java:45-47`, + `ScoreResetCommand.java:44-46`). +- **Name resolution:** `getServer().getPlayerExact(name)` — **exact, case-sensitive** + lookup by the *currently online* name (`ScoreGetCommand.java:50`, + `ScoreAddCommand.java:57`, `ScoreResetCommand.java:50`). A name that is not online + yields `null` → "Can't find a player with that name". Note the *map key is a UUID*, but + *lookup by name only works while that player is online* — you cannot change an offline + player's score by name today. +- **No permission nodes anywhere.** `plugin.yml` declares no `permission:`/`permissions:` + and the code checks none. The README's note ("the commands aside from get are intended + for admins … Permissions support is coming") is **intent, not enforcement** — a vanilla + player can run `/score-add` today. + +### 2.4 Reading a score (`/score-get`) + +`ScoreGetCommand.java:38-84` → `ScoreKeeperPlugin.getScore(player)` → `getPlayerScore(player)` +(`ScoreKeeperPlugin.java:76-78, 100-106`). It returns the live integer (0 for a known-but- +unwritten player, which also *materializes* a 0 entry, see §3.1). Output formatting +("Your score is N" vs. "PLAYER's score is N") depends only on whether the sender is the +target or not — it carries no ranking or table. + +--- + +## 3. The high-score-table algorithm *(task Q3)* + +### 3.1 What exists: **nothing. It is aspirational.** + +There is **no high-score table, no ranking structure, no sort, no tie-break rule, no max +entry count, and no decay/rotation** anywhere in the codebase. The task's premise ("the +high-score table") reflects the **product intent** in the README, not the **code**. Concretely: + +- The README (`README.md:11`) says `/score-archive` *"Saves the player's score to a + 'High Scores' table and resets their current score to 0."* +- The manifest (`plugin.yml:18-20`) declares `/score-archive` with that very description. +- But the command is a stub: `ScoreArchiveCommand.java:37-39` (`onCommand` body) replies + *"archive command unimplemented"*, and the underlying `archiveScore` + (`ScoreKeeperPlugin.java:72-74`) only logs and returns — it neither writes a table nor + resets anything. + +Therefore the "algorithm" is: **no-op.** A new engineer asked to "implement the high-score +table" is starting from zero; the design questions in §3.2 are **open**, not answered by code. + +### 3.2 Open design questions the table must settle (no code today decides these) + +Since no implementation exists, these are *decisions to be made*, recorded here so the +implementation task doesn't reinvent the debate: + +- **Storage/persistence format.** The load/save TODOs (`:47`, `:59`) say "to file"; + `plugin.yml`/Bukkit convention favors YAML (`YamlConfiguration`), but JSON/SQLite are all + viable. **Unchosen.** Persisting to `data/` via `getDataFolder()` is the idiomatic Bukkit + path. +- **What an entry is.** Today a live score is just `{uuid -> int}`. A high-score entry + likely needs **player name + score + (timestamp?) + (date earned?)**. Name is *not* + currently stored with the score, so a durable table must capture the name at archive + time (a UUID-only entry can't render a leaderboard without the name being resolvable). +- **Sorting / ordering.** Almost certainly **descending by score**. +- **Tie-breaking.** Undefined. Candidate rules: by *score only*; by *earliest* archived + first (stable, time-ordered); by *name* (alphabetical); or "first to reach that score." +- **Max entries / cap.** Undefined. A leaderboard needs a cap (e.g. top 10 / top 30); + today there is no cap because there is no table. +- **Decay / rotation / time window.** None, and none described in docs — so the safe + default is **"scores are permanent, no decay."** +- **Reset semantics on archive.** README promises archive *resets to 0* *and* records. + `ScoreArchiveCommand` must decide: does archiving also reset? Does it allow multi-entries + (a player appearing more than once) or one row per player? All open. + +### 3.3 Full lifecycle of a score: creation → (intended) display on the high-score table + +This is the acceptance-criterion walk-through. Each stage is marked **[REAL]** (in code now) +or **[GAP]** (intended, not built). + +1. **Player joins / acts** — a score *doesn't* exist yet. First interaction with the map is + lazy (see stage 4a). **[REAL]** +2. **Admin records a score** — `admin: /score-add Alice 10`. Parsed in `ScoreAddCommand`, + routed to `addScore`. The first time this runs for Alice, `getPlayerScore` **creates** + her entry at 0 and returns it; then `10` is written. **[REAL]** +3. **Score accumulates** — later `/score-add Alice 5` → 15; `/score-subtract Alice 2` → 13; + `/score-reset Alice` → 0. All in the in-memory map, live only. **[REAL]** +4. **Read** — `/score-get Alice` → "Alice's score is 13". No ranking shown. **[REAL]** +5. **Server restart** — `onDisable` *cannot* save (stub), `onEnable` *cannot* load (stub); + the map is **lost**. Alice's 13 is gone; every score starts empty again. **[REAL] — + this is the current data-loss reality.** +6. **Intended: archive to the high-score table** — `admin: /score-archive Alice` should + freeze Alice's 13 into a durable, sorted leaderboard and reset her live score to 0, so + it survives restarts. **[GAP]** — currently returns "archive command unimplemented". +7. **Intended: display on the high-score table** — a command (not yet built) renders the + capped, sorted list. **[GAP]** — no such command, no structure to render. + +**Edge cases (explicitly, per the acceptance criteria):** + +- **First-time player / first write.** A player has no map entry until `getPlayerScore` is + called, which **inserts `0` on first access** (`ScoreKeeperPlugin.java:100-106`). So a + first `/score-add X 10` yields **10, not** `10 - 0` ambiguity, and a first `/score-subtract + X 3` yields **−3** (no minimum-0 clamp). A first `/score-get X` returns **0** and + *materializes* the entry at 0. This `get-or-create-0` behavior is the de-facto "first-time + player" rule. **[REAL]** +- **Ties.** **Undefined** because no table exists. When the table is built, decide a + tie-break (§3.2). No code today can observe or sort ties — `HashMap` gives no order. **[GAP]** +- **Table full.** **Undefined** because no cap exists. When a cap is introduced, decide + what happens to the displaced entry: drop it, or "rotate" it into overflow. No code + today enforces a maximum. **[GAP]** +- **Offline / unknown name.** `getPlayerExact(name)` returns `null` for anyone not + currently online or any misspelling → the command prints "Can't find a player with that + name" and changes nothing. You **cannot** archive or reset an *offline* player by name + today; by-UUID writes (if a future table stores names+UUIDs) would fix that. **[REAL]** +- **Negative / large scores.** No clamp; negatives allowed, `int` overflow only near + ~2.1 billion. **[REAL]** +- **Double-spawn / relogin.** Keyed by `UUID`, **not name**, so a name change or two + players sharing a name can't collide on the *live* map; but a *name* is never stored, + so a future table that only persists the UUID can later fail to render a name. + +--- + +## 4. Scoring formulas & weighting *(task Q4)* + +**None.** There is: + +- No formula, no multiplier, no combo/bonus/streak logic. +- No weighting between action types. +- No time-based decay or "points per game" normalization. + +Scores are **pure integer accumulators**: `score = Σ(adds) − Σ(subtracts)` over the session, +resettable to 0 by `/score-reset` (intended: on `/score-archive`). The only "arithmetic" in +the code is the `+`/`−` in `addScore`/`subtractScore` and the `0` floor written by +`resetScore`. `int` semantics apply (wrapping overflow; no checked arithmetic). Any +formula/weighting is a **future-design** decision, not encoded today. + +--- + +## 5. Data-flow diagram (text) + +``` + ADMINISTRATOR / CONSOLE / RCON (no game events, no scheduler, no auto-scoring) + │ types a command + ▼ + ┌─────────────────────────────┐ + │ command executors │ commands/Score*Command.java + │ parse args + resolve player│ getPlayerExact(name) ── must be ONLINE (name→UUID) + │ (self-target if player) │ no permission checks + └──────────────┬──────────────┘ + │ calls + ▼ + ┌───────────────────────────────────────────────┐ + │ ScoreKeeperPlugin (entry point; plugin.yml main)│ + │ │ + │ addScore / subtractScore / resetScore / setScore │ read-modify-write on... + │ getScore (read) archiveScore (STUB)│ + └──────────────┬───────────────────────┬──────────┘ + │ read-modify-write │ (intended: snapshot→table, reset 0) + ▼ ▼ + live map: HashMap ╳ NOT IMPLEMENTED + "score entry = one int per UUID" high-score table: + default 0, no clamp, in RAM only ─ no structure, no sort, + │ ─ no persistence, + ├─ READ: /score-get → "N" ─ no cap/decay + │ + └─ LIFECYCLE: + onEnable : load from file → TODO (stays {} ; lost on restart) + onDisable : save to file → TODO (map discarded at shutdown) + + PERSISTENCE: none │ SCORE FORMULA: none │ EVENTS/TIMERS: none │ + PERMISSIONS: none │ HIGH-SCORE TABLE: aspirational stub only +``` + +--- + +## 6. Quick reference — "where to look first" + +| I want to understand… | Go to | +|-----------------------|-------| +| The entire live data model | `ScoreKeeperPlugin.java:38` (one `HashMap`) | +| How a score changes | `addScore`/`subtractScore`/`resetScore`/`setScore` `:67-91` | +| How a score is read | `getScore` `:76-78` → `getPlayerScore` `:100-106` (note get-or-create-0) | +| The "high-score table" | **Doesn't exist** — see `archiveScore` stub `:72-74`; design open in §3.2 | +| Persistence (save/load) | `onDisable` `:46-49`, `onEnable` `:59-60` — both `TODO`, not built | +| Command behaviors | `commands/Score*Command.java` (self-target default; RCON requires a name; exact online-name lookup) | +| What's declared/intended but not wired | `plugin.yml:18-20` (`/score-archive`), `README.md:11`, `README.md` notes | +| Build/runtime target | `build.gradle` (paper-api 1.21.7, Java 21), `plugin.yml` (`api-version: 1.21`) | + +--- + +## 7. Risks a new engineer should know (correctness gaps in the *current* live domain) + +These are real, not future-work — flagging because the doc's job is to prevent a new +developer from assuming the system is more mature than it is: + +1. **Data loss on restart.** No save/load (`:47`, `:59`). Every server boot wipes all + scores. Any "leaderboard across sessions" is broken by design. +2. **No permissions.** Anyplayer can self-add points. README says this is by intent + ("coming after I get archive to work") but it is not enforced. +3. **Race condition on add/subtract.** `read → compute → write` is not atomic and the + `HashMap` is not thread-safe; concurrent command executions could lose an update. + (Low likelihood today since commands are serial on one thread, but it's a latent bug for + any async/event-driven scoring that gets added.) +4. **No minimum-0 / no validation of amount sign.** Subtracts produce negatives freely. +5. **Name lookups are online-only & exact.** Can't target offline players by name; + case-sensitive; "Alice" ≠ "alice". +6. **No high-score table.** The flagship advertised feature (`/score-archive` → "High + Scores" table) is a no-op; a new engineer must not expect ranking/persistence to work. + +--- + +## 8. Summary for the synthesizer (`Project.md` § "domain model & scoring logic") + +Distilled, code-grounded, and honest: + +- **Model:** one `int` per player `UUID`, held in `ScoreKeeperPlugin._playerScores` + (`HashMap`), default `0`. +- **Recording:** manual, admin-driven commands; **no events/timers**; no permission guard. +- **Persistence:** **none** — load/save are `TODO`; scores are lost on restart. +- **High-score table:** **does not exist** — `/score-archive` is a stub; sorting/tie-break/ + cap/decay are **undecided design items** (listed in §3.2). +- **Formula/weighting:** **none** — plain `int` accumulation. +- **Lifecycle:** create-on-first-access (0) → add/subtract/reset (live only) → read via + `/score-get` → *intended* archive+reset + table display **not built** → lost on restart. +- **Edge cases:** first-time/first-write ⇒ 0-or-delta, no floor; ties/table-full ⇒ undefined + (no table yet); offline/unknown name ⇒ "can't find", unchanged. + +Everything below the "high-score table" line of the README is **future work**, and the +write-up's job is to make that boundary explicit so the next team member isn't misled. From c3d76e84f2c389e1c496fb4e60b0a7da815a5d93 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Thu, 24 Sep 2026 14:01:45 -0700 Subject: [PATCH 6/7] docs: Add Project.md reference doc for ScoreKeeper project --- Project.md | 280 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 Project.md diff --git a/Project.md b/Project.md new file mode 100644 index 0000000..4b90f9d --- /dev/null +++ b/Project.md @@ -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 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 | From 4bb0046b2644798d35536eed5df941be742c5443 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Thu, 24 Sep 2026 14:16:46 -0700 Subject: [PATCH 7/7] docs: add Project.md team reference doc Lands the synthesized 8-section project reference at the repo root so team members can understand ScoreKeeper without reading the whole codebase. Cross-checked against live source: in-memory HashMap scores, five /score-* commands, /score-archive + persistence as unimplemented stubs. Sourced from completed children t_2b93fd59 / t_1f4982b3 / t_f5d60eac / t_710fadae. --- Project.md | 325 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 Project.md diff --git a/Project.md b/Project.md new file mode 100644 index 0000000..605a011 --- /dev/null +++ b/Project.md @@ -0,0 +1,325 @@ +# ScoreKeeper — Project Reference + +> **Purpose of this document.** A fast reference for the team so anyone can +> understand what ScoreKeeper is, how it is built, and what is and isn't done +> — without having to read the whole codebase first. Read this before touching +> the code. Source: everything here is grounded in the current code (see +> "Where things live" / "Status") and the repo's own docs +> (`README.md`, `CONTRIBUTING.md`, `TODO.md`, `.cursor/plans/Modernize.md`). +> Treat the **code** as the single source of truth where any doc disagrees. + +--- + +## 1. What this project is + +**ScoreKeeper** is a **Minecraft server plugin** (Bukkit / **PaperMC** plugin +API) that tracks a numeric **score for each player** and is intended to keep a +**high-score table**. It is a small, early-stage Java plugin. + +- **It is a plugin** for a Minecraft server (Paper/Spigot/Bukkit). It runs on + the server and exposes in-game commands. +- **Core capability today:** record, read, add, subtract, and reset a player's + score via in-game commands. +- **Intended future capability:** a "High Scores" table that persists scores + (see §6 — this part is **not** implemented yet). + +**Audience / job-to-be-done:** server admins and other plugins should be able to +adjust and read player scores. The authors intend the mutating commands to be +**admin-oriented** and expect that, eventually, *another plugin will call the +score methods directly* rather than having players run the commands themselves +(see `README.md`). Permissions to back that are **not yet implemented** (§6). + +**License:** AGPL-3.0. Every Java file must carry the AGPL-3.0 license header +(the header text lives in `config/license-header.txt` and is enforced by +Spotless — §5). + +--- + +## 2. Tech stack & key facts + +| Item | Value | Source | +|------|-------|--------| +| Language | Java | — | +| Java version | **Java 21**, via Gradle Java Toolchains | `build.gradle`, `gradle.properties` | +| Server API | **PaperMC** (`io.papermc.paper:paper-api`), **`1.21.7-R0.1-SNAPSHOT`** | `build.gradle` | +| Plugin API version (runtime) | `1.21` | `src/main/resources/plugin.yml` | +| Build tool | **Gradle 8.14.3** via wrapper (`./gradlew` / `gradlew.bat`) | `gradle/wrapper/...` | +| Plugin version | **`0.2.2-SNAPSHOT`** | `gradle.properties` | +| Group / coordinates | `com.majinnaibu.minecraft.plugins` / `ScoreKeeper` | `build.gradle` | +| Output artifact | `build/libs/ScoreKeeper-.jar` | `build.gradle` | +| Format/lint | **Spotless** (Google Java Format) | `build.gradle` | +| Releases | `net.researchgate.release` plugin | `build.gradle` | +| Repo remote | `git@github.com:headhunter45/ScoreKeeper.git` | `git remote -v` | + +### Java toolchain / "automatic JDK download" +`gradle.properties` sets `org.gradle.java.installations.auto-download=true`, and +`settings.gradle` applies the **Foojay resolver** plugin +(`org.gradle.toolchains.foojay-resolver-convention`). Together these let Gradle +**auto-download a Java 21 JDK** when one isn't present, so a developer doesn't +have to hand-install a JDK just to build. (The plugin *also* still recommends +installing JDK 21 — see `CONTRIBUTING.md`.) + +### ⚠️ Version caveat — read this before building +The Paper API version is pinned to **`1.21.7`** in `build.gradle`, but +`README.md` / `CONTRIBUTING.md` reference a **`1.21.8`** server jar +(`paper-1.21.8-25.jar`). These don't match yet. **Confirm which MC/Paper +version is the target before treating either number as authoritative.** +Everything else in this doc is safe; this one mismatch is a real gotcha. + +--- + +## 3. Commands + +The plugin registers **five commands**, all declared in `plugin.yml` and all +backed by a dedicated `CommandExecutor` class. Each executor takes +`ScoreKeeperPlugin` via its constructor (dependency injection by +hand), talks to the plugin through its public methods, and replies with +**adventure `Component`** messages (colored text). + +| Command | `plugin.yml` usage | What it does | Handler class | +|---------|-------------------|--------------|---------------| +| `/score-get` | `[playerName]` | Shows a player's current score | `ScoreGetCommand` | +| `/score-add` | `[playerName] ` | Adds points to a player's score | `ScoreAddCommand` | +| `/score-subtract` | `[playerName] ` | Subtracts points | `ScoreSubtractCommand` | +| `/score-reset` | `[playerName]` | Resets a player's score to 0 | `ScoreResetCommand` | +| `/score-archive` | `[playerName]` | **Unimplemented** — prints "archive command unimplemented" | `ScoreArchiveCommand` | + +**Behavior notes (read these):** +- **`[playerName]` is optional for in-game players.** If a player omits the + name, the command acts on the **executor/invoker themselves**. +- **RCON / console callers must always pass a player name** — there is no + "self", so the executors print usage and refuse. +- **Player lookup uses `getServer().getPlayerExact(name)`**, which only matches + **online** players by exact name. Offline / unknown names produce + "Can't find a player with that name" — this is a known limitation the team + has noted (see `Modernize.md` / §6). +- **Amounts are parsed with `Integer.parseInt`**; non-integer amounts produce + "amount must be an integer" and do not change anything. +- **Messages are prefixed** with a colored `[ScoreKeeper] ` banner + (`_messagePrefix`), and log lines are prefixed with `[ScoreKeeper] ` + (`_logPrefix`). +- **All mutating commands are admin-oriented** (per `README.md`), but there is + **no permission check yet** (§6). + +--- + +## 4. Architecture + +### 4.1 Where things live +``` +ScoreKeeper/ +├── build.gradle # build config, plugins, deps, spotless, release +├── settings.gradle # project name + Foojay (toolchain auto-download) +├── gradle.properties # version + auto-download flag +├── config/license-header.txt # AGPL header enforced on all .java files +├── tools/ +│ ├── bash/ # build-plugin.sh, deploy-plugin.sh, start/stop-server.sh +│ └── powershell/ # same 4 scripts, .ps1 form (Windows) +├── .cursor/plans/Modernize.md# old modernization checklist (see §6) +└── src/main/ + ├── java/com/majinnaibu/minecraft/plugins/scorekeeper/ + │ ├── ScoreKeeperPlugin.java # plugin entry point + score store + methods + │ └── commands/ # one CommandExecutor per command + │ ├── ScoreAddCommand.java + │ ├── ScoreGetCommand.java + │ ├── ScoreSubtractCommand.java + │ ├── ScoreResetCommand.java + │ └── ScoreArchiveCommand.java + └── resources/ + └── plugin.yml # plugin metadata + command declarations +``` + +### 4.2 The one core class +**`ScoreKeeperPlugin`** (extends `JavaPlugin`) is the heart of the plugin: + +- **Score store:** `HashMap _playerScores` — keyed by the + player's **UUID** (not name — this was a deliberate modernization, see §6). + An unknown player is lazily seeded to `0`. +- **Public command surface** (what the command classes call): + `addScore`, `subtractScore`, `getScore`, `resetScore`, `setScore`, + `archiveScore`. +- **Lifecycle:** + - `onEnable()` wires up all five executors + (`getCommand("score-…").setExecutor(new …(this))`) and logs that it's on. + - `onDisable()` currently **does nothing** except log a warning. +- **Logging helpers:** `logInfo`, `logWarning`, `logError` (all via + `getLogger()` with the `[ScoreKeeper] ` prefix). +- **Messaging:** `sendMessage(CommandSender, Component)`, which prepends the + `[ScoreKeeper] ` prefix component. + +### 4.3 Data flow +``` +player/rcon ──► /score-* ──► Command.onCommand() + │ parse args, find Player via getPlayerExact + ▼ + ScoreKeeperPlugin.addScore / getScore / ... + │ read/write HashMap + ▼ + reply via sendMessage() (adventure Component, colored) +``` +There is **no external storage today** — the map is in-memory only. + +--- + +## 5. Build, test, and deploy + +### 5.1 Build +- **Linux/macOS (and WSL):** `./gradlew build` +- **Windows:** `.\gradlew.bat build` +- Produces `build/libs/ScoreKeeper-.jar` (version comes from + `gradle.properties`). +- **Wrappers exist** for the common tasks: + - `tools/bash/build-plugin.sh` / `tools/powershell/Build-Plugin.ps1` + - `tools/bash/deploy-plugin.sh` / `tools/powershell/Deploy-Plugin.ps1` + - `tools/bash/start-server.sh` / `tools/powershell/Start-Server.ps1` + - `tools/bash/stop-server.sh` / `tools/powershell/Stop-Server.ps1` + +### 5.2 Spotless (format + license check) +The `build.gradle` configures **Spotless** with `googleJavaFormat()`, +`removeUnusedImports()`, a fixed import order +(`java, javax, com, net, org, ''`), and the AGPL license header. `release` +binds `spotlessCheck` before `build`, so formatting is enforced on release. +**Run `./gradlew spotlessApply` to auto-format before committing**; a bad format +will fail the build. + +### 5.3 Deploy & server scripts +The helper scripts operate on **two environment variables** you must set: +- `MINECRAFT_SERVER_PATH` — root dir of your Paper server. +- `MINECRAFT_SERVER_JAR` — the server jar filename relative to that path. + +`start-server` launches the Paper server; `stop-server` kills it; +`deploy-plugin` copies the newest `build/libs/*.jar` into the server's +`plugins/` dir (and warns if the jar is older than your source files). +Full setup, including the EULA step and client connection, is in +`CONTRIBUTING.md`. + +### 5.4 Releases +The `net.researchgate.release` plugin drives versioning: it reads/writes the +version from `gradle.properties`, builds `spotlessCheck` + `build`, tags +releases as `v$version`, and reverts the version bump on failure +(`revertOnFail = true`). + +### 5.5 What's NOT tested +There are **no automated tests** in the repo and no test task is configured. +"Testing" today means running the plugin on a local Paper server via the +scripts above. This is a candidate for the team to add (CI, e.g. +`Minebench`/`PaperTests`, is a reasonable first step). + +> Note: this reference document was authored without running a build. To confirm +> the build is green, run `./gradlew build` on a dev machine (requires network +> to fetch the Paper API + toolchain JDK on first run) and confirm the jar in +> `build/libs` loads on a Paper `1.21.x` server. + +--- + +## 6. Status & known gaps (what is / isn't done) + +This is the **most useful section for the team** — it captures what the +project-aspirations say vs. what the code actually does. + +### 6.1 What is done +| Area | State | +|------|-------| +| Five in-game commands (`/score-*`) | ✅ Wired up and functional for get/add/subtract/reset | +| Per-player UUID-keyed score store | ✅ In-memory `HashMap` | +| Modern Paper API, no deprecations | ✅ `getPlayerExact`, `getLogger()`, `@...` (no legacy event code in active path) | +| Java 21 + auto-download toolchain | ✅ | +| Gradle build + wrapper + release plugin + Spotless | ✅ | +| Bash + PowerShell dev scripts | ✅ | +| License headers on all `.java` | ✅ (enforced by Spotless) | + +### 6.2 What is NOT done (in code, not just doc) +These are the real "high score table" features the README aspires to but the +code doesn't yet do: + +| Gap | Where it shows up | Notes | +|-----|-------------------|-------| +| **`/score-archive` unimplemented** | `ScoreArchiveCommand.onCommand` just sends "archive command unimplemented"; `ScoreKeeperPlugin.archiveScore` only logs a warning | This is the central "high scores" feature and it's a stub | +| **No persistence on save** | `onDisable()` logs `logWarning("Unable to save scores to file. This feature is not implemented yet.")` | All scores lost on restart or `/score-archive` today | +| **No persistence on load** | `onEnable()` logs `logWarning("Unable to load scores from file. ...")` | Same feature, load side | +| **No permissions / access control** | No `permission` fields in `plugin.yml`, no `hasPermission` check in any executor | `README.md` states the mutating commands should be admin-only; that isn't enforced yet | +| **No automated tests** | No test sources / test task | Testing is manual on a running server | +| **Case-sensitive offline lookup** | `getPlayerExact(name)` returns null for offline players | Noted in `Modernize.md`; players must be **online** to be looked up by name | + +> **Important re: "High Scores table" in the README.** The README advertises +> `/score-archive` as "Saves the player's score to a 'High Scores' table and +> resets their current score to 0." That table **does not exist yet**. The +> command and the archive method are both stubs. **Do not assume it works.** +> When a teammate asks "does the high-score table exist?", the honest answer +> today is: no, it's planned and stubbed out. + +### 6.3 Historical / superseded docs +- **`.cursor/plans/Modernize.md`** is a pre-migration plan (Maven → Gradle, + `Player` key → `UUID`, old event registration → modern, etc.). The + substantive modernization is **mostly done and merged** — the TODO checklist at + the top of that file is checked, and the current code reflects it. Treat the + appendix (API comparisons, Gradle-vs-Maven notes) as background only. +- **`TODO.md`** (the "Migration TODOs for Paper Modernization") is similarly + **mostly checked** — the migration is effectively complete. It is a + historical artifact for the modernization effort, not the active roadmap. +- **`README.md` "Permissions support is coming AFTER I get archive to work."** + This line still reflects the original intent and remains true: archive is + still not worked, permissions are still not there. + +### 6.4 Suggested next steps (open, not committed) +These are candidates — **not** a decision; surface in grooming/planning: +1. Implement archive + a real high-scores table with on-disk persistence + (the obvious first product step). +2. Reconcile the `1.21.7` vs `1.21.8` version question (see §2). +3. Add a basic permission check on the mutating commands (`score-add`, + `score-subtract`, `score-reset`, and later `score-archive`). +4. Introduce a lightweight test harness for the score store and command parsing + (the pure-logic portions of the `*Command` classes are amenable to + unit tests even without a running server). +5. Decide offline lookup strategy — `getPlayerExact` requires the player to be + online. + +--- + +## 7. Conventions & gotchas + +- **Formatting:** Spotless with Google Java Format. Run `./gradlew + spotlessApply` before committing. +- **License header:** Every `.java` file must start with the AGPL-3.0 block + from `config/license-header.txt`. Spotless enforces it. +- **Log vs. message:** `logInfo`/`logWarning`/`logError` write server-side logs + (with `[ScoreKeeper] ` prefix). `sendMessage` sends colored in-chat messages + **from** the `[ScoreKeeper] ` banner. +- **Command design:** each `Command` class is the pattern. To add a new + command: add an entry to `plugin.yml`, write a `CommandExecutor` in + `src/main/java/.../commands/`, register it in + `ScoreKeeperPlugin.onEnable`, and expose any new behavior via a new + public method on `ScoreKeeperPlugin`. +- **UUID, not name, is the key.** All internal storage is by UUID. Names are + only used at the command edge. +- **Adventure `Component`, not raw strings.** Messages are built with + `net.kyori.adventure.text.Component`; new messages should follow suit. +- **Dev scripts use env vars.** `MINECRAFT_SERVER_PATH` and + `MINECRAFT_SERVER_JAR` must be set for the `tools/` scripts to work. See + `CONTRIBUTING.md` for the full setup. +- **Naming:** the plugin's own coordinates, package, and group are + `com.majinnaibu.*`; the public GitHub repo is + `headhunter45/ScoreKeeper`. If a teammate is confused by the two names, this + is why. + +--- + +## 8. Where to read next + +| Read this to | Open | +|-------------|------| +| Understand the original intent / use the plugin | `README.md` | +| Set up a dev environment | `CONTRIBUTING.md` | +| See the original modernization plan (historical) | `.cursor/plans/Modernize.md` | +| See the original migration TODO list (historical) | `TODO.md` | +| Understand a specific command's behavior | `src/main/java/.../commands/*.java` | +| Understand the plugin's core logic | `src/main/java/.../ScoreKeeperPlugin.java` | +| Understand what the build does | `build.gradle`, `settings.gradle`, `gradle.properties` | +| Understand the command declarations | `src/main/resources/plugin.yml` | +| See the license | `LICENSE`, `config/license-header.txt` | + +--- + +*This document is a living reference. When you learn something new about the +project, update it — keep it honest (what's done vs. planned), and cite the +source file when something is non-obvious.*