> ## 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.

# API

> RPG Leveling API for external mods - add XP, set player language, listen for events, get player level, override config defaults.

# RPG Leveling API

API for external mods to interact with the RPG Leveling system.

**Mod authors are welcome to integrate with RPG Leveling** - use this API to grant XP, listen for level-ups, read player level, override config defaults, and more. See [Share XP with teammates](party-xp-share) for an example (PartyPro); dependency setup and full reference are below.

**Simple integration!** Import the API classes directly. Just declare the dependency in `manifest.json` and the plugin must be installed on the server or same mods folder.

## Adding as Dependency

### Step 1: Declare Dependency in manifest.json

Add to your plugin's `manifest.json`:

```json theme={null}
{
  "OptionalDependencies": {
    "Zuxaw:RPGLeveling": "*"
  }
}
```

Or if you require it:

```json theme={null}
{
  "Dependencies": {
    "Zuxaw:RPGLeveling": ">=0.2.0"
  }
}
```

### Step 2: Import and Use

Just import the API classes directly:

```java theme={null}
import org.zuxaw.plugin.api.RPGLevelingAPI;
import org.zuxaw.plugin.api.XPSource;

// Check if available and use
if (RPGLevelingAPI.isAvailable()) {
    RPGLevelingAPI api = RPGLevelingAPI.get();
    if (api != null) {
        api.addXP(playerUuid, 100.0);
    }
}
```

## Quick Start

```java theme={null}
import org.zuxaw.plugin.api.RPGLevelingAPI;
import org.zuxaw.plugin.api.XPSource;

// Check API availability
if (RPGLevelingAPI.isAvailable()) {
    RPGLevelingAPI api = RPGLevelingAPI.get();
    if (api != null) {
        // Add XP to a player
        api.addXP(playerUuid, 100.0);
        
        // Listen for XP gains
        api.registerExperienceGainedListener(event -> {
            event.setXpAmount(event.getXpAmount() * 1.5);
        });
        
        // Listen for level ups
        api.registerLevelUpListener(event -> {
            int level = event.getNewLevel();
            // Your code here
        });
    }
}
```

## Adding Experience

```java theme={null}
// Basic usage
api.addXP(playerUuid, 100.0);

// With custom source
XPSource QUEST = XPSource.create("QUEST");
api.addXP(playerUuid, 100.0, QUEST);
```

**Built-in Sources:**

* `XPSource.ENTITY_KILL` - XP from killing entities
* `XPSource.COMMAND` - XP from admin commands
* `XPSource.BLOCK_BREAK` - XP from breaking blocks (mining/woodcutting)
* `XPSource.HARVEST` - XP from harvesting crops (F key / sickle)
* `XPSource.TAME` - XP from taming creatures (NPC becomes a `Tamed_*` role)
* `XPSource.create("TAMEWORK")` - XP mirrored from **Alec's Tamework** companion feed / harvest / breeding (when that mod is installed; internal bridge only)

**Create Custom Sources:**

```java theme={null}
XPSource QUEST = XPSource.create("QUEST");
XPSource PARTY_SHARE = XPSource.create("PARTY_SHARE");
```

<h2 id="player-language">
  Player language
</h2>

Set the RPG Leveling UI/message language (same codes as `languages/MessagesLanguageMapping_<code>.json`, e.g. `english`). Blank string clears the preference (server default from **`DefaultLanguage`** in **`RPGLevelingConfig.json`**).

```java theme={null}
import org.zuxaw.plugin.api.RPGLevelingAPI;

if (RPGLevelingAPI.isAvailable()) {
    RPGLevelingAPI api = RPGLevelingAPI.get();
    if (api != null) {
        api.setPlayerLanguage(playerUuid, "english");
        api.setPlayerLanguageByUsername("zuxaw", "english");
        api.setPlayerLanguage(playerUuid, "");
        api.setPlayerLanguage(playerRef, store, entityRef, "french");
    }
}
```

Returns **`false`** if the plugin/registry is unavailable, the player was not found, or the language code is non-empty but no bundle is loaded for it.

## Experience Gained Events

Modify or cancel XP before it's awarded.

```java theme={null}
api.registerExperienceGainedListener(event -> {
    // Access all event properties
    PlayerRef player = event.getPlayer();
    double xp = event.getXpAmount();
    XPSource source = event.getSource();
    boolean cancelled = event.isCancelled();
    Object sourceContext = event.getSourceContext();
    
    // Modify XP
    event.setXpAmount(xp * 2.0);
    
    // Cancel XP
    event.setCancelled(true);
});
```

### All Available Properties

| Property                 | Type                | Description                              | Mutable                    |
| ------------------------ | ------------------- | ---------------------------------------- | -------------------------- |
| `getPlayer()`            | `PlayerRef`         | Player gaining XP                        | No                         |
| `getXpAmount()`          | `double`            | Current XP amount                        | Yes (via `setXpAmount()`)  |
| `setXpAmount(double)`    | `void`              | Change XP amount                         | -                          |
| `getSource()`            | `XPSource`          | Source of XP                             | No                         |
| `isCancelled()`          | `boolean`           | Check if cancelled                       | Yes (via `setCancelled()`) |
| `setCancelled(boolean)`  | `void`              | Cancel or allow XP                       | -                          |
| `getSourceContext()`     | `Object`            | Raw source context (nullable)            | No                         |
| `getEntityKillContext()` | `EntityKillContext` | Entity kill data (for ENTITY\_KILL only) | No                         |
| `getBlockBreakContext()` | `BlockBreakContext` | Block break data (for BLOCK\_BREAK only) | No                         |
| `getHarvestContext()`    | `HarvestContext`    | Crop harvest data (for HARVEST only)     | No                         |
| `getTameContext()`       | `TameContext`       | Tame data (for TAME only)                | No                         |

### XPSource Properties

| Property         | Type      | Description                                 |
| ---------------- | --------- | ------------------------------------------- |
| `getName()`      | `String`  | Source name (e.g., "ENTITY\_KILL", "QUEST") |
| `getMetadata()`  | `Object`  | Optional metadata (nullable)                |
| `equals(Object)` | `boolean` | Compare sources by name                     |

### EntityKillContext Properties (for ENTITY\_KILL only)

| Property           | Type      | Description                           |
| ------------------ | --------- | ------------------------------------- |
| `getEntityUuid()`  | `UUID`    | UUID of killed entity                 |
| `getEntityLevel()` | `int`     | Level of killed entity (0 if unknown) |
| `hasEntityLevel()` | `boolean` | Whether level is known                |

### HarvestContext Properties (for HARVEST only)

| Property              | Type      | Description                        |
| --------------------- | --------- | ---------------------------------- |
| `getBlockTypeId()`    | `String`  | Harvested crop block ID            |
| `getSeedId()`         | `String`  | Linked seed item ID                |
| `getTier()`           | `int`     | Crop tier (1-4)                    |
| `getTierMultiplier()` | `double`  | Tier XP multiplier applied         |
| `getZoneId()`         | `int`     | Zone ID (0 if unknown)             |
| `getZoneLevelMid()`   | `int`     | Zone level midpoint (0 if unknown) |
| `hasZoneInfo()`       | `boolean` | Whether zone info is known         |

### TameContext Properties (for TAME only)

| Property           | Type      | Description                              |
| ------------------ | --------- | ---------------------------------------- |
| `getEntityUuid()`  | `UUID`    | UUID of the tamed NPC entity             |
| `getEntityLevel()` | `int`     | Level of the tamed entity (0 if unknown) |
| `hasEntityLevel()` | `boolean` | Whether level is known                   |
| `getRoleName()`    | `String`  | NPC role name (nullable)                 |

### Example - Entity Kill

```java theme={null}
api.registerExperienceGainedListener(event -> {
    XPSource source = event.getSource();
    
    // Check if entity kill
    if (source.equals(XPSource.ENTITY_KILL)) {
        EntityKillContext context = event.getEntityKillContext();
        if (context != null) {
            UUID entityUuid = context.getEntityUuid();
            int entityLevel = context.getEntityLevel();
            
            // Apply bonus based on entity level
            if (entityLevel >= 50) {
                event.setXpAmount(event.getXpAmount() * 1.5);
            }
        }
    }
});
```

### Example - Custom Source

```java theme={null}
XPSource QUEST = XPSource.create("QUEST");

api.registerExperienceGainedListener(event -> {
    XPSource source = event.getSource();
    
    // Check source by name
    if (source.getName().equals("QUEST")) {
        // Or check by equals
        if (source.equals(QUEST)) {
            // Modify XP for quests
            event.setXpAmount(event.getXpAmount() * 1.2);
        }
    }
    
    // Access source metadata (if custom source has it)
    Object metadata = source.getMetadata();
    if (metadata != null) {
        // Your custom logic
    }
});
```

## Level Up Events

React when players level up.

```java theme={null}
api.registerLevelUpListener(event -> {
    PlayerRef player = event.getPlayer();
    int oldLevel = event.getOldLevel();
    int newLevel = event.getNewLevel();
    int levelsGained = event.getLevelsGained();
    
    // Your code here
});
```

**Available Methods:**

| Method              | Returns     | Description              |
| ------------------- | ----------- | ------------------------ |
| `getPlayer()`       | `PlayerRef` | Player who leveled up    |
| `getOldLevel()`     | `int`       | Level before leveling up |
| `getNewLevel()`     | `int`       | Level after leveling up  |
| `getLevelsGained()` | `int`       | Number of levels gained  |

## Getting Player Information

When you only have a **UUID** (e.g. from a command or async context), use the UUID overload. It uses Holder only and may return incorrect data when Holder is null.

```java theme={null}
// By UUID (Holder-only; use playerRef+store when you have Store)
PlayerLevelInfo info = api.getPlayerLevelInfo(playerUuid);
if (info != null) {
    int level = info.getLevel();
    double xp = info.getExperience();
    double xpNeeded = info.getXpNeededForNext();
    boolean isMax = info.isMaxLevel();
}

// Quick access by UUID
int level = api.getPlayerLevel(playerUuid);
double xp = api.getPlayerXP(playerUuid);
boolean isMax = api.isPlayerMaxLevel(playerUuid);
```

When you have a **PlayerRef and Store** (e.g. in a system tick or GUI), use the two-arg overload for correct level/XP. **Store is required.**

```java theme={null}
// By PlayerRef + Store (required; call from world thread)
PlayerLevelInfo info = api.getPlayerLevelInfo(playerRef, store);
int level = info.getLevel();
double xp = info.getExperience();
double xpNeeded = info.getXpNeededForNext();
boolean isMax = info.isMaxLevel();
```

## Common Use Cases

### Party XP Sharing

```java theme={null}
XPSource PARTY_SHARE = XPSource.create("PARTY_SHARE");

api.registerExperienceGainedListener(event -> {
    if (event.getSource().equals(XPSource.ENTITY_KILL)) {
        // Cancel original XP
        event.setCancelled(true);
        
        // Share XP with party (your party system code)
        double sharedXP = event.getXpAmount() / partySize;
        for (UUID member : partyMembers) {
            api.addXP(member, sharedXP, PARTY_SHARE);
        }
    }
});
```

### Quest Rewards

```java theme={null}
XPSource QUEST = XPSource.create("QUEST");

// When quest completes
api.addXP(playerUuid, questXPReward, QUEST);
```

### Level-Based Actions

```java theme={null}
api.registerLevelUpListener(event -> {
    int level = event.getNewLevel();
    // Your reward/notification code here
});
```

## API Methods Reference

### Availability & Version

| Method          | Returns          | Description                           |
| --------------- | ---------------- | ------------------------------------- |
| `get()`         | `RPGLevelingAPI` | Get API instance (null if not loaded) |
| `isAvailable()` | `boolean`        | Check if API is available             |
| `getVersion()`  | `String`         | Get API version (e.g., "0.2.0")       |

### Listener Management

| Method                                           | Returns   | Description                                         |
| ------------------------------------------------ | --------- | --------------------------------------------------- |
| `registerExperienceGainedListener(listener)`     | `void`    | Register XP gain handler                            |
| `unregisterExperienceGainedListener(listener)`   | `boolean` | Unregister handler (returns true if was registered) |
| `isExperienceGainedListenerRegistered(listener)` | `boolean` | Check if handler is registered                      |
| `getExperienceGainedListenerCount()`             | `int`     | Get number of registered handlers                   |
| `registerLevelUpListener(listener)`              | `void`    | Register level up handler                           |
| `unregisterLevelUpListener(listener)`            | `boolean` | Unregister handler (returns true if was registered) |
| `isLevelUpListenerRegistered(listener)`          | `boolean` | Check if handler is registered                      |
| `getLevelUpListenerCount()`                      | `int`     | Get number of registered handlers                   |

### Level reads (player and monster)

Use the API for all level reads so behavior is consistent across the plugin and external mods.

| Method                                 | Returns                   | Description                                                                                                                                                                                        |
| -------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getPlayerLevelInfo(playerUuid)`       | `PlayerLevelInfo` or null | Full level/XP info for a player by UUID (Holder-only; use playerRef+store when you have Store for correct data)                                                                                    |
| `getPlayerLevelInfo(playerRef, store)` | `PlayerLevelInfo`         | Full level/XP info for a player by PlayerRef; **store required** (call from world thread)                                                                                                          |
| `getPlayerLevel(playerUuid)`           | `int`                     | Player level (-1 if not found)                                                                                                                                                                     |
| `getMonsterLevel(store, npcRef)`       | `int`                     | Monster/NPC level (1–100, or 0 if unknown). Uses cache, entity overrides (ZoneLevelConfig), or computed from HP/zone. Use when you have the entity Store and Ref (e.g. in damage/defense systems). |

## Getting Player Class Information

Read the player's selected class, tier, and all class progress.

```java theme={null}
// By UUID (Holder-only)
RPGLevelingAPI.PlayerClassInfo classInfo = api.getPlayerClassInfo(playerUuid);
if (classInfo != null && classInfo.hasClass()) {
    String classId = classInfo.getClassId();           // e.g. "Heavy"
    String display = classInfo.getDisplayName();        // e.g. "Heavy Warrior"
    int tier = classInfo.getSelectedClassTier();        // 0–4
    int kills = classInfo.getKillsInCurrentTier();      // kills towards next tier
    
    Map<String, Integer> allTiers = classInfo.getAllClassTiers();
    // e.g. {"Heavy": 3, "Edge": 1, "Arcane": 0}
}

// Quick access by UUID
String classId = api.getPlayerClassId(playerUuid);     // null if player not found
int tier = api.getPlayerClassTier(playerUuid);          // -1 if not found or no class
Map<String, Integer> all = api.getPlayerAllClassTiers(playerUuid); // null if not found
```

When you have a **PlayerRef, entity Ref, and Store** (e.g. in a system tick or GUI), use the three-arg overload for correct data:

```java theme={null}
RPGLevelingAPI.PlayerClassInfo classInfo = api.getPlayerClassInfo(playerRef, entityRef, store);
```

**PlayerClassInfo Properties:**

| Method                    | Returns                | Description                                                    |
| ------------------------- | ---------------------- | -------------------------------------------------------------- |
| `getClassId()`            | `String`               | Selected class ID (e.g. "Heavy"), or empty string              |
| `getDisplayName()`        | `String`               | Display name (e.g. "Heavy Warrior"), or empty string           |
| `getSelectedClassTier()`  | `int`                  | Tier of selected class (0–4), or 0 if none                     |
| `getKillsInCurrentTier()` | `int`                  | Kills towards next tier for selected class                     |
| `hasClass()`              | `boolean`              | Whether the player has a class selected                        |
| `getAllClassTiers()`      | `Map<String, Integer>` | All class IDs the player has progress in, mapped to tier (0–4) |

### Class reads (player)

| Method                                            | Returns                        | Description                                                               |
| ------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------- |
| `getPlayerClassInfo(playerUuid)`                  | `PlayerClassInfo` or null      | Full class info by UUID (Holder-only)                                     |
| `getPlayerClassInfo(playerRef, entityRef, store)` | `PlayerClassInfo`              | Full class info by PlayerRef; **store required** (call from world thread) |
| `getPlayerClassId(playerUuid)`                    | `String` or null               | Selected class ID (null if player not found)                              |
| `getPlayerClassTier(playerUuid)`                  | `int`                          | Selected class tier (-1 if not found or no class)                         |
| `getPlayerAllClassTiers(playerUuid)`              | `Map<String, Integer>` or null | All class tiers (null if player not found)                                |

## Config Defaults Override API

Override RPG Leveling's default config values from your mod. Admin-customized values are never overwritten.

Call `registerConfigDefaults` in your plugin's `setup()` method. RPG Leveling applies overrides in its `start()` phase (after all mods have registered).

<Note>Config default overrides are startup-only - they are **not** re-applied on `/lvl reload`.</Note>

<Warning>
  **Only one mod can override a given field.** If two mods both try to set `MaxLevel`, only the first to register wins - the second is silently ignored (a warning is logged). There is no merging or conflict resolution beyond first-come-first-served. **Prefer adding content via array appends** (e.g. new instances, zones, rewards) rather than replacing hard-coded scalar values - arrays compose safely across multiple mods, while scalar overrides do not. Only override global values like `MaxLevel`, `RateExp`, or `DifficultyPreset` if your mod absolutely requires it.
</Warning>

### Basic Usage

```java theme={null}
import org.zuxaw.plugin.api.RPGLevelingAPI;

// In your plugin's setup() method:
RPGLevelingAPI.registerConfigDefaults("MyMod", defaults -> {
    defaults
        .forConfig("RPGLevelingConfig.json")
            .setDefault("MaxLevel", 155)
            .setDefault("RateExp", 5.0)
            .done();
});
```

**How it works:** when the server starts, RPG Leveling reads the on-disk config and its bundled default. If the on-disk value matches the bundled default (meaning the admin hasn't touched it), the override is applied. If the admin has customized the value, the override is skipped.

### Updating a Previous Override

When you release a new version that changes your override value, pass the previous value(s) in `alsoReplaces` so the applier recognizes them as "not admin-customized":

```java theme={null}
RPGLevelingAPI.registerConfigDefaults("MyMod", defaults -> {
    defaults
        .forConfig("RPGLevelingConfig.json")
            // v1.0 set MaxLevel to 155, v2.0 now wants 200
            .setDefault("MaxLevel", 200, 155)
            .done();
});
```

Without `alsoReplaces`, the applier would see `155` on disk, compare it to the bundled default `100`, find a mismatch, and skip it (thinking an admin changed it). With `alsoReplaces(155)`, it knows 155 was a previous mod default and migrates it to 200.

### Appending to Arrays

Add elements to JSON arrays (e.g. new dungeon instances). The element is only appended if no existing element has the same ID:

```java theme={null}
import com.google.gson.JsonObject;

JsonObject dungeon = new JsonObject();
dungeon.addProperty("Id", "FrozenDungeon");
dungeon.addProperty("LevelMin", 60);
dungeon.addProperty("LevelMax", 80);

RPGLevelingAPI.registerConfigDefaults("MyMod", defaults -> {
    defaults
        .forConfig("InstanceLevelConfig.json")
            .appendToArray("Instances", "Id", dungeon)
            .done();
});
```

### Multiple Config Files

Chain overrides for multiple config files in a single call:

```java theme={null}
RPGLevelingAPI.registerConfigDefaults("MyMod", defaults -> {
    defaults
        .forConfig("RPGLevelingConfig.json")
            .setDefault("MaxLevel", 155)
            .done()
        .forConfig("StatsLevelConfig.json")
            .setDefault("StatPointsPerLevel", 10)
            .done()
        .forConfig("InstanceLevelConfig.json")
            .appendToArray("Instances", "Id", myDungeonJson)
            .done();
});
```

### Supported Config Files

Any RPG Leveling config file can be targeted:

| Config File                        | Key Format | Example Fields                                                         |
| ---------------------------------- | ---------- | ---------------------------------------------------------------------- |
| `RPGLevelingConfig.json`           | PascalCase | `MaxLevel`, `RateExp`, `EnableHUD`, `DifficultyPreset`                 |
| `StatsLevelConfig.json`            | PascalCase | `StatPointsPerLevel`, `MaxStatPointsHealth`, `DamageStatValuePerPoint` |
| `DatabaseConfig.json`              | PascalCase | `Enabled`, `JdbcUrl`                                                   |
| `ZoneLevelConfig.json`             | PascalCase | `Zones` (array), zone entries                                          |
| `InstanceLevelConfig.json`         | PascalCase | `Instances` (array), instance entries                                  |
| `LevelRewardsConfig.json`          | PascalCase | `Rewards` (array)                                                      |
| `Classes/ClassesGlobalConfig.json` | PascalCase | `EnableClasses`, `ClassChangeCooldownSeconds`                          |
| `MessagesLanguageMapping.json`     | mixed      | Message keys                                                           |

### Behavior Summary

| On-Disk Value                   | Action      | Reason                                    |
| ------------------------------- | ----------- | ----------------------------------------- |
| Matches RPG Leveling's default  | **Applied** | Admin hasn't customized it                |
| Matches an `alsoReplaces` value | **Applied** | Mod is updating its own previous override |
| Already at the new target value | **Skipped** | Already applied (e.g. second restart)     |
| Any other value                 | **Skipped** | Admin customized it, preserved            |
| Field missing from file         | **Added**   | Field was absent, mod provides it         |

### Conflict Resolution

* **Scalar fields**: first mod to register for a `(configFile, jsonPath)` wins. The second mod's registration is rejected and a warning is logged at startup.
* **Array appends**: multiple mods can append different elements. If two mods append an element with the same ID, the first one stays (duplicate skipped at apply time).
* **Startup log**: all registered overrides are listed with source mod IDs.

### Static Methods

| Method                                        | Returns                  | Description                                           |
| --------------------------------------------- | ------------------------ | ----------------------------------------------------- |
| `registerConfigDefaults(modId, configurator)` | `void`                   | Register config default overrides (call in `setup()`) |
| `getConfigDefaultsRegistry()`                 | `ConfigDefaultsRegistry` | Get the registry (internal use)                       |

## Notes

* All methods are thread-safe
* **Level reads:** Use `getPlayerLevelInfo` / `getPlayerLevel` for player level and `getMonsterLevel(store, npcRef)` for monster level so behavior is consistent everywhere (plugin and external mods use the same API). When you have an entity Store (e.g. in systems or GUI), call `getPlayerLevelInfo(playerRef, store)` - store is required for correct data when Holder is null.
* XP amounts must be positive
* Players must be online to receive XP
* Level ups are handled automatically
* Events fire in registration order
* Handler errors are logged but don't stop other handlers
* API instance is automatically refreshed on plugin reload
* Config default overrides are startup-only (not re-applied on `/lvl reload`)
