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();
}fabric.mod.json:
"suggests": {
"reskillable": ">=4.1.4"
}if (FabricLoader.getInstance().isModLoaded("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:
| Status | Means |
|---|---|
ALLOWED | The level-up would go through |
UNKNOWN_SKILL | Not the id of an enabled skill |
LEVELING_DISABLED | enableSkillLeveling is false |
MAX_LEVEL | The skill is at maximumLevel |
SPENT_LEVEL_CAP | The player has spent maxTotalSpentLevels levels |
GATE_BLOCKED | A skill_level_gates rule isn't met. getGateMissing lists what's missing |
NOT_ENOUGH_XP | Less XP than getLevelUpCost. Never returned for creative players |
REFUSED | A 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;
}| Type | Checks | Works on |
|---|---|---|
USE | skill_locks.json | Item stacks, blocks and entities |
CRAFT | craft_skill_locks.json | Item stacks |
ATTACK | attack_skill_locks.json | Entities |
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,setorrespec, and the reset on death whendeathSkillResetis 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
levelUpfor 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);
}
}
}SkillLevelChangedCallback and AllowLevelUpCallback. For the second, return false to refuse; the first listener that returns false decides.
import net.bandit.reskillable.event.AllowLevelUpCallback;
import net.bandit.reskillable.event.SkillLevelChangedCallback;
public static void register() {
SkillLevelChangedCallback.EVENT.register((player, skillId, oldLevel, newLevel) -> {
if (skillId.equals("magic") && oldLevel < 20 && newLevel >= 20) {
MySpells.unlock(player, "fireball");
}
});
AllowLevelUpCallback.EVENT.register((player, skillId) -> {
if (skillId.equals("magic") && !QuestData.done(player, "arcane_trial")) {
player.displayClientMessage(Component.literal("Complete the Arcane Trial first."), true);
return false;
}
return 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:
| Call | Does |
|---|---|
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:
| Option | Does |
|---|---|
hideSkillScreen = true | Removes the skills screen, its inventory tab and its key. Needs a restart |
hideRequirementTooltips = true | Hides 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
| Group | Methods |
|---|---|
| Skills | getSkillIds, isSkillEnabled, getDisplayName, getIcon, getPerks, getMaxLevel, getMaxSpentLevels |
| Player | getLevel, getTotalLevel, getSpentLevels, isPerkEnabled, getXp, getLevelUpCost, canLevelUp, getGateMissing |
| Requirements | getRequirements, getMissingRequirements, meetsRequirements, for item stacks, blocks and entities |
| Client | requestLevelUp, requestTogglePerk, requestGateStatus |
| Server | levelUp |
| Events | NeoForge: SkillLevelChangedEvent, AllowLevelUpEvent. Fabric: SkillLevelChangedCallback, AllowLevelUpCallback |
Every method is documented in ReskillableApi.java.
