Merge remote-tracking branch 'oldgh/wt/t_2b93fd59' into temp-merge

This commit is contained in:
2026-09-24 19:26:19 -07:00

280
Project.md Normal file
View File

@@ -0,0 +1,280 @@
# ScoreKeeper — Project Reference
> **For a developer joining the team cold.** Read top-to-bottom in ~8 minutes;
> you'll know where to look and what's real vs. planned on day one.
---
## 1. What is ScoreKeeper?
ScoreKeeper is a **Paper Minecraft plugin** (Java 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 |