docs: Add Project.md reference doc for ScoreKeeper project
This commit is contained in:
280
Project.md
Normal file
280
Project.md
Normal 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 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] <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
|
||||
|
||||
```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<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 |
|
||||
Reference in New Issue
Block a user