API Reference
Read and modify player skill data from your mod
Overview
MMO Skill Tree provides a Java API that lets other mods read and modify player skill data at runtime. The API is built around two main classes:
MMOSkillTreeAPI- Static methods for reading XP, levels, and modifying player skill dataSkillRegistry- Discover available skills, get display names, and query categories
All methods accept string-based skill IDs (e.g. "MINING", "HERBALISM") and work identically for both built-in and custom skills.
Setup
Add the API jar as a compileOnly dependency. MMO Skill Tree provides it at runtime.
// build.gradle
dependencies {
compileOnly 'ziggfreed:mmoskilltree-api:1.6.0'
}Import the classes you need:
import com.ziggfreed.mmoskilltree.api.MMOSkillTreeAPI;
import com.ziggfreed.mmoskilltree.skill.SkillRegistry;Accessing Player Data
Player data lives in Hytale's Entity Component System (ECS). You need a Store<EntityStore> and a Ref<EntityStore> to access it. These are available from the player entity in event handlers and command contexts.
// From a player reference
Ref<EntityStore> ref = player.getReference();
Store<EntityStore> store = ref.getStore();
// Now you can query skill data
int miningLevel = MMOSkillTreeAPI.getLevel(store, ref, "MINING");
long totalXp = MMOSkillTreeAPI.getTotalXp(store, ref);Thread safety: Store operations must run on the world thread. If you're in an async context, use world.execute(() -> { ... }) to dispatch to the world thread before accessing the store.Skill Discovery
Use SkillRegistry to discover what skills are available on the server, including any custom skills the server owner has defined.
Discovery Methods
| Method | Returns | Description |
|---|---|---|
allSkillIds() | List<String> | All skill IDs (built-in + custom) |
allSkillIdsByCategory(category) | List<String> | Skill IDs in a category ("GATHERING", "COMBAT", "CRAFTING", "MISC") |
isKnownSkill(name) | boolean | Check if a skill ID exists |
isCustomSkill(name) | boolean | Check if a skill is server-defined (not built-in) |
hasCustomSkills() | boolean | Whether any custom skills are registered |
Metadata Methods
| Method | Returns | Description |
|---|---|---|
getDisplayName(skillId) | String | Human-readable name (e.g. "Mining", "Herbalism") |
getCategoryName(skillId) | String | Category name: "GATHERING", "COMBAT", "CRAFTING", or "MISC" |
Example: List All Skills
SkillRegistry registry = SkillRegistry.getInstance();
for (String skillId : registry.allSkillIds()) {
String name = registry.getDisplayName(skillId);
String category = registry.getCategoryName(skillId);
System.out.println(category + " > " + name + " (" + skillId + ")");
}
// Output:
// GATHERING > Mining (MINING)
// GATHERING > Woodcutting (WOODCUTTING)
// COMBAT > Swords (SWORDS)
// MISC > Herbalism (HERBALISM) <-- custom skill
// ...Reading Skill Data
Single Skill Queries
| Method | Returns | Description |
|---|---|---|
getLevel(store, ref, skillId) | int | Player's level in a skill (minimum 1) |
getXp(store, ref, skillId) | long | Player's total XP in a skill |
getLevelProgress(store, ref, skillId) | double | Progress to next level (0.0 to 1.0) |
Aggregate Queries
| Method | Returns | Description |
|---|---|---|
getTotalXp(store, ref) | long | Sum of XP across all skills |
getTotalLevel(store, ref) | int | Combined level (calculated from total XP) |
getAllXpByName(store, ref) | Map<String, Long> | XP for every skill as a map |
getAllLevelsByName(store, ref) | Map<String, Integer> | Levels for every skill as a map |
hasSkillData(store, ref) | boolean | Whether the player has any skill data |
Example: Check a Level Requirement
// Gate access to a dungeon based on combat level
int swordsLevel = MMOSkillTreeAPI.getLevel(store, ref, "SWORDS");
int defenseLevel = MMOSkillTreeAPI.getLevel(store, ref, "DEFENSE");
if (swordsLevel >= 30 && defenseLevel >= 20) {
// Allow entry
} else {
player.sendMessage("Requires Swords 30 and Defense 20");
}Modifying Skill Data
These methods modify XP directly without triggering level-up notifications or XP gain messages. All return boolean - true on success, false if the player has no skill data or the operation is invalid.
| Method | Description |
|---|---|
addXp(store, ref, skillId, amount) | Add XP silently (respects per-skill max level cap). Delegates to the 5-arg overload with showNotification=false |
addXp(store, ref, skillId, amount, showNotification) Since 1.5.0 | Add XP, optionally surfacing the standard XP-gain banner the player would see from a normal action. This is the seam a companion mod (e.g. MMO Mob Scaling) uses so a bonus kill's extra XP stays visible |
removeXp(store, ref, skillId, amount) | Remove XP (fails if insufficient XP) |
setXp(store, ref, skillId, amount) | Set XP to an exact value |
Note:addXpenforces the per-skill max level cap configured in the server settings. If a player is already at max level, the call returnsfalse. If the amount would exceed the cap, it's automatically clamped.
Example: Award XP for a Custom Event
// Award 500 Mining XP when player completes a mining challenge
boolean success = MMOSkillTreeAPI.addXp(store, ref, "MINING", 500);
if (success) {
player.sendMessage("You earned 500 Mining XP!");
}XP Listeners
Instead of polling for XP changes, subscribe to PlayerGainXpEvent and react the moment a player gains XP in any skill. The event fires after XP has been committed to the player'sSkillComponent, after level-up side effects, and after skill-tree milestone checks, so subscribers see final committed state and can safely query the player's new level or XP.
Subscribe Example
Register a listener from your plugin's setup() method. The listener receives aPlayerGainXpEvent and is free to read the event fields or call back intoMMOSkillTreeAPI.
import com.ziggfreed.mmoskilltree.api.MMOSkillTreeAPI;
import com.ziggfreed.mmoskilltree.api.events.PlayerGainXpEvent;
public class MyPlugin extends JavaPlugin {
@Override
protected void setup() {
MMOSkillTreeAPI.addXpListener(event -> {
String skillId = event.getSkillId();
long gained = event.getFinalAmount();
if (event.isLevelUp()) {
int newLevel = event.getLevelAfter();
// e.g. broadcast, grant an achievement, run a command...
}
});
}
}Event Fields
| Method | Returns | Description |
|---|---|---|
getPlayerRef() | PlayerRef | The player who gained XP |
getSkillId() | String | Unified skill identifier (built-in like "MINING" or a custom skill id) |
getBaseAmount() | long | Caller-supplied XP before skill-tree bonuses and XP boosts |
getFinalAmount() | long | XP actually added, after all multipliers and max-level clamping |
getXpBefore() | long | Player's XP in this skill before the gain |
getXpAfter() | long | Player's XP in this skill after the gain |
getLevelBefore() | int | Player's level in this skill before the gain |
getLevelAfter() | int | Player's level in this skill after the gain |
isLevelUp() | boolean | True if getLevelAfter() > getLevelBefore() |
Unsubscribing
Keep a reference to the listener if you plan to remove it later - removeXpListenermatches by identity, so an anonymous lambda cannot be removed after it's registered.
Consumer<PlayerGainXpEvent> listener = event -> { /* ... */ };
MMOSkillTreeAPI.addXpListener(listener);
// Later, during shutdown or when disabling a feature:
MMOSkillTreeAPI.removeXpListener(listener);Registering the same listener twice fires it twice - addXpListener does not deduplicate.
Thread & exception safety: Listeners run synchronously on the world tick thread (the same thread the XP award happened on), so you can touchStore/Refdirectly withoutworld.execute. Exceptions thrown from a listener are caught and logged - one misbehaving subscriber cannot break XP accounting for other listeners or the plugin itself.
Example: Announce Level Milestones
Broadcast a server-wide message when any player hits level 50 or 100 in any skill:
MMOSkillTreeAPI.addXpListener(event -> {
if (!event.isLevelUp()) return;
int level = event.getLevelAfter();
if (level != 50 && level != 100) return;
String skillName = SkillRegistry.getInstance().getDisplayName(event.getSkillId());
String username = event.getPlayerRef().getUsername();
Universe.get().sendMessage(Message.raw(
username + " reached " + skillName + " level " + level + "!"
));
});Utility Methods
These static helpers don't require player data - they work with the server's leveling formula.
| Method | Returns | Description |
|---|---|---|
calculateLevelFromXp(xp) | int | What level a given XP amount corresponds to |
getXpRequiredForLevel(level) | long | XP required to reach a specific level |
// How much XP to reach level 50?
long xpNeeded = MMOSkillTreeAPI.getXpRequiredForLevel(50);
// ~446,000 XP with default settings
// What level is 1,000,000 XP?
int level = MMOSkillTreeAPI.calculateLevelFromXp(1_000_000);Parry Bonuses
If the Perfect Parries integration is active, these methods return the player's accumulated parry bonuses based on their claimed skill tree rewards and currently held weapon.
| Method | Returns | Description |
|---|---|---|
getParryCounterattackBonus(store, ref) | double | Counterattack damage bonus (e.g. 0.05 = 5%) |
getParryReflectBonus(store, ref) | double | Reflect damage bonus |
getParryStaminaDrainBonus(store, ref) | double | Stamina drain bonus on parry |
getParryStunDamageBonus(store, ref) | double | Stun damage bonus on parry |
These methods automatically resolve the player's held weapon to the correct combat skill and sum matching parry rewards. Returns 0.0 if the integration is not active or the player has no parry rewards.
Power / Combat Metrics Since 1.5.0
Frozen, additive reads over a player's multi-pillar Power Level (combat level, claimed skill-tree stat rewards, unlocked abilities, ability mastery, and claimed achievements folded through per-mode weights, clamped [1, 200]).
| Method | Returns | Description |
|---|---|---|
getPowerLevel(store, ref) | double | The player's current Power Level, clamped to [getPowerLevelMin(), getPowerLevelMax()] |
getPowerLevelMin() | double | Lower bound every getPowerLevel value lives on (also the no-skill-data fallback) |
getPowerLevelMax() | double | Upper bound every getPowerLevel value lives on |
getCombatLevel(store, ref) | int | The player's combat level, one of the Power Level pillar inputs |
statRewardSum(store, ref, statKey) | double | Sum of a specific stat across every claimed skill-tree reward, another pillar input |
A companion mod that wants to react to per-kill XP can also register a multiplier hook:
| Method | Description |
|---|---|
registerMobKillXpMultiplier(provider) | Register a MobKillXpMultiplier that scales per-kill XP |
removeMobKillXpMultiplier(provider) | Unregister a previously registered multiplier |
NPC / Entity Ability Casting Since 1.6.0
Lets any entity with a UUIDComponent cast from the ability catalog, bypassing the player-only unlock/cooldown/cost gates a normal player cast goes through.
public static boolean castNpcAbility(Store<EntityStore> store, Ref<EntityStore> casterRef, String abilityId)Native events (no listener registry) Since 1.6.0
The achievement listener registry - MMOSkillTreeAPI.addAchievementListener,removeAchievementListener, fireAchievementEvent, and thePlayerAchievementEvent payload type - is deleted, not deprecated, as of API version 1.6.0. A call to any of them no longer compiles against this version.
Achievement moments are ordinary Hytale events now, dispatched on the world tick thread through the engine's own event bus. A subscriber registers them exactly the way it registers any other server event, and needs nothing from MMOSkillTreeAPI to do it:
// Before (1.5.x and earlier - no longer compiles against 1.6.0)
MMOSkillTreeAPI.addAchievementListener(event -> {
if (event.getPointsAwarded() >= 100) {
announce(event.getAchievementId());
}
});
// After (1.6.0)
import com.ziggfreed.common.achievement.event.AchievementUnlockedEvent;
getEventRegistry().registerGlobal(AchievementUnlockedEvent.class, event -> {
if (event.points() >= 100) {
announce(event.achievementId());
}
});Three events replace the old single callback, all living in ziggfreed-common'scom.ziggfreed.common.achievement.event package. Every server running MMO Skill Tree already loads that library, so no extra dependency is needed to subscribe:
| Event | Fires when | Carries |
|---|---|---|
AchievementProgressedEvent | One criterion on an in-progress achievement moved | achievement id, criterion id, current/required amounts, whether this advance just finished the criterion |
AchievementUnlockedEvent | A player earned the achievement | achievement id, its points, whether rewards are still waiting to be collected |
AchievementClaimedEvent | The achievement's rewards were paid out (immediately after unlock, or later when the player collects) | how many rewards were granted, queued for retry, and failed |
All three name the player by UUID rather than a PlayerRef, and all three carry the achievement's authored tags - its category, its subcategory, and the two flags server_first and feat_of_strength - so a listener can branch on what kind of achievement moved without asking this mod for its catalogue.
The old payload's running-total field has no direct replacement on the event, because it was only ever a convenience: read it when you need it withMMOSkillTreeAPI.getAchievementPoints(store, ref), which is unchanged. Every achievement read on the API is kept verbatim:
getAchievementComponent(store, ref)- same name and arguments, but it returns the shared library'sZigProgressComponentnow: one record per player holding quests, achievements and what conversations remember. A caller that assigned the result to the oldAchievementComponenttype recompiles against the new one; a caller that only passed it back into the reads below needs no change.isAchievementUnlocked,getAchievementProgress,getAchievementPointspin/unpin/getPinnedAchievementsgetServerFirstClaim
On a server upgraded from 1.5.x, a player's saved quests and achievements are copied onto that one record the first time they connect after the update, once, and never again; the reads above answer off it from that moment on. Under the optional database backend the record persists as one PROGRESS blob (see Server Operations).
addXpListener and PlayerGainXpEvent, described above underXP Listeners, are unaffected by any of this.
Quest lifecycle events
Quest moments follow the same pattern: five native events in ziggfreed-common'scom.ziggfreed.common.quest.event package, dispatched on the firing thread and skipped entirely when nothing is listening.
| Event | Fires when |
|---|---|
QuestAcceptedEvent | A player takes on a quest |
QuestObjectiveProgressedEvent | One objective on an active quest moved (the highest-frequency event of the set) |
QuestCompletedEvent | Every objective is met; a parked flag says whether the reward is waiting to be claimed rather than already granted |
QuestClaimedEvent | The quest's rewards were paid out - immediately after completion for a quest that pays out on its own, or later when a parked quest is collected |
QuestAbandonedEvent | A player deliberately gave up an active quest |
Each event carries the quest id, the player's UUID, and the quest's authored tags; QuestObjectiveProgressedEvent adds the objective id and its current/required counts, and QuestClaimedEvent adds the same granted/queued/failed reward counts as its achievement counterpart. The same runtime that fires these events is what the shared library's /zigprogress admin family and the MMO's /mmoquestadmin, /quest and /mmoachadmin aliases drive, so a quest given, completed or reset from the console fires exactly the events a player's own action would (see Commands).
Conversation credit
com.ziggfreed.common.npc.NpcTalkedEvent fires once per credited conversation with a character, after every registered credit sink has already run - so a listener counts the same conversations a quest's TALK_TO_NPC objective does, including conversations credited by MarkTalked inside a dialogue or by theZigTalkCredit NPC action on a character with no dialogue at all. It carries the player's UUID, the character's primary id, every alias that character answers to, and the optional secondary qualifier the credit was tagged with.
Ability cast events
Two native events on the engine bus bracket a cast attempt. MmoAbilityCastEventfires the moment a cast has passed every activation gate and its effect chain has been queued - the earliest point a third party can observe a cast going out, before cost is drained or the cooldown is stamped. MmoAbilityResolvedEvent fires once with the final outcome: success once every post-cast effect has run, or a refusal naming the gate that stopped it. A cast that never reaches the queued point fires only the resolved event, with a failure reason attached.
PlayerAbilityUsedEvent.playerRef can be null: it fires for a non-player caster too (an NPC or a summon), so a listener registered through the internal ability-cast observer must null-check it rather than assume a player. The two engine-bus events above identify the caster by entity reference for exactly this reason, and cover a non-player cast (including one started through castNpcAbility) the same as a player one.Statistics & Server-First
| Method | Returns | Description |
|---|---|---|
getStatTotal(store, ref, canonicalKey) | long | A player's lifetime total for a canonical statistics key (e.g. blocks broken, mobs killed) |
getServerFirstClaim(achievementId) | ServerFirstClaimStore.Claim | Reads the server-first claim record for an achievement (null if unclaimed) |
Direct Component Access
For advanced use cases, you can access the underlying SkillComponent directly. This gives you access to all player data including rewards, settings, and boost tokens.
| Method | Returns | Description |
|---|---|---|
getSkillComponent(store, ref) | SkillComponent | Get existing component (null if none) |
getOrCreateSkillComponent(store, cmdBuf, ref) | SkillComponent | Get or create component (requires CommandBuffer) |
SkillComponent skills = MMOSkillTreeAPI.getSkillComponent(store, ref);
if (skills != null) {
// Read settings
boolean showsXpGains = skills.getShowXpGains();
// Read specific skill data using ByName methods
long miningXp = skills.getXpByName("MINING");
int miningLevel = skills.getLevelByName("MINING");
}Full Example
Here's a complete example that discovers all skills on the server, reads a player's data, and awards bonus XP:
import com.ziggfreed.mmoskilltree.api.MMOSkillTreeAPI;
import com.ziggfreed.mmoskilltree.skill.SkillRegistry;
// Discover available skills
SkillRegistry registry = SkillRegistry.getInstance();
List<String> gatheringSkills = registry.allSkillIdsByCategory("GATHERING");
// Read player data
Ref<EntityStore> ref = player.getReference();
Store<EntityStore> store = ref.getStore();
for (String skillId : gatheringSkills) {
int level = MMOSkillTreeAPI.getLevel(store, ref, skillId);
String name = registry.getDisplayName(skillId);
player.sendMessage(name + ": Level " + level);
}
// Award 1000 XP to the player's highest gathering skill
String bestSkill = null;
int bestLevel = 0;
for (String skillId : gatheringSkills) {
int level = MMOSkillTreeAPI.getLevel(store, ref, skillId);
if (level > bestLevel) {
bestLevel = level;
bestSkill = skillId;
}
}
if (bestSkill != null) {
MMOSkillTreeAPI.addXp(store, ref, bestSkill, 1000);
String name = registry.getDisplayName(bestSkill);
player.sendMessage("Awarded 1000 " + name + " XP!");
}Class Reference
| Class | Package | Description |
|---|---|---|
MMOSkillTreeAPI | com.ziggfreed.mmoskilltree.api | Static methods for all skill data operations |
SkillRegistry | com.ziggfreed.mmoskilltree.skill | Skill discovery, metadata, and category queries |
SkillComponent | com.ziggfreed.mmoskilltree.data | ECS component holding all player skill data |
PlayerGainXpEvent | com.ziggfreed.mmoskilltree.api.events | Payload fired to listeners registered via addXpListener |