diff --git a/README.md b/README.md index 8bc503b13f..7c7d96d7a8 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,25 @@ [![Banner](https://i.imgur.com/HiDAPmf.png)](https://hangar.papermc.io/xenondevs/Nova)

- - + + - - + + - - + + Modrinth Game Versions

+

+ + + + + + +

+ # Nova @@ -19,8 +28,6 @@ With Nova, developers don't have to deal with resource pack tricks, data seriali just focus in adding content to the game. As a server administrator, you can choose from a set of Nova addons, which will add content to the game. -You can find Nova on: [SpigotMC](https://www.spigotmc.org/resources/93648/) | [Hangar](https://hangar.papermc.io/xenondevs/Nova) | [Modrinth](https://modrinth.com/plugin/nova-framework) - ## Features * Custom items diff --git a/gradle.properties b/gradle.properties index 8eebc75d06..e4e75fa0b3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version = 0.14.2 +version = 0.14.3 kotlin.daemon.jvmargs=-Xmx2g org.gradle.jvmargs=-Xmx2g diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c7b9e7f488..7ad8d9f9b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ format.version = "1.1" [versions] -cbf = "0.5" +cbf = "0.7" invui = "1.12" kotlin = "1.8.22" ktor = "2.3.1" diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/addon/registry/ItemRegistry.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/addon/registry/ItemRegistry.kt index cd6b61d1e8..df838ee5fa 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/addon/registry/ItemRegistry.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/addon/registry/ItemRegistry.kt @@ -21,7 +21,7 @@ interface ItemRegistry : AddonGetter { fun registerItem( name: String, - vararg behaviors: ItemBehaviorHolder<*>, + vararg behaviors: ItemBehaviorHolder, localizedName: String = "item.${addon.description.id}.$name", isHidden: Boolean = false ): NovaItem { @@ -37,7 +37,7 @@ interface ItemRegistry : AddonGetter { fun registerItem( block: NovaBlock, - vararg behaviors: ItemBehaviorHolder<*>, + vararg behaviors: ItemBehaviorHolder, localizedName: String = block.localizedName, isHidden: Boolean = false ): NovaItem { diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/data/config/ConfigAccess.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/data/config/ConfigAccess.kt index 9d75e73d4e..bac258b386 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/data/config/ConfigAccess.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/data/config/ConfigAccess.kt @@ -11,7 +11,7 @@ import xyz.xenondevs.nova.item.NovaItem import kotlin.reflect.KType import kotlin.reflect.typeOf -abstract class ConfigAccess(private val configReceiver: () -> YamlConfiguration) { +open class ConfigAccess(private val configReceiver: () -> YamlConfiguration) { val cfg: YamlConfiguration get() = configReceiver() @@ -20,11 +20,11 @@ abstract class ConfigAccess(private val configReceiver: () -> YamlConfiguration) constructor(item: NovaItem) : this({ NovaConfig[item] }) - protected inline fun getEntry(key: String): Provider { + inline fun getEntry(key: String): Provider { return RequiredConfigEntryAccessor(key, typeOf()).also(ConfigEntryAccessor<*>::reload) } - protected inline fun getEntry(key: String, vararg fallbackKeys: String): Provider { + inline fun getEntry(key: String, vararg fallbackKeys: String): Provider { val type = typeOf() var provider: Provider = NullableConfigEntryAccessor(key, type) fallbackKeys.forEach { provider = provider.orElse(NullableConfigEntryAccessor(it, type)) } @@ -33,32 +33,35 @@ abstract class ConfigAccess(private val configReceiver: () -> YamlConfiguration) .also(Provider<*>::update) } - protected inline fun getOptionalEntry(key: String): Provider { + inline fun getOptionalEntry(key: String): Provider { return NullableConfigEntryAccessor(key, typeOf()).also(Provider<*>::update) } - protected inline fun getOptionalEntry(key: String, vararg fallbackKeys: String): Provider { + inline fun getOptionalEntry(key: String, vararg fallbackKeys: String): Provider { val type = typeOf() var provider: Provider = NullableConfigEntryAccessor(key, type) fallbackKeys.forEach { provider = provider.orElse(NullableConfigEntryAccessor(it, type)) } return provider.also(Provider<*>::update) } - protected inner class RequiredConfigEntryAccessor(key: String, type: KType) : ConfigEntryAccessor(key, type) { + @PublishedApi + internal inner class RequiredConfigEntryAccessor(key: String, type: KType) : ConfigEntryAccessor(key, type) { override fun loadValue(): T { check(key in cfg) { "No such config entry: $key" } return cfg.getDeserialized(key, type)!! } } - protected inner class NullableConfigEntryAccessor(key: String, type: KType) : ConfigEntryAccessor(key, type) { + @PublishedApi + internal inner class NullableConfigEntryAccessor(key: String, type: KType) : ConfigEntryAccessor(key, type) { override fun loadValue(): T? { return cfg.getDeserialized(key, type) } } @Suppress("LeakingThis") - protected abstract class ConfigEntryAccessor(protected val key: String, protected val type: KType) : Provider(), Reloadable { + @PublishedApi + internal abstract class ConfigEntryAccessor(protected val key: String, protected val type: KType) : Provider(), Reloadable { init { NovaConfig.reloadables += this diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/data/recipe/impl/RepairItemRecipe.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/data/recipe/impl/RepairItemRecipe.kt index acafb5ffe8..ffe8a267f4 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/data/recipe/impl/RepairItemRecipe.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/data/recipe/impl/RepairItemRecipe.kt @@ -10,7 +10,6 @@ import net.minecraft.world.item.enchantment.Enchantment import net.minecraft.world.item.enchantment.EnchantmentHelper import net.minecraft.world.level.Level import xyz.xenondevs.nova.item.behavior.Damageable -import xyz.xenondevs.nova.util.item.DamageableUtils import xyz.xenondevs.nova.util.item.novaItem import xyz.xenondevs.nova.util.nmsCopy import kotlin.math.max @@ -26,7 +25,7 @@ internal class RepairItemRecipe(id: ResourceLocation) : MojangRepairItemRecipe(i if (itemStack.isEmpty) continue - if (!secondStackFound && DamageableUtils.isDamageable(itemStack) && itemStack.count == 1) { + if (!secondStackFound && Damageable.isDamageable(itemStack) && itemStack.count == 1) { if (firstStack == null) { firstStack = itemStack } else if (isSameItem(firstStack, itemStack)) { @@ -49,8 +48,8 @@ internal class RepairItemRecipe(id: ResourceLocation) : MojangRepairItemRecipe(i require(items.size == 2) { "Item size is not 2" } val novaItem = items[0].novaItem if (novaItem != null) { - val damageable = novaItem.getBehavior(Damageable::class)!! - val maxDurability = damageable.options.durability + val damageable = novaItem.getBehavior(Damageable::class) + val maxDurability = damageable.maxDurability val itemStackA = items[0] val itemStackB = items[1] diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/data/serialization/cbf/NamespacedCompound.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/data/serialization/cbf/NamespacedCompound.kt index b9c256b22e..1e1c9b6746 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/data/serialization/cbf/NamespacedCompound.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/data/serialization/cbf/NamespacedCompound.kt @@ -9,11 +9,12 @@ import xyz.xenondevs.cbf.io.ByteReader import xyz.xenondevs.cbf.io.ByteWriter import xyz.xenondevs.nova.addon.Addon import xyz.xenondevs.nova.util.name +import java.util.* import kotlin.reflect.KType import kotlin.reflect.typeOf class NamespacedCompound internal constructor( - private val map: HashMap + private val map: MutableMap ) { val keys: Set @@ -152,6 +153,13 @@ class NamespacedCompound internal constructor( } + fun putAll(other: NamespacedCompound) { + for ((namespace, otherCompound) in other.map) { + val compound = map.getOrPut(namespace, ::Compound) + compound.putAll(otherCompound) + } + } + fun copy(): NamespacedCompound = NamespacedCompound(map.mapValuesTo(HashMap()) { (_, value) -> value.copy() }) fun isEmpty(): Boolean = map.isNotEmpty() @@ -169,6 +177,12 @@ class NamespacedCompound internal constructor( return builder.toString().replace("\n", "\n ") + "\n}" } + companion object { + + val EMPTY = NamespacedCompound(Collections.unmodifiableMap(HashMap())) + + } + internal object NamespacedCompoundBinaryAdapter : BinaryAdapter { override fun write(obj: NamespacedCompound, type: KType, writer: ByteWriter) { diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/DefaultItems.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/DefaultItems.kt index ec5ea0979e..97cd70163e 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/DefaultItems.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/DefaultItems.kt @@ -3,8 +3,8 @@ package xyz.xenondevs.nova.item import net.minecraft.resources.ResourceLocation -import xyz.xenondevs.nova.initialize.InternalInitStage import xyz.xenondevs.nova.initialize.InternalInit +import xyz.xenondevs.nova.initialize.InternalInitStage import xyz.xenondevs.nova.item.behavior.ItemBehaviorHolder import xyz.xenondevs.nova.item.behavior.impl.WrenchBehavior import xyz.xenondevs.nova.item.logic.ItemLogic @@ -137,7 +137,7 @@ object DefaultBlockOverlays { private fun registerCoreItem( name: String, - vararg itemBehaviors: ItemBehaviorHolder<*>, + vararg itemBehaviors: ItemBehaviorHolder, localizedName: String = "item.nova.$name", isHidden: Boolean = false ): NovaItem = register(NovaItem( @@ -150,7 +150,7 @@ private fun registerCoreItem( private fun registerUnnamedHiddenCoreItem( name: String, localizedName: String = "", - vararg itemBehaviors: ItemBehaviorHolder<*> + vararg itemBehaviors: ItemBehaviorHolder ): NovaItem = register(NovaItem( ResourceLocation("nova", name), localizedName, diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/NovaItem.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/NovaItem.kt index 5d074c2c6d..0c8b19263a 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/NovaItem.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/NovaItem.kt @@ -109,26 +109,38 @@ class NovaItem internal constructor( /** * Checks whether this [NovaItem] has an [ItemBehavior] of the reified type [T], or a subclass of it. */ - inline fun hasBehavior(): Boolean = + inline fun hasBehavior(): Boolean = hasBehavior(T::class) /** * Checks whether this [NovaItem] has an [ItemBehavior] of the specified class [behavior], or a subclass of it. */ - fun hasBehavior(behavior: KClass): Boolean = + fun hasBehavior(behavior: KClass): Boolean = logic.hasBehavior(behavior) /** - * Gets the [ItemBehavior] instance of the reified type [T], or a subclass of it. + * Gets the first [ItemBehavior] that is an instance of [T], or null if there is none. */ - inline fun getBehavior(): T? = + inline fun getBehaviorOrNull(): T? = + getBehaviorOrNull(T::class) + + /** + * Gets the first [ItemBehavior] that is an instance of [behavior], or null if there is none. + */ + fun getBehaviorOrNull(behavior: KClass): T? = + logic.getBehaviorOrNull(behavior) + + /** + * Gets the first [ItemBehavior] that is an instance of [T], or throws an [IllegalStateException] if there is none. + */ + inline fun getBehavior(): T = getBehavior(T::class) /** - * Gets the [ItemBehavior] instance of the specified class [behavior], or a subclass of it. + * Gets the first [ItemBehavior] that is an instance of [behavior], or throws an [IllegalStateException] if there is none. */ - fun getBehavior(behavior: KClass): T? = - logic.getBehavior(behavior) + fun getBehavior(behavior: KClass): T = + getBehaviorOrNull(behavior) ?: throw IllegalStateException("Item $id does not have a behavior of type ${behavior.simpleName}") override fun toString() = id.toString() diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/NovaItemBuilder.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/NovaItemBuilder.kt index dc17e2f61b..6267565089 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/NovaItemBuilder.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/NovaItemBuilder.kt @@ -16,7 +16,7 @@ import xyz.xenondevs.nova.world.block.NovaTileEntityBlock class NovaItemBuilder internal constructor(id: ResourceLocation): RegistryElementBuilder(NovaRegistries.ITEM, id) { - private var logic: MutableList> = ArrayList() + private var logic: MutableList = ArrayList() private var localizedName = "item.${id.namespace}.${id.name}" private var maxStackSize = 64 private var craftingRemainingItem: ItemBuilder? = null @@ -35,12 +35,12 @@ class NovaItemBuilder internal constructor(id: ResourceLocation): RegistryElemen return this } - fun behaviors(vararg itemBehaviors: ItemBehaviorHolder<*>): NovaItemBuilder { + fun behaviors(vararg itemBehaviors: ItemBehaviorHolder): NovaItemBuilder { this.logic = itemBehaviors.toMutableList() return this } - fun addBehavior(vararg itemBehaviors: ItemBehaviorHolder<*>): NovaItemBuilder { + fun addBehavior(vararg itemBehaviors: ItemBehaviorHolder): NovaItemBuilder { this.logic += itemBehaviors return this } @@ -86,7 +86,7 @@ class NovaItemBuilder internal constructor(id: ResourceLocation): RegistryElemen this.block = block localizedName(block.localizedName) if (block is NovaTileEntityBlock) - behaviors(TileEntityItemBehavior()) + behaviors(TileEntityItemBehavior) } } diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Chargeable.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Chargeable.kt index 2850c1a7b6..07375868c4 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Chargeable.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Chargeable.kt @@ -2,103 +2,125 @@ package xyz.xenondevs.nova.item.behavior import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.NamedTextColor -import org.bukkit.inventory.ItemStack -import xyz.xenondevs.invui.item.builder.ItemBuilder +import xyz.xenondevs.commons.provider.Provider +import xyz.xenondevs.nova.data.config.ConfigAccess import xyz.xenondevs.nova.data.serialization.cbf.NamespacedCompound import xyz.xenondevs.nova.item.NovaItem import xyz.xenondevs.nova.item.logic.PacketItemData -import xyz.xenondevs.nova.item.options.ChargeableOptions import xyz.xenondevs.nova.item.vanilla.VanillaMaterialProperty import xyz.xenondevs.nova.util.NumberFormatUtils import xyz.xenondevs.nova.util.item.novaCompound import net.minecraft.world.item.ItemStack as MojangStack +import org.bukkit.inventory.ItemStack as BukkitStack @Suppress("FunctionName") -fun Chargeable(affectsItemDurability: Boolean): ItemBehaviorFactory = - object : ItemBehaviorFactory() { - override fun create(item: NovaItem): Chargeable = - Chargeable(ChargeableOptions.configurable(item), affectsItemDurability) +fun Chargeable(affectsItemDurability: Boolean): ItemBehaviorFactory = + object : ItemBehaviorFactory { + override fun create(item: NovaItem): Chargeable.Default { + val cfg = ConfigAccess(item) + return Chargeable.Default(cfg.getEntry("max_energy"), affectsItemDurability) + } } -class Chargeable( - val options: ChargeableOptions, - private val affectsItemDurability: Boolean = true -) : ItemBehavior() { +/** + * Allows items to store energy and be charged. + */ +interface Chargeable { - @Deprecated("Replaced by ChargeableOptions", ReplaceWith("options.maxEnergy")) + /** + * The maximum amount of energy this item can store. + */ val maxEnergy: Long - get() = options.maxEnergy - // - fun getEnergy(itemStack: ItemStack): Long { - return getEnergy(itemStack.novaCompound) - } + /** + * Gets the current amount of energy stored in the given [itemStack]. + */ + fun getEnergy(itemStack: BukkitStack): Long - fun setEnergy(itemStack: ItemStack, energy: Long) { - return setEnergy(itemStack.novaCompound, energy) - } + /** + * Sets the current amount of energy stored in the given [itemStack] to [energy]. + */ + fun setEnergy(itemStack: BukkitStack, energy: Long) - fun addEnergy(itemStack: ItemStack, energy: Long) { - return addEnergy(itemStack.novaCompound, energy) - } - // + /** + * Adds the given [energy] to the current amount of energy stored in the given [itemStack], capped at [maxEnergy]. + */ + fun addEnergy(itemStack: BukkitStack, energy: Long) - // - fun getEnergy(itemStack: MojangStack): Long { - return getEnergy(itemStack.novaCompound) - } + /** + * Gets the current amount of energy stored in the given [itemStack]. + */ + fun getEnergy(itemStack: MojangStack): Long - fun setEnergy(itemStack: MojangStack, energy: Long) { - return setEnergy(itemStack.novaCompound, energy) - } + /** + * Sets the current amount of energy stored in the given [itemStack] to [energy]. + */ + fun setEnergy(itemStack: MojangStack, energy: Long) - fun addEnergy(itemStack: MojangStack, energy: Long) { - return addEnergy(itemStack.novaCompound, energy) - } - // + /** + * Adds the given [energy] to the current amount of energy stored in the given [itemStack], capped at [maxEnergy]. + */ + fun addEnergy(itemStack: MojangStack, energy: Long) - // - fun getEnergy(data: NamespacedCompound): Long { - val currentEnergy = data["nova", "energy"] ?: 0L - if (currentEnergy > options.maxEnergy) { - setEnergy(data, options.maxEnergy) - return options.maxEnergy + companion object : ItemBehaviorFactory { + + override fun create(item: NovaItem): Default { + val cfg = ConfigAccess(item) + return Default(cfg.getEntry("max_energy"), true) } - return currentEnergy - } - - fun setEnergy(data: NamespacedCompound, energy: Long) { - data["nova", "energy"] = energy.coerceIn(0, options.maxEnergy) - } - - fun addEnergy(data: NamespacedCompound, energy: Long) { - setEnergy(data, getEnergy(data) + energy) - } - // - - override fun modifyItemBuilder(itemBuilder: ItemBuilder): ItemBuilder { - itemBuilder.addModifier { setEnergy(it.novaCompound, 0); it } - return itemBuilder + } - override fun updatePacketItemData(data: NamespacedCompound, itemData: PacketItemData) { - val energy = getEnergy(data) + class Default( + maxEnergy: Provider, + private val affectsItemDurability: Boolean + ) : ItemBehavior, Chargeable { - itemData.addLore(Component.text(NumberFormatUtils.getEnergyString(energy, options.maxEnergy), NamedTextColor.GRAY)) + override val maxEnergy by maxEnergy + + override fun getVanillaMaterialProperties(): List { + return if (affectsItemDurability) + listOf(VanillaMaterialProperty.DAMAGEABLE) + else emptyList() + } + + override fun getDefaultCompound(): NamespacedCompound { + val compound = NamespacedCompound() + compound["nova", "energy"] = 0L + return compound + } + + override fun updatePacketItemData(data: NamespacedCompound, itemData: PacketItemData) { + val energy = getEnergy(data) + itemData.addLore(Component.text(NumberFormatUtils.getEnergyString(energy, maxEnergy), NamedTextColor.GRAY)) + if (affectsItemDurability) + itemData.durabilityBar = energy.toDouble() / maxEnergy.toDouble() + } + + override fun getEnergy(itemStack: BukkitStack) = getEnergy(itemStack.novaCompound) + override fun setEnergy(itemStack: BukkitStack, energy: Long) = setEnergy(itemStack.novaCompound, energy) + override fun addEnergy(itemStack: BukkitStack, energy: Long) = addEnergy(itemStack.novaCompound, energy) + override fun getEnergy(itemStack: MojangStack) = getEnergy(itemStack.novaCompound) + override fun setEnergy(itemStack: MojangStack, energy: Long) = setEnergy(itemStack.novaCompound, energy) + override fun addEnergy(itemStack: MojangStack, energy: Long) = addEnergy(itemStack.novaCompound, energy) + + private fun getEnergy(data: NamespacedCompound): Long { + val currentEnergy = data["nova", "energy"] ?: 0L + if (currentEnergy > maxEnergy) { + setEnergy(data, maxEnergy) + return maxEnergy + } + return currentEnergy + } + + private fun setEnergy(data: NamespacedCompound, energy: Long) { + data["nova", "energy"] = energy.coerceIn(0, maxEnergy) + } + + private fun addEnergy(data: NamespacedCompound, energy: Long) { + setEnergy(data, getEnergy(data) + energy) + } - if (affectsItemDurability) - itemData.durabilityBar = energy.toDouble() / options.maxEnergy.toDouble() - } - - override fun getVanillaMaterialProperties(): List { - return if (affectsItemDurability) - listOf(VanillaMaterialProperty.DAMAGEABLE) - else emptyList() - } - - companion object : ItemBehaviorFactory() { - override fun create(item: NovaItem): Chargeable = - Chargeable(ChargeableOptions.configurable(item), true) } } \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Consumable.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Consumable.kt index e3f00de5d7..ad28b7a46b 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Consumable.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Consumable.kt @@ -11,13 +11,17 @@ import org.bukkit.event.block.Action import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.inventory.EquipmentSlot import org.bukkit.inventory.ItemStack +import org.bukkit.potion.PotionEffect +import xyz.xenondevs.commons.provider.Provider +import xyz.xenondevs.commons.provider.immutable.map +import xyz.xenondevs.commons.provider.immutable.orElse +import xyz.xenondevs.commons.provider.immutable.provider import xyz.xenondevs.nmsutils.network.PacketIdRegistry import xyz.xenondevs.nmsutils.network.event.serverbound.ServerboundPlayerActionPacketEvent import xyz.xenondevs.nmsutils.network.send import xyz.xenondevs.nmsutils.util.removeIf +import xyz.xenondevs.nova.data.config.ConfigAccess import xyz.xenondevs.nova.item.NovaItem -import xyz.xenondevs.nova.item.options.FoodOptions -import xyz.xenondevs.nova.item.options.FoodOptions.FoodType import xyz.xenondevs.nova.item.vanilla.VanillaMaterialProperty import xyz.xenondevs.nova.util.getPlayersNearby import xyz.xenondevs.nova.util.intValue @@ -34,125 +38,234 @@ import kotlin.random.Random private const val PACKET_DISTANCE = 500.0 -data class Eater(val itemStack: ItemStack, val hand: EquipmentSlot, val startTime: Int) +private data class Eater(val itemStack: ItemStack, val hand: EquipmentSlot, val startTime: Int) -class Consumable(private val options: FoodOptions) : ItemBehavior() { +fun FoodOptions( + type: Consumable.FoodType, + consumeTime: Int, + nutrition: Int, + saturationModifier: Float, + instantHealth: Double = 0.0, + effects: List? = null +) = Consumable.Default( + provider(type), + provider(consumeTime), + provider(nutrition), + provider(saturationModifier), + provider(instantHealth), + provider(effects) +) + +/** + * Allows items to be consumed. + */ +sealed interface Consumable { // TODO: remove sealed & make more customizable (make logic a patch?) - private val eaters = HashMap() + /** + * The type of food. + */ + val type: FoodType - init { - runTaskTimer(0, 1) { - eaters.removeIf { (player, eater) -> - if (serverTick >= eater.startTime + options.consumeTime) { - finishEating(player, eater) - return@removeIf true - } else handleEating(player, eater) - - return@removeIf false - } - } - } + /** + * The time it takes for the food to be consumed, in ticks. + */ + val consumeTime: Int - override fun getVanillaMaterialProperties(): List { - return listOf(options.type.vanillaMaterialProperty) - } + /** + * The nutrition value of the food. + */ + val nutrition: Int + + /** + * The saturation modifier this food provides. The saturation is calculated like this: + * ``` + * saturation = min(saturation + nutrition * saturationModifier * 2.0f, foodLevel) + * ``` + */ + val saturationModifier: Float - override fun handleInteract(player: Player, itemStack: ItemStack, action: Action, event: PlayerInteractEvent) { - // only right-clicks in the air or on non-interactive blocks will cause an eating process - if (!action.isRightClick() || (action == Action.RIGHT_CLICK_BLOCK && event.clickedBlock!!.type.isInteractable)) - return + /** + * The amount of health to be restored immediately. + */ + val instantHealth: Double + + /** + * A list of effects to apply to the player when this food is consumed. + */ + val effects: List? + + enum class FoodType(internal val property: VanillaMaterialProperty) { + + /** + * Behaves like normal food. + * + * Has a small delay before the eating animation starts. + * + * Can only be eaten when hungry. + */ + NORMAL(VanillaMaterialProperty.CONSUMABLE_NORMAL), - // food which is not always consumable cannot be eaten in survival with a full hunger bar - if (options.type != FoodType.ALWAYS_EATABLE && player.gameMode != GameMode.CREATIVE && event.player.foodLevel == 20) - return + /** + * The eating animation starts immediately. + * + * Can only be eaten when hungry. + */ + FAST(VanillaMaterialProperty.CONSUMABLE_FAST), - event.isCancelled = true - beginEating(player, itemStack, event.hand!!) + /** + * The food can always be eaten, no hunger is required. + * + * Has a small delay before the eating animation starts. + */ + ALWAYS_EATABLE(VanillaMaterialProperty.CONSUMABLE_ALWAYS) } - override fun handleRelease(player: Player, itemStack: ItemStack, event: ServerboundPlayerActionPacketEvent) { - runTask { - val eater = eaters.remove(player) - if (eater != null) - stopEatingAnimation(player, eater) + class Default( + type: Provider, + consumeTime: Provider, + nutrition: Provider, + saturationModifier: Provider, + instantHealth: Provider, + effects: Provider?> + ) : ItemBehavior, Consumable { + + override val type by type + override val consumeTime by consumeTime + override val nutrition by nutrition + override val saturationModifier by saturationModifier + override val instantHealth by instantHealth + override val effects by effects + + private val eaters = HashMap() + + init { + runTaskTimer(0, 1) { + eaters.removeIf { (player, eater) -> + if (serverTick >= eater.startTime + this.consumeTime) { + finishEating(player, eater) + return@removeIf true + } else handleEating(player, eater) + + return@removeIf false + } + } } - } - - private fun handleEating(player: Player, eater: Eater) { - if (options.consumeTime < 20) { - // if the consumeTime is smaller than 20 ticks, the initial wait of 7 ticks will be skipped - playEatSound(player) - } else { - val eatTimePassed = serverTick - eater.startTime - if ((options.type == FoodType.FAST || eatTimePassed > 7) && eatTimePassed % 4 == 0) - playEatSound(player) + + override fun getVanillaMaterialProperties(): List { + return listOf(type.property) } - } - - private fun playEatSound(player: Player) { - player.location.playSoundNearby(Sound.ENTITY_GENERIC_EAT, SoundCategory.PLAYERS, 1f, Random.nextDouble(0.8, 1.2).toFloat(), player) - } - - private fun beginEating(player: Player, itemStack: ItemStack, hand: EquipmentSlot) { - // add to eaters map - eaters[player] = Eater(itemStack, hand, serverTick) - // send packet to begin eating particles for players nearby - val packet = createEatingAnimationPacket(player.entityId, true, hand) - player.location.getPlayersNearby(PACKET_DISTANCE, player).forEach { it.send(packet) } - } - - private fun stopEatingAnimation(player: Player, eater: Eater) { - // send packet to stop eating animation to other players - val stopEatingAnimationPacket = createEatingAnimationPacket(player.entityId, false, eater.hand) - player.location.getPlayersNearby(PACKET_DISTANCE, player).forEach { it.send(stopEatingAnimationPacket) } - } - - private fun finishEating(player: Player, eater: Eater) { - if (!player.isOnline) - return + override fun handleInteract(player: Player, itemStack: ItemStack, action: Action, event: PlayerInteractEvent) { + // only right-clicks in the air or on non-interactive blocks will cause an eating process + if (!action.isRightClick() || (action == Action.RIGHT_CLICK_BLOCK && event.clickedBlock!!.type.isInteractable)) + return + + // food which is not always consumable cannot be eaten in survival with a full hunger bar + if (type != FoodType.ALWAYS_EATABLE && player.gameMode != GameMode.CREATIVE && event.player.foodLevel == 20) + return + + event.isCancelled = true + beginEating(player, itemStack, event.hand!!) + } - // stop eating animation for other players - stopEatingAnimation(player, eater) + override fun handleRelease(player: Player, itemStack: ItemStack, event: ServerboundPlayerActionPacketEvent) { + runTask { + val eater = eaters.remove(player) + if (eater != null) + stopEatingAnimation(player, eater) + } + } - // send packet to stop eating - val stopEatingPacket = ClientboundEntityEventPacket(player.serverPlayer, 9) - player.send(stopEatingPacket) + private fun handleEating(player: Player, eater: Eater) { + if (consumeTime < 20) { + // if the consumeTime is smaller than 20 ticks, the initial wait of 7 ticks will be skipped + playEatSound(player) + } else { + val eatTimePassed = serverTick - eater.startTime + if ((type == FoodType.FAST || eatTimePassed > 7) && eatTimePassed % 4 == 0) + playEatSound(player) + } + } - // food level / saturation / health - player.foodLevel = min(player.foodLevel + options.nutrition, 20) - player.saturation = min(player.saturation + options.nutrition * options.saturationModifier * 2.0f, player.foodLevel.toFloat()) - player.health = min(player.health + options.instantHealth, player.genericMaxHealth) + private fun playEatSound(player: Player) { + player.location.playSoundNearby(Sound.ENTITY_GENERIC_EAT, SoundCategory.PLAYERS, 1f, Random.nextDouble(0.8, 1.2).toFloat(), player) + } - // effects - options.effects?.forEach { player.addPotionEffect(it) } + private fun beginEating(player: Player, itemStack: ItemStack, hand: EquipmentSlot) { + // add to eaters map + eaters[player] = Eater(itemStack, hand, serverTick) + + // send packet to begin eating particles for players nearby + val packet = createEatingAnimationPacket(player.entityId, true, hand) + player.location.getPlayersNearby(PACKET_DISTANCE, player).forEach { it.send(packet) } + } - // sounds - player.location.playSoundNearby(Sound.ENTITY_PLAYER_BURP, SoundCategory.PLAYERS, 0.5f, Random.nextDouble(0.9, 1.0).toFloat()) - player.location.playSoundNearby(Sound.ENTITY_GENERIC_EAT, SoundCategory.PLAYERS, 1.0f, Random.nextDouble(0.6, 1.4).toFloat()) + private fun stopEatingAnimation(player: Player, eater: Eater) { + // send packet to stop eating animation to other players + val stopEatingAnimationPacket = createEatingAnimationPacket(player.entityId, false, eater.hand) + player.location.getPlayersNearby(PACKET_DISTANCE, player).forEach { it.send(stopEatingAnimationPacket) } + } + + private fun finishEating(player: Player, eater: Eater) { + if (!player.isOnline) + return + + // stop eating animation for other players + stopEatingAnimation(player, eater) + + // send packet to stop eating + val stopEatingPacket = ClientboundEntityEventPacket(player.serverPlayer, 9) + player.send(stopEatingPacket) + + // food level / saturation / health + player.foodLevel = min(player.foodLevel + nutrition, 20) + player.saturation = min(player.saturation + nutrition * saturationModifier * 2.0f, player.foodLevel.toFloat()) + player.health = min(player.health + instantHealth, player.genericMaxHealth) + + // effects + effects?.forEach { player.addPotionEffect(it) } + + // sounds + player.location.playSoundNearby(Sound.ENTITY_PLAYER_BURP, SoundCategory.PLAYERS, 0.5f, Random.nextDouble(0.9, 1.0).toFloat()) + player.location.playSoundNearby(Sound.ENTITY_GENERIC_EAT, SoundCategory.PLAYERS, 1.0f, Random.nextDouble(0.6, 1.4).toFloat()) + + // take item + if (player.gameMode != GameMode.CREATIVE) + eater.itemStack.amount -= 1 + } + + private fun createEatingAnimationPacket(entityId: Int, active: Boolean, hand: EquipmentSlot): FriendlyByteBuf { + val isOffHand = hand == EquipmentSlot.OFF_HAND + + val buf = FriendlyByteBuf(Unpooled.buffer()) + buf.writeVarInt(PacketIdRegistry.CLIENTBOUND_SET_ENTITY_DATA_PACKET) + buf.writeVarInt(entityId) // entity id + buf.writeByte(8) // type for eating animation + buf.writeVarInt(0) // following is byte + buf.writeByte(active.intValue or (isOffHand.intValue shl 1)) + buf.writeByte(0xff) // no more data + + return buf + } - // take item - if (player.gameMode != GameMode.CREATIVE) - eater.itemStack.amount -= 1 - } - - private fun createEatingAnimationPacket(entityId: Int, active: Boolean, hand: EquipmentSlot): FriendlyByteBuf { - val isOffHand = hand == EquipmentSlot.OFF_HAND - - val buf = FriendlyByteBuf(Unpooled.buffer()) - buf.writeVarInt(PacketIdRegistry.CLIENTBOUND_SET_ENTITY_DATA_PACKET) - buf.writeVarInt(entityId) // entity id - buf.writeByte(8) // type for eating animation - buf.writeVarInt(0) // following is byte - buf.writeByte(active.intValue or (isOffHand.intValue shl 1)) - buf.writeByte(0xff) // no more data - - return buf } - companion object : ItemBehaviorFactory() { - override fun create(item: NovaItem): Consumable = - Consumable(FoodOptions.configurable(item)) + companion object : ItemBehaviorFactory { + + override fun create(item: NovaItem): Default { + val cfg = ConfigAccess(item) + return Default( + cfg.getOptionalEntry("food_type") + .map { FoodType.valueOf(it.uppercase()) } + .orElse(FoodType.NORMAL), + cfg.getEntry("consume_time"), + cfg.getEntry("nutrition"), + cfg.getEntry("saturation_modifier"), + cfg.getOptionalEntry("instant_health").orElse(0.0), + cfg.getOptionalEntry>("effects") + ) + } + } } \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Damageable.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Damageable.kt index 2da13d5240..33875106a6 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Damageable.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Damageable.kt @@ -1,106 +1,517 @@ +@file:Suppress("UNCHECKED_CAST") + package xyz.xenondevs.nova.item.behavior -import net.kyori.adventure.text.Component +import net.minecraft.advancements.CriteriaTriggers +import net.minecraft.server.level.ServerPlayer +import net.minecraft.stats.Stats +import net.minecraft.world.item.enchantment.EnchantmentHelper +import net.minecraft.world.item.enchantment.Enchantments +import org.bukkit.GameMode +import org.bukkit.Statistic +import org.bukkit.craftbukkit.v1_20_R1.event.CraftEventFactory +import org.bukkit.craftbukkit.v1_20_R1.util.CraftMagicNumbers +import org.bukkit.enchantments.Enchantment +import org.bukkit.event.player.PlayerItemBreakEvent +import org.bukkit.event.player.PlayerItemDamageEvent import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.RecipeChoice +import xyz.xenondevs.commons.provider.Provider +import xyz.xenondevs.commons.provider.immutable.map +import xyz.xenondevs.commons.provider.immutable.orElse +import xyz.xenondevs.commons.provider.immutable.provider +import xyz.xenondevs.nova.data.config.ConfigAccess import xyz.xenondevs.nova.data.serialization.cbf.NamespacedCompound +import xyz.xenondevs.nova.data.serialization.json.serializer.RecipeDeserializer import xyz.xenondevs.nova.item.NovaItem import xyz.xenondevs.nova.item.logic.PacketItemData -import xyz.xenondevs.nova.item.options.DamageableOptions import xyz.xenondevs.nova.item.vanilla.VanillaMaterialProperty +import xyz.xenondevs.nova.util.bukkitMirror +import xyz.xenondevs.nova.util.callEvent +import xyz.xenondevs.nova.util.item.isEmpty import xyz.xenondevs.nova.util.item.novaCompound -import kotlin.math.min +import xyz.xenondevs.nova.util.item.novaCompoundOrNull +import xyz.xenondevs.nova.util.item.novaItem +import xyz.xenondevs.nova.util.nmsCopy +import xyz.xenondevs.nova.util.serverPlayer +import kotlin.random.Random +import net.minecraft.world.entity.LivingEntity as MojangLivingEntity +import net.minecraft.world.entity.player.Player as MojangPlayer import net.minecraft.world.item.ItemStack as MojangStack +import org.bukkit.entity.LivingEntity as BukkitLivingEntity +import org.bukkit.entity.Player as BukkitPlayer +import org.bukkit.inventory.ItemStack as BukkitStack +import org.bukkit.inventory.meta.Damageable as BukkitDamageable + +fun Damageable( + maxDurability: Int, + itemDamageOnAttackEntity: Int, + itemDamageOnBreakBlock: Int, + repairIngredient: RecipeChoice? = null +) = Damageable.Default( + provider(maxDurability), + provider(itemDamageOnAttackEntity), + provider(itemDamageOnBreakBlock), + provider(repairIngredient), + true +) -class Damageable(val options: DamageableOptions) : ItemBehavior() { +/** + * Allows items to store and receive damage. + */ +interface Damageable { - @Deprecated("Replaced by DamageableOptions", ReplaceWith("options.durability")) - val durability: Int by options.durabilityProvider + val maxDurability: Int + val itemDamageOnAttackEntity: Int + val itemDamageOnBreakBlock: Int + val repairIngredient: RecipeChoice? - // - fun getDamage(itemStack: ItemStack): Int { - return getDamage(itemStack.novaCompound) - } + /** + * Returns the current damage of the [itemStack]. + */ + fun getDamage(itemStack: BukkitStack): Int - fun setDamage(itemStack: ItemStack, damage: Int) { - setDamage(itemStack.novaCompound, damage) - } + /** + * Sets the damage of the [itemStack] to [damage]. + */ + fun setDamage(itemStack: BukkitStack, damage: Int) - fun addDamage(itemStack: ItemStack, damage: Int) { - addDamage(itemStack.novaCompound, damage) - } - - fun getDurability(itemStack: ItemStack): Int { - return getDurability(itemStack.novaCompound) - } + /** + * Returns the current durability of the [itemStack]. + */ + fun getDurability(itemStack: BukkitStack): Int = maxDurability - getDamage(itemStack) - fun setDurability(itemStack: ItemStack, durability: Int) { - return setDurability(itemStack.novaCompound, durability) - } - // + /** + * Sets the durability of the [itemStack] to [durability]. + */ + fun setDurability(itemStack: BukkitStack, durability: Int) = setDamage(itemStack, maxDurability - durability) - // - fun getDamage(itemStack: MojangStack): Int { - return getDamage(itemStack.novaCompound) - } + /** + * Adds [damage] to the current damage of the [itemStack] and returns whether the item broke. + */ + fun damageAndBreak(itemStack: BukkitStack, damage: Int): Boolean - fun setDamage(itemStack: MojangStack, damage: Int) { - setDamage(itemStack.novaCompound, damage) - } + /** + * Returns the current damage of the [itemStack]. + */ + fun getDamage(itemStack: MojangStack): Int - fun addDamage(itemStack: MojangStack, damage: Int) { - addDamage(itemStack.novaCompound, damage) - } + /** + * Sets the damage of the [itemStack] to [damage]. + */ + fun setDamage(itemStack: MojangStack, damage: Int) - fun getDurability(itemStack: MojangStack): Int { - return getDurability(itemStack.novaCompound) - } + /** + * Returns the current durability of the [itemStack]. + */ + fun getDurability(itemStack: MojangStack): Int = maxDurability - getDamage(itemStack) - fun setDurability(itemStack: MojangStack, durability: Int) { - return setDurability(itemStack.novaCompound, durability) - } - // + /** + * Sets the durability of the [itemStack] to [durability]. + */ + fun setDurability(itemStack: MojangStack, durability: Int) = setDamage(itemStack, maxDurability - durability) - // - fun getDamage(data: NamespacedCompound): Int { - return min(options.durability, data["nova", "damage"] ?: 0) - } + /** + * Adds [damage] to the current damage of the [itemStack] and returns whether the item broke. + */ + fun damageAndBreak(itemStack: MojangStack, damage: Int): Boolean - fun setDamage(data: NamespacedCompound, damage: Int) { - val coercedDamage = damage.coerceIn(0..options.durability) - data["nova", "damage"] = coercedDamage - } - - fun addDamage(data: NamespacedCompound, damage: Int) { - setDamage(data, getDamage(data) + damage) - } - - fun getDurability(data: NamespacedCompound): Int { - return options.durability - getDamage(data) - } - - fun setDurability(data: NamespacedCompound, durability: Int) { - setDamage(data, options.durability - durability) - } - // - - override fun updatePacketItemData(data: NamespacedCompound, itemData: PacketItemData) { - val damage = getDamage(data) - val durability = options.durability - damage + class Default( + maxDurability: Provider, + damageOnAttackEntity: Provider, + damageOnBreakBlock: Provider, + repairIngredient: Provider, + private val affectsItemDurability: Boolean + ) : ItemBehavior, Damageable { - itemData.durabilityBar = durability / options.durability.toDouble() + override val maxDurability by maxDurability + override val itemDamageOnAttackEntity by damageOnAttackEntity + override val itemDamageOnBreakBlock by damageOnBreakBlock + override val repairIngredient by repairIngredient + + override fun getDamage(itemStack: BukkitStack) = getDamage(itemStack.novaCompoundOrNull) + override fun setDamage(itemStack: BukkitStack, damage: Int) = setDamage(itemStack.novaCompound, damage) + override fun getDamage(itemStack: MojangStack) = getDamage(itemStack.novaCompoundOrNull) + override fun setDamage(itemStack: MojangStack, damage: Int) = setDamage(itemStack.novaCompound, damage) + + override fun damageAndBreak(itemStack: BukkitStack, damage: Int): Boolean { + val compound = itemStack.novaCompound + val newDamage = getDamage(compound) + damage + setDamage(compound, newDamage) + return newDamage > maxDurability + } + + override fun damageAndBreak(itemStack: MojangStack, damage: Int): Boolean { + val compound = itemStack.novaCompound + val newDamage = getDamage(compound) + damage + setDamage(compound, newDamage) + return newDamage >= maxDurability + } + + private fun getDamage(compound: NamespacedCompound?): Int { + return compound?.get("nova", "damage")?.coerceIn(0..maxDurability) ?: 0 + } + + private fun setDamage(compound: NamespacedCompound, damage: Int) { + compound["nova", "damage"] = damage + } + + private fun addDamage(compound: NamespacedCompound, damage: Int) { + setDamage(compound, getDamage(compound) + damage) + } + + override fun getVanillaMaterialProperties(): List { + return if (affectsItemDurability) + listOf(VanillaMaterialProperty.DAMAGEABLE) + else emptyList() + } + + override fun getDefaultCompound(): NamespacedCompound { + val compound = NamespacedCompound() + compound["nova", "damage"] = 0 + return compound + } + + override fun updatePacketItemData(data: NamespacedCompound, itemData: PacketItemData) { + if (affectsItemDurability) + itemData.durabilityBar = (maxDurability - getDamage(data)) / maxDurability.toDouble() + } - itemData.addAdvancedTooltipsLore( - Component.translatable("item.durability", Component.text(durability), Component.text(options.durability)) - ) - } - - override fun getVanillaMaterialProperties(): List { - return listOf(VanillaMaterialProperty.DAMAGEABLE) } - companion object : ItemBehaviorFactory() { - override fun create(item: NovaItem) = - Damageable(DamageableOptions.configurable(item)) + companion object : ItemBehaviorFactory { + + override fun create(item: NovaItem): Default { + val cfg = ConfigAccess(item) + return Default( + cfg.getEntry("durability", "max_durability"), + cfg.getOptionalEntry("item_damage_on_attack_entity").orElse(0), + cfg.getOptionalEntry("item_damage_on_break_block").orElse(0), + cfg.getOptionalEntry("repair_ingredient").map { + val list = when (it) { + is String -> listOf(it) + else -> it as List + } + RecipeDeserializer.parseRecipeChoice(list) + }, + true + ) + } + + // -- Bukkit ItemStack -- + + /** + * Damages the given [itemStack] while respecting the unbreaking enchantment. + * + * @param itemStack The [ItemStack] to damage + * @param damage The amount of damage to add + * @param breakCallback A callback that is called if the item breaks + */ + fun damageAndBreak(itemStack: BukkitStack, damage: Int, breakCallback: () -> Unit) = + damageAndBreak(itemStack, damage, null) { breakCallback() } + + /** + * Damages the given [itemStack] while respecting the unbreaking enchantment, calling events and criteria triggers and incrementing stats. + * + * @param itemStack The [ItemStack] to damage + * @param damage The amount of damage to add + * @param damager The entity that damaged the item, used for events, criteria triggers and stats + * @param breakCallback A callback that is called if the item breaks + */ + fun damageAndBreak(itemStack: BukkitStack, damage: Int, damager: T, breakCallback: (T) -> Unit) { + // check for creative mode + if (damager is BukkitPlayer && damager.gameMode == GameMode.CREATIVE) + return + // check if item is empty + if (itemStack.isEmpty()) + return + + val item = itemStack.type + val novaItem = itemStack.novaItem + val damageable = novaItem?.getBehaviorOrNull() + + // check if the item is damageable + if (novaItem != null && damageable == null || item.maxDurability <= 0 || itemStack.itemMeta?.isUnbreakable == true) + return + + // build damage based on enchantments and events + var actualDamage = damage + val unbreakingLevel = itemStack.getEnchantmentLevel(Enchantment.DURABILITY) + if (unbreakingLevel > 0) + actualDamage = calculateActualDamage(actualDamage, unbreakingLevel, Wearable.isWearable(itemStack)) + + if (damager is BukkitPlayer) { + val event = PlayerItemDamageEvent(damager, itemStack, actualDamage).also(::callEvent) // TODO: On Paper: Include original damage + + if (actualDamage != event.damage || event.isCancelled) + damager.updateInventory() + if (event.isCancelled) + return + } // TODO: On Paper: Call EntityDamageItemEvent + + if (actualDamage <= 0) + return + + // damage item + val broken: Boolean + if (damageable != null) { + broken = damageable.damageAndBreak(itemStack, actualDamage) + } else { + val itemMeta = itemStack.itemMeta as BukkitDamageable + val damageValue = itemMeta.damage + actualDamage + broken = damageValue >= item.maxDurability + if (damager is BukkitPlayer) { + CriteriaTriggers.ITEM_DURABILITY_CHANGED.trigger(damager.serverPlayer, itemStack.nmsCopy, damageValue) + if (broken) damager.incrementStatistic(Statistic.BREAK_ITEM, item) + } + itemMeta.damage = damageValue + itemStack.itemMeta = itemMeta + } + + if (broken) { + breakCallback(damager) + + if (itemStack.amount == 1 && damager is BukkitPlayer) + callEvent(PlayerItemBreakEvent(damager, itemStack)) + + itemStack.amount = itemStack.amount - 1 + + // reset damage value + if (damageable != null) { + damageable.setDamage(itemStack, 0) + } else { + val itemMeta = itemStack.itemMeta as BukkitDamageable + itemMeta.damage = 0 + itemStack.itemMeta = itemMeta + } + } + } + + /** + * Checks whether this [itemStack] is of a damageable type and does not have the unbreakable tag. + */ + fun isDamageable(itemStack: BukkitStack): Boolean { + val novaItem = itemStack.novaItem + if (novaItem != null) + return novaItem.hasBehavior() && itemStack.itemMeta?.isUnbreakable != true + + return itemStack.type.maxDurability > 0 && itemStack.itemMeta?.isUnbreakable != true + } + + /** + * Gets the maximum durability of this [itemStack] or 0 if it is not of a damageable type. + * + * Note that a maximum durability larger than 0 does not necessarily mean that the item is damageable, + * as the unbreakable tag is not checked. + */ + fun getMaxDurability(itemStack: BukkitStack): Int { + val novaItem = itemStack.novaItem + if (novaItem != null) + return novaItem.getBehaviorOrNull()?.maxDurability ?: 0 + + return itemStack.type.maxDurability.toInt() + } + + /** + * Gets the current damage of this [itemStack] or 0 if it is not of a damageable type. + */ + fun getDamage(itemStack: BukkitStack): Int { + val novaItem = itemStack.novaItem + if (novaItem != null) + return novaItem.getBehaviorOrNull()?.getDamage(itemStack) ?: 0 + + return (itemStack.itemMeta as? BukkitDamageable)?.damage ?: 0 + } + + /** + * Sets the current damage of this [itemStack] if it is of a damageable type. + */ + fun setDamage(itemStack: BukkitStack, damage: Int) { + val novaItem = itemStack.novaItem + if (novaItem != null) { + novaItem.getBehaviorOrNull()?.setDamage(itemStack, damage) + } else { + val damageable = itemStack.itemMeta as? BukkitDamageable ?: return + damageable.damage = damage + itemStack.itemMeta = damageable + } + } + + /** + * Checks whether [repairItem] is a valid repair ingredient for [item]. + */ + fun isValidRepairItem(item: BukkitStack, repairItem: BukkitStack): Boolean { + val novaItem = item.novaItem + if (novaItem != null) + return novaItem.getBehaviorOrNull()?.repairIngredient?.test(repairItem) ?: false + + return CraftMagicNumbers.getItem(item.type).isValidRepairItem(item.nmsCopy, repairItem.nmsCopy) + } + + // -- Mojang ItemStack -- + + /** + * Damages the given [itemStack] while respecting the unbreaking enchantment. + * + * @param itemStack The [ItemStack] to damage + * @param damage The amount of damage to add + * @param breakCallback A callback that is called if the item breaks + */ + fun damageAndBreak(itemStack: MojangStack, damage: Int, breakCallback: () -> Unit) = + damageAndBreak(itemStack, damage, null) { breakCallback() } + + /** + * Damages the given [itemStack] while respecting the unbreaking enchantment, calling events and criteria triggers and incrementing stats. + * + * @param itemStack The [ItemStack] to damage + * @param damage The amount of damage to add + * @param damager The entity that damaged the item, used for events, criteria triggers and stats + * @param breakCallback A callback that is called if the item breaks + */ + fun damageAndBreak(itemStack: MojangStack, damage: Int, damager: T, breakCallback: (T) -> Unit) { + // check for creative mode + if (damager is MojangPlayer && damager.abilities.instabuild) + return + + // check if item is empty + if (itemStack.isEmpty) + return + + val item = itemStack.item + val novaItem = itemStack.novaItem + val damageable = novaItem?.getBehaviorOrNull() + + // check if the item is damageable + if (novaItem != null && damageable == null || item.maxDamage <= 0 || itemStack.tag?.getBoolean("Unbreakable") == true) + return + + // build actual damage value based on enchantments and events + var actualDamage = damage + val unbreakingLevel = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.UNBREAKING, itemStack) + if (unbreakingLevel > 0) + actualDamage = calculateActualDamage(actualDamage, unbreakingLevel, Wearable.isWearable(itemStack)) + + if (damager is ServerPlayer) { + val event = PlayerItemDamageEvent(damager.bukkitEntity, itemStack.bukkitMirror, actualDamage).also(::callEvent) // TODO: On Paper: Include original damage + + if (actualDamage != event.damage || event.isCancelled) + event.player.updateInventory() + if (event.isCancelled) + return + + actualDamage = event.damage + } // TODO: On Paper: Call EntityDamageItemEvent + + if (actualDamage <= 0) + return + + // damage item + val broken: Boolean + if (damageable != null) { + broken = damageable.damageAndBreak(itemStack, actualDamage) + } else { + val damageValue = itemStack.damageValue + actualDamage + broken = damageValue >= itemStack.maxDamage + if (damager is ServerPlayer) { + CriteriaTriggers.ITEM_DURABILITY_CHANGED.trigger(damager, itemStack, damageValue) + if (broken) damager.awardStat(Stats.ITEM_BROKEN.get(item)) + } + itemStack.damageValue = damageValue + } + + if (broken) { + breakCallback(damager) + + if (itemStack.count == 1 && damager is MojangPlayer) + CraftEventFactory.callPlayerItemBreakEvent(damager, itemStack) + + itemStack.shrink(1) + + // reset damage value + if (damageable != null) { + damageable.setDamage(itemStack, 0) + } else { + itemStack.damageValue = 0 + } + } + } + + /** + * Checks whether this [itemStack] is of a damageable type and does not have the unbreakable tag. + */ + fun isDamageable(itemStack: MojangStack): Boolean { + val novaItem = itemStack.novaItem + if (novaItem != null) + return novaItem.hasBehavior() && itemStack.tag?.getBoolean("Unbreakable") != true + + return itemStack.item.canBeDepleted() + } + + /** + * Gets the maximum durability of this [itemStack] or 0 if it is not of a damageable type. + * + * Note that a maximum durability larger than 0 does not necessarily mean that the item is damageable, + * as the unbreakable tag is not checked. + */ + fun getMaxDurability(itemStack: MojangStack): Int { + val novaItem = itemStack.novaItem + if (novaItem != null) + return novaItem.getBehaviorOrNull()?.maxDurability ?: 0 + + return itemStack.maxDamage + } + + /** + * Gets the current damage of this [itemStack] or 0 if it is not of a damageable type. + */ + fun getDamage(itemStack: MojangStack): Int { + val novaItem = itemStack.novaItem + if (novaItem != null) + return novaItem.getBehaviorOrNull()?.getDamage(itemStack) ?: 0 + + return itemStack.damageValue + } + + /** + * Sets the current damage of this [itemStack] if it is of a damageable type. + */ + fun setDamage(itemStack: MojangStack, damage: Int) { + val novaItem = itemStack.novaItem + if (novaItem != null) { + novaItem.getBehaviorOrNull()?.setDamage(itemStack, damage) + } else { + itemStack.damageValue = damage + } + } + + /** + * Checks whether [repairItem] is a valid repair ingredient for [item]. + */ + fun isValidRepairItem(item: MojangStack, repairItem: MojangStack): Boolean { + val novaItem = item.novaItem + if (novaItem != null) + return novaItem.getBehaviorOrNull()?.repairIngredient?.test(repairItem.bukkitMirror) ?: false + + return item.item.isValidRepairItem(item, repairItem) + } + + // -- Misc -- + + private fun calculateActualDamage(damage: Int, unbreakingLevel: Int, isArmor: Boolean): Int { + var actualDamage = 0 + repeat(damage) { + if (isArmor) { + if (Random.nextFloat() > 0.6f) + actualDamage++ + } else { + if (Random.nextInt(unbreakingLevel + 1) == 0) + actualDamage++ + } + } + + return actualDamage + } + } } \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Enchantable.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Enchantable.kt index b0495988d7..9763ae0c2a 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Enchantable.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Enchantable.kt @@ -3,35 +3,185 @@ package xyz.xenondevs.nova.item.behavior import net.minecraft.nbt.CompoundTag import net.minecraft.nbt.ListTag import net.minecraft.nbt.StringTag +import org.bukkit.inventory.meta.EnchantmentStorageMeta import xyz.xenondevs.commons.collections.isNotNullOrEmpty +import xyz.xenondevs.commons.provider.Provider +import xyz.xenondevs.commons.provider.immutable.map +import xyz.xenondevs.commons.provider.immutable.provider +import xyz.xenondevs.nova.data.config.ConfigAccess import xyz.xenondevs.nova.data.serialization.cbf.NamespacedCompound import xyz.xenondevs.nova.item.NovaItem import xyz.xenondevs.nova.item.enchantment.Enchantment +import xyz.xenondevs.nova.item.enchantment.EnchantmentCategory import xyz.xenondevs.nova.item.enchantment.NovaEnchantment import xyz.xenondevs.nova.item.enchantment.VanillaEnchantment -import xyz.xenondevs.nova.item.options.EnchantableOptions import xyz.xenondevs.nova.registry.NovaRegistries import xyz.xenondevs.nova.util.data.getOrNull import xyz.xenondevs.nova.util.data.getOrPut import xyz.xenondevs.nova.util.get +import xyz.xenondevs.nova.util.getOrThrow import xyz.xenondevs.nova.util.item.novaCompound import xyz.xenondevs.nova.util.item.novaCompoundOrNull import xyz.xenondevs.nova.util.item.novaItem +import xyz.xenondevs.nova.util.nmsCopy import net.minecraft.world.item.ItemStack as MojangStack +import org.bukkit.enchantments.Enchantment as BukkitEnchantment +import org.bukkit.inventory.ItemStack as BukkitStack private const val ENCHANTMENTS_CBF = "enchantments" private const val ENCHANTMENTS_NBT = "Enchantments" private const val STORED_ENCHANTMENTS_CBF = "stored_enchantments" private const val STORED_ENCHANTMENTS_NBT = "StoredEnchantments" -class Enchantable(val options: EnchantableOptions) : ItemBehavior() { +fun Enchantable( + enchantmentValue: Int, + enchantmentCategories: List +) = Enchantable.Default(provider(enchantmentValue), provider(enchantmentCategories)) + +/** + * Allows items to be enchanted. + */ +interface Enchantable { + + /** + * The enchantment value of this item. + * Items with a higher enchantment value have a higher chance of getting more secondary enchantments + * when enchanted in the enchanting table. + * + * As an example, these are the vanilla enchantment values depending on the material: + * + * * Wood: 15 + * * Stone: 5 + * * Iron: 14 + * * Diamond: 10 + * * Gold: 22 + * * Netherite: 15 + */ + val enchantmentValue: Int + + /** + * A list of enchantment categories that can be applied to this item. + */ + val enchantmentCategories: List + + class Default( + enchantmentValue: Provider, + enchantmentCategories: Provider>, + ) : ItemBehavior, Enchantable { + + override val enchantmentValue by enchantmentValue + override val enchantmentCategories by enchantmentCategories + + } - companion object : ItemBehaviorFactory() { + companion object : ItemBehaviorFactory { + + override fun create(item: NovaItem): Default { + val cfg = ConfigAccess(item) + return Default( + cfg.getEntry("enchantment_value"), + cfg.getEntry>("enchantment_categories") + .map { list -> list.map { NovaRegistries.ENCHANTMENT_CATEGORY.getOrThrow(it) } } + ) + } + + // -- Bukkit ItemStack -- + + fun isEnchantable(itemStack: BukkitStack): Boolean = + isEnchantable(itemStack.nmsCopy) + + fun isEnchanted(itemStack: BukkitStack): Boolean = + isEnchanted(itemStack.nmsCopy) + + fun hasStoredEnchantments(itemStack: BukkitStack): Boolean = + hasStoredEnchantments(itemStack.nmsCopy) + + fun getEnchantments(itemStack: BukkitStack): Map = + getEnchantments(itemStack.nmsCopy) + + fun getStoredEnchantments(itemStack: BukkitStack): Map = + getStoredEnchantments(itemStack.nmsCopy) + + fun setEnchantments(itemStack: BukkitStack, enchantments: Map) { + // clear vanilla enchants + for ((enchantment, _) in itemStack.enchantments) + itemStack.removeEnchantment(enchantment) + + setEnchantments(ENCHANTMENTS_CBF, itemStack::addUnsafeEnchantment, itemStack, enchantments) + } + + fun setStoredEnchantments(itemStack: BukkitStack, enchantments: Map) { + val meta = itemStack.itemMeta as? EnchantmentStorageMeta ?: return + // clear vanilla enchants + for ((enchantment, _) in meta.storedEnchants) + meta.removeStoredEnchant(enchantment) + + setEnchantments(STORED_ENCHANTMENTS_CBF, { ench, lvl -> meta.addStoredEnchant(ench, lvl, true) }, itemStack, enchantments) + } + + private fun setEnchantments(cbfName: String, addVanillaEnchantment: (BukkitEnchantment, Int) -> Unit, itemStack: BukkitStack, enchantments: Map) { + val novaEnchantmentsMap = HashMap() + for ((enchantment, level) in enchantments) { + if (enchantment is VanillaEnchantment) { + addVanillaEnchantment(Enchantment.asBukkitEnchantment(enchantment), level) + } else { + novaEnchantmentsMap[enchantment] = level + } + } + + if (novaEnchantmentsMap.isNotEmpty()) { + itemStack.novaCompound["nova", cbfName] = novaEnchantmentsMap + } else { + itemStack.novaCompoundOrNull?.remove("nova", cbfName) + } + } + + fun addEnchantment(itemStack: BukkitStack, enchantment: Enchantment, level: Int) = + addEnchantment(ENCHANTMENTS_CBF, itemStack::addUnsafeEnchantment, itemStack, enchantment, level) + + fun addStoredEnchantment(itemStack: BukkitStack, enchantment: Enchantment, level: Int) { + val meta = itemStack.itemMeta as? EnchantmentStorageMeta ?: return + addEnchantment(STORED_ENCHANTMENTS_CBF, { ench, lvl -> meta.addStoredEnchant(ench, lvl, true) }, itemStack, enchantment, level) + } + + private fun addEnchantment(cbfName: String, addVanillaEnchantment: (BukkitEnchantment, Int) -> Unit, itemStack: BukkitStack, enchantment: Enchantment, level: Int) { + if (enchantment is VanillaEnchantment) { + addVanillaEnchantment(Enchantment.asBukkitEnchantment(enchantment), level) + } else { + itemStack.novaCompound.getEnchantments(cbfName)[enchantment] = level + } + } + + fun removeEnchantment(itemStack: BukkitStack, enchantment: Enchantment) = + removeEnchantment(ENCHANTMENTS_CBF, itemStack::removeEnchantment, itemStack, enchantment) + + fun removeStoredEnchantment(itemStack: BukkitStack, enchantment: Enchantment) { + val meta = itemStack.itemMeta as? EnchantmentStorageMeta ?: return + removeEnchantment(STORED_ENCHANTMENTS_CBF, meta::removeStoredEnchant, itemStack, enchantment) + } + + private fun removeEnchantment(cbfName: String, removeVanillaEnchantment: (BukkitEnchantment) -> Unit, itemStack: BukkitStack, enchantment: Enchantment) { + if (enchantment is VanillaEnchantment) { + removeVanillaEnchantment(Enchantment.asBukkitEnchantment(enchantment)) + } else { + itemStack.novaCompoundOrNull?.getEnchantmentsOrNull(cbfName)?.remove(enchantment) + } + } + + fun removeAllEnchantments(itemStack: BukkitStack) { + for ((enchantment, _) in itemStack.enchantments) + itemStack.removeEnchantment(enchantment) + itemStack.novaCompoundOrNull?.remove("nova", ENCHANTMENTS_CBF) + } - override fun create(item: NovaItem) = - Enchantable(EnchantableOptions.configurable(item)) + fun removeAllStoredEnchantments(itemStack: BukkitStack) { + val meta = itemStack.itemMeta as? EnchantmentStorageMeta ?: return + for ((enchantment, _) in meta.enchants) + meta.removeStoredEnchant(enchantment) + itemStack.novaCompoundOrNull?.remove("nova", STORED_ENCHANTMENTS_CBF) + } - // TODO: Bukkit methods + // -- Mojang ItemStack -- @JvmStatic fun isEnchantable(itemStack: MojangStack): Boolean { @@ -53,13 +203,13 @@ class Enchantable(val options: EnchantableOptions) : ItemBehavior() { return itemStack.tag?.getOrNull(nbtName).isNotNullOrEmpty() } - fun getEnchantments(itemStack: MojangStack): MutableMap = + fun getEnchantments(itemStack: MojangStack): Map = getEnchantments(ENCHANTMENTS_CBF, ENCHANTMENTS_NBT, itemStack) - fun getStoredEnchantments(itemStack: MojangStack): MutableMap = + fun getStoredEnchantments(itemStack: MojangStack): Map = getEnchantments(STORED_ENCHANTMENTS_CBF, STORED_ENCHANTMENTS_NBT, itemStack) - private fun getEnchantments(cbfName: String, nbtName: String, itemStack: MojangStack): MutableMap { + private fun getEnchantments(cbfName: String, nbtName: String, itemStack: MojangStack): Map { val enchantments = HashMap() itemStack.tag?.getOrNull(nbtName)?.asSequence() ?.filterIsInstance() @@ -73,13 +223,13 @@ class Enchantable(val options: EnchantableOptions) : ItemBehavior() { return enchantments } - fun setEnchantments(itemStack: MojangStack, enchantments: MutableMap) = + fun setEnchantments(itemStack: MojangStack, enchantments: Map) = setEnchantments(ENCHANTMENTS_CBF, ENCHANTMENTS_NBT, itemStack, enchantments) - fun setStoredEnchantments(itemStack: MojangStack, enchantments: MutableMap) = + fun setStoredEnchantments(itemStack: MojangStack, enchantments: Map) = setEnchantments(STORED_ENCHANTMENTS_CBF, STORED_ENCHANTMENTS_NBT, itemStack, enchantments) - private fun setEnchantments(cbfName: String, nbtName: String, itemStack: MojangStack, enchantments: MutableMap) { + private fun setEnchantments(cbfName: String, nbtName: String, itemStack: MojangStack, enchantments: Map) { val vanillaEnchantmentsTag = ListTag() val novaEnchantmentsMap = HashMap() @@ -95,11 +245,17 @@ class Enchantable(val options: EnchantableOptions) : ItemBehavior() { } } - if (vanillaEnchantmentsTag.isNotEmpty()) + if (vanillaEnchantmentsTag.isNotEmpty()) { itemStack.orCreateTag.put(nbtName, vanillaEnchantmentsTag) + } else { + itemStack.tag?.remove(nbtName) + } - if (novaEnchantmentsMap.isNotEmpty()) + if (novaEnchantmentsMap.isNotEmpty()) { itemStack.novaCompound["nova", cbfName] = novaEnchantmentsMap + } else { + itemStack.novaCompoundOrNull?.remove("nova", cbfName) + } } fun addEnchantment(itemStack: MojangStack, enchantment: Enchantment, level: Int) = @@ -149,18 +305,14 @@ class Enchantable(val options: EnchantableOptions) : ItemBehavior() { itemStack.novaCompoundOrNull?.remove("nova", cbfName) } + // -- Misc -- + private fun NamespacedCompound.getEnchantmentsOrNull(name: String): MutableMap? = get("nova", name) private fun NamespacedCompound.getEnchantments(name: String): MutableMap = getOrPut("nova", name, ::HashMap) - private fun CompoundTag.getEnchantmentsOrNull(name: String): ListTag? = - getOrNull(name) - - private fun CompoundTag.getEnchantments(name: String): ListTag = - getOrPut(name, ::ListTag) - } } \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Extinguishing.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Extinguishing.kt index 9ff50da008..c2fa94eeb6 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Extinguishing.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Extinguishing.kt @@ -12,7 +12,7 @@ import org.bukkit.event.block.Action import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.inventory.ItemStack import xyz.xenondevs.nmsutils.particle.particle -import xyz.xenondevs.nova.util.item.damageItemInHand +import xyz.xenondevs.nova.util.damageItemInHand import xyz.xenondevs.nova.util.nmsState import xyz.xenondevs.nova.util.runTaskLater import xyz.xenondevs.nova.util.sendTo @@ -22,9 +22,12 @@ import xyz.xenondevs.nova.util.swingHand import xyz.xenondevs.nova.world.pos import kotlin.random.Random -private const val EXTINGuiSH_CAMPFIRE_LEVEL_EVENT = 1009 +private const val EXTINGUISH_CAMPFIRE_LEVEL_EVENT = 1009 -object Extinguishing : ItemBehavior() { +/** + * Allows items to extinguish campfires. + */ +object Extinguishing : ItemBehavior { override fun handleInteract(player: Player, itemStack: ItemStack, action: Action, event: PlayerInteractEvent) { if (action == Action.RIGHT_CLICK_BLOCK) { @@ -45,7 +48,7 @@ object Extinguishing : ItemBehavior() { val newState = state.setValue(CampfireBlock.LIT, false) level.setBlock(pos, newState, 11) - level.levelEvent(null, EXTINGuiSH_CAMPFIRE_LEVEL_EVENT, pos, 0) + level.levelEvent(null, EXTINGUISH_CAMPFIRE_LEVEL_EVENT, pos, 0) level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(serverPlayer, newState)) val hand = event.hand!! diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/FireResistant.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/FireResistant.kt index 91266e93ec..291541a3b1 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/FireResistant.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/FireResistant.kt @@ -1,16 +1,14 @@ package xyz.xenondevs.nova.item.behavior -import xyz.xenondevs.nova.item.NovaItem import xyz.xenondevs.nova.item.vanilla.VanillaMaterialProperty -class FireResistant : ItemBehavior() { +/** + * Makes items fire-resistant. + */ +object FireResistant : ItemBehavior { override fun getVanillaMaterialProperties(): List { return listOf(VanillaMaterialProperty.FIRE_RESISTANT) } - companion object : ItemBehaviorFactory() { - override fun create(item: NovaItem) = FireResistant() - } - } \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Flattening.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Flattening.kt index f4b3649643..f2806e6501 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Flattening.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Flattening.kt @@ -7,7 +7,7 @@ import org.bukkit.entity.Player import org.bukkit.event.block.Action import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.inventory.ItemStack -import xyz.xenondevs.nova.util.item.damageItemInMainHand +import xyz.xenondevs.nova.util.damageItemInMainHand import xyz.xenondevs.nova.util.playSoundNearby import xyz.xenondevs.nova.util.runTaskLater import xyz.xenondevs.nova.util.swingHand @@ -21,7 +21,10 @@ private val FLATTENABLES: Set = hashSetOf( Material.ROOTED_DIRT ) -object Flattening : ItemBehavior() { +/** + * Allows items to flatten the ground. + */ +object Flattening : ItemBehavior { override fun handleInteract(player: Player, itemStack: ItemStack, action: Action, event: PlayerInteractEvent) { if (action == Action.RIGHT_CLICK_BLOCK) { diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Fuel.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Fuel.kt index 444830b1fa..1a573f8e68 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Fuel.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Fuel.kt @@ -1,16 +1,84 @@ package xyz.xenondevs.nova.item.behavior +import net.minecraft.world.item.Item +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity +import org.bukkit.Material +import org.bukkit.craftbukkit.v1_20_R1.util.CraftMagicNumbers +import xyz.xenondevs.commons.collections.enumMap +import xyz.xenondevs.commons.provider.Provider +import xyz.xenondevs.commons.provider.immutable.provider +import xyz.xenondevs.nova.data.config.ConfigAccess import xyz.xenondevs.nova.item.NovaItem -import xyz.xenondevs.nova.item.options.FuelOptions +import xyz.xenondevs.nova.util.item.novaItem +import net.minecraft.world.item.ItemStack as MojangStack +import org.bukkit.inventory.ItemStack as BukkitStack -class Fuel(val options: FuelOptions) : ItemBehavior() { +fun Fuel(burnTime: Int) = Fuel.Default(provider(burnTime)) + +/** + * Allows items to be used as fuel in furnaces. + */ +interface Fuel { + + /** + * The burn time of this fuel, in ticks. + */ + val burnTime: Int + + class Default( + burnTime: Provider + ): ItemBehavior, Fuel { + override val burnTime by burnTime + } - companion object : ItemBehaviorFactory() { + companion object : ItemBehaviorFactory { - override fun create(item: NovaItem): Fuel { - return Fuel(FuelOptions.configurable(item)) + private val NMS_VANILLA_FUELS: Map = AbstractFurnaceBlockEntity.getFuel() + private val VANILLA_FUELS: Map = NMS_VANILLA_FUELS + .mapKeysTo(enumMap()) { (item, _) -> CraftMagicNumbers.getMaterial(item) } + + override fun create(item: NovaItem): Default { + val cfg = ConfigAccess(item) + return Default(cfg.getEntry("burn_time")) } - + + fun isFuel(material: Material): Boolean = material in VANILLA_FUELS + fun getBurnTime(material: Material): Int? = VANILLA_FUELS[material] + + fun isFuel(itemStack: BukkitStack): Boolean { + val novaItem = itemStack.novaItem + if (novaItem != null) { + return novaItem.hasBehavior(Fuel::class) + } + + return itemStack.type in VANILLA_FUELS + } + + fun getBurnTime(itemStack: BukkitStack): Int? { + val novaItem = itemStack.novaItem + if (novaItem != null) + return novaItem.getBehaviorOrNull(Fuel::class)?.burnTime + + return getBurnTime(itemStack.type) + } + + fun isFuel(itemStack: MojangStack): Boolean { + val novaItem = itemStack.novaItem + if (novaItem != null) + return novaItem.hasBehavior() + + return itemStack.item in NMS_VANILLA_FUELS + } + + fun getBurnTime(itemStack: MojangStack): Int? { + val novaItem = itemStack.novaItem + if (novaItem != null) { + return novaItem.getBehaviorOrNull(Fuel::class)?.burnTime + } + + return NMS_VANILLA_FUELS[itemStack.item] + } + } } \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/ItemBehavior.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/ItemBehavior.kt index f15f0b6e58..e11bd6115c 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/ItemBehavior.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/ItemBehavior.kt @@ -21,49 +21,34 @@ import xyz.xenondevs.nova.item.vanilla.VanillaMaterialProperty import xyz.xenondevs.nova.player.equipment.ArmorEquipEvent import xyz.xenondevs.nova.world.block.event.BlockBreakActionEvent -abstract class ItemBehavior : ItemBehaviorHolder() { - - lateinit var item: NovaItem - internal set - - open fun getVanillaMaterialProperties(): List = emptyList() - open fun getAttributeModifiers(): List = emptyList() - - open fun handleInteract(player: Player, itemStack: ItemStack, action: Action, event: PlayerInteractEvent) = Unit - open fun handleEntityInteract(player: Player, itemStack: ItemStack, clicked: Entity, event: PlayerInteractAtEntityEvent) = Unit - open fun handleAttackEntity(player: Player, itemStack: ItemStack, attacked: Entity, event: EntityDamageByEntityEvent) = Unit - open fun handleBreakBlock(player: Player, itemStack: ItemStack, event: BlockBreakEvent) = Unit - open fun handleDamage(player: Player, itemStack: ItemStack, event: PlayerItemDamageEvent) = Unit - open fun handleBreak(player: Player, itemStack: ItemStack, event: PlayerItemBreakEvent) = Unit - open fun handleEquip(player: Player, itemStack: ItemStack, equipped: Boolean, event: ArmorEquipEvent) = Unit - open fun handleInventoryClick(player: Player, itemStack: ItemStack, event: InventoryClickEvent) = Unit - open fun handleInventoryClickOnCursor(player: Player, itemStack: ItemStack, event: InventoryClickEvent) = Unit - open fun handleInventoryHotbarSwap(player: Player, itemStack: ItemStack, event: InventoryClickEvent) = Unit - open fun handleBlockBreakAction(player: Player, itemStack: ItemStack, event: BlockBreakActionEvent) = Unit - open fun handleRelease(player: Player, itemStack: ItemStack, event: ServerboundPlayerActionPacketEvent) = Unit +sealed interface ItemBehaviorHolder + +interface ItemBehavior : ItemBehaviorHolder { - open fun modifyItemBuilder(itemBuilder: ItemBuilder): ItemBuilder = itemBuilder - open fun updatePacketItemData(data: NamespacedCompound, itemData: PacketItemData) = Unit + fun getVanillaMaterialProperties(): List = emptyList() + fun getAttributeModifiers(): List = emptyList() + fun getDefaultCompound(): NamespacedCompound = NamespacedCompound() - final override fun get(item: NovaItem): ItemBehavior { - setMaterial(item) - return this - } + fun handleInteract(player: Player, itemStack: ItemStack, action: Action, event: PlayerInteractEvent) = Unit + fun handleEntityInteract(player: Player, itemStack: ItemStack, clicked: Entity, event: PlayerInteractAtEntityEvent) = Unit + fun handleAttackEntity(player: Player, itemStack: ItemStack, attacked: Entity, event: EntityDamageByEntityEvent) = Unit + fun handleBreakBlock(player: Player, itemStack: ItemStack, event: BlockBreakEvent) = Unit + fun handleDamage(player: Player, itemStack: ItemStack, event: PlayerItemDamageEvent) = Unit + fun handleBreak(player: Player, itemStack: ItemStack, event: PlayerItemBreakEvent) = Unit + fun handleEquip(player: Player, itemStack: ItemStack, equipped: Boolean, event: ArmorEquipEvent) = Unit + fun handleInventoryClick(player: Player, itemStack: ItemStack, event: InventoryClickEvent) = Unit + fun handleInventoryClickOnCursor(player: Player, itemStack: ItemStack, event: InventoryClickEvent) = Unit + fun handleInventoryHotbarSwap(player: Player, itemStack: ItemStack, event: InventoryClickEvent) = Unit + fun handleBlockBreakAction(player: Player, itemStack: ItemStack, event: BlockBreakActionEvent) = Unit + fun handleRelease(player: Player, itemStack: ItemStack, event: ServerboundPlayerActionPacketEvent) = Unit - internal fun setMaterial(item: NovaItem) { - if (this::item.isInitialized) - throw IllegalStateException("The same item behavior instance cannot be used for multiple materials") - - this.item = item - } + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Use getDefaultCompound or updatePacketItemData instead") + fun modifyItemBuilder(itemBuilder: ItemBuilder): ItemBuilder = itemBuilder + fun updatePacketItemData(data: NamespacedCompound, itemData: PacketItemData) = Unit } -abstract class ItemBehaviorFactory : ItemBehaviorHolder() { - abstract fun create(item: NovaItem): T - final override fun get(item: NovaItem) = create(item).apply { setMaterial(item) } -} - -abstract class ItemBehaviorHolder internal constructor() { - internal abstract fun get(item: NovaItem): T +interface ItemBehaviorFactory : ItemBehaviorHolder { + fun create(item: NovaItem): T } \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Stripping.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Stripping.kt index 25b767aff3..5040f16798 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Stripping.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Stripping.kt @@ -16,7 +16,6 @@ import org.bukkit.event.block.Action import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.inventory.ItemStack import xyz.xenondevs.nova.util.interactionHand -import xyz.xenondevs.nova.util.item.DamageableUtils import xyz.xenondevs.nova.util.nmsState import xyz.xenondevs.nova.util.runTaskLater import xyz.xenondevs.nova.util.serverLevel @@ -49,7 +48,10 @@ private val STRIPPABLES: Map = mapOf( Blocks.CHERRY_LOG to Blocks.STRIPPED_CHERRY_LOG ) -object Stripping : ItemBehavior() { +/** + * Allows items to strip blocks. + */ +object Stripping : ItemBehavior { override fun handleInteract(player: Player, itemStack: ItemStack, action: Action, event: PlayerInteractEvent) { if (action == Action.RIGHT_CLICK_BLOCK) { @@ -71,7 +73,7 @@ object Stripping : ItemBehavior() { fun setNewState(newState: BlockState) { runTaskLater(1) { player.swing(hand, true) } level.setBlock(pos, newState, 11) - DamageableUtils.damageAndBreakItem(itemStack, 1, player) + Damageable.damageAndBreak(itemStack, 1, player) { player.broadcastBreakEvent(hand) } } val stripped = STRIPPABLES[block]?.defaultBlockState()?.apply { setValue(RotatedPillarBlock.AXIS, state.getValue(RotatedPillarBlock.AXIS)) } diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Tilling.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Tilling.kt index ec02ddac76..76c4aaf2c3 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Tilling.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Tilling.kt @@ -15,7 +15,6 @@ import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.inventory.ItemStack import xyz.xenondevs.nova.util.above import xyz.xenondevs.nova.util.interactionHand -import xyz.xenondevs.nova.util.item.DamageableUtils import xyz.xenondevs.nova.util.nmsDirection import xyz.xenondevs.nova.util.nmsState import xyz.xenondevs.nova.util.runTaskLater @@ -36,7 +35,10 @@ private fun onlyIfAirAbove(event: PlayerInteractEvent): Boolean { return event.blockFace != BlockFace.DOWN && event.clickedBlock!!.above.type.isAir } -object Tilling : ItemBehavior() { +/** + * Allows items to till the ground. + */ +object Tilling : ItemBehavior { override fun handleInteract(player: Player, itemStack: ItemStack, action: Action, event: PlayerInteractEvent) { if (action == Action.RIGHT_CLICK_BLOCK) { @@ -58,7 +60,7 @@ object Tilling : ItemBehavior() { // drop items drops.forEach { Block.popResourceFromFace(level, pos, event.blockFace.nmsDirection, MojangStack(it)) } // damage item - DamageableUtils.damageAndBreakItem(serverPlayer.getItemInHand(interactionHand), 1, serverPlayer) + Damageable.damageAndBreak(serverPlayer.getItemInHand(interactionHand), 1, serverPlayer) { serverPlayer.broadcastBreakEvent(interactionHand) } // swing hand runTaskLater(1) { serverPlayer.swing(interactionHand, true) } } diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Tool.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Tool.kt index e38c37a3a1..290f22aa7b 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Tool.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Tool.kt @@ -3,64 +3,170 @@ package xyz.xenondevs.nova.item.behavior import net.minecraft.world.entity.EquipmentSlot import net.minecraft.world.entity.ai.attributes.AttributeModifier.Operation import net.minecraft.world.entity.ai.attributes.Attributes +import xyz.xenondevs.commons.provider.Provider +import xyz.xenondevs.commons.provider.immutable.map +import xyz.xenondevs.commons.provider.immutable.orElse +import xyz.xenondevs.commons.provider.immutable.provider +import xyz.xenondevs.nova.data.config.ConfigAccess import xyz.xenondevs.nova.item.NovaItem -import xyz.xenondevs.nova.item.options.ToolOptions +import xyz.xenondevs.nova.item.tool.ToolCategory +import xyz.xenondevs.nova.item.tool.ToolTier +import xyz.xenondevs.nova.item.tool.VanillaToolCategories import xyz.xenondevs.nova.item.vanilla.AttributeModifier import xyz.xenondevs.nova.item.vanilla.VanillaMaterialProperty +import xyz.xenondevs.nova.registry.NovaRegistries +import xyz.xenondevs.nova.util.get import java.util.* private const val PLAYER_ATTACK_SPEED = 4.0 private const val PLAYER_ATTACK_DAMAGE = 1.0 -class Tool(val options: ToolOptions) : ItemBehavior() { +fun Tool( + tier: ToolTier, + category: ToolCategory, + breakSpeed: Double, + attackDamage: Double?, + attackSpeed: Double?, + knockbackBonus: Int, + canSweepAttack: Boolean = false, + canBreakBlocksInCreative: Boolean = category != VanillaToolCategories.SWORD +) = Tool.Default( + provider(tier), + provider(category), + provider(breakSpeed), + provider(attackDamage), + provider(attackSpeed), + provider(knockbackBonus), + provider(canSweepAttack), + provider(canBreakBlocksInCreative) +) + +/** + * Allows items to be used as tools, by specifying break and attack properties. + */ +sealed interface Tool { - override fun getVanillaMaterialProperties(): List { - val properties = ArrayList() - properties += VanillaMaterialProperty.DAMAGEABLE - if (!options.canBreakBlocksInCreative) - properties += VanillaMaterialProperty.CREATIVE_NON_BLOCK_BREAKING - return properties - } + /** + * The [ToolTier] of this tool. + */ + val tier: ToolTier + + /** + * The [ToolCategory] of this tool. + */ + val category: ToolCategory + + /** + * The break speed of this tool. + */ + val breakSpeed: Double + + /** + * The attack damage of this tool. + */ + val attackDamage: Double? + + /** + * The attack speed of this tool. + */ + val attackSpeed: Double? + + /** + * The knockback bonus of this tool when attacking. + */ + val knockbackBonus: Int + + /** + * Whether this tool can perform a sweep attack. + */ + val canSweepAttack: Boolean - override fun getAttributeModifiers(): List { - val modifiers = ArrayList() + /** + * Whether this tool can break blocks in creative mode. + */ + val canBreakBlocksInCreative: Boolean + + class Default( + tier: Provider, + category: Provider, + breakSpeed: Provider, + attackDamage: Provider, + attackSpeed: Provider, + knockbackBonus: Provider, + canSweepAttack: Provider, + canBreakBlocksInCreative: Provider + ) : ItemBehavior, Tool { - val attackDamage = options.attackDamage - if (attackDamage != null) { - modifiers += AttributeModifier( - BASE_ATTACK_DAMAGE_UUID, - "Nova Attack Damage", - Attributes.ATTACK_DAMAGE, - Operation.ADDITION, - options.attackDamage!! - PLAYER_ATTACK_DAMAGE, - true, - EquipmentSlot.MAINHAND - ) + override val tier by tier + override val category by category + override val breakSpeed by breakSpeed + override val attackDamage by attackDamage + override val attackSpeed by attackSpeed + override val knockbackBonus by knockbackBonus + override val canSweepAttack by canSweepAttack + override val canBreakBlocksInCreative by canBreakBlocksInCreative + + override fun getVanillaMaterialProperties(): List { + val properties = ArrayList() + properties += VanillaMaterialProperty.DAMAGEABLE + if (!canBreakBlocksInCreative) + properties += VanillaMaterialProperty.CREATIVE_NON_BLOCK_BREAKING + return properties } - val attackSpeed = options.attackSpeed - if (attackSpeed != null) { - modifiers += AttributeModifier( - BASE_ATTACK_SPEED_UUID, - "Nova Attack Speed", - Attributes.ATTACK_SPEED, - Operation.ADDITION, - options.attackSpeed!! - PLAYER_ATTACK_SPEED, - true, - EquipmentSlot.MAINHAND - ) + override fun getAttributeModifiers(): List { + val modifiers = ArrayList() + + val attackDamage = attackDamage + if (attackDamage != null) { + modifiers += AttributeModifier( + BASE_ATTACK_DAMAGE_UUID, + "Nova Attack Damage", + Attributes.ATTACK_DAMAGE, + Operation.ADDITION, + attackDamage - PLAYER_ATTACK_DAMAGE, + true, + EquipmentSlot.MAINHAND + ) + } + + val attackSpeed = attackSpeed + if (attackSpeed != null) { + modifiers += AttributeModifier( + BASE_ATTACK_SPEED_UUID, + "Nova Attack Speed", + Attributes.ATTACK_SPEED, + Operation.ADDITION, + attackSpeed - PLAYER_ATTACK_SPEED, + true, + EquipmentSlot.MAINHAND + ) + } + + return modifiers } - return modifiers } - companion object : ItemBehaviorFactory() { + companion object : ItemBehaviorFactory { val BASE_ATTACK_DAMAGE_UUID: UUID = UUID.fromString("CB3F55D3-645C-4F38-A497-9C13A33DB5CF") val BASE_ATTACK_SPEED_UUID: UUID = UUID.fromString("FA233E1C-4180-4865-B01B-BCCE9785ACA3") - override fun create(item: NovaItem) = - Tool(ToolOptions.configurable(item)) + override fun create(item: NovaItem): Default { + val cfg = ConfigAccess(item) + return Default( + cfg.getEntry("tool_tier", "tool_level").map { NovaRegistries.TOOL_TIER[it]!! }, + cfg.getEntry("tool_category").map { NovaRegistries.TOOL_CATEGORY[it]!! }, + cfg.getEntry("break_speed"), + cfg.getOptionalEntry("attack_damage"), + cfg.getOptionalEntry("attack_speed"), + cfg.getOptionalEntry("knockback_bonus").orElse(0), + cfg.getOptionalEntry("can_sweep_attack").orElse(false), + cfg.getOptionalEntry("can_break_blocks_in_creative").orElse(true) + ) + } + } } \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Wearable.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Wearable.kt index 49ebc6cb17..a66a82c93e 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Wearable.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/Wearable.kt @@ -6,123 +6,218 @@ import net.minecraft.nbt.CompoundTag import net.minecraft.sounds.SoundEvent import net.minecraft.world.entity.ai.attributes.AttributeModifier.Operation import net.minecraft.world.entity.ai.attributes.Attributes +import net.minecraft.world.item.BlockItem +import net.minecraft.world.item.Equipable import org.bukkit.GameMode import org.bukkit.Sound import org.bukkit.entity.Player import org.bukkit.event.block.Action import org.bukkit.event.player.PlayerInteractEvent -import org.bukkit.inventory.ItemStack +import xyz.xenondevs.commons.collections.enumMap +import xyz.xenondevs.commons.provider.Provider +import xyz.xenondevs.commons.provider.immutable.orElse +import xyz.xenondevs.commons.provider.immutable.provider +import xyz.xenondevs.nova.data.config.ConfigAccess import xyz.xenondevs.nova.data.resources.lookup.ResourceLookups import xyz.xenondevs.nova.data.serialization.cbf.NamespacedCompound import xyz.xenondevs.nova.item.NovaItem import xyz.xenondevs.nova.item.logic.PacketItemData -import xyz.xenondevs.nova.item.options.WearableOptions import xyz.xenondevs.nova.item.vanilla.AttributeModifier import xyz.xenondevs.nova.item.vanilla.HideableFlag import xyz.xenondevs.nova.item.vanilla.VanillaMaterialProperty -import xyz.xenondevs.nova.player.equipment.ArmorType +import xyz.xenondevs.nova.util.bukkitEquipmentSlot import xyz.xenondevs.nova.util.data.getOrPut import xyz.xenondevs.nova.util.item.isActuallyInteractable +import xyz.xenondevs.nova.util.item.novaItem import xyz.xenondevs.nova.util.item.takeUnlessEmpty import xyz.xenondevs.nova.util.nmsCopy import xyz.xenondevs.nova.util.nmsEquipmentSlot import xyz.xenondevs.nova.util.serverPlayer import xyz.xenondevs.nova.util.swingHand +import java.util.* +import net.minecraft.world.entity.EquipmentSlot as MojangEquipmentSlot +import net.minecraft.world.item.ItemStack as MojangStack +import org.bukkit.inventory.EquipmentSlot as BukkitEquipmentSlot +import org.bukkit.inventory.ItemStack as BukkitStack -fun Wearable(type: ArmorType, equipSound: Sound): ItemBehaviorFactory = - Wearable(type, equipSound.key.toString()) +fun Wearable(slot: BukkitEquipmentSlot, equipSound: Sound): ItemBehaviorFactory = + Wearable(slot, equipSound.key.toString()) -fun Wearable(type: ArmorType, equipSound: SoundEvent): ItemBehaviorFactory = - Wearable(type, equipSound.location.toString()) +fun Wearable(slot: BukkitEquipmentSlot, equipSound: SoundEvent): ItemBehaviorFactory = + Wearable(slot, equipSound.location.toString()) -fun Wearable(type: ArmorType, equipSound: String? = null): ItemBehaviorFactory = - object : ItemBehaviorFactory() { - override fun create(item: NovaItem): Wearable = - Wearable(WearableOptions.configurable(type, equipSound, item)) +fun Wearable(slot: BukkitEquipmentSlot, equipSound: String? = null): ItemBehaviorFactory { + return object : ItemBehaviorFactory { + override fun create(item: NovaItem): Wearable.Default { + val texture = ResourceLookups.MODEL_DATA_LOOKUP[item.id]?.armor + ?.let { ResourceLookups.ARMOR_DATA_LOOKUP[it] }?.color + val cfg = ConfigAccess(item) + return Wearable.Default( + provider(texture), + provider(slot), + cfg.getOptionalEntry("armor").orElse(0.0), + cfg.getOptionalEntry("armor_toughness").orElse(0.0), + cfg.getOptionalEntry("knockback_resistance").orElse(0.0), + provider(equipSound) + ) + } } +} -class Wearable(val options: WearableOptions) : ItemBehavior() { +/** + * Allows items to be worn in armor slots. + */ +sealed interface Wearable { - private val textureColor: Int? by lazy { - ResourceLookups.MODEL_DATA_LOOKUP[item.id] - ?.armor - ?.let { ResourceLookups.ARMOR_DATA_LOOKUP[it] } - ?.color - } + val texture: Int? + val slot: BukkitEquipmentSlot + val armor: Double + val armorToughness: Double + val knockbackResistance: Double + val equipSound: String? - override fun getVanillaMaterialProperties(): List { - if (textureColor == null) - return emptyList() + class Default( + texture: Provider, + slot: Provider, + armor: Provider, + armorToughness: Provider, + knockbackResistance: Provider, + equipSound: Provider + ) : ItemBehavior, Wearable { - return listOf( - when (options.armorType) { - ArmorType.HELMET -> VanillaMaterialProperty.HELMET - ArmorType.CHESTPLATE -> VanillaMaterialProperty.CHESTPLATE - ArmorType.LEGGINGS -> VanillaMaterialProperty.LEGGINGS - ArmorType.BOOTS -> VanillaMaterialProperty.BOOTS - } - ) - } - - override fun getAttributeModifiers(): List { - val equipmentSlot = options.armorType.equipmentSlot.nmsEquipmentSlot - return listOf( - AttributeModifier( - "Nova Armor (${item.id}})", - Attributes.ARMOR, - Operation.ADDITION, - options.armor, - true, - equipmentSlot - ), - AttributeModifier( - "Nova Armor Toughness (${item.id}})", - Attributes.ARMOR_TOUGHNESS, - Operation.ADDITION, - options.armorToughness, - true, - equipmentSlot - ), - AttributeModifier( - "Nova Knockback Resistance (${item.id}})", - Attributes.KNOCKBACK_RESISTANCE, - Operation.ADDITION, - options.knockbackResistance, - true, - equipmentSlot - ) - ) - } - - override fun handleInteract(player: Player, itemStack: ItemStack, action: Action, event: PlayerInteractEvent) { - if (action == Action.RIGHT_CLICK_AIR || (action == Action.RIGHT_CLICK_BLOCK && !event.clickedBlock!!.type.isActuallyInteractable())) { - event.isCancelled = true + override val texture by texture + override val slot by slot + override val armor by armor + override val armorToughness by armorToughness + override val knockbackResistance by knockbackResistance + override val equipSound by equipSound + + override fun getVanillaMaterialProperties(): List { + if (texture == null) + return emptyList() - val hand = event.hand!! - val equipmentSlot = options.armorType.equipmentSlot - val previous = player.inventory.getItem(equipmentSlot)?.takeUnlessEmpty() - if (previous != null) { - // swap armor - player.inventory.setItem(equipmentSlot, itemStack) - player.inventory.setItem(hand, previous) - } else { - // equip armor - player.inventory.setItem(equipmentSlot, itemStack) - if (player.gameMode != GameMode.CREATIVE) player.inventory.setItem(hand, null) + return listOf( + when (slot) { + BukkitEquipmentSlot.HEAD -> VanillaMaterialProperty.HELMET + BukkitEquipmentSlot.CHEST -> VanillaMaterialProperty.CHESTPLATE + BukkitEquipmentSlot.LEGS -> VanillaMaterialProperty.LEGGINGS + BukkitEquipmentSlot.FEET -> VanillaMaterialProperty.BOOTS + else -> throw IllegalArgumentException("Invalid wearable slot: $slot") + } + ) + } + + override fun getAttributeModifiers(): List { + val equipmentSlot = slot.nmsEquipmentSlot + return listOf( + AttributeModifier( + ARMOR_MODIFIER_UUIDS[slot]!!, + "Nova Wearable Armor", + Attributes.ARMOR, + Operation.ADDITION, + armor, + true, + equipmentSlot + ), + AttributeModifier( + ARMOR_TOUGHNESS_MODIFIER_UUIDS[slot]!!, + "Nova Wearable Armor Toughness", + Attributes.ARMOR_TOUGHNESS, + Operation.ADDITION, + armorToughness, + true, + equipmentSlot + ), + AttributeModifier( + KNOCKBACK_RESISTANCE_MODIFIER_UUIDS[slot]!!, + "Nova Wearable Knockback Resistance", + Attributes.KNOCKBACK_RESISTANCE, + Operation.ADDITION, + knockbackResistance, + true, + equipmentSlot + ) + ) + } + + override fun handleInteract(player: Player, itemStack: BukkitStack, action: Action, event: PlayerInteractEvent) { + if (action == Action.RIGHT_CLICK_AIR || (action == Action.RIGHT_CLICK_BLOCK && !event.clickedBlock!!.type.isActuallyInteractable())) { + event.isCancelled = true + + val hand = event.hand!! + val previous = player.inventory.getItem(slot)?.takeUnlessEmpty() + if (previous != null) { + // swap armor + player.inventory.setItem(slot, itemStack) + player.inventory.setItem(hand, previous) + } else { + // equip armor + player.inventory.setItem(slot, itemStack) + if (player.gameMode != GameMode.CREATIVE) player.inventory.setItem(hand, null) + } + + player.swingHand(hand) + player.serverPlayer.onEquipItem(slot.nmsEquipmentSlot, previous.nmsCopy, itemStack.nmsCopy) + } + } + + override fun updatePacketItemData(data: NamespacedCompound, itemData: PacketItemData) { + val texture = texture + if (texture != null) { + itemData.nbt.getOrPut("display", ::CompoundTag).putInt("color", texture) + itemData.hide(HideableFlag.DYE) } - - player.swingHand(hand) - player.serverPlayer.onEquipItem(options.armorType.equipmentSlot.nmsEquipmentSlot, previous.nmsCopy, itemStack.nmsCopy) } + } - override fun updatePacketItemData(data: NamespacedCompound, itemData: PacketItemData) { - val textureColor = textureColor - if (textureColor != null) { - itemData.nbt.getOrPut("display", ::CompoundTag).putInt("color", textureColor) - itemData.hide(HideableFlag.DYE) + companion object { + + val ARMOR_MODIFIER_UUIDS: Map = BukkitEquipmentSlot.values().associateWithTo(enumMap()) { UUID.randomUUID() } + val ARMOR_TOUGHNESS_MODIFIER_UUIDS: Map = BukkitEquipmentSlot.values().associateWithTo(enumMap()) { UUID.randomUUID() } + val KNOCKBACK_RESISTANCE_MODIFIER_UUIDS: Map = BukkitEquipmentSlot.values().associateWithTo(enumMap()) { UUID.randomUUID() } + + /** + * Checks whether the specified [itemStack] is wearable. + */ + fun isWearable(itemStack: BukkitStack): Boolean = + isWearable(itemStack.nmsCopy) + + /** + * Checks whether the specified [itemStack] is wearable. + */ + fun isWearable(itemStack: MojangStack): Boolean { + val novaItem = itemStack.novaItem + if (novaItem != null) + return novaItem.hasBehavior() + + val item = itemStack.item + return item is Equipable || item is BlockItem && item.block is Equipable } - textureColor?.let { itemData.nbt.getOrPut("display", ::CompoundTag).putInt("color", it) } + + /** + * Gets the [BukkitEquipmentSlot] of the specified [itemStack], or null if it is not wearable. + */ + fun getSlot(itemStack: BukkitStack): BukkitEquipmentSlot? = + getSlot(itemStack.nmsCopy)?.bukkitEquipmentSlot + + /** + * Gets the [MojangEquipmentSlot] of the specified [itemStack], or null if it is not wearable. + */ + fun getSlot(itemStack: MojangStack): MojangEquipmentSlot? { + val novaItem = itemStack.novaItem + if (novaItem != null) + return novaItem.getBehaviorOrNull()?.slot?.nmsEquipmentSlot + + val equipable = when (val item = itemStack.item) { + is Equipable -> item + is BlockItem -> item.block as? Equipable + else -> null + } ?: return null + + return equipable.equipmentSlot + } + } } \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/impl/TileEntityItemBehavior.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/impl/TileEntityItemBehavior.kt index be0f06ab11..ed135c242b 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/impl/TileEntityItemBehavior.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/impl/TileEntityItemBehavior.kt @@ -10,7 +10,7 @@ import xyz.xenondevs.nova.tileentity.TileEntity import xyz.xenondevs.nova.tileentity.network.fluid.FluidType import xyz.xenondevs.nova.util.NumberFormatUtils -internal class TileEntityItemBehavior : ItemBehavior() { +object TileEntityItemBehavior : ItemBehavior { override fun updatePacketItemData(data: NamespacedCompound, itemData: PacketItemData) { val tileEntityData: Compound? = data[TileEntity.TILE_ENTITY_DATA_KEY] diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/impl/WrenchBehavior.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/impl/WrenchBehavior.kt index fd654fdff9..91b4f96caf 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/impl/WrenchBehavior.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/behavior/impl/WrenchBehavior.kt @@ -29,7 +29,7 @@ import xyz.xenondevs.nova.util.swingHand import xyz.xenondevs.nova.util.toString import xyz.xenondevs.nova.world.pos -internal object WrenchBehavior : ItemBehavior() { +internal object WrenchBehavior : ItemBehavior { private val WRENCH_MODE_KEY = NamespacedKey(NOVA, "wrench_mode") private val NETWORK_TYPES = arrayOf(DefaultNetworkTypes.ENERGY, DefaultNetworkTypes.ITEMS, DefaultNetworkTypes.FLUID) diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/enchantment/EnchantmentCategory.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/enchantment/EnchantmentCategory.kt index d83916c5de..c500985108 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/enchantment/EnchantmentCategory.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/enchantment/EnchantmentCategory.kt @@ -58,8 +58,8 @@ abstract class EnchantmentCategory { * Checks if this [EnchantmentCategory] can be applied to the specified [novaItem]. */ fun canEnchant(novaItem: NovaItem): Boolean { - val enchantable = novaItem.getBehavior() - return enchantable?.options?.enchantmentCategories?.contains(this) ?: false + val enchantable = novaItem.getBehaviorOrNull() + return enchantable?.enchantmentCategories?.contains(this) ?: false } /** diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/logic/ItemLogic.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/logic/ItemLogic.kt index e8a378451d..1f44d5c321 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/logic/ItemLogic.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/logic/ItemLogic.kt @@ -31,6 +31,7 @@ import xyz.xenondevs.nova.data.resources.builder.task.material.info.VanillaMater import xyz.xenondevs.nova.data.serialization.cbf.NamespacedCompound import xyz.xenondevs.nova.item.NovaItem import xyz.xenondevs.nova.item.behavior.ItemBehavior +import xyz.xenondevs.nova.item.behavior.ItemBehaviorFactory import xyz.xenondevs.nova.item.behavior.ItemBehaviorHolder import xyz.xenondevs.nova.item.vanilla.AttributeModifier import xyz.xenondevs.nova.player.equipment.ArmorEquipEvent @@ -38,25 +39,50 @@ import xyz.xenondevs.nova.util.data.getConfigurationSectionList import xyz.xenondevs.nova.util.data.getDoubleOrNull import xyz.xenondevs.nova.util.data.logExceptionMessages import xyz.xenondevs.nova.util.item.novaCompound +import xyz.xenondevs.nova.util.item.novaCompoundOrNull import xyz.xenondevs.nova.world.block.event.BlockBreakActionEvent import java.util.logging.Level import kotlin.reflect.KClass import kotlin.reflect.full.isSuperclassOf import net.minecraft.world.item.ItemStack as MojangStack +private fun loadBehaviors(item: NovaItem, holders: List): List = + holders.map { holder -> + when (holder) { + is ItemBehavior -> holder + is ItemBehaviorFactory<*> -> holder.create(item) + } + } + +// TODO: merge with NovaItem? /** - * Handles actions performed on [ItemStack]s of a [NovaItem] + * Handles actions performed on [ItemStacks][ItemStack] of a [NovaItem]. */ -internal class ItemLogic internal constructor(holders: List>) : Reloadable { +internal class ItemLogic internal constructor(holders: List) : Reloadable { - private val behaviors: List by lazy { holders.map { it.get(item) } } + private val behaviors: List by lazy { loadBehaviors(item, holders) } private lateinit var item: NovaItem private lateinit var name: Component - lateinit var vanillaMaterial: Material private set lateinit var attributeModifiers: Map> private set + private var defaultCompound: NamespacedCompound? = null - internal constructor(vararg holders: ItemBehaviorHolder<*>) : this(holders.toList()) + internal constructor(vararg holders: ItemBehaviorHolder) : this(holders.asList()) + + fun getBehaviorOrNull(type: KClass): T? = + behaviors.firstOrNull { type.isSuperclassOf(it::class) } as T? + + fun hasBehavior(type: KClass): Boolean = + behaviors.any { it::class == type } + + fun setMaterial(item: NovaItem) { + if (this::item.isInitialized) + throw IllegalStateException("NovaItems cannot be used for multiple materials") + + this.item = item + this.name = Component.translatable(item.localizedName) + reload() + } override fun reload() { vanillaMaterial = VanillaMaterialTypes.getMaterial(behaviors.flatMap { it.getVanillaMaterialProperties() }.toHashSet()) @@ -69,25 +95,25 @@ internal class ItemLogic internal constructor(holders: List getBehavior(type: KClass): T? = - behaviors.firstOrNull { type.isSuperclassOf(it::class) } as T? - - fun hasBehavior(type: KClass): Boolean = - behaviors.any { it::class == type } - - fun setMaterial(item: NovaItem) { - if (this::item.isInitialized) - throw IllegalStateException("NovaItems cannot be used for multiple materials") - this.item = item - this.name = Component.translatable(item.localizedName) - reload() + var defaultCompound: NamespacedCompound? = null + for (behavior in behaviors) { + val behaviorCompound = behavior.getDefaultCompound() + if (behaviorCompound.isNotEmpty()) { + if (defaultCompound == null) + defaultCompound = NamespacedCompound() + + defaultCompound.putAll(behaviorCompound) + } + } + this.defaultCompound = defaultCompound } + @Suppress("DEPRECATION") fun modifyItemBuilder(itemBuilder: ItemBuilder): ItemBuilder { var builder = itemBuilder + if (defaultCompound != null) + builder.addModifier { it.novaCompound.putAll(defaultCompound!!.copy()); it } behaviors.forEach { builder = it.modifyItemBuilder(builder) } return builder } @@ -95,7 +121,7 @@ internal class ItemLogic internal constructor(holders: List("max_energy") - - constructor(path: String) : super(path) - constructor(item: NovaItem) : super(item) - -} \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/DamageableOptions.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/DamageableOptions.kt deleted file mode 100644 index e1b62ae282..0000000000 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/DamageableOptions.kt +++ /dev/null @@ -1,77 +0,0 @@ -package xyz.xenondevs.nova.item.options - -import org.bukkit.inventory.RecipeChoice -import xyz.xenondevs.commons.provider.Provider -import xyz.xenondevs.commons.provider.immutable.map -import xyz.xenondevs.commons.provider.immutable.orElse -import xyz.xenondevs.commons.provider.immutable.provider -import xyz.xenondevs.nova.data.config.ConfigAccess -import xyz.xenondevs.nova.data.serialization.json.serializer.RecipeDeserializer -import xyz.xenondevs.nova.item.NovaItem - -@HardcodedMaterialOptions -fun DamageableOptions( - maxDurability: Int, - itemDamageOnAttackEntity: Int, - itemDamageOnBreakBlock: Int, - repairIngredient: RecipeChoice? = null -): DamageableOptions = HardcodedDamageableOptions(maxDurability, itemDamageOnAttackEntity, itemDamageOnBreakBlock, repairIngredient) - -sealed interface DamageableOptions { - - val durabilityProvider: Provider - val itemDamageOnAttackEntityProvider: Provider - val itemDamageOnBreakBlockProvider: Provider - val repairIngredientProvider: Provider - - val durability: Int - get() = durabilityProvider.value - val itemDamageOnAttackEntity: Int - get() = itemDamageOnAttackEntityProvider.value - val itemDamageOnBreakBlock: Int - get() = itemDamageOnBreakBlockProvider.value - val repairIngredient: RecipeChoice? - get() = repairIngredientProvider.value - - companion object { - - fun configurable(item: NovaItem): DamageableOptions = - ConfigurableDamageableOptions(item) - - fun configurable(path: String): DamageableOptions = - ConfigurableDamageableOptions(path) - - } - -} - -private class HardcodedDamageableOptions( - maxDurability: Int, - itemDamageOnAttackEntity: Int, - itemDamageOnBreakBlock: Int, - repairIngredient: RecipeChoice? -) : DamageableOptions { - override val durabilityProvider = provider(maxDurability) - override val itemDamageOnAttackEntityProvider = provider(itemDamageOnAttackEntity) - override val itemDamageOnBreakBlockProvider = provider(itemDamageOnBreakBlock) - override val repairIngredientProvider = provider(repairIngredient) -} - -@Suppress("UNCHECKED_CAST") -private class ConfigurableDamageableOptions : ConfigAccess, DamageableOptions { - - override val durabilityProvider = getEntry("durability", "max_durability") - override val itemDamageOnAttackEntityProvider = getOptionalEntry("item_damage_on_attack_entity").orElse(0) - override val itemDamageOnBreakBlockProvider = getOptionalEntry("item_damage_on_break_block").orElse(0) - override val repairIngredientProvider = getOptionalEntry("repair_ingredient").map { - val list = when (it) { - is String -> listOf(it) - else -> it as List - } - RecipeDeserializer.parseRecipeChoice(list) - } - - constructor(path: String) : super(path) - constructor(item: NovaItem) : super(item) - -} \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/EnchantableOptions.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/EnchantableOptions.kt deleted file mode 100644 index ccf23fc51c..0000000000 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/EnchantableOptions.kt +++ /dev/null @@ -1,47 +0,0 @@ -package xyz.xenondevs.nova.item.options - -import xyz.xenondevs.commons.provider.immutable.map -import xyz.xenondevs.nova.data.config.ConfigAccess -import xyz.xenondevs.nova.item.NovaItem -import xyz.xenondevs.nova.item.enchantment.EnchantmentCategory -import xyz.xenondevs.nova.registry.NovaRegistries -import xyz.xenondevs.nova.util.getOrThrow - -@HardcodedMaterialOptions -fun EnchantableOptions( - enchantmentValue: Int, - enchantmentCategories: List -): EnchantableOptions = HardcodedEnchantableOptions(enchantmentValue, enchantmentCategories) - -sealed interface EnchantableOptions { - - val enchantmentValue: Int - val enchantmentCategories: List - - companion object { - - fun configurable(item: NovaItem): EnchantableOptions = - ConfigurableEnchantableOptions(item) - - fun configurable(path: String): EnchantableOptions = - ConfigurableEnchantableOptions(path) - - } - -} - -private class HardcodedEnchantableOptions( - override val enchantmentValue: Int, - override val enchantmentCategories: List -) : EnchantableOptions - -private class ConfigurableEnchantableOptions : ConfigAccess, EnchantableOptions { - - override val enchantmentValue by getEntry("enchantment_value") - override val enchantmentCategories by getEntry>("enchantment_categories") - .map { list -> list.map { NovaRegistries.ENCHANTMENT_CATEGORY.getOrThrow(it) } } - - constructor(path: String) : super(path) - constructor(item: NovaItem) : super(item) - -} \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/FoodOptions.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/FoodOptions.kt deleted file mode 100644 index a5bfd099f6..0000000000 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/FoodOptions.kt +++ /dev/null @@ -1,124 +0,0 @@ -package xyz.xenondevs.nova.item.options - -import org.bukkit.potion.PotionEffect -import xyz.xenondevs.commons.provider.Provider -import xyz.xenondevs.commons.provider.immutable.map -import xyz.xenondevs.commons.provider.immutable.orElse -import xyz.xenondevs.commons.provider.immutable.provider -import xyz.xenondevs.nova.data.config.ConfigAccess -import xyz.xenondevs.nova.item.NovaItem -import xyz.xenondevs.nova.item.options.FoodOptions.FoodType -import xyz.xenondevs.nova.item.vanilla.VanillaMaterialProperty - -/** - * @param type The type of food - * @param consumeTime The time it takes for the food to be consumed, in ticks. - * @param nutrition The nutrition value this food provides. - * @param saturationModifier The saturation modifier this food provides. The saturation is calculated like this: - * ``` - * saturation = min(saturation + nutrition * saturationModifier * 2.0f, foodLevel) - * ``` - * @param instantHealth The amount of health to be restored immediately. - * @param effects A list of effects to apply to the player when this food is consumed. - */ -@HardcodedMaterialOptions -fun FoodOptions( - type: FoodType, - consumeTime: Int, - nutrition: Int, - saturationModifier: Float, - instantHealth: Double = 0.0, - effects: List? = null -): FoodOptions = HardcodedFoodOptions(type, consumeTime, nutrition, saturationModifier, instantHealth, effects) - -sealed interface FoodOptions { - - val typeProvider: Provider - val consumeTimeProvider: Provider - val nutritionProvider: Provider - val saturationModifierProvider: Provider - val instantHealthProvider: Provider - val effectsProvider: Provider?> - - val type: FoodType - get() = typeProvider.value - val consumeTime: Int - get() = consumeTimeProvider.value - val nutrition: Int - get() = nutritionProvider.value - val saturationModifier: Float - get() = saturationModifierProvider.value - val instantHealth: Double - get() = instantHealthProvider.value - val effects: List? - get() = effectsProvider.value - - enum class FoodType(internal val vanillaMaterialProperty: VanillaMaterialProperty) { - - /** - * Behaves like normal food. - * - * Has a small delay before the eating animation starts. - * - * Can only be eaten when hungry. - */ - NORMAL(VanillaMaterialProperty.CONSUMABLE_NORMAL), - - /** - * The eating animation starts immediately. - * - * Can only be eaten when hungry. - */ - FAST(VanillaMaterialProperty.CONSUMABLE_FAST), - - /** - * The food can always be eaten, no hunger is required. - * - * Has a small delay before the eating animation starts. - */ - ALWAYS_EATABLE(VanillaMaterialProperty.CONSUMABLE_ALWAYS) - } - - companion object { - - fun configurable(item: NovaItem): FoodOptions = - ConfigurableFoodOptions(item) - - fun configurable(path: String): FoodOptions = - ConfigurableFoodOptions(path) - - } - -} - -private class HardcodedFoodOptions( - type: FoodType, - consumeTime: Int, - nutrition: Int, - saturationModifier: Float, - instantHealth: Double, - effects: List? -) : FoodOptions { - override val typeProvider = provider(type) - override val consumeTimeProvider = provider(consumeTime) - override val nutritionProvider = provider(nutrition) - override val saturationModifierProvider = provider(saturationModifier) - override val instantHealthProvider = provider(instantHealth) - override val effectsProvider = provider(effects) -} - -private class ConfigurableFoodOptions : ConfigAccess, FoodOptions { - - override val typeProvider = getOptionalEntry("food_type") - .map { FoodType.valueOf(it.uppercase()) } - .orElse(FoodType.NORMAL) - override val consumeTimeProvider = getEntry("consume_time") - override val nutritionProvider = getEntry("nutrition") - override val saturationModifierProvider = getEntry("saturation_modifier") - override val instantHealthProvider = getOptionalEntry("instant_health").orElse(0.0) - override val effectsProvider = getOptionalEntry>("effects") - - constructor(path: String) : super(path) - constructor(item: NovaItem) : super(item) - -} \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/FuelOptions.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/FuelOptions.kt deleted file mode 100644 index 125a5d0ed1..0000000000 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/FuelOptions.kt +++ /dev/null @@ -1,41 +0,0 @@ -package xyz.xenondevs.nova.item.options - -import xyz.xenondevs.nova.data.config.ConfigAccess -import xyz.xenondevs.nova.item.NovaItem - -@HardcodedMaterialOptions -fun FuelOptions( - burnTime: Int, -): FuelOptions = HardcodedFuelOptions(burnTime) - -sealed interface FuelOptions { - - /** - * The burn time of this fuel, in ticks. - */ - val burnTime: Int - - companion object { - - fun configurable(item: NovaItem): FuelOptions = - ConfigurableFuelOptions(item) - - fun configurable(path: String): FuelOptions = - ConfigurableFuelOptions(path) - - } - -} - -private class HardcodedFuelOptions( - override val burnTime: Int -) : FuelOptions - -private class ConfigurableFuelOptions : ConfigAccess, FuelOptions { - - override val burnTime by getEntry("burn_time") - - constructor(path: String) : super(path) - constructor(item: NovaItem) : super(item) - -} \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/HardcodedMaterialOptions.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/HardcodedMaterialOptions.kt deleted file mode 100644 index 8de2100aab..0000000000 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/HardcodedMaterialOptions.kt +++ /dev/null @@ -1,9 +0,0 @@ -package xyz.xenondevs.nova.item.options - -/** - * It is generally recommended to make your material options configurable. - * - * If you still want to hardcode your material options, you can use this annotation to opt-in. - */ -@RequiresOptIn -annotation class HardcodedMaterialOptions \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/ToolOptions.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/ToolOptions.kt deleted file mode 100644 index f088535d99..0000000000 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/ToolOptions.kt +++ /dev/null @@ -1,104 +0,0 @@ -package xyz.xenondevs.nova.item.options - -import xyz.xenondevs.commons.provider.Provider -import xyz.xenondevs.commons.provider.immutable.map -import xyz.xenondevs.commons.provider.immutable.orElse -import xyz.xenondevs.commons.provider.immutable.provider -import xyz.xenondevs.nova.data.config.ConfigAccess -import xyz.xenondevs.nova.item.NovaItem -import xyz.xenondevs.nova.item.tool.ToolCategory -import xyz.xenondevs.nova.item.tool.ToolTier -import xyz.xenondevs.nova.item.tool.VanillaToolCategories -import xyz.xenondevs.nova.registry.NovaRegistries.TOOL_CATEGORY -import xyz.xenondevs.nova.registry.NovaRegistries.TOOL_TIER -import xyz.xenondevs.nova.util.get - -@HardcodedMaterialOptions -fun ToolOptions( - tier: ToolTier, - category: ToolCategory, - breakSpeed: Double, - attackDamage: Double?, - attackSpeed: Double?, - knockbackBonus: Int, - canSweepAttack: Boolean = false, - canBreakBlocksInCreative: Boolean = category != VanillaToolCategories.SWORD -): ToolOptions = HardcodedToolOptions(tier, category, breakSpeed, attackDamage, attackSpeed, knockbackBonus, canSweepAttack, canBreakBlocksInCreative) - -sealed interface ToolOptions { - - val tierProvider: Provider - val categoryProvider: Provider - val breakSpeedProvider: Provider - val attackDamageProvider: Provider - val attackSpeedProvider: Provider - val knockbackBonusProvider: Provider - val canSweepAttackProvider: Provider - val canBreakBlocksInCreativeProvider: Provider - - val tier: ToolTier - get() = tierProvider.value - val category: ToolCategory - get() = categoryProvider.value - val breakSpeed: Double - get() = breakSpeedProvider.value - val attackDamage: Double? - get() = attackDamageProvider.value - val attackSpeed: Double? - get() = attackSpeedProvider.value - val knockbackBonus: Int - get() = knockbackBonusProvider.value - val canSweepAttack: Boolean - get() = canSweepAttackProvider.value - val canBreakBlocksInCreative: Boolean - get() = canBreakBlocksInCreativeProvider.value - - companion object { - - fun configurable(item: NovaItem): ToolOptions = - ConfigurableToolOptions(item) - - fun configurable(path: String): ToolOptions = - ConfigurableToolOptions(path) - - } - -} - -private class HardcodedToolOptions( - tier: ToolTier, - category: ToolCategory, - breakSpeed: Double, - attackDamage: Double?, - attackSpeed: Double?, - knockbackBonus: Int, - canSweepAttack: Boolean, - canBreakBlocksInCreative: Boolean -) : ToolOptions { - - override val tierProvider = provider(tier) - override val categoryProvider = provider(category) - override val breakSpeedProvider = provider(breakSpeed) - override val attackDamageProvider = provider(attackDamage) - override val attackSpeedProvider = provider(attackSpeed) - override val knockbackBonusProvider = provider(knockbackBonus) - override val canSweepAttackProvider = provider(canSweepAttack) - override val canBreakBlocksInCreativeProvider = provider(canBreakBlocksInCreative) - -} - -private class ConfigurableToolOptions : ConfigAccess, ToolOptions { - - override val tierProvider = getEntry("tool_tier", "tool_level").map { TOOL_TIER[it]!! } - override val categoryProvider = getEntry("tool_category").map { TOOL_CATEGORY[it]!! } - override val breakSpeedProvider = getEntry("break_speed") - override val attackDamageProvider = getOptionalEntry("attack_damage") - override val attackSpeedProvider = getOptionalEntry("attack_speed") - override val knockbackBonusProvider = getOptionalEntry("knockback_bonus").orElse(0) - override val canSweepAttackProvider = getOptionalEntry("can_sweep_attack").orElse(false) - override val canBreakBlocksInCreativeProvider = getOptionalEntry("can_break_blocks_in_creative").orElse(true) - - constructor(path: String) : super(path) - constructor(item: NovaItem) : super(item) - -} \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/WearableOptions.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/WearableOptions.kt deleted file mode 100644 index 635d0ce9b2..0000000000 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/options/WearableOptions.kt +++ /dev/null @@ -1,82 +0,0 @@ -package xyz.xenondevs.nova.item.options - -import xyz.xenondevs.commons.provider.Provider -import xyz.xenondevs.commons.provider.immutable.orElse -import xyz.xenondevs.commons.provider.immutable.provider -import xyz.xenondevs.nova.data.config.ConfigAccess -import xyz.xenondevs.nova.item.NovaItem -import xyz.xenondevs.nova.player.equipment.ArmorType - -@HardcodedMaterialOptions -fun WearableOptions( - armorType: ArmorType, - armor: Double = 0.0, - armorToughness: Double = 0.0, - knockbackResistance: Double = 0.0, - equipSound: String? = null -): WearableOptions = HardcodedWearableOptions(armorType, armor, armorToughness, knockbackResistance, equipSound) - -sealed interface WearableOptions { - - val armorTypeProvider: Provider - val armorProvider: Provider - val armorToughnessProvider: Provider - val knockbackResistanceProvider: Provider - val equipSoundProvider: Provider - - val armorType: ArmorType - get() = armorTypeProvider.value - val armor: Double - get() = armorProvider.value - val armorToughness: Double - get() = armorToughnessProvider.value - val knockbackResistance: Double - get() = knockbackResistanceProvider.value - val equipSound: String? - get() = equipSoundProvider.value - - companion object { - - fun configurable(armorType: ArmorType, equipSound: String?, item: NovaItem): WearableOptions = - ConfigurableWearableOptions(armorType, equipSound, item) - - fun configurable(armorType: ArmorType, equipSound: String?, path: String): WearableOptions = - ConfigurableWearableOptions(armorType, equipSound, path) - - } - -} - -private class HardcodedWearableOptions( - armorType: ArmorType, - armor: Double, - armorToughness: Double, - knockbackResistance: Double, - equipSound: String? -) : WearableOptions { - override val armorTypeProvider = provider(armorType) - override val armorProvider = provider(armor) - override val armorToughnessProvider = provider(armorToughness) - override val knockbackResistanceProvider = provider(knockbackResistance) - override val equipSoundProvider = provider(equipSound) -} - -private class ConfigurableWearableOptions : ConfigAccess, WearableOptions { - - override val equipSoundProvider: Provider - override val armorTypeProvider: Provider - override val armorProvider = getOptionalEntry("armor").orElse(0.0) - override val armorToughnessProvider = getOptionalEntry("armor_toughness").orElse(0.0) - override val knockbackResistanceProvider = getOptionalEntry("knockback_resistance").orElse(0.0) - - constructor(armorType: ArmorType, soundEvent: String?, path: String) : super(path) { - this.armorTypeProvider = provider(armorType) - this.equipSoundProvider = provider(soundEvent) - } - - constructor(armorType: ArmorType, soundEvent: String?, item: NovaItem) : super(item) { - this.armorTypeProvider = provider(armorType) - this.equipSoundProvider = provider(soundEvent) - } - -} \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/tool/ToolCategory.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/tool/ToolCategory.kt index a30135cca7..b4ad8a5fbc 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/tool/ToolCategory.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/tool/ToolCategory.kt @@ -28,7 +28,7 @@ open class ToolCategory internal constructor( if (item == null) return null - val novaCategory = item.novaItem?.getBehavior(Tool::class)?.options?.category + val novaCategory = item.novaItem?.getBehaviorOrNull(Tool::class)?.category if (novaCategory != null) return novaCategory diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/tool/ToolTier.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/tool/ToolTier.kt index ec6a088799..28849b5d49 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/tool/ToolTier.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/tool/ToolTier.kt @@ -52,7 +52,7 @@ class ToolTier( if (item == null) return null - val novaLevel = item.novaItem?.getBehavior(Tool::class)?.options?.tier + val novaLevel = item.novaItem?.getBehaviorOrNull(Tool::class)?.tier if (novaLevel != null) return novaLevel diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/AnvilResultPatch.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/AnvilResultPatch.kt index e0196f9b04..8713353e3a 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/AnvilResultPatch.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/AnvilResultPatch.kt @@ -14,14 +14,14 @@ import org.bukkit.event.inventory.PrepareAnvilEvent import org.bukkit.inventory.InventoryView import xyz.xenondevs.bytebase.asm.buildInsnList import xyz.xenondevs.nova.i18n.LocaleManager +import xyz.xenondevs.nova.item.behavior.Damageable.Companion.getDamage +import xyz.xenondevs.nova.item.behavior.Damageable.Companion.getMaxDurability +import xyz.xenondevs.nova.item.behavior.Damageable.Companion.isDamageable +import xyz.xenondevs.nova.item.behavior.Damageable.Companion.isValidRepairItem +import xyz.xenondevs.nova.item.behavior.Damageable.Companion.setDamage import xyz.xenondevs.nova.transformer.MethodTransformer import xyz.xenondevs.nova.util.bukkitMirror import xyz.xenondevs.nova.util.callEvent -import xyz.xenondevs.nova.util.item.DamageableUtils.getDamage -import xyz.xenondevs.nova.util.item.DamageableUtils.getMaxDurability -import xyz.xenondevs.nova.util.item.DamageableUtils.isDamageable -import xyz.xenondevs.nova.util.item.DamageableUtils.isValidRepairItem -import xyz.xenondevs.nova.util.item.DamageableUtils.setDamage import xyz.xenondevs.nova.util.item.localizedName import xyz.xenondevs.nova.util.item.novaItem import xyz.xenondevs.nova.util.reflection.ReflectionRegistry diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/DamageablePatches.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/DamageablePatches.kt index 75a9f77218..7a24c32daf 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/DamageablePatches.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/DamageablePatches.kt @@ -22,9 +22,6 @@ import xyz.xenondevs.nova.item.tool.ToolCategory import xyz.xenondevs.nova.item.tool.VanillaToolCategory import xyz.xenondevs.nova.transformer.MultiTransformer import xyz.xenondevs.nova.util.bukkitMirror -import xyz.xenondevs.nova.util.item.DamageableUtils -import xyz.xenondevs.nova.util.item.ItemDamageResult -import xyz.xenondevs.nova.util.item.novaCompound import xyz.xenondevs.nova.util.item.novaItem import xyz.xenondevs.nova.util.reflection.ReflectionRegistry import xyz.xenondevs.nova.util.reflection.ReflectionRegistry.ITEM_STACK_HURT_AND_BREAK_METHOD @@ -56,9 +53,7 @@ internal object DamageablePatches : MultiTransformer(ItemStack::class, Item::cla @JvmStatic fun hurtAndBreak(itemStack: ItemStack, damage: Int, entity: LivingEntity, consumer: Consumer) { - if (DamageableUtils.damageAndBreakItem(itemStack, damage, entity) == ItemDamageResult.BROKEN) { - consumer.accept(entity) - } + Damageable.damageAndBreak(itemStack, damage, entity, consumer::accept) } /** @@ -77,13 +72,14 @@ internal object DamageablePatches : MultiTransformer(ItemStack::class, Item::cla fun hurtEnemy(itemStack: ItemStack, player: Player) { val novaItem = itemStack.novaItem - val damage = if (novaItem != null) { - val damageable = novaItem.getBehavior(Damageable::class) ?: return - damageable.options.itemDamageOnAttackEntity + val damage: Int + if (novaItem != null) { + val damageable = novaItem.getBehaviorOrNull(Damageable::class) ?: return + damage = damageable.itemDamageOnAttackEntity } else { val category = ToolCategory.ofItem(itemStack.bukkitMirror) as? VanillaToolCategory ?: return player.awardStat(Stats.ITEM_USED.get(itemStack.item)) - category.itemDamageOnAttackEntity + damage = category.itemDamageOnAttackEntity } itemStack.hurtAndBreak(damage, player) { @@ -121,7 +117,7 @@ internal object DamageablePatches : MultiTransformer(ItemStack::class, Item::cla val itemStack = EnchantmentHelper.getRandomItemWith(Enchantments.MENDING, player) { val novaItem = it.novaItem if (novaItem != null) { - val damage = novaItem.getBehavior(Damageable::class)?.getDamage(it.novaCompound) + val damage = novaItem.getBehaviorOrNull(Damageable::class)?.getDamage(it) return@getRandomItemWith damage != null && damage > 0 } @@ -129,14 +125,13 @@ internal object DamageablePatches : MultiTransformer(ItemStack::class, Item::cla }?.value if (itemStack != null) { - val damageable = itemStack.novaItem?.getBehavior(Damageable::class) + val damageable = itemStack.novaItem?.getBehaviorOrNull(Damageable::class) val repair: Int if (damageable != null) { - val novaCompound = itemStack.novaCompound - val damageValue = damageable.getDamage(novaCompound) + val damageValue = damageable.getDamage(itemStack) repair = min(orb.value * 2, damageValue) - damageable.setDamage(novaCompound, damageValue - repair) + damageable.setDamage(itemStack, damageValue - repair) } else { repair = min(orb.value * 2, itemStack.damageValue) itemStack.damageValue -= repair diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/FuelPatches.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/FuelPatches.kt index 490d1e7cdd..1648b64ddd 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/FuelPatches.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/FuelPatches.kt @@ -4,8 +4,8 @@ package xyz.xenondevs.nova.transformer.patch.item import net.minecraft.world.item.ItemStack import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity +import xyz.xenondevs.nova.item.behavior.Fuel import xyz.xenondevs.nova.transformer.MultiTransformer -import xyz.xenondevs.nova.util.item.FuelUtils import xyz.xenondevs.nova.util.reflection.ReflectionRegistry.ABSTRACT_FURNACE_BLOCK_ENTITY_GET_BURN_DURATION_METHOD internal object FuelPatches : MultiTransformer(AbstractFurnaceBlockEntity::class) { @@ -17,7 +17,7 @@ internal object FuelPatches : MultiTransformer(AbstractFurnaceBlockEntity::class @JvmStatic fun isFuel(itemStack: ItemStack): Boolean { - return FuelUtils.isFuel(itemStack) + return Fuel.isFuel(itemStack) } @JvmStatic @@ -25,7 +25,7 @@ internal object FuelPatches : MultiTransformer(AbstractFurnaceBlockEntity::class if (itemStack.isEmpty) return 0 - return FuelUtils.getBurnTime(itemStack) ?: 0 + return Fuel.getBurnTime(itemStack) ?: 0 } } \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/ToolPatches.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/ToolPatches.kt index 0925cc670a..eb856099d0 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/ToolPatches.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/ToolPatches.kt @@ -66,7 +66,7 @@ internal object ToolPatches : MultiTransformer(CraftBlock::class, MojangPlayer:: val novaItem = itemStack.novaItem return if (novaItem != null) { - novaItem.getBehavior(Tool::class)?.options?.canSweepAttack ?: false + novaItem.getBehaviorOrNull(Tool::class)?.canSweepAttack ?: false } else { (ToolCategory.ofItem(itemStack.bukkitMirror) as? VanillaToolCategory)?.canSweepAttack ?: false } @@ -86,7 +86,7 @@ internal object ToolPatches : MultiTransformer(CraftBlock::class, MojangPlayer:: @JvmStatic fun getKnockbackBonus(entity: LivingEntity): Int { return EnchantmentHelper.getEnchantmentLevel(Enchantments.KNOCKBACK, entity) + - (entity.mainHandItem.novaItem?.getBehavior(Tool::class)?.options?.knockbackBonus ?: 0) + (entity.mainHandItem.novaItem?.getBehaviorOrNull(Tool::class)?.knockbackBonus ?: 0) } } \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/WearablePatch.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/WearablePatch.kt index 226893e2a0..c685579a50 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/WearablePatch.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/item/WearablePatch.kt @@ -88,14 +88,14 @@ internal object WearablePatch : MultiTransformer(Equipable::class, LivingEntity: } fun getNovaEquipable(itemStack: ItemStack): Equipable? { - val wearable = itemStack.novaItem?.getBehavior(Wearable::class) + val wearable = itemStack.novaItem?.getBehaviorOrNull(Wearable::class) ?: return null return object : Equipable { - override fun getEquipmentSlot() = wearable.options.armorType.equipmentSlot.nmsEquipmentSlot + override fun getEquipmentSlot() = wearable.slot.nmsEquipmentSlot - override fun getEquipSound() = wearable.options.equipSound?.let { + override fun getEquipSound() = wearable.equipSound?.let { SoundEvent.createVariableRangeEvent(ResourceLocation.tryParse(it)) } ?: SoundEvents.ARMOR_EQUIP_GENERIC diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/sound/SoundPatches.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/sound/SoundPatches.kt index c1ce004eab..a316de2236 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/sound/SoundPatches.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/sound/SoundPatches.kt @@ -216,7 +216,7 @@ internal object SoundPatches : MultiTransformer(MojangPlayer::class, MojangLivin fun getEquipSound(itemStack: MojangStack): SoundEvent? { val novaItem = itemStack.novaItem if (novaItem != null) { - val soundEventName = novaItem.getBehavior(Wearable::class)?.options?.equipSound + val soundEventName = novaItem.getBehaviorOrNull(Wearable::class)?.equipSound ?: return null return SoundEvent.createVariableRangeEvent(ResourceLocation.tryParse(soundEventName)) } diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/update/UpdateReminder.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/update/UpdateReminder.kt index 95cf4c93a0..ff37f3d143 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/update/UpdateReminder.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/update/UpdateReminder.kt @@ -25,8 +25,7 @@ import xyz.xenondevs.nova.util.unregisterEvents private val NOVA_DISTRIBUTORS = listOf( // GitHub is intentionally omitted because in our current setup releases are created before the jar is uploaded ProjectDistributor.hangar("xenondevs/Nova"), - ProjectDistributor.modrinth("nova-framework"), - ProjectDistributor.spigotmc(93648) + ProjectDistributor.modrinth("nova-framework") ) private val ENABLED by configReloadable { DEFAULT_CONFIG.getBoolean("update_reminder.enabled") } diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/util/EntityUtils.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/util/EntityUtils.kt index 14c66e89e7..e6a1ca8a37 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/util/EntityUtils.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/util/EntityUtils.kt @@ -12,10 +12,11 @@ import org.bukkit.Bukkit import org.bukkit.Location import org.bukkit.craftbukkit.v1_20_R1.CraftServer import org.bukkit.craftbukkit.v1_20_R1.entity.CraftEntity -import org.bukkit.entity.Entity -import org.bukkit.entity.Player -import org.bukkit.inventory.EquipmentSlot +import xyz.xenondevs.nova.item.behavior.Damageable +import xyz.xenondevs.nova.item.tool.ToolCategory +import xyz.xenondevs.nova.item.tool.VanillaToolCategory import xyz.xenondevs.nova.util.data.NBTUtils +import xyz.xenondevs.nova.util.item.novaItem import xyz.xenondevs.nova.world.block.logic.`break`.BlockBreaking import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -23,45 +24,108 @@ import java.util.* import java.util.concurrent.CopyOnWriteArrayList import net.minecraft.world.entity.Entity as MojangEntity import net.minecraft.world.entity.EntityType as NMSEntityType +import net.minecraft.world.entity.EquipmentSlot as MojangEquipmentSlot +import net.minecraft.world.entity.LivingEntity as MojangLivingEntity +import org.bukkit.entity.Entity as BukkitEntity +import org.bukkit.entity.LivingEntity as BukkitLivingEntity +import org.bukkit.entity.Player as BukkitPlayer +import org.bukkit.inventory.EquipmentSlot as BukkitEquipmentSlot /** * The current block destroy progress of the player. * Between 0 and 1 or null if the player is not breaking a block at the moment. */ -val Player.destroyProgress: Double? +val BukkitPlayer.destroyProgress: Double? get() = BlockBreaking.getBreaker(this)?.progress?.coerceAtMost(1.0) /** * Swings the [hand] of the player. * @throws IllegalArgumentException If the [hand] is not a valid hand. */ -fun Player.swingHand(hand: EquipmentSlot) { +fun BukkitPlayer.swingHand(hand: BukkitEquipmentSlot) { when (hand) { - EquipmentSlot.HAND -> swingMainHand() - EquipmentSlot.OFF_HAND -> swingOffHand() + BukkitEquipmentSlot.HAND -> swingMainHand() + BukkitEquipmentSlot.OFF_HAND -> swingOffHand() else -> throw IllegalArgumentException("EquipmentSlot is not a hand") } } /** - * Teleports the [Entity] after modifying its location using the [modifyLocation] lambda. + * Damages the item in the [entity's][BukkitLivingEntity] main hand by [damage] amount. */ -fun Entity.teleport(modifyLocation: Location.() -> Unit) { +fun BukkitLivingEntity.damageItemInMainHand(damage: Int = 1) { + if (damage <= 0) + return + val serverPlayer = nmsEntity as MojangLivingEntity + Damageable.damageAndBreak(serverPlayer.mainHandItem, damage) { serverPlayer.broadcastBreakEvent(MojangEquipmentSlot.MAINHAND) } +} + +/** + * Damages the item in the [entity's][BukkitLivingEntity] offhand by [damage] amount. + */ +fun BukkitLivingEntity.damageItemInOffHand(damage: Int = 1) { + if (damage <= 0) + return + val serverPlayer = nmsEntity as MojangLivingEntity + Damageable.damageAndBreak(serverPlayer.offhandItem, damage) { serverPlayer.broadcastBreakEvent(MojangEquipmentSlot.OFFHAND) } +} + +/** + * Damages the item in the specified [hand] by [damage] amount. + */ +fun BukkitLivingEntity.damageItemInHand(hand: BukkitEquipmentSlot, damage: Int = 1) { + when (hand) { + BukkitEquipmentSlot.HAND -> damageItemInMainHand(damage) + BukkitEquipmentSlot.OFF_HAND -> damageItemInOffHand(damage) + else -> throw IllegalArgumentException("Not a hand: $hand") + } +} + +/** + * Damages the tool in the [entity's][BukkitLivingEntity] main hand as if they've broken a block. + */ +fun BukkitLivingEntity.damageToolBreakBlock() = damageToolInMainHand(Damageable::itemDamageOnBreakBlock, VanillaToolCategory::itemDamageOnBreakBlock) + +/** + * Damages the tool in the [entity's][BukkitLivingEntity] main hand as if they've attack an entity. + */ +fun BukkitLivingEntity.damageToolAttackEntity() = damageToolInMainHand(Damageable::itemDamageOnAttackEntity, VanillaToolCategory::itemDamageOnAttackEntity) + +private inline fun BukkitLivingEntity.damageToolInMainHand(getNovaDamage: (Damageable) -> Int, getVanillaDamage: (VanillaToolCategory) -> Int) { + val itemStack = (nmsEntity as MojangLivingEntity).mainHandItem + val novaItem = itemStack.novaItem + + val damage: Int + if (novaItem != null) { + val damageable = novaItem.getBehaviorOrNull() ?: return + damage = getNovaDamage(damageable) + } else { + val toolCategory = ToolCategory.ofItem(itemStack.bukkitMirror) as? VanillaToolCategory ?: return + damage = getVanillaDamage(toolCategory) + } + + damageItemInMainHand(damage) +} + +/** + * Teleports the [BukkitEntity] after modifying its location using the [modifyLocation] lambda. + */ +fun BukkitEntity.teleport(modifyLocation: Location.() -> Unit) { val location = location location.modifyLocation() teleport(location) } /** - * The translation key for the name of this [Entity]. + * The translation key for the name of this [BukkitEntity]. */ -val Entity.localizedName: String? +val BukkitEntity.localizedName: String? get() = (this as CraftEntity).handle.type.descriptionId /** - * If the [Entity's][Entity] eye is underwater. + * If the [Entity's][BukkitEntity] eye is underwater. */ -val Entity.eyeInWater: Boolean +val BukkitEntity.eyeInWater: Boolean get() = (this as CraftEntity).handle.isEyeInFluid(FluidTags.WATER) object EntityUtils { @@ -69,11 +133,11 @@ object EntityUtils { internal val DUMMY_PLAYER = createFakePlayer(Location(Bukkit.getWorlds()[0], 0.0, 0.0, 0.0), UUID.randomUUID(), "Nova Dummy Player") /** - * Gets a list of all passengers of this [Entity], including passengers of passengers. + * Gets a list of all passengers of this [BukkitEntity], including passengers of passengers. */ - fun getAllPassengers(entity: Entity): List { - val entitiesToCheck = CopyOnWriteArrayList().apply { add(entity) } - val passengers = ArrayList() + fun getAllPassengers(entity: BukkitEntity): List { + val entitiesToCheck = CopyOnWriteArrayList().apply { add(entity) } + val passengers = ArrayList() while (entitiesToCheck.isNotEmpty()) { for (entityToCheck in entitiesToCheck) { @@ -88,13 +152,13 @@ object EntityUtils { } /** - * Serializes an [Entity] to a [ByteArray]. + * Serializes an [BukkitEntity] to a [ByteArray]. * - * @param remove If the serialized [Entity] should be removed from the world. + * @param remove If the serialized [BukkitEntity] should be removed from the world. * @param nbtModifier Called before the [CompoundTag] gets compressed to a [ByteArray] to allow modifications. */ fun serialize( - entity: Entity, + entity: BukkitEntity, remove: Boolean = false, nbtModifier: ((CompoundTag) -> CompoundTag)? = null ): ByteArray { @@ -124,9 +188,9 @@ object EntityUtils { } /** - * Spawns an [Entity] based on serialized [data] and a [location]. + * Spawns an [BukkitEntity] based on serialized [data] and a [location]. * - * @param nbtModifier Called before the [Entity] gets spawned into the world to allow nbt modifications. + * @param nbtModifier Called before the [BukkitEntity] gets spawned into the world to allow nbt modifications. */ fun deserializeAndSpawn( data: ByteArray, diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/util/FakeOnlinePlayer.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/util/FakeOnlinePlayer.kt index b8ed11ac77..824775fb91 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/util/FakeOnlinePlayer.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/util/FakeOnlinePlayer.kt @@ -72,6 +72,7 @@ import org.bukkit.util.RayTraceResult import org.bukkit.util.Vector import org.jetbrains.annotations.Contract import xyz.xenondevs.nova.integration.permission.PermissionManager +import java.net.InetAddress import java.net.InetSocketAddress import java.util.* @@ -183,7 +184,7 @@ class FakeOnlinePlayer( throw UnsupportedOperationException("Player is not online") } - override fun banIp(reason: String?, expires: Date?, source: String?, kickPlayer: Boolean): BanEntry? { + override fun banIp(reason: String?, expires: Date?, source: String?, kickPlayer: Boolean): BanEntry? { throw UnsupportedOperationException("Player is not online") } @@ -1166,6 +1167,10 @@ class FakeOnlinePlayer( throw UnsupportedOperationException("Player is not online") } + override fun playHurtAnimation(yaw: Float) { + throw UnsupportedOperationException("Player is not online") + } + override fun setCollidable(collidable: Boolean) { throw UnsupportedOperationException("Player is not online") } diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/DamageableUtils.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/DamageableUtils.kt deleted file mode 100644 index 7b2bed2f89..0000000000 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/DamageableUtils.kt +++ /dev/null @@ -1,264 +0,0 @@ -@file:Suppress("unused") - -package xyz.xenondevs.nova.util.item - -import net.minecraft.advancements.CriteriaTriggers -import net.minecraft.server.level.ServerPlayer -import net.minecraft.stats.Stats -import net.minecraft.world.entity.LivingEntity -import net.minecraft.world.item.enchantment.EnchantmentHelper -import net.minecraft.world.item.enchantment.Enchantments -import org.bukkit.craftbukkit.v1_20_R1.util.CraftMagicNumbers -import org.bukkit.enchantments.Enchantment -import org.bukkit.entity.Player -import org.bukkit.event.player.PlayerItemBreakEvent -import org.bukkit.event.player.PlayerItemDamageEvent -import org.bukkit.inventory.EquipmentSlot -import org.bukkit.inventory.ItemStack -import org.bukkit.inventory.meta.Damageable -import xyz.xenondevs.nova.util.bukkitMirror -import xyz.xenondevs.nova.util.callEvent -import xyz.xenondevs.nova.util.nmsCopy -import xyz.xenondevs.nova.util.serverPlayer -import kotlin.random.Random -import net.minecraft.world.entity.player.Player as MojangPlayer -import net.minecraft.world.item.ItemStack as MojangStack -import xyz.xenondevs.nova.item.behavior.Damageable as NovaDamageable - -/** - * Damages the tool in the [player's][MojangPlayer] main hand by [damage] amount. - */ -fun Player.damageItemInMainHand(damage: Int = 1) { - val serverPlayer = serverPlayer - if (DamageableUtils.damageAndBreakItem(serverPlayer.mainHandItem, damage, serverPlayer) == ItemDamageResult.BROKEN) { - serverPlayer.broadcastBreakEvent(net.minecraft.world.entity.EquipmentSlot.MAINHAND) - } -} - -/** - * Damages the tool in the [player's][MojangPlayer] offhand by [damage] amount. - */ -fun Player.damageItemInOffHand(damage: Int = 1) { - val serverPlayer = serverPlayer - if (DamageableUtils.damageAndBreakItem(serverPlayer.offhandItem, damage, serverPlayer) == ItemDamageResult.BROKEN) { - serverPlayer.broadcastBreakEvent(net.minecraft.world.entity.EquipmentSlot.OFFHAND) - } -} - -/** - * Damages the tool in the specified [hand] by [damage] amount. - */ -fun Player.damageItemInHand(hand: EquipmentSlot, damage: Int = 1) { - when (hand) { - EquipmentSlot.HAND -> damageItemInMainHand(damage) - EquipmentSlot.OFF_HAND -> damageItemInOffHand(damage) - else -> throw IllegalArgumentException("Not a hand: $hand") - } -} - -object DamageableUtils { - - /** - * Damages the given [itemStack] while taking the unbreaking enchantment and unbreakable property into account. - * - * This method works for both vanilla and Nova tools. - * - * @return The same [ItemStack] with the durability possibly reduced or null if the item was broken. - */ - fun damageItem(itemStack: ItemStack, damage: Int = 1): ItemStack? { - val meta = itemStack.itemMeta ?: return itemStack - - if (meta.isUnbreakable) - return itemStack - - val unbreakingLevel = meta.getEnchantLevel(Enchantment.DURABILITY) - if (unbreakingLevel > 0 && Random.nextInt(0, unbreakingLevel + 1) > 0) - return itemStack - - val novaDamageable = itemStack.novaItem?.getBehavior(NovaDamageable::class) - if (novaDamageable != null) { - val newDamage = novaDamageable.getDamage(itemStack) + damage - novaDamageable.setDamage(itemStack, newDamage) - if (newDamage >= novaDamageable.options.durability) - return null - } else if (meta is Damageable && itemStack.type.maxDurability > 0) { - meta.damage += damage - if (meta.damage >= itemStack.type.maxDurability) - return null - itemStack.itemMeta = meta - } - - return itemStack - } - - /** - * Damages an [itemStack] with damage amount [damage] for [entity]. - * - * This method takes the unbreaking enchantment into consideration and also calls the item_durability_changed - * criteria trigger if [entity] is not null. - * - * @return If the item is now broken - */ - @Suppress("NAME_SHADOWING") - internal fun damageAndBreakItem(itemStack: MojangStack, damage: Int, entity: LivingEntity?): ItemDamageResult { - var damage = damage - - // check for creative mode - if (entity is MojangPlayer && entity.abilities.instabuild) - return ItemDamageResult.UNDAMAGED - - // check if the item is damageable - val novaDamageable = itemStack.novaItem?.getBehavior(NovaDamageable::class) - if (novaDamageable == null && !itemStack.isDamageableItem) - return ItemDamageResult.UNDAMAGED - - // consider unbreaking level - val unbreakingLevel = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.UNBREAKING, itemStack) - if (unbreakingLevel > 0 && Random.nextInt(0, unbreakingLevel + 1) > 0) - return ItemDamageResult.UNDAMAGED - - // fire PlayerItemDamageEvent - if (entity is ServerPlayer) { - val event = PlayerItemDamageEvent(entity.bukkitEntity, itemStack.bukkitMirror, damage) - callEvent(event) - - damage = event.damage - if (damage <= 0 || event.isCancelled) - return ItemDamageResult.UNDAMAGED - } - - // damage item - var broken = false - if (novaDamageable != null) { - val novaCompound = itemStack.novaCompound - - if (entity is ServerPlayer) - CriteriaTriggers.ITEM_DURABILITY_CHANGED.trigger(entity, itemStack, novaDamageable.getDamage(novaCompound) + damage) - - val newDamage = novaDamageable.getDamage(novaCompound) + damage - novaDamageable.setDamage(novaCompound, newDamage) - if (newDamage >= novaDamageable.options.durability) - broken = true - } else if (itemStack.isDamageableItem) { - if (entity is ServerPlayer) - CriteriaTriggers.ITEM_DURABILITY_CHANGED.trigger(entity, itemStack, itemStack.damageValue + damage) - - itemStack.damageValue += damage - if (itemStack.damageValue >= itemStack.maxDamage) - broken = true - } else return ItemDamageResult.UNDAMAGED - - if (broken) { - if (entity is ServerPlayer) { - // only award stats for non-nova items - if (novaDamageable == null) - entity.awardStat(Stats.ITEM_BROKEN.get(itemStack.item)) - - // fire PlayerBreakItemEvent - callEvent(PlayerItemBreakEvent(entity.bukkitEntity, itemStack.bukkitMirror)) - } - - // remove item - itemStack.shrink(1) - } - - return if (broken) ItemDamageResult.BROKEN else ItemDamageResult.DAMAGED - } - - fun isDamageable(itemStack: ItemStack): Boolean { - val novaDamageable = itemStack.novaItem?.getBehavior(NovaDamageable::class) - if (novaDamageable != null) - return true - - return itemStack.type.maxDurability > 0 - } - - fun getMaxDurability(itemStack: ItemStack): Int { - val damageable = itemStack.novaItem?.getBehavior(NovaDamageable::class) - if (damageable != null) { - return damageable.options.durability - } - - return itemStack.type.maxDurability.toInt() - } - - fun getDamage(itemStack: ItemStack): Int { - val damageable = itemStack.novaItem?.getBehavior(NovaDamageable::class) - if (damageable != null) { - return damageable.getDamage(itemStack) - } - - return (itemStack.itemMeta as? Damageable)?.damage ?: 0 - } - - fun setDamage(itemStack: ItemStack, damage: Int) { - val damageable = itemStack.novaItem?.getBehavior(NovaDamageable::class) - if (damageable != null) { - damageable.setDamage(itemStack, damage) - } else { - val itemMeta = itemStack.itemMeta as? Damageable ?: return - itemMeta.damage = damage - itemStack.itemMeta = itemMeta - } - } - - fun isValidRepairItem(first: ItemStack, second: ItemStack): Boolean { - val damageable = first.novaItem?.getBehavior(NovaDamageable::class) - if (damageable != null) { - return damageable.options.repairIngredient?.test(second) ?: false - } - - return CraftMagicNumbers.getItem(first.type).isValidRepairItem(first.nmsCopy, second.nmsCopy) - } - - internal fun isDamageable(itemStack: MojangStack): Boolean { - val novaDamageable = itemStack.novaItem?.getBehavior(NovaDamageable::class) - if (novaDamageable != null) - return true - - return itemStack.item.canBeDepleted() - } - - internal fun getMaxDurability(itemStack: MojangStack): Int { - val damageable = itemStack.novaItem?.getBehavior(NovaDamageable::class) - if (damageable != null) { - return damageable.options.durability - } - - return itemStack.maxDamage - } - - internal fun getDamage(itemStack: MojangStack): Int { - val damageable = itemStack.novaItem?.getBehavior(NovaDamageable::class) - if (damageable != null) { - return damageable.getDamage(itemStack) - } - - return itemStack.damageValue - } - - internal fun setDamage(itemStack: MojangStack, damage: Int) { - val damageable = itemStack.novaItem?.getBehavior(NovaDamageable::class) - if (damageable != null) { - damageable.setDamage(itemStack, damage) - } else { - itemStack.damageValue = damage - } - } - - internal fun isValidRepairItem(first: MojangStack, second: MojangStack): Boolean { - val damageable = first.novaItem?.getBehavior(NovaDamageable::class) - if (damageable != null) { - return damageable.options.repairIngredient?.test(second.bukkitMirror) ?: false - } - - return first.item.isValidRepairItem(first, second) - } - -} - -internal enum class ItemDamageResult { - UNDAMAGED, - DAMAGED, - BROKEN -} \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/FuelUtils.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/FuelUtils.kt deleted file mode 100644 index 25c597efdd..0000000000 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/FuelUtils.kt +++ /dev/null @@ -1,66 +0,0 @@ -package xyz.xenondevs.nova.util.item - -import net.minecraft.world.item.Item -import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity -import org.bukkit.Material -import org.bukkit.craftbukkit.v1_20_R1.util.CraftMagicNumbers -import org.bukkit.inventory.ItemStack -import xyz.xenondevs.commons.collections.enumMap -import xyz.xenondevs.nova.item.behavior.Fuel -import net.minecraft.world.item.ItemStack as MojangStack - -val Material.burnTime: Int? - get() = FuelUtils.getBurnTime(this) - -val ItemStack.isFuel: Boolean - get() = FuelUtils.isFuel(this) - -val ItemStack.burnTime: Int? - get() = FuelUtils.getBurnTime(this) - -object FuelUtils { - - private val NMS_VANILLA_FUELS: Map = AbstractFurnaceBlockEntity.getFuel() - private val VANILLA_FUELS: Map = NMS_VANILLA_FUELS - .mapKeysTo(enumMap()) { (item, _) -> CraftMagicNumbers.getMaterial(item) } - - fun isFuel(material: Material): Boolean = material in VANILLA_FUELS - fun getBurnTime(material: Material): Int? = VANILLA_FUELS[material] - - fun isFuel(itemStack: ItemStack): Boolean { - val novaItem = itemStack.novaItem - if (novaItem != null) { - return novaItem.hasBehavior(Fuel::class) - } - - return itemStack.type in VANILLA_FUELS - } - - fun getBurnTime(itemStack: ItemStack): Int? { - val novaItem = itemStack.novaItem - if (novaItem != null) { - return novaItem.getBehavior(Fuel::class)?.options?.burnTime - } - - return getBurnTime(itemStack.type) - } - - fun isFuel(itemStack: MojangStack): Boolean { - val novaItem = itemStack.novaItem - if (novaItem != null) { - return novaItem.hasBehavior(Fuel::class) - } - - return itemStack.item in NMS_VANILLA_FUELS - } - - fun getBurnTime(itemStack: MojangStack): Int? { - val novaItem = itemStack.novaItem - if (novaItem != null) { - return novaItem.getBehavior(Fuel::class)?.options?.burnTime - } - - return NMS_VANILLA_FUELS[itemStack.item] - } - -} \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/ItemUtils.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/ItemUtils.kt index 2ace6b4080..1006c99e77 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/ItemUtils.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/ItemUtils.kt @@ -132,10 +132,18 @@ internal val ItemStack.handle: MojangStack? val ItemMeta.unhandledTags: MutableMap get() = ReflectionRegistry.CRAFT_META_ITEM_UNHANDLED_TAGS_FIELD.get(this) as MutableMap -val ItemStack.canDestroy: List +// fixme: does not work on paper because paper actually has an api for this +val ItemStack.canDestroy: Set get() { - val tag = itemMeta?.unhandledTags?.get("CanDestroy") as? ListTag ?: return emptyList() - return tag.mapNotNull { runCatching { ResourceLocation.of(it.asString, ':') }.getOrNull()?.let { Material.valueOf(it.name) } } + val tag = itemMeta?.unhandledTags?.get("CanDestroy") as? ListTag ?: return emptySet() + return tag.mapNotNullTo(HashSet()) { strTag -> ResourceLocation.tryParse(strTag.asString)?.let { Material.valueOf(it.name) } } + } + +// fixme: does not work on paper because paper actually has an api for this +val ItemStack.canPlaceOn: Set + get() { + val tag = itemMeta?.unhandledTags?.get("CanPlaceOn") as? ListTag ?: return emptySet() + return tag.mapNotNullTo(HashSet()) { strTag -> ResourceLocation.tryParse(strTag.asString)?.let { Material.valueOf(it.name) } } } val ItemStack.craftingRemainingItem: ItemStack? @@ -151,7 +159,7 @@ val ItemStack.equipSound: String? get() { val novaItem = novaItem if (novaItem != null) - return novaItem.getBehavior(Wearable::class)?.options?.equipSound + return novaItem.getBehaviorOrNull(Wearable::class)?.equipSound return (CraftMagicNumbers.getItem(type) as? ArmorItem)?.material?.equipSound?.location?.toString() } @@ -166,6 +174,9 @@ fun ItemStack.isSimilarIgnoringName(other: ItemStack?): Boolean { fun ItemStack.takeUnlessEmpty(): ItemStack? = if (type.isAir || amount <= 0) null else this +fun ItemStack?.isEmpty(): Boolean = + this == null || type.isAir || amount <= 0 + internal var MojangStack.adventureName: Component get() = tag ?.getOrNull("display") diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/ToolUtils.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/ToolUtils.kt index 001ecede9d..3e0e9a680e 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/ToolUtils.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/util/item/ToolUtils.kt @@ -21,49 +21,8 @@ import xyz.xenondevs.nova.util.bukkitMirror import xyz.xenondevs.nova.util.eyeInWater import xyz.xenondevs.nova.util.nmsCopy import xyz.xenondevs.nova.util.roundToDecimalPlaces -import xyz.xenondevs.nova.util.serverPlayer import xyz.xenondevs.nova.world.block.BlockManager import xyz.xenondevs.nova.world.pos -import net.minecraft.world.entity.EquipmentSlot as MojangEquipmentSlot -import net.minecraft.world.item.ItemStack as MojangStack -import xyz.xenondevs.nova.item.behavior.Damageable as NovaDamageable - -/** - * Damages the tool in the [player's][Player] main hand as if they've broken a block. - */ -fun Player.damageToolBreakBlock() = damageToolInMainHand { - val novaItem = it.novaItem - if (novaItem != null) { - if (novaItem.hasBehavior(Tool::class)) { - novaItem.getBehavior(NovaDamageable::class)?.options?.itemDamageOnBreakBlock ?: 0 - } else 0 - } else (ToolCategory.ofItem(it.bukkitMirror) as? VanillaToolCategory)?.itemDamageOnBreakBlock ?: 0 -} - -/** - * Damages the tool in the [player's][Player] main hand as if they've attack an entity. - */ -fun Player.damageToolAttackEntity() = damageToolInMainHand { - val novaItem = it.novaItem - if (novaItem != null) { - if (novaItem.hasBehavior(Tool::class)) { - novaItem.getBehavior(NovaDamageable::class)?.options?.itemDamageOnAttackEntity ?: 0 - } else 0 - } else (ToolCategory.ofItem(it.bukkitMirror) as? VanillaToolCategory)?.itemDamageOnAttackEntity ?: 0 -} - -private inline fun Player.damageToolInMainHand(damageReceiver: (MojangStack) -> Int) { - val serverPlayer = serverPlayer - val itemStack = serverPlayer.mainHandItem - val damage = damageReceiver(itemStack) - - if (damage <= 0) - return - - if (DamageableUtils.damageAndBreakItem(itemStack, damage, serverPlayer) == ItemDamageResult.BROKEN) { - serverPlayer.broadcastBreakEvent(MojangEquipmentSlot.MAINHAND) - } -} object ToolUtils { @@ -166,7 +125,7 @@ object ToolUtils { when (player.gameMode) { GameMode.CREATIVE -> { - val canBreakBlocks = tool?.novaItem?.getBehavior(Tool::class)?.options?.canBreakBlocksInCreative + val canBreakBlocks = tool?.novaItem?.getBehaviorOrNull(Tool::class)?.canBreakBlocksInCreative ?: (ToolCategory.ofItem(tool) as? VanillaToolCategory)?.canBreakBlocksInCreative ?: (tool?.type != Material.DEBUG_STICK && tool?.type != Material.TRIDENT) @@ -229,7 +188,7 @@ object ToolUtils { val novaItem = itemStack.novaItem if (novaItem != null) - return novaItem.getBehavior(Tool::class)?.options?.breakSpeed ?: 1.0 + return novaItem.getBehaviorOrNull(Tool::class)?.breakSpeed ?: 1.0 val vanillaToolCategory = ToolCategory.ofItem(itemStack) as? VanillaToolCategory if (vanillaToolCategory != null) { diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/break/BlockBreaker.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/break/BlockBreaker.kt index e6a76fdc6f..3b928a8ea3 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/break/BlockBreaker.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/break/BlockBreaker.kt @@ -35,9 +35,9 @@ import xyz.xenondevs.nova.util.BlockUtils import xyz.xenondevs.nova.util.advance import xyz.xenondevs.nova.util.axis import xyz.xenondevs.nova.util.callEvent +import xyz.xenondevs.nova.util.damageToolBreakBlock import xyz.xenondevs.nova.util.hardness import xyz.xenondevs.nova.util.item.ToolUtils -import xyz.xenondevs.nova.util.item.damageToolBreakBlock import xyz.xenondevs.nova.util.item.takeUnlessEmpty import xyz.xenondevs.nova.util.nmsCopy import xyz.xenondevs.nova.util.nmsState diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/place/BlockPlacing.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/place/BlockPlacing.kt index acb78e4861..df860f063e 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/place/BlockPlacing.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/place/BlockPlacing.kt @@ -4,6 +4,7 @@ import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.NamedTextColor import org.bukkit.GameMode import org.bukkit.Location +import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.Listener @@ -11,6 +12,7 @@ import org.bukkit.event.block.Action import org.bukkit.event.block.BlockMultiPlaceEvent import org.bukkit.event.block.BlockPlaceEvent import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.inventory.ItemStack import xyz.xenondevs.nova.data.world.WorldDataManager import xyz.xenondevs.nova.data.world.block.state.NovaBlockState import xyz.xenondevs.nova.integration.protection.ProtectionManager @@ -23,6 +25,7 @@ import xyz.xenondevs.nova.util.facing import xyz.xenondevs.nova.util.isCompletelyDenied import xyz.xenondevs.nova.util.isInsideWorldRestrictions import xyz.xenondevs.nova.util.isUnobstructed +import xyz.xenondevs.nova.util.item.canPlaceOn import xyz.xenondevs.nova.util.item.isActuallyInteractable import xyz.xenondevs.nova.util.item.isReplaceable import xyz.xenondevs.nova.util.item.novaItem @@ -32,6 +35,7 @@ import xyz.xenondevs.nova.util.runTask import xyz.xenondevs.nova.util.serverPlayer import xyz.xenondevs.nova.util.swingHand import xyz.xenondevs.nova.util.yaw +import xyz.xenondevs.nova.world.BlockPos import xyz.xenondevs.nova.world.block.BlockManager import xyz.xenondevs.nova.world.block.NovaBlock import xyz.xenondevs.nova.world.block.context.BlockPlaceContext @@ -116,7 +120,7 @@ internal object BlockPlacing : Listener { ?.also(futures::add) CombinedBooleanFuture(futures).runIfTrueOnSimilarThread { - if (!placeLoc.block.type.isReplaceable() || WorldDataManager.getBlockState(placeLoc.pos) != null) + if (!canPlace(player, handItem, placeLoc.pos, placeLoc.advance(event.blockFace.oppositeFace).pos)) return@runIfTrueOnSimilarThread val ctx = BlockPlaceContext( @@ -129,7 +133,7 @@ internal object BlockPlacing : Listener { if (result.allowed) { BlockManager.placeBlockState(material, ctx) - if (player.gameMode == GameMode.SURVIVAL) handItem.amount-- + if (player.gameMode != GameMode.CREATIVE) handItem.amount-- runTask { player.swingHand(event.hand!!) } } else { player.sendMessage(Component.text(result.message, NamedTextColor.RED)) @@ -142,23 +146,31 @@ internal object BlockPlacing : Listener { val player = event.player val handItem = event.item!! - val block = event.clickedBlock!! + val placedOn = event.clickedBlock!!.pos + val block = event.clickedBlock!!.location.advance(event.blockFace).pos - val replaceLocation = block.location.advance(event.blockFace) - val replaceBlock = replaceLocation.block - - // check if the player is allowed to place a block there - if (replaceLocation.isInsideWorldRestrictions()) { - ProtectionManager.canPlace(player, handItem, replaceLocation).runIfTrueOnSimilarThread { - // check that there isn't already a block there (which is not replaceable) - if (replaceBlock.type.isReplaceable() && WorldDataManager.getBlockState(replaceBlock.pos) == null) { - val placed = replaceBlock.placeVanilla(player.serverPlayer, handItem, true) - if (placed && player.gameMode != GameMode.CREATIVE) { - player.inventory.setItem(event.hand!!, handItem.apply { amount -= 1 }) - } + ProtectionManager.canPlace(player, handItem, block.location).runIfTrueOnSimilarThread { + if (canPlace(player, handItem, block, placedOn)) { + val placed = block.block.placeVanilla(player.serverPlayer, handItem, true) + if (placed && player.gameMode != GameMode.CREATIVE) { + player.inventory.setItem(event.hand!!, handItem.apply { amount -= 1 }) } } } } + private fun canPlace(player: Player, item: ItemStack, block: BlockPos, placedOn: BlockPos): Boolean { + if ( + player.gameMode == GameMode.SPECTATOR + || !block.location.isInsideWorldRestrictions() + || !block.block.type.isReplaceable() + || WorldDataManager.getBlockState(block) != null + ) return false + + if (player.gameMode == GameMode.ADVENTURE) + return placedOn.block.type in item.canPlaceOn + + return true + } + } \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/tileentity/EnchantmentTableLogic.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/tileentity/EnchantmentTableLogic.kt index 242db0ba79..7c92fda7b6 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/tileentity/EnchantmentTableLogic.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/tileentity/EnchantmentTableLogic.kt @@ -272,7 +272,7 @@ internal object EnchantmentTableLogic { val enchantments: Sequence val novaItem = itemStack.novaItem if (novaItem != null) { - val categories = novaItem.getBehavior()?.options?.enchantmentCategories?.takeUnlessEmpty() + val categories = novaItem.getBehaviorOrNull()?.enchantmentCategories?.takeUnlessEmpty() ?: return ArrayList() enchantments = categories.asSequence() @@ -310,7 +310,7 @@ internal object EnchantmentTableLogic { private fun getEnchantmentValue(itemStack: ItemStack): Int { val novaItem = itemStack.novaItem if (novaItem != null) { - return novaItem.getBehavior()?.options?.enchantmentValue ?: 0 + return novaItem.getBehaviorOrNull()?.enchantmentValue ?: 0 } else { return itemStack.item.enchantmentValue }