Pixel Dream StudiosWiki

API for mod developers

Read skills and levels, react to level-ups, refuse them, respect the pack's locks and replace Reskillable's screen, on NeoForge and Fabric 1.21.1.

Reskillable already handles skill levels, XP costs, perks, locks, gates, custom skills, saving and syncing between server and client. The API lets another mod build on all of that, so players get one progression system instead of several.

With it, a mod can:

  • Read every skill, its icon, perks, the player's levels, XP and level-up costs, to draw its own UI.
  • Explain why a level-up is blocked: not enough XP, the level cap, a gate, and what the gate still needs.
  • React when a skill levels up: unlock a spell, a title or a quest.
  • Refuse a level-up from its own quest, class or faction system.
  • Respect the pack's item, block and entity locks with one call.
  • Replace Reskillable's skill screen and tooltips with its own.

Custom skills from custom_skills.json work through the same calls, with no extra code.

Versions

The API is in Reskillable for Minecraft 1.21.1, on NeoForge and Fabric: the core from 4.1.2, and everything on this page from 4.1.4. The 1.20.1 Forge and 26.1 versions don't have it.

Adding Reskillable to your project

Use CurseMaven. The project ID is 1170464. The file ID is the number at the end of a file's URL on CurseForge.

repositories {
    maven {
        url = "https://www.cursemaven.com"
        content { includeGroup "curse.maven" }
    }
}

dependencies {
    compileOnly "curse.maven:reskillable-reimagined-1170464:<fileId>"
    runtimeOnly "curse.maven:reskillable-reimagined-1170464:<fileId>" // optional: test in dev
}

The mod id is reskillable on both loaders. To keep Reskillable optional, declare it optional and only touch its classes when it is loaded. Keeping those calls in a class of their own is the easiest way.

neoforge.mods.toml:

[[dependencies.yourmod]]
modId = "reskillable"
type = "optional"
versionRange = "[4.1.4,)"
ordering = "NONE"
side = "BOTH"
if (ModList.get().isLoaded("reskillable")) {
    ReskillableHooks.register();
}

Reading skills and levels

Everything is a static method on net.bandit.reskillable.common.ReskillableApi. It works on the server and the client. On the client it reads what the server synced, so a screen can call it every frame.

import net.bandit.reskillable.common.ReskillableApi;

for (String skillId : ReskillableApi.getSkillIds()) {          // built-in, then custom skills
    Component name = ReskillableApi.getDisplayName(skillId);  // the pack's name for it
    int level      = ReskillableApi.getLevel(player, skillId);
    int cost       = ReskillableApi.getLevelUpCost(player, skillId); // XP points

    switch (ReskillableApi.canLevelUp(player, skillId)) {
        case ALLOWED       -> { /* show the level-up button */ }
        case NOT_ENOUGH_XP -> { /* "Needs " + cost + " XP" */ }
        case GATE_BLOCKED  -> { /* "Needs: " + ReskillableApi.getGateMissing(player, skillId).getString() */ }
        default            -> { /* MAX_LEVEL, SPENT_LEVEL_CAP, ... */ }
    }
}

Skill ids are the id values in built_in_skills.json and custom_skills.json. Built-in skills also accept their original names, so attack still works after a pack renames Attack to combat. An unknown id reads as level 1.

Why a level-up is blocked

canLevelUp runs the same checks as a real level-up, in the same order, and returns a LevelUpStatus:

StatusMeans
ALLOWEDThe level-up would go through
UNKNOWN_SKILLNot the id of an enabled skill
LEVELING_DISABLEDenableSkillLeveling is false
MAX_LEVELThe skill is at maximumLevel
SPENT_LEVEL_CAPThe player has spent maxTotalSpentLevels levels
GATE_BLOCKEDA skill_level_gates rule isn't met. getGateMissing lists what's missing
NOT_ENOUGH_XPLess XP than getLevelUpCost. Never returned for creative players
REFUSEDA listener refused it (below). Only levelUp returns this

Perks

getPerks(skillId) lists what a skill gives, as ReskillableApi.Perk records: a name, the attribute, the operation, amountPerStep and perkStep. perk.bonusAt(level) gives the bonus at a level.

for (ReskillableApi.Perk perk : ReskillableApi.getPerks("defense")) {
    double bonus = perk.bonusAt(ReskillableApi.getLevel(player, "defense"));
    lines.add(perk.name().copy().append(": +" + bonus));
}

The built-in Mining, Gathering and Farming perks (break speed, bonus XP, crop growth) aren't attributes, so their attribute is null and their amount is a fraction: 0.25 is 25%.

Gating your own features

Lock a feature behind a skill level:

if (ReskillableApi.getLevel(player, "magic") < 15) {
    player.displayClientMessage(Component.literal("Needs Magic 15"), true);
    return;
}

Or follow the pack's own locks, so your feature agrees with what the pack set in skill_locks.json, craft_skill_locks.json and attack_skill_locks.json:

import net.bandit.reskillable.common.skills.Requirement;
import net.bandit.reskillable.common.skills.RequirementType;

if (!ReskillableApi.meetsRequirements(player, RequirementType.USE, stack)) {
    for (Requirement r : ReskillableApi.getMissingRequirements(player, RequirementType.USE, stack)) {
        // r.skill is the skill id, r.level the level needed
    }
    return;
}
TypeChecksWorks on
USEskill_locks.jsonItem stacks, blocks and entities
CRAFTcraft_skill_locks.jsonItem stacks
ATTACKattack_skill_locks.jsonEntities

getRequirements(type, thing) gives the requirements without a player. These calls don't let creative players through, though Reskillable itself does.

Events

Both events run on the server, and the skill id is the public id.

  • Level changed fires after any level change: a level-up, /skills add, set or respec, and the reset on death when deathSkillReset is on. The player's skill data already holds the new level.
  • Allow level-up fires when a level-up has passed Reskillable's checks, before the XP is taken. Refuse it and nothing changes. Reskillable sends no message on a refusal, so tell the player why. Don't call levelUp for the same player from inside it.

SkillLevelChangedEvent and AllowLevelUpEvent, on the NeoForge event bus:

import net.bandit.reskillable.event.AllowLevelUpEvent;
import net.bandit.reskillable.event.SkillLevelChangedEvent;

@EventBusSubscriber(modid = MyMod.MODID)
public class ReskillableHooks {
    @SubscribeEvent
    static void onLevelChanged(SkillLevelChangedEvent e) {
        if (e.getSkillId().equals("magic") && e.getOldLevel() < 20 && e.getNewLevel() >= 20) {
            MySpells.unlock(e.getEntity(), "fireball");
        }
    }

    @SubscribeEvent
    static void onAllowLevelUp(AllowLevelUpEvent e) {
        if (e.getSkillId().equals("magic") && !QuestData.done(e.getEntity(), "arcane_trial")) {
            e.getEntity().displayClientMessage(Component.literal("Complete the Arcane Trial first."), true);
            e.setCanceled(true);
        }
    }
}

Leveling up from your mod

On the server, levelUp(serverPlayer, skillId) behaves exactly like the player pressing the button: it checks, takes the XP, plays the sound, syncs the client and fires the level-changed event. When a check fails, it tells the player why and returns the status.

// A trainer NPC that levels a skill when the player talks to it
ReskillableApi.LevelUpStatus status = ReskillableApi.levelUp(serverPlayer, "attack");
if (status == ReskillableApi.LevelUpStatus.ALLOWED) {
    trainer.playCelebration();
}

To make a trainer the only way to level, refuse every other level-up:

@SubscribeEvent
static void onAllowLevelUp(AllowLevelUpEvent e) {
    if (Trainers.isTalkingToOne(e.getEntity())) return;
    e.getEntity().displayClientMessage(Component.literal("Find a trainer to level up."), true);
    e.setCanceled(true);
}

On the client, ask the server instead. It runs every check again:

CallDoes
requestLevelUp(skillId)Asks for a level-up, as the button does
requestTogglePerk(skillId)Turns the skill's perk off, or back on
requestGateStatus()Asks for the gate status of every skill

Replacing Reskillable's screen

Turn off what you replace in config/reskillable-common.toml:

OptionDoes
hideSkillScreen = trueRemoves the skills screen, its inventory tab and its key. Needs a restart
hideRequirementTooltips = trueHides the Skill Requirements lines in item tooltips and in Jade

Then draw your own from the API. The client has:

  • Skills, requirements and leveling settings from when the player joins and after /skills reload.
  • Levels and perk toggles after every change.
  • Gate status on join, after each level-up, and after requestGateStatus().

Call requestGateStatus() when your screen opens and about once a second while it's open, as Reskillable's own screen does. A gate that waits on an advancement then opens without the player reopening the screen.

A minimal skill list:

@Override
public void render(GuiGraphics g, int mouseX, int mouseY, float partialTick) {
    super.render(g, mouseX, mouseY, partialTick);
    int y = 20;
    for (String id : ReskillableApi.getSkillIds()) {
        int level = ReskillableApi.getLevel(minecraft.player, id);
        g.drawString(font, ReskillableApi.getDisplayName(id).copy().append(" " + level), 20, y, 0xFFFFFF);
        y += 12;
    }
}

@Override
public void tick() {
    if (++ticks % 20 == 0) ReskillableApi.requestGateStatus();
}

getIcon(skillId) gives the texture a pack set in a skill's icon field, or null when the skill uses Reskillable's own sprite from reskillable:textures/gui/skills.png.

API at a glance

GroupMethods
SkillsgetSkillIds, isSkillEnabled, getDisplayName, getIcon, getPerks, getMaxLevel, getMaxSpentLevels
PlayergetLevel, getTotalLevel, getSpentLevels, isPerkEnabled, getXp, getLevelUpCost, canLevelUp, getGateMissing
RequirementsgetRequirements, getMissingRequirements, meetsRequirements, for item stacks, blocks and entities
ClientrequestLevelUp, requestTogglePerk, requestGateStatus
ServerlevelUp
EventsNeoForge: SkillLevelChangedEvent, AllowLevelUpEvent. Fabric: SkillLevelChangedCallback, AllowLevelUpCallback

Every method is documented in ReskillableApi.java.

On this page