Adding a Skill
Inside your plugin's registerExperienceType, call one of the registry's register overloads. Each skill needs a unique name, a SkillsConfig (its default settings) and the attribute it buffs.
The register Overloads
// Attribute bonus only
void register(String name, SkillsConfig config, Holder<Attribute> attributeHolder);
// Attribute bonus + custom per-refresh logic
void register(String name, SkillsConfig config, Holder<Attribute> attributeHolder, Handler handler);name— the skill id. Every derived id is built from it (for examplename + "_experience"for the XP attachment), so it must be unique.config— the defaultSkillsConfig. It's written to the config file on first run and players can override every field afterward.attributeHolder— the attribute the level bonus is applied to, e.g.Attributes.MOVEMENT_SPEED.handler— optional; extra work run each time the skill is refreshed (see Handlers).
The SkillsConfig Record
new SkillsConfig(
boolean enabled,
float incrementPerLevel,
int xpAwardActionCount,
int xpPointToAward,
int rgbColor,
AttributeModifier.Operation operation,
int maxLevel,
Identifier stat
)Every field is documented on the Skill Config page. Note that stat must be a custom statistic — a built-in Experience Skills stat, a stat you registered yourself, or a minecraft:custom stat. Per-block / per-item statistics can't be used directly; pool them into a custom stat first (see Adding a Stat).
Example: A Simple Skill
A "Mining Fortune" style skill that raises Attack Damage as you break blocks:
@Override
public void registerExperienceType(IExperienceTypeRegistry registry) {
registry.register(
"prospecting",
new SkillsConfig(
true, // enabled
0.02f, // +2% per level
50, // every 50 blocks broken
5, // award 5 points
0x33CC66, // orb/bar colour
AttributeModifier.Operation.ADD_MULTIPLIED_BASE,
100, // level cap
ModStats.BLOCK_BROKEN.id() // driving stat
),
Attributes.LUCK // attribute buffed
);
}Referencing stats
Built-in stat ids are exposed through ModStats (e.g. ModStats.BLOCK_BROKEN.id()). Vanilla custom stats come from net.minecraft.stats.Stats (e.g. Stats.JUMP, Stats.DAMAGE_DEALT).
Handlers
A Handler runs in addition to the automatic attribute bonus — use it for effects a single modifier can't express, like adjusting a second attribute or running custom logic.
@FunctionalInterface
interface Handler {
void onUpdateSkill(ServerPlayer player, int level, ExperienceType experienceType);
}It fires whenever the skill's status is refreshed — on login, on /experienceskills command changes, and each time the player levels the skill up — and only when the skill's attribute is present on the player.
Recompute from level, don't assume a delta
A handler can fire at any level, not just on a change. If its output depends on the level, recompute it from the supplied level each call rather than assuming it changed by one.
Example: The Built-in Jump Handler
The built-in jump skill uses a handler to keep SAFE_FALL_DISTANCE in step with the jump-height bonus, so higher jumps never turn into fall damage:
registry.register(
"jump",
new SkillsConfig(true, 0.02f, 100, 5, 14392458,
AttributeModifier.Operation.ADD_MULTIPLIED_BASE, 100, Stats.JUMP),
Attributes.JUMP_STRENGTH,
(player, level, type) -> {
AttributeInstance attr = player.getAttribute(Attributes.SAFE_FALL_DISTANCE);
if (attr == null) return;
float perLevel = type.getConfig().incrementPerLevel();
float jumpStrengthAtLevel = 0.42f + (0.42f * perLevel * level);
float safeFallBonus = safeFallDistanceForLevel(jumpStrengthAtLevel) - 3.0f;
Identifier id = Constants.id("update_for_jump");
attr.removeModifier(id);
attr.addPermanentModifier(
new AttributeModifier(id, safeFallBonus, AttributeModifier.Operation.ADD_VALUE));
}
);Note how it removes its old modifier before re-adding, and computes the bonus purely from level — both essential because the handler re-runs on every refresh.
Enabling / Disabling at Runtime
If a skill is disabled in config, its attribute bonus is removed and it awards no experience. A handler should mirror this — if it manages an extra modifier, remove that modifier when type.getConfig().enabled() is false.
