Files
ScoreKeeper/docs/domain-score-tracking.md
2026-10-10 22:57:27 -07:00

8.2 KiB

Score Keeper - Score-Tracking Domain

This documents implemented behavior in ScoreKeeperPlugin.java, ScoreBucket.java, and ScoreReporting.java. For command syntax see README.md; for runtime wiring see the integration reference.

Buckets and live scores

  • A bucket has an immutable ID, singular label, plural label, initial integer value, and reporting policy. IDs match [A-Za-z0-9_-]+, are case-sensitive, and must be unique. Labels must be nonblank; reporting cannot be null.
  • A fresh server has points (point/points, initial value 0, reporting none). Loading bucket data restores this bucket if it is missing.
  • _bucketScores maps bucket ID to UUID -> int. Players can have independent scores in multiple buckets. Names are not used as identity keys.
  • The server default bucket and optional UUID-specific assignments determine a player's active bucket. Selecting the default removes their override. switchAllPlayers changes the default and clears all assignments, including offline players' assignments, without deleting scores.
  • getScore lazily registers a missing score at the bucket's initial value. Joining alone does not create an entry. Reads therefore affect who appears in a later archive or run ranking.
  • Add/subtract perform ordinary Java integer arithmetic. Zero/negative amounts are allowed, scores can be negative, and overflow wraps. There are no bonuses, weighting, decay, or automatic gameplay scoring.
  • Reset writes the bucket initial value, not necessarily zero. Existing no-bucket API methods use the active bucket; explicit-bucket overloads do not change the player's assignment.

Reporting and bucket changes

For a changed value written through setScore, outside a run:

Policy Audience
none (default) Nobody
player Scored player
global All online players
admin Online operators or holders of scorekeeper.admin, plus the server log

Writing the same value sends no score-change report. Commands that change scores do not independently promise success messages; inspect /score-get when reporting is none. A bucket change tells an affected online player the ID and current score. Unit formatting uses the singular label for exactly 1, plural otherwise. /score-get itself displays only the number, without bucket or unit labels.

Run reporting overrides the bucket policy. Archive resets and run start/restore directly update maps and do not send ordinary per-score change reports.

High-score runs

Each bucket can have at most one active run; different buckets may run concurrently. Top N must be at least one. There are three lifetime modes:

  • Target score: finishes when any recorded score reaches/exceeds the target. With reset enabled, the target must exceed the initial value. Without reset, existing scores can finish a run immediately when it starts.
  • Duration: a positive duration, stored as an absolute epoch-millisecond expiration. Time offline counts toward expiration.
  • Until disabled: no automatic end; stop explicitly. Plugin disable saves the run rather than finishing it, so it resumes on restart.

Starting with reset enabled snapshots existing bucket entries and sets them to the initial value. During the run, a changed score is always reported to its player. If that player is in the top N after the change, the report is sent to all other online players too, once per recipient.

Completion is checked immediately at start/load, synchronously every 20 ticks, and on a changed target-run score. Manual stop also completes the run. The scheduled check uses wall-clock time; under server lag it need not fire at an exact real-time second.

Ranking and results

Rankings include all recorded entries in that bucket, including offline players and entries read/created before the run. The order is:

  1. Descending score.
  2. Case-insensitive player name, using Paper's offline-player lookup.
  3. UUID string to break remaining ties.

Unknown/blank names fall back to the UUID string. Ties have distinct consecutive places, not shared ranks. Top N limits display/reporting, not stored entries. Every online player receives the final top-N table and their own full-ranking place/score, or an unranked result at the initial value. The final table is logged.

With reset enabled, completion replaces the bucket map with its pre-run snapshot. Players first recorded during the run are removed from that map and get the initial value on their next read. Personal results refer to the finished run, not restored scores. Without reset, earned scores remain. Run completion does not create a score archive.

Archives

Player, single-bucket, and all-bucket archives create immutable snapshots with a random UUID ID, scope text, epoch-millisecond creation time, and nested bucket ID -> UUID -> int scores. A player archive uses their active bucket; bucket/all archives include all recorded entries, even offline players.

Live archived entries reset to their respective initial values. Archiving fails before changing any scores if a selected bucket has an active run; stop the run first. Archives do not cap or prune entries.

ScoreArchiveListCommand.java lists archives newest first. Within an archive, scores display descending with UUID-string tie-breaking (not the run's name tie-break). Names and unit labels are resolved at display time, not captured in the snapshot; missing bucket definitions use the generic unit scores.

Persistence

Both files live in plugins/ScoreKeeper/. Loading occurs on enable and saving on disable. There is no periodic autosave or immediate archive write despite the command's "Saved score archive" acknowledgement; a crash can lose recent state. Back up only after a successful clean shutdown.

scores.yml (currently writes version 3)

version: 3
default-bucket: points
buckets:
  points:
    singular: point
    plural: points
    initial-value: 0
    reporting: none
    scores:
      "00000000-0000-0000-0000-000000000001": 10
player-buckets:
  "00000000-0000-0000-0000-000000000002": points
high-score-runs:
  points:
    top-n: 3
    reset-scores: true
    lifetime: duration
    target-score: 0
    expires-at: 1800000000000
    previous-scores:
      "00000000-0000-0000-0000-000000000001": 25

This illustrates field shapes, not a file to install unchanged. Overrides equal to the default bucket are normally omitted. Saved lifetime strings are target_score, duration, and until_disabled. The loader reads structure, not a version-dispatch migration table.

If buckets is absent, the loader treats top-level UUID/integer entries as legacy points scores. Invalid bucket definitions/UUIDs/runs are skipped with warnings; invalid reporting falls back to none, and an unknown default falls back to points. Unknown bucket assignments are ignored. Bukkit YAML getters supply defaults for some missing/wrongly typed values: do not assume every malformed value is rejected or logged.

archives.yml

archives:
  "00000000-0000-0000-0000-000000000003":
    scope: bucket points
    created-at: 1800000000000
    buckets:
      points:
        scores:
          "00000000-0000-0000-0000-000000000001": 10

This file has no explicit format version. Empty bucket score maps may be omitted on serialization because only individual score entries are written. The two files are saved separately; there is no cross-file transaction or atomic replacement guarantee. Save I/O errors use the plugin logger. Never edit live data while the server is running: shutdown can overwrite it.

Validation status

These are source-level behavior descriptions, not claims of passing automated tests. Pending coverage is tracked in TODO.md. Before release, use human-tests.md, particularly ranking ties, empty buckets, restoration, malformed data, and restart/upgrade cases.