Developer Documentation

Build on top of ItemsCore

ItemsCore ships a public Java API and an addon system. Read and give out custom items from your own plugin, or register an addon that extends the in-game editor with your own categories, methods, attributes, listeners and actions.

Java 8+Spigot / PaperMaven or Gradlev5.0

Introduction

There are a few ways to build on ItemsCore, and you can mix them freely:

  • The API - call into ItemsCore from your plugin to look up a custom item by name, give it to a player, read the custom name off an ItemStack, or ask a player for typed input.
  • Addons - register a PluginAddon that adds your own categories, methods, attributes, config screens, listeners and actions to the editor, and lets you modify items as they are created or live-updated.
  • Core extension points - register service providers that feed ItemsCore directly: stat bridges, lore placeholders, action suppressors, worn-item providers, anvil modules, and a cross-addon injection bus.

The video below walks through building a complete addon from scratch. The pages after it document each piece on its own.

Project setup

Drop ItemsCore.jar into a libs/ folder in your project and add it as a system-scoped dependency:

xml
<!-- pom.xml -->
<dependency>
<groupId>me.TastyCake</groupId>
<artifactId>ItemsCore</artifactId>
<version>1.0</version>
<systemPath>${project.basedir}/libs/ItemsCore.jar</systemPath>
<scope>system</scope>
</dependency>
Copy

Depend on ItemsCore in your plugin.yml so it always loads first:

yaml
# plugin.yml
depend: [ItemsCore]
Copy

If you add custom methods, compile your addon classes with the -parameters flag and a Java 8+ target. That keeps the real argument names so they show up correctly in the editor instead of arg0, arg1:

xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
Copy

The API

Grab the API instance once ItemsCore is enabled:

java
ItemsCoreAPI api = ItemsCore.getItemsCoreAPI();
Copy
MethodReturnsWhat it does
getItemByName(String)ItemFinds one of your custom items by its name, or null if none matches.
giveItem(Entity, Item)voidGives a player the custom item (ignored for non-players).
getItemName(ItemStack)StringReads the internal ItemsCore name stored on an item stack.
tryGetItemFromPlayerHand(Entity)ItemReturns the custom item the player is holding, or null if it is not one.
isUsingVault()booleanWhether the Vault economy hook is active.
getInput()PlayerInputThe helper used to ask a player for typed chat input.
java
Item sword = api.getItemByName("magic_sword");
if (sword != null) {
api.giveItem(player, sword);
}
Item held = api.tryGetItemFromPlayerHand(player);
if (held != null) {
player.sendMessage("You are holding " + api.getItemName(held));
}
Copy

Addons

An addon is a PluginAddon you register with the AddonProvider. It can add editor categories and methods, attributes, listeners and actions, and modify items as they are built.

Get the provider and register your addon, usually in your plugin's onEnable:

java
AddonProvider provider = AddonProvider.get();
provider.addAddon(
new PluginAddon("YourPluginName", XMaterial.WRITABLE_BOOK.get())
// .addon(...) add a methods category
// .attribute(...) add a toggle/value attribute
// .config(...) add an auto-generated settings screen
// .listener(...) listen to item events
// .action(...) add a custom action block
// .onItemCreate(...) modify the ItemStack as it is built
// .onItemUpdate(...) react each time a copy is live-updated
// .settings(...) open a fully custom settings GUI from /addons
);
Copy
!The plugin name must match your plugin exactly - check it with /plugins in game. The material is the icon shown for your addon in the editor; the no-argument new PluginAddon(name) defaults to a firework star.

You can look an addon up again later with provider.getAddonByName("YourPluginName").

Custom methods

An Addon<T> exposes a class whose public methods become a new category in the editor. The first argument is the identifier players call it by; the second builds the object per player.

java
AddonProvider provider = AddonProvider.get();
provider.addAddon(
new PluginAddon("YourPluginName")
.addon(new Addon<>("myCategory", (Player player) -> new MyMethods()))
);
Copy

The class holding your methods:

java
public class MyMethods {
public void greet(Player player) {
player.sendMessage("Hello from my addon!");
}
public int doubled(int value) {
return value * 2;
}
}
Copy
!Every method you want available in the editor must be public.

Inside an item action the methods are then called on the identifier, for example myCategory.greet(player) or myCategory.doubled(5).

Attributes

An attribute is a per-item value (often a boolean toggle) shown in the editor that your plugin can read back. It takes a name, a GUI material, a default value, an update callback, and lore lines.

java
provider.addAddon(
new PluginAddon("PowerScrolls", XMaterial.WRITABLE_BOOK.get())
.attribute(new AddonAttribute<>(
"Is a scroll", // attribute name
XMaterial.WRITTEN_BOOK.get(), // GUI material
false, // default value
(player, gui, editor, callback) -> {
Boolean current = (Boolean) editor
.getAttributeSerializableByName("PowerScrolls_Is a scroll")
.getValue();
callback.result(!current); // store the new value
editor.createAddonGui(provider.getAddonByName("PowerScrolls"))
.openInventory(player); // reopen the addon GUI
},
"&7If true, the item will be used to upgrade",
"&7other items and add abilities to them.",
"",
"&aClick to toggle"
))
);
Copy
iThe stored key is %AddonName%_%AttributeName% - here PowerScrolls_Is a scroll. Read it back anywhere with editor.getAttributeSerializableByName(key).getValue().

Config screens

An AddonConfig is a set of typed options that ItemsCore turns into a ready-made settings GUI - steppers for numbers, dye toggles for booleans, pickers for choices. You declare the options; the editor screen is generated for you and opens from /addons.

java
AddonConfig config = new AddonConfig("settings")
.option(ConfigOption.bool("enabled", true)
.name("Enabled")
.describe("&7Turn the addon on or off."))
.option(ConfigOption.integer("max-slots", 4)
.name("Max slots").min(1).max(9).step(1))
.option(ConfigOption.selection("mode", "balanced", "fast", "balanced", "thorough")
.name("Mode"));
provider.addAddon(
new PluginAddon("YourPluginName", XMaterial.WRITABLE_BOOK.get())
.config(config)
);
Copy

The factory you call decides the control that is rendered:

OptionEditor control
ConfigOption.text(key, def)A string value typed in chat / dialog.
ConfigOption.bool(key, def)A dye toggle (on / off).
ConfigOption.integer(key, def)A whole-number stepper (use .min/.max/.step).
ConfigOption.decimal(key, def)A decimal stepper.
ConfigOption.selection(key, def, choices…)Cycle through a fixed list of choices.
ConfigOption.material(key, def)Opens the material picker.
ConfigOption.button(key, icon)A clickable button - wire it with .opens(...).

Refine any option with .name(...), .icon(...) and .describe(...). Read a value back at runtime straight off the config:

java
boolean enabled = config.getBoolean("enabled", true);
int maxSlots = config.getInt("max-slots", 4);
String mode = config.getString("mode", "balanced");
Copy
For a screen too custom for typed options, use .settings(...) instead - it hands you the player and lets you open any GUI you like from the /addons entry.

Listeners

Implement ItemsCoreListener to react to item events. Return an ItemEventResult(boolean cancel, BukkitRunnable runnable) - set cancel to true to stop the item's ability from running. Leave the runnable null for now.

java
provider.addAddon(
new PluginAddon("PowerScrolls", XMaterial.WRITABLE_BOOK.get())
.listener(new MyListener())
);
public class MyListener implements ItemsCoreListener {
@Override
public ItemEventResult itemEvent(Player player, Item item, String action, Event event) {
// your logic here
// return true to cancel the item's ability
return new ItemEventResult(false, null);
}
}
Copy

Actions

An AddonAction is a custom action block players can place on an item. You trigger it from your own code and pass in custom variables that become available inside the action's script.

java
AddonAction action = new AddonAction(
"TestAction", // action name
Material.REDSTONE, // GUI material
new String[] { "&7This is a test" } // lore
);
provider.addAddon(
new PluginAddon("PluginName", XMaterial.WRITABLE_BOOK.get())
.action(action)
);
// run it for an item that has this action attached
action.call(player, itemStack, new HashMap<String, Object>() {{
put("exampleVariable", player.getDisplayName());
}});
Copy
iEach key in the map becomes a variable available inside that action's script - here exampleVariable.

Modify item creation

onItemCreate lets you transform the ItemStack as it is built from an Item. Every addon runs in a chain - you receive the most recent stack and return your modified one, and later addons may still change it after you.

java
provider.addAddon(
new PluginAddon("PluginName", XMaterial.WRITABLE_BOOK.get())
.onItemCreate(new ItemCreationRunnable() {
@Override
public ItemStack onCreate(Item used, ItemStack created) {
// inspect "used", modify and return "created"
return created;
}
})
);
Copy

Annotate the runnable with @AddonModifier to set a priority. The chain runs from highest to lowest, so a higher priority gets the final say:

java
.onItemCreate(new @AddonModifier(priority = AddonPriority.LOW) ItemCreationRunnable() {
@Override
public ItemStack onCreate(Item used, ItemStack created) {
return created;
}
})
Copy
iPriorities, highest first: MONITOR, HIGHEST, HIGH, NORMAL, LOW, LOWEST. The default is NORMAL.

React to live updates

ItemsCore keeps every copy of an item in sync with its template. onItemUpdate runs on each of those live updates, so your addon can re-stamp data that rides on the stack - a reforge prefix, a skin name, a charge counter.

java
provider.addAddon(
new PluginAddon("YourPluginName", XMaterial.WRITABLE_BOOK.get())
.onItemUpdate((template, stack) -> {
if (!needsUpdate(stack)) {
return null; // null = no change, don't re-stamp
}
return reapplyYourData(stack); // the stack to keep
})
);
Copy
iReturning null means "nothing to change" and skips a needless rewrite. Return the stack (mutated or new) only when you actually changed it.

Item extension points

ItemExtensions (from itemsCore.getExtensions()) is a set of service-provider hooks that change how core builds, names and updates items - without core knowing your addon exists. They are how the official Skins, Reforges and Equipment addons plug in.

java
ItemExtensions ext = itemsCore.getExtensions();
Copy
RegisterWhat it does
registerLorePlaceholder(token, provider)Render extra lore lines where a {token} appears in the item's lore.
registerActionSuppressor(suppressor)Return true to make core ignore an item's own actions (e.g. a cosmetic-only stack).
registerNameClaim(claim)Return true when your addon owns a stack's display name, so core's passive name refresh stands aside.
registerCarriedTag(key)Preserve an NMS custom-data key when an item's material changes.
registerImportSection(name, section)Read and write a named section in .import files (export + apply).
java
ItemExtensions ext = itemsCore.getExtensions();
ext.registerLorePlaceholder("applied_skin", (template, stack) ->
Collections.singletonList("&7Skin: &b" + readSkin(stack)));
ext.registerActionSuppressor((item, stack) -> isCosmetic(stack));
ext.registerImportSection("skin", new ItemExtensions.ImportSection() {
@Override public Object export(Item item) { return readSkinData(item); }
@Override public void apply(Item item, Object value) { writeSkinData(item, value); }
});
Copy

Two more extension points live on their own systems:

  • Worn items - itemsCore.getStatEngine().registerWornProvider(player -> ...) contributes extra equipped items (beyond armor and hand) to stat and action calculation. EquipmentCore uses this for its custom slots.
  • Anvil modules - itemsCore.getAnvilRegistry().register(module) adds a station to the advanced anvil: an AnvilModule decides which stacks are modifiers, previews the outcome and applies the result. ReforgesCore and SkinsCore register their own.

Stat bridges

A StatBridge receives a player's live, combined ItemsCore stat totals and pushes them into another system - the way the AuraSkills bridge mirrors stats into its own attributes. ItemsCore calls it on the main thread, and only when a player's totals actually change.

java
public class MyBridge implements StatBridge {
@Override public String id() { return "myplugin"; }
@Override public boolean isAvailable() {
return Bukkit.getPluginManager().isPluginEnabled("MyPlugin");
}
@Override public void apply(Player player, Map<String, Integer> totals) {
// totals is e.g. { "Strength": 50, "Defense": 20 }
totals.forEach((stat, value) -> pushToMyPlugin(player, stat, value));
}
@Override public void clear(Player player) {
resetMyPlugin(player);
}
}
// register once ItemsCore is enabled
itemsCore.getStatBridgeManager().register(new MyBridge());
Copy

Injection bus

The injection bus lets one addon add entries to another addon's menu without either one depending on the other. A host declares a named point and collects whatever was contributed; a contributor registers an injector against that point. ProfileCore hosts its profile menu, and EquipmentCore contributes a button to it.

Contributor - register in onEnable, clean up in onDisable:

java
InjectionRegistry.get().register("profilecore:profile", "YourAddon", context -> {
ItemStack icon = buildIcon(context.getTarget());
return Collections.singletonList(
InjectedEntry.of(icon)
.priority(10)
.onClick(viewer -> openYourMenu(viewer, context.getTarget()))
);
});
// in onDisable
InjectionRegistry.get().unregisterOwner("YourAddon");
Copy

Host - collect entries while building your menu and place each one in the GUI:

java
InjectionContext context = new InjectionContext(viewer, target);
List<InjectedEntry> entries = InjectionRegistry.get().collect("youraddon:menu", context);
// place each entry by its icon, slot, priority and click handler
Copy
iThe point name is just a string both sides agree on - namespace it with your addon, like youraddon:menu. Contributors that aren't installed simply add nothing.

Serialized classes

!If your addon stores custom ConfigurationSerializable objects on items, register those classes in your plugin's onLoad() - not onEnable().

ItemsCore loads every saved item during its own onEnable, which runs before any addon's onEnable. If your serializable classes aren't registered by then, those item files fail to deserialize and the whole server start can break. onLoad runs early enough for every plugin, so register there:

java
@Override
public void onLoad() {
ConfigurationSerialization.registerClass(SkinConfig.class);
ConfigurationSerialization.registerClass(SkinTarget.class);
}
Copy

Addon updates

ItemsCore tracks installed addon versions against SpigotMC (via Spiget) and surfaces an Updates menu inside /addons, plus an operator notice when a new version is out. Installed versions are recorded in installed-addons.yml.

Addons are recognised by their SpigotMC resource id, which lives in ItemsCore's supported-addon list alongside the in-game installer entry (/ic install addon browse). To have a new addon listed and version-tracked, send us its resource id - there is nothing to add inside the addon jar itself.

Hooks

!Hooks are mostly superseded by addons now, but they still work for quick plugin-to-plugin calls from item code.

A hook is like a plugin dependency you can reach from item code. Put the other plugin's jar in the hooks folder inside the ItemsCore plugin folder, then register it once with core.setHook:

java
// core.setHook(accessName, jarName, groupId, artifactId)
// accessName - how you reference it from item code (e.g. "bla")
// jarName - the file name you put in the hooks folder
// groupId - the plugin's Maven groupId
// artifactId - the plugin's Maven artifactId
core.setHook("bla", "OtherPlugin", "com.example", "otherplugin");
Copy

Call a method on the hook by its access name with runMethod:

java
// bla.runMethod(methodName, Class<?>[] argTypes, Object... values)
// build the argument types with core.getClassByName(...)
bla.runMethod("test", new Class[]{ core.getClassByName("String") }, "hello");
Copy