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 data
  • SkillRegistry - 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

MethodReturnsDescription
allSkillIds()List<String>All skill IDs (built-in + custom)
allSkillIdsByCategory(category)List<String>Skill IDs in a category ("GATHERING", "COMBAT", "CRAFTING", "MISC")
isKnownSkill(name)booleanCheck if a skill ID exists
isCustomSkill(name)booleanCheck if a skill is server-defined (not built-in)
hasCustomSkills()booleanWhether any custom skills are registered

Metadata Methods

MethodReturnsDescription
getDisplayName(skillId)StringHuman-readable name (e.g. "Mining", "Herbalism")
getCategoryName(skillId)StringCategory 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

MethodReturnsDescription
getLevel(store, ref, skillId)intPlayer's level in a skill (minimum 1)
getXp(store, ref, skillId)longPlayer's total XP in a skill
getLevelProgress(store, ref, skillId)doubleProgress to next level (0.0 to 1.0)

Aggregate Queries

MethodReturnsDescription
getTotalXp(store, ref)longSum of XP across all skills
getTotalLevel(store, ref)intCombined 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)booleanWhether 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.

MethodDescription
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.0Add 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: addXp enforces the per-skill max level cap configured in the server settings. If a player is already at max level, the call returns false. 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

MethodReturnsDescription
getPlayerRef()PlayerRefThe player who gained XP
getSkillId()StringUnified skill identifier (built-in like "MINING" or a custom skill id)
getBaseAmount()longCaller-supplied XP before skill-tree bonuses and XP boosts
getFinalAmount()longXP actually added, after all multipliers and max-level clamping
getXpBefore()longPlayer's XP in this skill before the gain
getXpAfter()longPlayer's XP in this skill after the gain
getLevelBefore()intPlayer's level in this skill before the gain
getLevelAfter()intPlayer's level in this skill after the gain
isLevelUp()booleanTrue 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 touch Store / Refdirectly without world.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.

MethodReturnsDescription
calculateLevelFromXp(xp)intWhat level a given XP amount corresponds to
getXpRequiredForLevel(level)longXP 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.

MethodReturnsDescription
getParryCounterattackBonus(store, ref)doubleCounterattack damage bonus (e.g. 0.05 = 5%)
getParryReflectBonus(store, ref)doubleReflect damage bonus
getParryStaminaDrainBonus(store, ref)doubleStamina drain bonus on parry
getParryStunDamageBonus(store, ref)doubleStun 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]).

Power Level itself changes nothing visible on its own. It exists for a separate, optional companion mod (MMO Mob Scaling) to read and scale mob difficulty against - it is not bundled with MMO Skill Tree or any of its content packs.
MethodReturnsDescription
getPowerLevel(store, ref)doubleThe player's current Power Level, clamped to [getPowerLevelMin(), getPowerLevelMax()]
getPowerLevelMin()doubleLower bound every getPowerLevel value lives on (also the no-skill-data fallback)
getPowerLevelMax()doubleUpper bound every getPowerLevel value lives on
getCombatLevel(store, ref)intThe player's combat level, one of the Power Level pillar inputs
statRewardSum(store, ref, statKey)doubleSum 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:

MethodDescription
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)
Inert without a caller. It is demoed by the NPC-only "Dragon Arcana" ability, but the real consumer is the standalone, optional MMO Mob Scaling mod - not bundled here.

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:

EventFires whenCarries
AchievementProgressedEventOne criterion on an in-progress achievement movedachievement id, criterion id, current/required amounts, whether this advance just finished the criterion
AchievementUnlockedEventA player earned the achievementachievement id, its points, whether rewards are still waiting to be collected
AchievementClaimedEventThe 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's ZigProgressComponent now: one record per player holding quests, achievements and what conversations remember. A caller that assigned the result to the old AchievementComponent type recompiles against the new one; a caller that only passed it back into the reads below needs no change.
  • isAchievementUnlocked, getAchievementProgress, getAchievementPoints
  • pin / unpin / getPinnedAchievements
  • getServerFirstClaim

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.

EventFires when
QuestAcceptedEventA player takes on a quest
QuestObjectiveProgressedEventOne objective on an active quest moved (the highest-frequency event of the set)
QuestCompletedEventEvery objective is met; a parked flag says whether the reward is waiting to be claimed rather than already granted
QuestClaimedEventThe 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
QuestAbandonedEventA 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

MethodReturnsDescription
getStatTotal(store, ref, canonicalKey)longA player's lifetime total for a canonical statistics key (e.g. blocks broken, mobs killed)
getServerFirstClaim(achievementId)ServerFirstClaimStore.ClaimReads 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.

MethodReturnsDescription
getSkillComponent(store, ref)SkillComponentGet existing component (null if none)
getOrCreateSkillComponent(store, cmdBuf, ref)SkillComponentGet 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

ClassPackageDescription
MMOSkillTreeAPIcom.ziggfreed.mmoskilltree.apiStatic methods for all skill data operations
SkillRegistrycom.ziggfreed.mmoskilltree.skillSkill discovery, metadata, and category queries
SkillComponentcom.ziggfreed.mmoskilltree.dataECS component holding all player skill data
PlayerGainXpEventcom.ziggfreed.mmoskilltree.api.eventsPayload fired to listeners registered via addXpListener