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.
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:
Copy<!-- 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>
Depend on ItemsCore in your plugin.yml so it always loads first:
Copy# plugin.ymldepend: [ItemsCore]
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:
Copy<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>
The API
Grab the API instance once ItemsCore is enabled:
CopyItemsCoreAPI api = ItemsCore.getItemsCoreAPI();
| Method | Returns | What it does |
|---|---|---|
| getItemByName(String) | Item | Finds one of your custom items by its name, or null if none matches. |
| giveItem(Entity, Item) | void | Gives a player the custom item (ignored for non-players). |
| getItemName(ItemStack) | String | Reads the internal ItemsCore name stored on an item stack. |
| tryGetItemFromPlayerHand(Entity) | Item | Returns the custom item the player is holding, or null if it is not one. |
| isUsingVault() | boolean | Whether the Vault economy hook is active. |
| getInput() | PlayerInput | The helper used to ask a player for typed chat input. |
CopyItem 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));}
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:
CopyAddonProvider 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);
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.
CopyAddonProvider provider = AddonProvider.get();provider.addAddon(new PluginAddon("YourPluginName").addon(new Addon<>("myCategory", (Player player) -> new MyMethods())));
The class holding your methods:
Copypublic class MyMethods {public void greet(Player player) {player.sendMessage("Hello from my addon!");}public int doubled(int value) {return value * 2;}}
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.
Copyprovider.addAddon(new PluginAddon("PowerScrolls", XMaterial.WRITABLE_BOOK.get()).attribute(new AddonAttribute<>("Is a scroll", // attribute nameXMaterial.WRITTEN_BOOK.get(), // GUI materialfalse, // default value(player, gui, editor, callback) -> {Boolean current = (Boolean) editor.getAttributeSerializableByName("PowerScrolls_Is a scroll").getValue();callback.result(!current); // store the new valueeditor.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")));
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.
CopyAddonConfig 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));
The factory you call decides the control that is rendered:
| Option | Editor 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:
Copyboolean enabled = config.getBoolean("enabled", true);int maxSlots = config.getInt("max-slots", 4);String mode = config.getString("mode", "balanced");
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.
Copyprovider.addAddon(new PluginAddon("PowerScrolls", XMaterial.WRITABLE_BOOK.get()).listener(new MyListener()));public class MyListener implements ItemsCoreListener {@Overridepublic ItemEventResult itemEvent(Player player, Item item, String action, Event event) {// your logic here// return true to cancel the item's abilityreturn new ItemEventResult(false, null);}}
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.
CopyAddonAction action = new AddonAction("TestAction", // action nameMaterial.REDSTONE, // GUI materialnew 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 attachedaction.call(player, itemStack, new HashMap<String, Object>() {{put("exampleVariable", player.getDisplayName());}});
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.
Copyprovider.addAddon(new PluginAddon("PluginName", XMaterial.WRITABLE_BOOK.get()).onItemCreate(new ItemCreationRunnable() {@Overridepublic ItemStack onCreate(Item used, ItemStack created) {// inspect "used", modify and return "created"return created;}}));
Annotate the runnable with @AddonModifier to set a priority. The chain runs from highest to lowest, so a higher priority gets the final say:
Copy.onItemCreate(new @AddonModifier(priority = AddonPriority.LOW) ItemCreationRunnable() {@Overridepublic ItemStack onCreate(Item used, ItemStack created) {return created;}})
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.
Copyprovider.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}));
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.
CopyItemExtensions ext = itemsCore.getExtensions();
| Register | What 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). |
CopyItemExtensions 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); }});
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.
Copypublic 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 enableditemsCore.getStatBridgeManager().register(new MyBridge());
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:
CopyInjectionRegistry.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 onDisableInjectionRegistry.get().unregisterOwner("YourAddon");
Host - collect entries while building your menu and place each one in the GUI:
CopyInjectionContext 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
Serialized classes
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:
Copy@Overridepublic void onLoad() {ConfigurationSerialization.registerClass(SkinConfig.class);ConfigurationSerialization.registerClass(SkinTarget.class);}
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
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:
Copy// 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 artifactIdcore.setHook("bla", "OtherPlugin", "com.example", "otherplugin");
Call a method on the hook by its access name with runMethod:
Copy// bla.runMethod(methodName, Class<?>[] argTypes, Object... values)// build the argument types with core.getClassByName(...)bla.runMethod("test", new Class[]{ core.getClassByName("String") }, "hello");