> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rpg-leveling.zuxaw.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Database Sync

> External database sync - mirror mode for analytics, or shared database for multi-server progression.

# Database Sync

<Callout type="warning" title="External database access - use at your own risk">
  Anyone with SQL or admin access to your database (or `/lvl syncdb`, manual `UPDATE`/`DELETE`, phpMyAdmin, etc.) can **read and change player progression** outside the game. Incorrect edits, failed migrations, or conflicting writes between servers can **corrupt or permanently lose** level, XP, stats, or class data. **Back up your database regularly** before enabling sync or running manual SQL. The RPG Leveling mod author is **not responsible** for data loss, rollback, or damage caused by your configuration, third-party tools, or direct database manipulation - **you** are responsible for securing the DB, testing changes, and keeping backups.
</Callout>

Store player level, XP, stat allocations, classes, and reset points in an external database (MySQL, MariaDB, PostgreSQL, or H2). Use **mirror mode** for dashboards and analytics, or turn off mirroring so the **database is the source of truth** across several game servers.

## Modes

| `Enabled` | `MirrorDataOnly` | Behavior                                                                                                 |
| --------- | ---------------- | -------------------------------------------------------------------------------------------------------- |
| `false`   | (ignored)        | No database; progression stays in Hytale `universe/players` files only.                                  |
| `true`    | `true` (default) | **Mirror:** in-game data is authoritative; the DB is updated when data changes.                          |
| `true`    | `false`          | **Shared progression:** the DB is authoritative; data loads when a player joins; changes save to the DB. |

<Info>
  For multi-server networks, set the **same** `JdbcUrl` on every node, set **`MirrorDataOnly`** to **`false`**, and keep leveling config (max level, XP rates, etc.) aligned on all servers.
</Info>

## Configuration File

`DatabaseConfig.json` lives in `mods/Zuxaw_RPGLeveling/`.

### Parameters

| Parameter          | Type    | Default          | Description                                                                    |
| ------------------ | ------- | ---------------- | ------------------------------------------------------------------------------ |
| **Version**        | string  | (plugin version) | Written by the plugin for compatibility.                                       |
| **Enabled**        | boolean | `false`          | Connect to the database when `true`.                                           |
| **MirrorDataOnly** | boolean | `true`           | `true` = one-way mirror to DB. `false` = DB is source of truth (multi-server). |
| **JdbcUrl**        | string  | H2 file URL      | JDBC connection string.                                                        |
| **Username**       | string  | `""`             | Database user (empty for H2 file).                                             |
| **Password**       | string  | `""`             | Database password.                                                             |
| **MaxPoolSize**    | integer | `10`             | Connection pool size; use **10–20** on large servers with a remote DB.         |

### Mirror mode (analytics)

```json theme={null}
{
  "Enabled": true,
  "MirrorDataOnly": true,
  "JdbcUrl": "jdbc:mariadb://localhost:3306/rpgleveling",
  "Username": "rpguser",
  "Password": "rpgpass",
  "MaxPoolSize": 10
}
```

On startup, player files on disk are copied to the DB once (and again after a short delay). Whenever progression changes in-game, the DB is updated asynchronously.

### Multi-server (database source of truth)

```json theme={null}
{
  "Enabled": true,
  "MirrorDataOnly": false,
  "JdbcUrl": "jdbc:mariadb://db.example.com:3306/rpgleveling",
  "Username": "rpguser",
  "Password": "rpgpass",
  "MaxPoolSize": 15
}
```

* **No** bulk disk→DB sync on startup (avoids overwriting shared data with empty local files).
* Each joining player: **one** DB read, then stats and HUD refresh. If a **row exists**, **level / XP / stats / classes** come from the database; **claimed rewards, language, notification/HUD prefs, and class-change cooldown** are merged from `universe/players/{uuid}.json` when those columns are empty in the DB, then written back. If there is **no row yet**, the player file is **copied to the database** (first visit on the network).
* On **first startup** with an **empty** database, all `universe/players/*.json` files are imported once (does not overwrite a DB that already has data).
* Writes are **async** and **coalesced** (\~1 second per player) to reduce load with 100+ players.
* Pending writes flush when the player disconnects.

## Admin command: `/lvl syncdb`

Permission: `rpgleveling.command.syncdb`

<Warning>
  The game uses the **`JdbcUrl` in `DatabaseConfig.json`**, not any other database you inspect separately. If you query Docker MariaDB but the config still points at the default **H2 file** (`jdbc:h2:file:...`), pull/push only touch H2 - your SQL changes in MariaDB will appear to do nothing. Run **`/lvl syncdb status`** in-game to see the active URL, then **restart the server** after changing the config.
</Warning>

After manual SQL edits, pull data into the game while the player is **online**:

| Command                         | Action                                           |
| ------------------------------- | ------------------------------------------------ |
| `/lvl syncdb status`            | Show JDBC URL and mirror vs source-of-truth mode |
| `/lvl syncdb pull`              | Pull DB → you (in-game)                          |
| `/lvl syncdb pullplayer <name>` | Pull DB → online player                          |
| `/lvl syncdb online`            | Pull DB → all online players (batched)           |
| `/lvl syncdb push`              | Push your in-game data → DB                      |
| `/lvl syncdb pushplayer <name>` | Push online player → DB                          |
| `/lvl syncdb pushonline`        | Push all online players → DB                     |

Or have the player **rejoin** after SQL changes (source-of-truth mode loads on join automatically).

Offline admin commands (`/lvl setlevel`, `setpoints`, `setreset`, `resetstats`, `setclasstier`, `resetrewards`, …) update `universe/players/{uuid}.json` and **write through to the database immediately** when sync is enabled.

## Connection URLs

| Database       | JDBC URL format                          |
| -------------- | ---------------------------------------- |
| **MariaDB**    | `jdbc:mariadb://host:port/database`      |
| **MySQL**      | `jdbc:mysql://host:port/database`        |
| **PostgreSQL** | `jdbc:postgresql://host:port/database`   |
| **H2 (file)**  | `jdbc:h2:file:./path/to/file;MODE=MySQL` |

Table name: `RPGLeveling_players` (created automatically).

## Table columns

| Column                                                                            | Description                       |
| --------------------------------------------------------------------------------- | --------------------------------- |
| `uuid`                                                                            | Player UUID (primary key)         |
| `username`                                                                        | Display name                      |
| `level`, `experience`                                                             | Level and XP                      |
| `available_stat_points`, `allocated_stats`                                        | Stat points (JSON map)            |
| `reset_points`                                                                    | Reset points from rewards         |
| `selected_class_id`, `class_tiers`, `class_kills_in_tier`                         | Class progression (JSON)          |
| `claimed_reward_levels`, `language`, `hide_*` prefs, `last_class_change_epoch_ms` | Rewards, UI prefs, class cooldown |
| `last_updated`                                                                    | Last write time (ms)              |

**Not synced:** inventory, position, vanilla health, or other non-RPG components.

## For developers

Adding a new persisted player field: use the Cursor skill **`add-player-sync-field`** and read **`src/main/java/org/zuxaw/plugin/database/README.md`**.

| Action                    | API                                              |
| ------------------------- | ------------------------------------------------ |
| Save after in-game change | `databaseSyncService.saveData(playerRef, store)` |
| Read world / disk         | `readData(playerRef, store)` / `readData(uuid)`  |
| Read SQL                  | `readDataFromDatabase(uuid)`                     |

`saveData` no-ops when sync is disabled. Deprecated for external mods: `syncIfEnabled`, `parseSnapshotFromDisk` (still delegate to the new API).

## Performance notes

* One `SELECT` per player on join (source-of-truth mode), not a full table scan.
* Writes are coalesced per player to limit DB QPS during XP bursts.
* Tune `MaxPoolSize` for your DB host; avoid oversized pools.
* Restart the server after changing `JdbcUrl` or `Enabled` (`/lvl reload` updates the JSON in memory only).

## Limitations

* See the **warning callout at the top of this page** before enabling database sync or editing rows manually.
* Prefer **one active session per player** across the network; two servers with the same player online can race (last write wins).
* Switching DB vendor does not migrate data automatically - export/import manually.
* Leaderboard still reads local player files; after DB load on join, local files should match.
