Developer API

Java Developer API

SkinEngine exposes a public Bukkit API through the rs.konfix.skinengine.api package.

This page explains:

  • dependency setup
  • how to fetch the API at runtime
  • available features and data types
  • apply options for permission and cost control
  • events
  • practical usage examples

Current API version

From the current SkinEngine project:

  • group: rs.konfix
  • artifact: SkinEngine
  • version: 1.0.0

So the current API coordinates are:

rs.konfix:SkinEngine:1.0.0

Important:

  • this repository does not currently show a public Maven publishing setup
  • use the coordinates above only if you install or publish the artifact to your own local/private repository
  • if you do not have the artifact in a repository, you will need to depend on the built jar manually

Dependency setup

Gradle

If you have the SkinEngine artifact available in your Maven repository:

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    compileOnly "rs.konfix:SkinEngine:1.0.0"
}

If you use Paper and want the usual compile-only plugin dependency style:

dependencies {
    compileOnly "io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT"
    compileOnly "rs.konfix:SkinEngine:1.0.0"
}

Maven

If the artifact is installed in your local or private repository:

<dependencies>
  <dependency>
    <groupId>rs.konfix</groupId>
    <artifactId>SkinEngine</artifactId>
    <version>1.0.0</version>
    <scope>provided</scope>
  </dependency>
</dependencies>

If you do not have a Maven artifact

If SkinEngine is not published to a repository you can use, the practical fallback is:

  • build or obtain the plugin jar
  • install it into mavenLocal or your own repository
  • or add it as a local compile-only file dependency in your plugin project

plugin.yml setup

If your plugin integrates with SkinEngine, declare it as a soft dependency so load order is safer:

softdepend:
  - SkinEngine

Use depend only if your plugin cannot run at all without SkinEngine.

How to get the API at runtime

SkinEngine registers its API in Bukkit's ServicesManager.

The service type is:

rs.konfix.skinengine.api.SkinEngineApi

Example:

import org.bukkit.Bukkit;
import org.bukkit.plugin.RegisteredServiceProvider;
import rs.konfix.skinengine.api.SkinEngineApi;

public final class SkinEngineBridge {
    public static SkinEngineApi findApi() {
        RegisteredServiceProvider<SkinEngineApi> registration =
                Bukkit.getServicesManager().getRegistration(SkinEngineApi.class);
        return registration == null ? null : registration.getProvider();
    }
}

Typical startup check:

SkinEngineApi api = SkinEngineBridge.findApi();
if (api == null || !api.isAvailable()) {
    getLogger().warning("SkinEngine API is not available.");
    return;
}

Basic hook registration

The API also exposes lightweight hook registration:

SkinEngineApi api = SkinEngineBridge.findApi();
if (api != null) {
    api.registerHook(this);
}

And on disable:

if (api != null) {
    api.unregisterHook(this);
}

What this does:

  • registers your plugin name in SkinEngine's hook list
  • lets SkinEngine report which external plugins are hooked

This is not the same thing as obtaining the API service. You still fetch the API through ServicesManager.

Main interface

The main interface is:

public interface SkinEngineApi {
    boolean isAvailable();
    boolean isFeatureAvailable(SkinFeature feature);

    boolean registerHook(Plugin plugin);
    void unregisterHook(Plugin plugin);
    List<String> getHookedPlugins();

    GuiOpenResult openMainGui(Player player);

    boolean hasSkin(ItemStack item);
    @Nullable String getSkinKey(ItemStack item);
    @Nullable UUID getSkinOwner(ItemStack item);

    @Nullable SkinView getSkin(String skinKey);
    List<SkinView> getSkins();
    List<SkinView> getSkins(Material material);
    List<PackView> getPacks();

    SkinApplyStatus getApplyStatus(Player player, ItemStack item, String skinKey);
    SkinApplyStatus getApplyStatus(Player player, ItemStack item, String skinKey, SkinApplyOptions options);

    SkinApplyResult applySkin(Player player, ItemStack item, String skinKey);
    SkinApplyResult applySkin(Player player, ItemStack item, String skinKey, SkinApplyOptions options);

    SkinClearStatus getClearStatus(Player player, ItemStack item);
    SkinClearResult clearSkin(Player player, ItemStack item);
}

The simpler overloads still exist and still use the normal strict behavior. In other words, permission checks and cost/economy checks are enforced unless you explicitly pass different apply options.

Feature gating

SkinEngine exposes feature availability through SkinFeature.

Current values:

  • GUI_ACCESS
  • SKIN_APPLY
  • PACK_BUILD
  • PACK_HOST
  • PACK_MERGE
  • COMMANDS

Example:

if (!api.isFeatureAvailable(SkinFeature.SKIN_APPLY)) {
    player.sendMessage("Skin application is not available right now.");
    return;
}

This matters because SkinEngine uses capability gating internally. Do not assume every feature is available just because the plugin is loaded.

Threading rules

These API methods must be called on the Bukkit main thread:

  • openMainGui
  • applySkin
  • clearSkin

If you call them off-thread, SkinEngine returns a WRONG_THREAD status instead of trying to do unsafe item or GUI work.

Example safe call:

Bukkit.getScheduler().runTask(this, () -> {
    SkinApplyResult result = api.applySkin(player, item, "frostfall/frostblade");
});

Apply options

SkinEngine now exposes SkinApplyOptions so plugins can choose how strict an apply check should be.

The type is:

public record SkinApplyOptions(
        boolean enforcePermission,
        boolean enforceCost
) {
    public static SkinApplyOptions strict();
    public static SkinApplyOptions ignorePermission();
    public static SkinApplyOptions ignoreCost();
    public static SkinApplyOptions ignoreAll();
}

What these mean in practice:

  • strict() keeps SkinEngine's normal behavior
  • ignorePermission() skips only the permission check
  • ignoreCost() skips only the cost/economy check
  • ignoreAll() skips both checks

This is useful when your own plugin wants finer control over the decision. For example, you might want to:

  • let an admin tool grant skins even without the normal permission node
  • preview whether a skin is technically compatible with an item without economy blocking the result
  • apply a skin for free after your own reward system already decided the player earned it

Basic examples:

SkinApplyStatus status =
        api.getApplyStatus(player, item, "frostfall/frostblade", SkinApplyOptions.strict());
SkinApplyStatus statusIgnoringPermission =
        api.getApplyStatus(player, item, "frostfall/frostblade", SkinApplyOptions.ignorePermission());
SkinApplyResult freeApply =
        api.applySkin(player, item, "frostfall/frostblade", SkinApplyOptions.ignoreCost());
SkinApplyResult forcedApply =
        api.applySkin(player, item, "frostfall/frostblade", SkinApplyOptions.ignoreAll());

Skin keys

Skin keys use the format:

<pack_id>/<skin_id>

Example:

frostfall/frostblade

View types

SkinView

SkinView contains:

  • key
  • id
  • packId
  • displayName
  • permission
  • categories

categories uses the current SkinEngine item keys, such as:

  • sword
  • mace
  • trident
  • axe
  • pickaxe
  • shovel
  • hoe
  • spear
  • bow
  • crossbow
  • fishing_rod
  • shield
  • elytra
  • helmet
  • chestplate
  • leggings
  • boots

PackView

PackView contains:

  • id
  • name
  • lore
  • iconMaterial

Result and status types

GuiOpenResult

Wraps GuiOpenStatus.

Current statuses:

  • OPENED
  • WRONG_THREAD
  • FEATURE_DISABLED
  • FAILED

SkinApplyResult

Contains:

  • status
  • item
  • skin

Current SkinApplyStatus values:

  • SUCCESS
  • WRONG_THREAD
  • INVALID_INPUT
  • FEATURE_DISABLED
  • SKIN_NOT_FOUND
  • UNSUPPORTED_ITEM
  • BLOCKED_ITEM
  • PERMISSION_DENIED
  • ECONOMY_UNAVAILABLE
  • INSUFFICIENT_FUNDS
  • ECONOMY_CHARGE_FAILED
  • ITEM_MODEL_UNAVAILABLE
  • ALREADY_APPLIED
  • CANCELLED

How to read these statuses:

  • PERMISSION_DENIED only matters when permission enforcement is enabled
  • ECONOMY_UNAVAILABLE means the skin has a cost, cost enforcement is enabled, and SkinEngine does not currently have a working economy backend
  • INSUFFICIENT_FUNDS means the player does not have enough money or points for that skin cost
  • ECONOMY_CHARGE_FAILED means the preview passed, but the real withdrawal failed during apply

SkinClearResult

Contains:

  • status
  • item

Current SkinClearStatus values:

  • SUCCESS
  • WRONG_THREAD
  • INVALID_INPUT
  • NO_SKIN
  • CANCELLED

Events

SkinEngine exposes two cancellable Bukkit events:

  • SkinApplyEvent
  • SkinClearEvent

SkinApplyEvent

Exposes:

  • player
  • original item
  • result item
  • skin
  • source

You can:

  • cancel the event
  • replace the result item via setResultItem(...)

SkinClearEvent

Exposes:

  • player
  • original item
  • result item
  • source

You can:

  • cancel the event
  • replace the result item via setResultItem(...)

SkinActionSource

Current source values:

  • API
  • GUI

Usage examples

Open the main SkinEngine GUI

SkinEngineApi api = SkinEngineBridge.findApi();
if (api == null) {
    return;
}

GuiOpenResult result = api.openMainGui(player);
if (!result.opened()) {
    player.sendMessage("Could not open SkinEngine: " + result.status().name());
}

List packs

SkinEngineApi api = SkinEngineBridge.findApi();
if (api == null) {
    return;
}

for (PackView pack : api.getPacks()) {
    getLogger().info(pack.id() + " -> " + pack.name());
}

Find skins for the item in hand

SkinEngineApi api = SkinEngineBridge.findApi();
if (api == null) {
    return;
}

ItemStack item = player.getInventory().getItemInMainHand();
for (SkinView skin : api.getSkins(item.getType())) {
    getLogger().info(skin.key() + " [" + skin.displayName() + "]");
}

Apply a skin

SkinEngineApi api = SkinEngineBridge.findApi();
if (api == null) {
    return;
}

ItemStack item = player.getInventory().getItemInMainHand();
SkinApplyResult result = api.applySkin(player, item, "frostfall/frostblade");

if (result.success() && result.item() != null) {
    player.getInventory().setItemInMainHand(result.item());
} else {
    player.sendMessage("Apply failed: " + result.status().name());
}

Preview a skin with normal checks

SkinApplyStatus status =
        api.getApplyStatus(player, player.getInventory().getItemInMainHand(), "frostfall/frostblade");

if (status != SkinApplyStatus.SUCCESS) {
    player.sendMessage("Cannot apply skin: " + status.name());
}

Apply a skin while ignoring permission

ItemStack item = player.getInventory().getItemInMainHand();
SkinApplyResult result =
        api.applySkin(player, item, "frostfall/frostblade", SkinApplyOptions.ignorePermission());

if (result.success() && result.item() != null) {
    player.getInventory().setItemInMainHand(result.item());
}

Use this when your own plugin has already decided that the action is allowed and you do not want SkinEngine's permission node to block it.


Apply a skin while ignoring cost

ItemStack item = player.getInventory().getItemInMainHand();
SkinApplyResult result =
        api.applySkin(player, item, "frostfall/frostblade", SkinApplyOptions.ignoreCost());

if (result.success() && result.item() != null) {
    player.getInventory().setItemInMainHand(result.item());
}

Use this when your plugin already handled payment, or when the skin should be granted for free.


Check raw compatibility while ignoring both permission and cost

ItemStack item = player.getInventory().getItemInMainHand();
SkinApplyStatus status =
        api.getApplyStatus(player, item, "frostfall/frostblade", SkinApplyOptions.ignoreAll());

if (status == SkinApplyStatus.SUCCESS || status == SkinApplyStatus.ALREADY_APPLIED) {
    player.sendMessage("This item can use that skin.");
} else {
    player.sendMessage("This skin still cannot be used here: " + status.name());
}

This is a useful pattern when you want to answer: "Is this item and skin combination valid at all?" without letting permission or economy hide the real technical result.


Clear a skin

SkinEngineApi api = SkinEngineBridge.findApi();
if (api == null) {
    return;
}

ItemStack item = player.getInventory().getItemInMainHand();
SkinClearResult result = api.clearSkin(player, item);

if (result.success() && result.item() != null) {
    player.getInventory().setItemInMainHand(result.item());
}

Check whether an item already has a SkinEngine skin

SkinEngineApi api = SkinEngineBridge.findApi();
if (api == null) {
    return;
}

ItemStack item = player.getInventory().getItemInMainHand();
if (api.hasSkin(item)) {
    String key = api.getSkinKey(item);
    UUID owner = api.getSkinOwner(item);
    getLogger().info("Skin key: " + key + ", owner: " + owner);
}

Listen for apply events

import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import rs.konfix.skinengine.api.event.SkinApplyEvent;

public final class SkinEngineListener implements Listener {
    @EventHandler
    public void onSkinApply(SkinApplyEvent event) {
        getLogger().info(
                event.getPlayer().getName()
                        + " applied "
                        + event.getSkin().key()
                        + " via "
                        + event.getSource().name()
        );
    }
}

Cancel or modify an apply event

@EventHandler
public void onSkinApply(SkinApplyEvent event) {
    if (event.getSkin().key().equalsIgnoreCase("frostfall/admin_only")) {
        event.setCancelled(true);
        return;
    }

    ItemStack result = event.getResultItem();
    if (result != null) {
        ItemMeta meta = result.getItemMeta();
        if (meta != null) {
            meta.setEnchantmentGlintOverride(true);
            result.setItemMeta(meta);
            event.setResultItem(result);
        }
    }
}

Practical notes

  • use compileOnly or provided, not shaded embedding
  • fetch the API service at runtime instead of hard-casting the plugin instance
  • treat feature availability as dynamic
  • keep GUI and item mutations on the main thread
  • use the result status enums instead of assuming success
  • use SkinApplyOptions when your plugin needs to bypass permission checks, cost checks, or both
  • the default overloads are still strict and enforce normal SkinEngine conditions