Why
A custom item in plain Bukkit is an ItemStack with meta, a PDC key, a click listener, a craft listener, a drop listener and a dozen "is this really my item?" checks. Rewritten in every plugin.
EffectiveItem turns an item into a class: three methods describe what it is and behaviours are enabled with a single call in init(). The PDC key is set for you, and any ItemStack can be matched back to its EffectiveItem.
Minimal example
object RubyItem : EffectiveItem() {
override fun getMaterial() = Material.FIREWORK_STAR
override fun getNamespacedData() = MyPlugin.instance to "ruby"
override fun getResourcePackData() =
ResourcePackData(texturePath = "textures/item/ruby.png")
override fun editMeta(meta: ItemMeta) {
meta.displayName(Component.text("Ruby"))
}
fun init() {}
}FIREWORK_STAR is the best base material for a custom-textured item: it has no vanilla behaviour you would need to suppress. For the texture from getResourcePackData to actually reach the player, the plugin's onEnable must call EffectiveResourcepack.addServerResourcepack(this, "", "") after RubyItem.init() — see Setup.
fun init() {
addClickHandler(Click.RIGHT, { e ->
e.player.sendMessage("Ruby!")
Result.CANCEL_EVENT
}, cooldownData = CooldownData(cooldownToUseInTicks = 20))
addShapelessCraft(listOf(Material.EMERALD, Tag.LOGS))
addToLoot(
dropChance = EffectiveDropable.chanceDependencyLuck(0.05, 0.02),
lootTables = null,
blocks = listOf(Material.STONE),
entities = null,
)
makeThrowable(velocity = 1.5) { hit ->
hit.hitEntity?.let { (it as? LivingEntity)?.damage(4.0) }
}
}val stack = player.inventory.itemInMainHand
if (RubyItem.equalByNamespacedKey(stack)) { … }
val key = EffectiveItem.getNamespacedKeyByItem(stack) // "myplugin:ruby"
val give = RubyItem.createItemStack(3)Behaviour interfaces
Each behaviour is enabled with one call in init(). Under the hood it is a separate Effective* interface with its own static helpers.
addClickHandler(Click.RIGHT, { e ->
e.player.sendMessage("clicked " + e.clickedBlock?.type)
Result.CANCEL_EVENT
})
addClickHandler(Click.LEFT_SHIFT, { e -> … Result.ALLOW_EVENT })
addClickHandler(Click.RIGHT, { e -> … }, ifRightClickOpenContainer = true)
addClickHandler(Click.RIGHT, { e -> … }, cooldownData = CooldownData(
cooldownToUseInTicks = 100,
cooldownType = CooldownType.ON_THIS_INSTANCE,
conditionForSkipCall = { e -> e.player.isOp },
))Click: LEFT, RIGHT, LEFT_SHIFT, RIGHT_SHIFT, LEFT_PLAIN, RIGHT_PLAIN (PLAIN — only without sneaking). In e: player, item, hand, clickedBlock, blockFace, clickedEntity. Result decides whether the Bukkit event is cancelled. Cooldown ON_CURRENT_PLAYER — per player for all stacks of this item, ON_THIS_INSTANCE — per stack. ifRightClickOpenContainer — do not intercept right-click on a chest/barrel.
addShapelessCraft(listOf(Material.EMERALD, Material.EMERALD, Material.STICK))
addShapelessCraft(listOf(
Tag.LOGS,
listOf(Material.DIAMOND, Material.EMERALD),
RubyDust.createItemStack(),
))An ingredient is a Material, ItemStack (custom too), Tag or a list of alternatives. Every combination of alternatives is registered as a separate recipe. Shapeless only.
addToLoot(
dropChance = { 0.1 },
lootTables = listOf(LootTables.SIMPLE_DUNGEON, LootTables.ABANDONED_MINESHAFT),
blocks = listOf(Material.DIAMOND_ORE),
entities = listOf(EntityType.ZOMBIE),
amount = { 1..3 },
)
addToLoot(
dropChance = EffectiveDropable.chanceDependencyLuck(0.10, 0.05),
lootTables = null,
blocks = listOf(Material.STONE),
entities = null,
amount = EffectiveDropable.amountDependencyLuck(1..2, 1),
)Chance and amount are functions of the player (may be null for non-players). chanceDependencyLuck(base, step) adds step per Fortune/Looting level of the held tool; amountDependencyLuck widens the range the same way.
makeThrowable(velocity = 2.0, consumeOnThrow = true, throwSound = Sound.ENTITY_EGG_THROW) { hit ->
val target = hit.hitEntity as? LivingEntity
target?.addPotionEffect(PotionEffect(PotionEffectType.POISON, 60, 0))
hit.hitBlock?.let { it.world.createExplosion(it.location, 0f) }
}Right-click launches a snowball carrying the item; onHit receives the ProjectileHitEvent of this item only. EffectiveThrowable.isThrowable(stack) checks it.
fun init() {
makeDurable(5)
}
addClickHandler(Click.RIGHT, { e ->
val stack = e.player.inventory.itemInMainHand
if (EffectiveDurability.consumeUse(stack)) {
e.player.sendMessage("left: " + EffectiveDurability.getUsesLeft(stack))
} else {
e.player.sendMessage("used up")
}
Result.CANCEL_EVENT
})The durability bar as a use counter: the stack becomes unstackable, maxDamage = maxUses. consumeUse — only on the real inventory stack, never on createItemStack(); on the last charge the stack disappears.
fun init() {
makeWearable()
}Right-click equips it to the head, the old helmet goes back to the inventory; dragging onto the helmet slot works too. Check: EffectiveWearable.isWearable(stack).
fun init() {
makeUndropable()
}Q does nothing, excluded from death drops — the item stays with the player. Check: EffectiveUndropable.isUndropable(stack).
val awkward = (ItemStack(Material.POTION).itemMeta as PotionMeta).apply {
basePotionType = PotionType.AWKWARD
}
addBrewRecipe(
inputIngredient = RubyDust.createItemStack(),
inputBasePotionMeta = awkward,
fuelUse = 1,
cookingTime = 400,
)Ingredient on top (custom allowed), the base potion in all three bottom slots must match inputBasePotionMeta. The result is this item in each of the three slots.
object Bomb : EffectiveItem() {
…
override fun getAdditionalArgs() = AdditionalArgs(
MyPlugin.instance,
listOf(
"radius" to PersistentDataType.INTEGER,
"owner" to PersistentDataType.STRING,
),
)
override fun showAdditionArgsInLore() = true
fun init() {
addClickHandler(Click.RIGHT, { e ->
val radius = EffectiveDataContainerUtils.getContainerValue<Int>(e.item, additionalKey("radius")) ?: 1
e.player.world.createExplosion(e.player.location, radius.toFloat())
Result.CANCEL_EVENT
})
}
}
val big = Bomb.createItemStack(1, listOf("6", "Steve"))
val small = Bomb.createItemStack(3, listOf("2", "Alex"))The same item, but each stack has its own values: they are parsed from strings in declaration order and written to the stack's PDC on creation. /egive bomb Steve 6 Alex takes the same arguments. Types: scalar PersistentDataType (STRING, INTEGER, LONG, DOUBLE, FLOAT, BYTE, SHORT, BOOLEAN) and comma-separated arrays (BYTE_ARRAY, INTEGER_ARRAY, LONG_ARRAY). Read via additionalKey("radius") + EffectiveDataContainerUtils; showAdditionArgsInLore appends the values to the lore.
What to override
- getMaterial() *Base vanilla material of the stack
- getNamespacedData() *Plugin and id — together form the key like myplugin:ruby
- editMeta(meta) *Name, lore, flags — everything that goes into ItemMeta
- getResourcePackData()Default: nullTexture or own model; item_model is set for you
- getAdditionalArgs()Default: nullPer-stack parameters stored in its PDC
- showAdditionArgsInLore()Default: falseShow parameter values in the lore
- createItemStackCallback(item)Default: —Final touch on the finished stack
* required
Pitfalls
- The item exists but has no texture — EffectiveResourcepack.addServerResourcepack(this, "", "") in onEnable after init() is missing. Without it no pack is built for the plugin.
- addClickHandler(click, callback, …) — callback is the second parameter. A trailing lambda lands in cooldownData and gives "No value passed for parameter callback". Write addClickHandler(Click.RIGHT, { … }).
- makeDurable must be called before the first createItemStack — durability is applied when the stack is created.
- getNamespacedData / getMaterial are called from the constructor — do not rely on subclass fields, only constants.
- player.itemOnCursor = x does not compile (getter and setter nullability differ in Paper) — use player.setItemOnCursor(x).