Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,31 @@ import io.github.pylonmc.pylon.core.datatypes.PylonSerializers
import io.github.pylonmc.pylon.core.i18n.PylonTranslator.Companion.translate
import io.github.pylonmc.pylon.core.item.PylonItemSchema
import io.github.pylonmc.pylon.core.util.editData
import io.github.pylonmc.pylon.core.util.editDataOrDefault
import io.github.pylonmc.pylon.core.util.editDataOrSet
import io.github.pylonmc.pylon.core.util.fromMiniMessage
import io.papermc.paper.datacomponent.DataComponentBuilder
import io.papermc.paper.datacomponent.DataComponentType
import io.papermc.paper.datacomponent.DataComponentTypes
import io.papermc.paper.datacomponent.item.CustomModelData
import io.papermc.paper.datacomponent.item.ItemAttributeModifiers
import io.papermc.paper.datacomponent.item.ItemLore
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.ComponentLike
import org.apache.commons.lang3.LocaleUtils
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.attribute.Attribute
import org.bukkit.attribute.AttributeModifier
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.ItemMeta
import org.bukkit.persistence.PersistentDataContainer
import xyz.xenondevs.invui.item.ItemProvider
import java.util.function.Consumer
import kotlin.collections.forEach

@Suppress("UnstableApiUsage")
open class ItemStackBuilder private constructor(val stack: ItemStack) : ItemProvider {

open class ItemStackBuilder internal constructor(val stack: ItemStack) : ItemProvider {
fun amount(amount: Int) = apply {
stack.amount = amount
}
Expand Down Expand Up @@ -66,6 +71,14 @@ open class ItemStackBuilder private constructor(val stack: ItemStack) : ItemProv
stack.editData(type, block)
}

fun <T : Any> editDataOrDefault(type: DataComponentType.Valued<T>, block: (T) -> T) = apply {
stack.editDataOrDefault(type, block)
}

fun <T : Any> editDataOrSet(type: DataComponentType.Valued<T>, block: (T?) -> T) = apply {
stack.editDataOrSet(type, block)
}

fun name(name: Component) = set(DataComponentTypes.ITEM_NAME, name)

fun name(name: String) = name(fromMiniMessage(name))
Expand All @@ -87,6 +100,20 @@ open class ItemStackBuilder private constructor(val stack: ItemStack) : ItemProv
fun defaultTranslatableLore(key: NamespacedKey) =
lore(Component.translatable(loreKey(key), ""))

@JvmOverloads
fun addAttributeModifier(
attribute: Attribute,
modifier: AttributeModifier,
replaceExisting: Boolean = true
) = apply {
editDataOrSet(DataComponentTypes.ATTRIBUTE_MODIFIERS) { modifiers ->
val copying = modifiers?.modifiers()?.filter { !replaceExisting || it.modifier().key != modifier.key }
ItemAttributeModifiers.itemAttributes().copy(copying)
.addModifier(attribute, modifier)
.build()
}
}

fun build(): ItemStack = stack.clone()

override fun get(lang: String?): ItemStack {
Expand All @@ -102,6 +129,9 @@ open class ItemStackBuilder private constructor(val stack: ItemStack) : ItemProv

companion object {

val baseAttackDamage = NamespacedKey.minecraft("base_attack_damage")
val baseAttackSpeed = NamespacedKey.minecraft("base_attack_speed")

fun nameKey(key: NamespacedKey)
= "pylon.${key.namespace}.item.${key.key}.name"

Expand All @@ -123,20 +153,20 @@ open class ItemStackBuilder private constructor(val stack: ItemStack) : ItemProv
* with the item's ID set to [key]
*/
@JvmStatic
fun pylonItem(material: Material, key: NamespacedKey): ItemStackBuilder {
return of(material)
fun pylonItem(material: Material, key: NamespacedKey): PylonItemStackBuilder {
return PylonItemStackBuilder(ItemStack(material), key)
.editPdc { pdc -> pdc.set(PylonItemSchema.pylonItemKeyKey, PylonSerializers.NAMESPACED_KEY, key) }
.set(DataComponentTypes.CUSTOM_MODEL_DATA, CustomModelData.customModelData().addString(key.toString()))
.defaultTranslatableName(key)
.defaultTranslatableLore(key)
.defaultTranslatableLore(key) as PylonItemStackBuilder
}

/**
* Returns an [ItemStackBuilder] with name and lore set to the default translation keys, and with the item's ID set to [key]
*/
@JvmStatic
fun pylonItem(stack: ItemStack, key: NamespacedKey): ItemStackBuilder {
return of(stack)
fun pylonItem(stack: ItemStack, key: NamespacedKey): PylonItemStackBuilder {
return PylonItemStackBuilder(stack, key)
.editPdc { it.set(PylonItemSchema.pylonItemKeyKey, PylonSerializers.NAMESPACED_KEY, key) }
.let {
// Adds the pylon item key as the FIRST string in custom model data, but preserve any pre-existing data
Expand All @@ -149,7 +179,14 @@ open class ItemStackBuilder private constructor(val stack: ItemStack) : ItemProv
it.set(DataComponentTypes.CUSTOM_MODEL_DATA, modelData)
}
.defaultTranslatableName(key)
.defaultTranslatableLore(key)
.defaultTranslatableLore(key) as PylonItemStackBuilder
}

fun ItemAttributeModifiers.Builder.copy(modifiers: List<ItemAttributeModifiers.Entry>?) : ItemAttributeModifiers.Builder {
modifiers?.forEach { entry ->
this.addModifier(entry.attribute(), entry.modifier(), entry.group, entry.display())
}
return this
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package io.github.pylonmc.pylon.core.item.builder

import io.github.pylonmc.pylon.core.config.Settings
import io.github.pylonmc.pylon.core.config.adapter.ConfigAdapter
import io.papermc.paper.datacomponent.DataComponentTypes
import io.papermc.paper.datacomponent.item.ItemAttributeModifiers
import io.papermc.paper.datacomponent.item.Tool
import io.papermc.paper.datacomponent.item.UseCooldown
import io.papermc.paper.datacomponent.item.Weapon
import io.papermc.paper.registry.keys.tags.BlockTypeTagKeys
import io.papermc.paper.registry.set.RegistryKeySet
import net.kyori.adventure.util.TriState
import org.bukkit.NamespacedKey
import org.bukkit.Registry
import org.bukkit.attribute.Attribute
import org.bukkit.attribute.AttributeModifier
import org.bukkit.block.BlockType
import org.bukkit.inventory.EquipmentSlotGroup
import org.bukkit.inventory.ItemStack

@Suppress("UnstableApiUsage")
class PylonItemStackBuilder : ItemStackBuilder {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToolItemStackBuilder would be better, no reason to call it Pylon

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does way more than just tools, requires a pylon item key, and uses pylon item settings, why would it not be pylon?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tbh this does feel more like it should be either part of the ItemStackBuilder, or possibly a set of utility methods that create the components. I can see there potentially being a lot of confusion over 'why are there 2 item stack builders' and 'why is the PylonItemStackBuilder not needed to create pylon items'. Is there any reason these can't just be methods on the original ItemStackBuilder?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially did that but then thought it may bring confusion, if someone is making an item stack and wants to give it a weapon, or custom durability or something, and doesn't read the docs, it would throw an exception because there are no settings for a non pylon item.

I included the overloads for people that don't want to have values configurable but I suppose it also would allow for non-pylon items to use the methods assuming they provide a value for every field and don't use any defaults.

I'd also then have to add a field to ItemStackBuilder for a pylon item key. Which isn't necessarily good or bad it's just a change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we move all the Pylon-specific stuff (like pylonItem) into PylonItemStackBuilder and have the normal one be just for vanilla/ui?

private val itemKey: NamespacedKey

internal constructor(stack: ItemStack, itemKey: NamespacedKey) : super(stack) {
this.itemKey = itemKey
}

@JvmOverloads
fun helmet(
armor: Double = Settings.get(itemKey).getOrThrow("armor", ConfigAdapter.DOUBLE),
armorToughness: Double = Settings.get(itemKey).getOrThrow("armor-toughness", ConfigAdapter.DOUBLE)
) = armor(EquipmentSlotGroup.HEAD, armor, armorToughness)

@JvmOverloads
fun chestPlate(
armor: Double = Settings.get(itemKey).getOrThrow("armor", ConfigAdapter.DOUBLE),
armorToughness: Double = Settings.get(itemKey).getOrThrow("armor-toughness", ConfigAdapter.DOUBLE)
) = armor(EquipmentSlotGroup.CHEST, armor, armorToughness)

@JvmOverloads
fun leggings(
armor: Double = Settings.get(itemKey).getOrThrow("armor", ConfigAdapter.DOUBLE),
armorToughness: Double = Settings.get(itemKey).getOrThrow("armor-toughness", ConfigAdapter.DOUBLE)
) = armor(EquipmentSlotGroup.LEGS, armor, armorToughness)

@JvmOverloads
fun boots(
armor: Double = Settings.get(itemKey).getOrThrow("armor", ConfigAdapter.DOUBLE),
armorToughness: Double = Settings.get(itemKey).getOrThrow("armor-toughness", ConfigAdapter.DOUBLE)
) = armor(EquipmentSlotGroup.FEET, armor, armorToughness)

@JvmOverloads
fun armor(
slot: EquipmentSlotGroup,
armor: Double = Settings.get(itemKey).getOrThrow("armor", ConfigAdapter.DOUBLE),
armorToughness: Double = Settings.get(itemKey).getOrThrow("armor-toughness", ConfigAdapter.DOUBLE)
) = apply {
editDataOrSet(DataComponentTypes.ATTRIBUTE_MODIFIERS) { modifiers ->
val copying = modifiers?.modifiers()?.filter { it.modifier().key.namespace != "minecraft" || !it.modifier().key.key.contains("armor.") }
ItemAttributeModifiers.itemAttributes().copy(copying)
.addModifier(Attribute.ARMOR, AttributeModifier(itemKey, armor, AttributeModifier.Operation.ADD_NUMBER, slot))
.addModifier(Attribute.ARMOR_TOUGHNESS, AttributeModifier(itemKey, armorToughness, AttributeModifier.Operation.ADD_NUMBER, slot))
.build()
}
}

@JvmOverloads
fun axe(
miningSpeed: Float = Settings.get(itemKey).getOrThrow("mining-speed", ConfigAdapter.FLOAT),
miningDurabilityDamage: Int = Settings.get(itemKey).getOrThrow("mining-durability-damage", ConfigAdapter.INT)
) = tool(Registry.BLOCK.getTag(BlockTypeTagKeys.MINEABLE_AXE), miningSpeed, miningDurabilityDamage)

@JvmOverloads
fun pickaxe(
miningSpeed: Float = Settings.get(itemKey).getOrThrow("mining-speed", ConfigAdapter.FLOAT),
miningDurabilityDamage: Int = Settings.get(itemKey).getOrThrow("mining-durability-damage", ConfigAdapter.INT)
) = tool(Registry.BLOCK.getTag(BlockTypeTagKeys.MINEABLE_PICKAXE), miningSpeed, miningDurabilityDamage)

@JvmOverloads
fun shovel(
miningSpeed: Float = Settings.get(itemKey).getOrThrow("mining-speed", ConfigAdapter.FLOAT),
miningDurabilityDamage: Int = Settings.get(itemKey).getOrThrow("mining-durability-damage", ConfigAdapter.INT)
) = tool(Registry.BLOCK.getTag(BlockTypeTagKeys.MINEABLE_SHOVEL), miningSpeed, miningDurabilityDamage)

@JvmOverloads
fun hoe(
miningSpeed: Float = Settings.get(itemKey).getOrThrow("mining-speed", ConfigAdapter.FLOAT),
miningDurabilityDamage: Int = Settings.get(itemKey).getOrThrow("mining-durability-damage", ConfigAdapter.INT)
) = tool(Registry.BLOCK.getTag(BlockTypeTagKeys.MINEABLE_HOE), miningSpeed, miningDurabilityDamage)

@JvmOverloads
fun tool(
blocks: RegistryKeySet<BlockType>,
miningSpeed: Float = Settings.get(itemKey).getOrThrow("mining-speed", ConfigAdapter.FLOAT),
miningDurabilityDamage: Int = Settings.get(itemKey).getOrThrow("mining-durability-damage", ConfigAdapter.INT)
) = apply {
set(DataComponentTypes.TOOL, Tool.tool()
.defaultMiningSpeed(miningSpeed)
.damagePerBlock(miningDurabilityDamage)
.addRule(Tool.rule(blocks, miningSpeed, TriState.TRUE)))
}

fun noTool() = unset(DataComponentTypes.TOOL) as PylonItemStackBuilder

@JvmOverloads
fun weapon(
disablesShield: Boolean = false,
attackDamage: Double = Settings.get(itemKey).getOrThrow("attack-damage", ConfigAdapter.DOUBLE),
attackSpeed: Double = Settings.get(itemKey).getOrThrow("attack-speed", ConfigAdapter.DOUBLE),
attackDurabilityDamage: Int = Settings.get(itemKey).getOrThrow("attack-durability-damage", ConfigAdapter.INT),
disableShieldSeconds: Float? = null
) = apply {
addAttributeModifier(Attribute.ATTACK_DAMAGE, AttributeModifier(baseAttackDamage, -1.0 + attackDamage, AttributeModifier.Operation.ADD_NUMBER, EquipmentSlotGroup.MAINHAND))
addAttributeModifier(Attribute.ATTACK_SPEED, AttributeModifier(baseAttackSpeed, -4.0 + attackSpeed, AttributeModifier.Operation.ADD_NUMBER, EquipmentSlotGroup.MAINHAND))
set(DataComponentTypes.WEAPON, Weapon.weapon()
.itemDamagePerAttack(attackDurabilityDamage)
.disableBlockingForSeconds(if (disablesShield) disableShieldSeconds ?: Settings.get(itemKey).getOrThrow("disable-shield-seconds", ConfigAdapter.FLOAT) else 0f))
}

@JvmOverloads
fun attackKnockback(knockback: Double = Settings.get(itemKey).getOrThrow("attack-knockback", ConfigAdapter.DOUBLE)) =
addAttributeModifier(Attribute.ATTACK_KNOCKBACK, AttributeModifier(itemKey, knockback, AttributeModifier.Operation.ADD_NUMBER, EquipmentSlotGroup.MAINHAND)) as PylonItemStackBuilder

@JvmOverloads
fun durability(
durability: Int = Settings.get(itemKey).getOrThrow("durability", ConfigAdapter.INT)
) = set(DataComponentTypes.MAX_DAMAGE, durability) as PylonItemStackBuilder

@JvmOverloads
fun useCooldown(
cooldownTicks: Int = Settings.get(itemKey).getOrThrow("cooldown-ticks", ConfigAdapter.INT)
) = set(DataComponentTypes.USE_COOLDOWN, UseCooldown.useCooldown(cooldownTicks / 20.0f).cooldownGroup(itemKey)) as PylonItemStackBuilder
}
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,27 @@ inline fun <T : Any> ItemStack.editData(
return this
}

@JvmSynthetic
@Suppress("UnstableApiUsage")
inline fun <T : Any> ItemStack.editDataOrDefault(
type: DataComponentType.Valued<T>,
block: (T) -> T
): ItemStack {
val data = getData(type) ?: this.type.getDefaultData(type) ?: return this
setData(type, block(data))
return this
}

@JvmSynthetic
@Suppress("UnstableApiUsage")
inline fun <T : Any> ItemStack.editDataOrSet(
type: DataComponentType.Valued<T>,
block: (T?) -> T
): ItemStack {
setData(type, block(getData(type)))
return this
}

/**
* Wrapper around [PersistentDataContainer.set] that allows nullable values to be passed
*
Expand Down