The API
For plugins that want to register their own items or ability types, or react to Sigil items.
<dependency>
<groupId>dev.bwmp</groupId>
<artifactId>sigil-api</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
# plugin.yml
depend: [Sigil]
Getting the API
SigilAPI api = SigilAPI.get().orElseThrow();
Published through Bukkit's service manager, so it unregisters cleanly and the dependency is explicit.
Identifying items
Optional<CustomItem> item = api.resolve(stack);
boolean isCustom = api.isCustom(stack);
int uses = api.remainingUses(stack); // -1 when unlimited
Use api.addUses(stack, amount) to repair or spend charges through Sigil, and
api.cooldownRemaining(player, stack, abilityId) for a read-only countdown. api.refresh(stack)
re-renders stale appearance while preserving charges and foreign persistent data.
Writing an ability
An ability declares when it runs and what it does. It does not enforce its own cooldown, check its own permission, or decrement its own uses. The dispatcher does all three, so forgetting one is not expressible.
public final class FreezeAbility implements Ability {
@Override
public AbilityMeta meta() {
return AbilityMeta.of("freeze", "Freeze")
.description("Roots your target in place.")
.cooldownSeconds(8)
.scope(CooldownScope.PLAYER);
}
@Override
public Set<TriggerBinding> triggers() {
return Set.of(TriggerBinding.of(Trigger.DAMAGE_ENTITY));
}
@Override
public ActionResult execute(AbilityContext ctx) {
Entity target = ctx.entity().orElse(null);
if (!(target instanceof LivingEntity living)) {
return ActionResult.fail(); // no cooldown for a miss
}
living.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, 60, 4));
return ActionResult.consume();
}
}
Return the right result
| Result | Cooldown | Use spent | Meaning |
|---|---|---|---|
pass() | no | no | not interested; other abilities still get a look |
success() | yes | no | fired |
consume() | yes | yes | fired and spent a charge |
fail() | no | no | tried and couldn't: no target, no room, nothing to do |
The fail() / pass() distinction is the point. A single boolean forces "I missed" and "I fired" into the same bucket, which is how an ability ends up on cooldown for having hit nothing.
Add .cancelEvent() to suppress the Bukkit event that triggered it.
Abilities must be stateless
One instance is shared by every stack of the item. Per-player or per-item state belongs in the item's persistent data or in your own plugin.
Scheduling
Use ctx.scheduler(), never BukkitRunnable. It is what makes an ability work on Folia unchanged.
ctx.scheduler().atEntityTimer(player, this::step, 1L, 1L); // follows the player's region
ctx.scheduler().atLocation(block.getLocation(), () -> block.breakNaturally());
If an ability touches blocks away from the player, check ctx.scheduler().ownsRegion(location) first and hand the work to atLocation when it returns false. Off Folia that check is always true, so the same code is correct on both.
Registering an ability type
An AbilityType builds abilities from YAML, which makes it available to every item on the server, including ones defined purely in config.
@Override
public void onEnable() {
SigilAPI.get().ifPresent(api ->
api.registerAbilityType(this, new NamespacedKey(this, "freeze"),
(id, name, config) -> new FreezeAbility(config.getInt("duration", 60))));
}
Server owners can then write:
abilities:
- type: yourplugin:freeze
duration: 100
The id's namespace must match your plugin. Registrations are dropped automatically when your plugin disables, so a reload cannot leave a handler pointing at a dead classloader.
Throw IllegalArgumentException from create for a bad config. The message is shown to the admin against the offending file.
Registering an item
ItemDefinition definition = new ItemDefinition(
new NamespacedKey(this, "frostblade"),
"<aqua>Frostblade",
Material.DIAMOND_SWORD,
"epic",
List.of("Cold to the touch."),
null, -1,
Uses.limited(200, true),
InteractionRules.inherit(),
List.of(),
null, true, "");
api.register(this, definition, new FreezeAbility());
Your item gets its own YAML file like any other, so server owners can retune it without touching
your code. Query one item with api.item(id), all items with api.items(), or a server-owned group
with api.itemsWithTag("frost").
Addon services
api.scheduler()provides Folia-safe work outside an ability activation.api.registerLoot(owner, rule)adds lifecycle-bound chest, structure or mob loot alongsideloot.yml.api.playerStore(owner)stores addon-owned numbers and strings by player UUID and flushes them periodically. Callsave()only after a change that must survive an immediate crash.api.send(...)andapi.sendActionBar(...)parse MiniMessage with Sigil's relocated text stack.api.displays()exposes shared display-entity effects. Checkavailable()because display entities require Minecraft 1.19.4 or newer.api.rarity(...)andapi.rarities()expose the resolved rarity registry.
Events
@EventHandler
public void onAbility(CustomItemAbilityEvent event) {
if (inProtectedRegion(event.getPlayer())) {
event.setCancelled(true); // no cooldown, no charge spent
}
}
CustomItemCraftEvent fires before a Sigil item is crafted and can replace or block the result.
Why the API looks the way it does
sigil-api references only Bukkit types and its own. It never exposes Adventure or anything from Sigil's internals.
That is not stylistic. Sigil shades its dependencies and relocates them into dev.bwmp.sigil.libs. If the API exposed such a type, you would compile against dev.bwmp.keystone.KeystoneScheduler while the shipped jar contains dev.bwmp.sigil.libs.keystone.KeystoneScheduler, giving you a NoClassDefFoundError at runtime that names a class which looks entirely correct.
So SigilScheduler is Sigil's own interface, and text crosses the boundary as MiniMessage strings rather than components. If you extend the API, keep that rule. Keystone's own docs cover the general shape of the problem.
Register during or after onEnable, after the declared depend: [Sigil] has enabled Sigil. The
service is absent before Sigil publishes it, so do not cache the result from class initialization.