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

ResultCooldownUse spentMeaning
pass()nononot interested; other abilities still get a look
success()yesnofired
consume()yesyesfired and spent a charge
fail()nonotried 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 alongside loot.yml.
  • api.playerStore(owner) stores addon-owned numbers and strings by player UUID and flushes them periodically. Call save() only after a change that must survive an immediate crash.
  • api.send(...) and api.sendActionBar(...) parse MiniMessage with Sigil's relocated text stack.
  • api.displays() exposes shared display-entity effects. Check available() because display entities require Minecraft 1.19.4 or newer.
  • api.rarity(...) and api.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.