← Roadmap

EffectiveItem

Items

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

Minimal item
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.

Behaviour
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) }
    }
}
Recognise the item
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.

EffectiveClickable — clicks
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.

EffectiveCraftable — recipes
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.

EffectiveDropable — drops
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.

EffectiveThrowable — throwing
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.

EffectiveDurability — charges
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.

EffectiveWearable — head slot
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).

EffectiveUndropable — cannot be dropped
fun init() {
    makeUndropable()
}

Q does nothing, excluded from death drops — the item stays with the player. Check: EffectiveUndropable.isUndropable(stack).

EffectiveBrewable — brewing stand
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.

AdditionalArgs — per-stack parameters
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: null
    Texture or own model; item_model is set for you
  • getAdditionalArgs()
    Default: null
    Per-stack parameters stored in its PDC
  • showAdditionArgsInLore()
    Default: false
    Show 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).