Add data persistence
Pick the right store for the data and wire it safely (async I/O, main-thread apply, clean
shutdown), matching the project's stack and conventions. Follow the collaborative rule: propose
the design, show the code, then write after the user approves.
Phase 1: Load context
- Read
.mcplugin/config.yml for platform, mc_version, package, main_class,
plugin_name, build_tool, group_id. If it's missing, tell the user to run
/setup-platform (and /scaffold) first.
- Read the references and pitfalls (relative to this skill;
Glob as fallback):
../../references/api/persistence-config.md — PDC, NamespacedKey, YAML config APIs
../../references/api/scheduler.md — async vs. main thread; Folia's schedulers
../../references/pitfalls.md — the "Persistence", "Threading", and "Memory leaks" sections
- Look at existing source to match style:
Glob src/main/java/**/*.java, read the main class
and the build file (pom.xml / build.gradle*) in case a driver + relocation is needed.
Phase 2: Clarify the data
Ask (AskUserQuestion or plain text) only what you can't infer:
- What data — fields and types (e.g. homes = name → world + x/y/z).
- Scope — per-player, per-item, per-entity, per-world, or global.
- Size & shape — a handful of values vs. thousands of rows; flat vs. relational (does it need
queries/joins, or just load-all-on-start)?
- Concurrency — is it read/written on hot paths, or occasionally?
- Multi-server — will more than one server instance share this data? That rules out SQLite
and a local YAML file and points at MySQL (or another shared DB).
- Loss tolerance — must every write be durable immediately, or is periodic/on-quit saving
acceptable? This decides between write-through and a batched save task.
Phase 3: Choose the store (explain the pick)
PersistentDataContainer (+ reused NamespacedKey) — best when the data belongs to a
specific item, entity, or chunk (custom item tags, mob metadata). Survives with the object; no
file to manage. Prefer this over the deprecated metadata API and NBT string hacks.
- A YAML data file (separate from
config.yml) — good for small/medium structured data
keyed by UUID (homes, small stat maps). Simple, human-readable; load on enable, save on
change/disable. Not for high-frequency writes or large datasets.
- A real database — SQLite (embedded) or MySQL (shared) — for large, relational, or
multi-server data. The JDBC driver must be added to the build and SHADED into the jar, then
RELOCATED (shade/shadow relocation) to avoid classpath clashes with other plugins. SQLite is
a single local file; MySQL needs connection details in
config.yml.
State the recommendation and why, given the scope and size from Phase 2.
Phase 4: Implement
Generate, in the project's package:
- The chosen store:
- PDC: build
NamespacedKeys once (reuse them) and read/write typed values on the object.
- YAML: a data-access class wrapping a
YamlConfiguration file under the data folder.
- DB: the build change (dependency + shade/relocate config), a connection/pool setup, schema
creation (
CREATE TABLE IF NOT EXISTS ...), and parameterised queries (never string-concat
SQL).
- Do all disk/DB I/O off the main thread (async task), then hop back to the main thread to
apply results to the world/players/inventories — the Bukkit API is not thread-safe. On Folia
use the appropriate scheduler (
scheduler.md); Bukkit.getScheduler() assumptions don't hold.
- Store
UUIDs, never Player/Entity objects (holding them leaks memory and pins logged-
off players). Look the object up from the UUID when needed.
- Lifecycle: load/open in
onEnable; flush and close in onDisable (save YAML, close DB
connections/pool) so nothing is lost on shutdown. Cancel any repeating save task there too.
Show the new/edited files (store class, main-class onEnable/onDisable diffs, and any build
file diff for the driver) and get approval before writing.
Phase 5: Verify + hand off
- Re-check the pitfalls: all I/O is async with a main-thread hop to apply; only
UUIDs
are stored (no retained Player/Entity); NamespacedKeys are reused not recreated; SQL is
parameterised; a DB driver is shaded and relocated in the build; and everything is flushed/
closed in onDisable.
- Suggest next steps: "
/build to compile, then /run-server; write some data, restart, and
confirm it reloads."
Do not fabricate APIs for a Minecraft version newer than ../../references/api/VERSION.md
documents — if unsure a method exists in the target version, say so and verify.
1---2name: add-storage3description: Add data persistence to a Minecraft plugin — pick and wire the right store: PersistentDataContainer on items/entities, a YAML data file, or a real database (SQLite/MySQL via a shaded, relocated JDBC driver). Use this whenever the user wants to save/load data, remember values across restarts, persist per-player stats, store custom item/entity tags, keep a database, homes/economy balances, or 'don't lose it when the server restarts' in their Bukkit/Paper plugin. Reads the target stack from .mcplugin/config.yml.4---56# Add data persistence78Pick the right store for the data and wire it safely (async I/O, main-thread apply, clean9shutdown), matching the project's stack and conventions. Follow the collaborative rule: propose10the design, show the code, then write after the user approves.1112## Phase 1: Load context1314- Read `.mcplugin/config.yml` for `platform`, `mc_version`, `package`, `main_class`,15 `plugin_name`, `build_tool`, `group_id`. If it's missing, tell the user to run16 `/setup-platform` (and `/scaffold`) first.17- Read the references and pitfalls (relative to this skill; `Glob` as fallback):18 - `../../references/api/persistence-config.md` — PDC, `NamespacedKey`, YAML config APIs19 - `../../references/api/scheduler.md` — async vs. main thread; Folia's schedulers20 - `../../references/pitfalls.md` — the "Persistence", "Threading", and "Memory leaks" sections21- Look at existing source to match style: `Glob` `src/main/java/**/*.java`, read the main class22 and the build file (`pom.xml` / `build.gradle*`) in case a driver + relocation is needed.2324## Phase 2: Clarify the data2526Ask (AskUserQuestion or plain text) only what you can't infer:27- **What data** — fields and types (e.g. homes = name → world + x/y/z).28- **Scope** — per-player, per-item, per-entity, per-world, or global.29- **Size & shape** — a handful of values vs. thousands of rows; flat vs. relational (does it need30 queries/joins, or just load-all-on-start)?31- **Concurrency** — is it read/written on hot paths, or occasionally?32- **Multi-server** — will more than one server instance share this data? That rules out SQLite33 and a local YAML file and points at MySQL (or another shared DB).34- **Loss tolerance** — must every write be durable immediately, or is periodic/on-quit saving35 acceptable? This decides between write-through and a batched save task.3637## Phase 3: Choose the store (explain the pick)3839- **`PersistentDataContainer` (+ reused `NamespacedKey`)** — best when the data *belongs to* a40 specific item, entity, or chunk (custom item tags, mob metadata). Survives with the object; no41 file to manage. Prefer this over the deprecated metadata API and NBT string hacks.42- **A YAML data file** (separate from `config.yml`) — good for small/medium structured data43 keyed by `UUID` (homes, small stat maps). Simple, human-readable; load on enable, save on44 change/disable. Not for high-frequency writes or large datasets.45- **A real database — SQLite (embedded) or MySQL (shared)** — for large, relational, or46 multi-server data. The **JDBC driver must be added to the build and SHADED into the jar, then47 RELOCATED** (shade/shadow relocation) to avoid classpath clashes with other plugins. SQLite is48 a single local file; MySQL needs connection details in `config.yml`.4950State the recommendation and why, given the scope and size from Phase 2.5152## Phase 4: Implement5354Generate, in the project's package:551. **The chosen store**:56 - *PDC:* build `NamespacedKey`s once (reuse them) and read/write typed values on the object.57 - *YAML:* a data-access class wrapping a `YamlConfiguration` file under the data folder.58 - *DB:* the build change (dependency + shade/relocate config), a connection/pool setup, schema59 creation (`CREATE TABLE IF NOT EXISTS ...`), and parameterised queries (never string-concat60 SQL).612. **Do all disk/DB I/O off the main thread** (async task), then hop back to the main thread to62 apply results to the world/players/inventories — the Bukkit API is not thread-safe. On Folia63 use the appropriate scheduler (`scheduler.md`); `Bukkit.getScheduler()` assumptions don't hold.643. **Store `UUID`s, never `Player`/`Entity` objects** (holding them leaks memory and pins logged-65 off players). Look the object up from the `UUID` when needed.664. **Lifecycle**: load/open in `onEnable`; **flush and close in `onDisable`** (save YAML, close DB67 connections/pool) so nothing is lost on shutdown. Cancel any repeating save task there too.6869Show the new/edited files (store class, main-class `onEnable`/`onDisable` diffs, and any build70file diff for the driver) and get approval before writing.7172## Phase 5: Verify + hand off7374- Re-check the pitfalls: all I/O is **async** with a **main-thread hop** to apply; only **`UUID`s**75 are stored (no retained `Player`/`Entity`); `NamespacedKey`s are reused not recreated; SQL is76 parameterised; a DB driver is **shaded and relocated** in the build; and everything is **flushed/77 closed in `onDisable`**.78- Suggest next steps: "`/build` to compile, then `/run-server`; write some data, restart, and79 confirm it reloads."8081Do not fabricate APIs for a Minecraft version newer than `../../references/api/VERSION.md`82documents — if unsure a method exists in the target version, say so and verify.