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
mavenLocalor 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_ACCESSSKIN_APPLYPACK_BUILDPACK_HOSTPACK_MERGECOMMANDS
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:
openMainGuiapplySkinclearSkin
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 behaviorignorePermission()skips only the permission checkignoreCost()skips only the cost/economy checkignoreAll()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:
keyidpackIddisplayNamepermissioncategories
categories uses the current SkinEngine item keys, such as:
swordmacetridentaxepickaxeshovelhoespearbowcrossbowfishing_rodshieldelytrahelmetchestplateleggingsboots
PackView
PackView contains:
idnameloreiconMaterial
Result and status types
GuiOpenResult
Wraps GuiOpenStatus.
Current statuses:
OPENEDWRONG_THREADFEATURE_DISABLEDFAILED
SkinApplyResult
Contains:
statusitemskin
Current SkinApplyStatus values:
SUCCESSWRONG_THREADINVALID_INPUTFEATURE_DISABLEDSKIN_NOT_FOUNDUNSUPPORTED_ITEMBLOCKED_ITEMPERMISSION_DENIEDECONOMY_UNAVAILABLEINSUFFICIENT_FUNDSECONOMY_CHARGE_FAILEDITEM_MODEL_UNAVAILABLEALREADY_APPLIEDCANCELLED
How to read these statuses:
PERMISSION_DENIEDonly matters when permission enforcement is enabledECONOMY_UNAVAILABLEmeans the skin has a cost, cost enforcement is enabled, and SkinEngine does not currently have a working economy backendINSUFFICIENT_FUNDSmeans the player does not have enough money or points for that skin costECONOMY_CHARGE_FAILEDmeans the preview passed, but the real withdrawal failed during apply
SkinClearResult
Contains:
statusitem
Current SkinClearStatus values:
SUCCESSWRONG_THREADINVALID_INPUTNO_SKINCANCELLED
Events
SkinEngine exposes two cancellable Bukkit events:
SkinApplyEventSkinClearEvent
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:
APIGUI
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
compileOnlyorprovided, 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
SkinApplyOptionswhen your plugin needs to bypass permission checks, cost checks, or both - the default overloads are still strict and enforce normal SkinEngine conditions