feat: Implement dynamic event handling system for items and components

This commit is contained in:
Kariya 2025-12-18 16:27:31 +00:00
parent 6bc2fb2be2
commit fe002a5372
15 changed files with 564 additions and 292 deletions

View File

@ -1,119 +1,98 @@
# Content Creation Guide # Content Creation Guide
[日本語 (Japanese)](creating_new_content_ja.md)
This project provides a framework for adding new items and features (components) using the HcuItems plugin. This project provides a framework for adding new items and features (components) using the HcuItems plugin.
Event handling is uniformly managed by the `EventListener`, which delegates to appropriate classes based on information in the item's PersistentDataContainer (PDC). ### Event Handling Architecture
## Architecture Overview The event system has been fully refactored to be dynamic.
The `EventListener` collects event interests from all registered items and components via their `getEventHandlers()` method and dynamically registers the necessary Bukkit listeners.
All events (right-click, damage, movement, etc.) are received by the `EventListener`. When an event occurs:
1. `EventListener` receives the event.
2. It determines the context (e.g., extracting the `ItemStack` from `PlayerInteractEvent`).
3. It dispatches the event to the appropriate `AbstractItem` or `CustomComponent` handler if the item matches.
The `EventListener` delegates processing through the following steps: ### 1. Creating an AbstractItem (Custom Item)
1. Retrieves the item involved in the event.
2. Checks the item's PDC.
- If an `AbstractItem` ID (`hcu_items:id`) exists → Retrieves the item definition from `ItemRegistry` and executes.
- If a component key (`hcu_items:component_id`) exists → Retrieves the component definition from `ComponentRegistry` and executes.
## 1. Creating an AbstractItem (Custom Item)
Use this when creating an item with completely unique behavior. Use this when creating an item with completely unique behavior.
### Steps #### Steps
1. Create a new class that inherits from the `AbstractItem` class. 1. Create a new class that inherits from the `AbstractItem` class.
2. Specify a unique ID in the constructor. 2. Specify a unique ID in the constructor.
3. Implement the `buildItem` method to define the item's appearance (Material, Name, Lore, etc.). 3. Implement the `buildItem` method to define the item's appearance.
4. Override necessary event handlers (`onInteract`, `onPlayerMove`, etc.). 4. **Implement `getEventHandlers`** to define which events this item handles.
5. Register by calling `ItemRegistry.register(YourItem())` in `App.kt`'s `onEnable`. 5. Register by calling `ItemRegistry.register(YourItem())` in `App.kt`'s `onEnable`.
#### Example
```kotlin ```kotlin
class MyCustomItem : AbstractItem("my_custom_item") { class MyCustomItem : AbstractItem("my_custom_item") {
// Define item appearance
override fun buildItem(tier: Tier): ItemStack { override fun buildItem(tier: Tier): ItemStack {
return ItemStack(Material.DIAMOND_SWORD).apply { return ItemStack(Material.DIAMOND_SWORD).apply {
// Meta configuration // ... meta ...
} }
} }
override fun onInteract(event: PlayerInteractEvent) { // Register Event Handlers
event.player.sendMessage("Used custom item!") override fun getEventHandlers(): Map<KClass<out Event>, ComponentEventHandler<*>> {
return mapOf(
// Handle Player Interact
PlayerInteractEvent::class to { event, item ->
val e = event as PlayerInteractEvent
e.player.sendMessage("You used the custom item!")
},
// Handle Entity Damage
EntityDamageEvent::class to { event, item ->
val e = event as EntityDamageEvent
e.damage *= 2.0 // Double damage
}
)
}
// Lifecycle method (Not a Bukkit event) - safe to override directly
override fun onTick(player: Player, item: ItemStack) {
// Run logic every tick (or configured interval)
} }
} }
``` ```
## 2. Creating a CustomComponent (Adding Features to Existing Items) ### 2. Creating a CustomComponent
Use this when adding common features (e.g., glider, passive effects) to existing items or items that meet specific conditions. Use this to add shared behavior (like a "Double Jump" ability) to multiple items.
### Steps #### Steps
1. Create a class that inherits from `AbstractComponent` and implements `CustomComponent` (or `EquippableComponent`). 1. Create a class that inherits from `AbstractComponent` and implements `CustomComponent`.
2. Specify a unique component ID in the constructor. 2. Implement `getEventHandlers` to define event logic.
3. Implement the `apply` method with logic to apply the component to the item (key registration to PDC is automatic, but describe if additional metadata is needed). 3. Register in `App.kt`.
4. Override necessary event handlers.
5. Register by calling `ComponentRegistry.register(YourComponent(this))` in `App.kt`'s `onEnable`. #### Example
```kotlin ```kotlin
class MyComponent(plugin: App) : AbstractComponent(plugin, "my_component") { class MyComponent(plugin: App) : AbstractComponent(plugin, "my_component") {
override fun onInteract(event: PlayerInteractEvent) {
event.player.sendMessage("Component interaction!") override fun getEventHandlers(): Map<KClass<out Event>, ComponentEventHandler<*>> {
return mapOf(
PlayerToggleSneakEvent::class to { event, item ->
val e = event as PlayerToggleSneakEvent
if (e.isSneaking) {
e.player.sendMessage("Component activated by sneaking!")
}
}
)
} }
} }
``` ```
### Event Mechanism ### 3. Configuration Management
`AbstractComponent` generates a `NamespacedKey` during initialization and writes this key to the item's PDC during `apply`. Use the `Config` object to manage values.
When the `EventListener` confirms this key exists in the item's PDC, it calls your component's event handler.
## Notes
- `AbstractItem` and `CustomComponent` can coexist. A single item can be an `AbstractItem` and have multiple `CustomComponent`s.
- Within event handlers, appropriately control `event.isCancelled` as needed.
## 3. Configuration Management
The project uses a centralized `Config` object to manage configuration values from `config.yml`. This allows for easy adjustment of values (like cooldowns) without code changes.
### Accessing Configuration
To access configuration values, simply access the public properties of the `Config` object:
```kotlin ```kotlin
import net.hareworks.hcu.items.config.Config val cooldown = Config.myFeatureCooldown
// Example usage
val cooldown = Cooldown(Config.blinkCooldown)
```
### Adding New Config Values
1. **Add Property to Config Object**: Add a new property to `net.hareworks.hcu.items.config.Config.kt`.
2. **Update Load Method**: Read the value from `plugin.config` in the `load` method.
3. **Update Defaults**: Add the default value in the `saveDefaults` method.
```kotlin
object Config {
// 1. Add Property
var newFeatureEnabled: Boolean = true
fun load(plugin: JavaPlugin) {
// ... existing code ...
// 2. Load Value
newFeatureEnabled = config.getBoolean("components.new_feature.enabled", true)
// ... existing code ...
}
private fun saveDefaults(plugin: JavaPlugin, config: FileConfiguration) {
// ... existing code ...
// 3. Set Default
config.addDefault("components.new_feature.enabled", true)
// ... existing code ...
}
}
``` ```

View File

@ -0,0 +1,98 @@
# コンテンツ作成ガイド
[English](creating_new_content.md)
HcuItemsプラグインを使用して新しいアイテムや機能コンポーネントを追加するためのガイドです。
### イベントハンドリングのアーキテクチャ
イベントシステムは動的登録方式にリファクタリングされました。
`EventListener` は、登録されたすべてのアイテムとコンポーネントの `getEventHandlers()` メソッドから「どのイベントに興味があるか」という情報を収集し、必要なBukkitリスナーを動的に登録します。
イベント発生時の流れ:
1. `EventListener` がイベントを受け取ります。
2. イベントのコンテキスト(例: `PlayerInteractEvent` から `ItemStack` を取得)を解決します。
3. アイテムIDやコンポーネントIDが一致する場合、対応する `AbstractItem``CustomComponent` のハンドラーを実行します。
### 1. AbstractItem (カスタムアイテム) の作成
独自の挙動を持つ新しいアイテムを作成する場合に使用します。
#### 手順
1. `AbstractItem` クラスを継承した新しいクラスを作成します。
2. コンストラクタでユニークIDを指定します。
3. `buildItem` メソッドでアイテムの外見(マテリアル、名前、説明文など)を定義します。
4. **`getEventHandlers` を実装**し、このアイテムが処理するイベントを定義します。
5. `App.kt``onEnable``ItemRegistry.register(YourItem())` を呼び出して登録します。
#### 実装例
```kotlin
class MyCustomItem : AbstractItem("my_custom_item") {
// アイテムの外見定義
override fun buildItem(tier: Tier): ItemStack {
return ItemStack(Material.DIAMOND_SWORD).apply {
// ... meta設定 ...
}
}
// イベントハンドラーの登録
override fun getEventHandlers(): Map<KClass<out Event>, ComponentEventHandler<*>> {
return mapOf(
// プレイヤーのクリックイベント
PlayerInteractEvent::class to { event, item ->
val e = event as PlayerInteractEvent
e.player.sendMessage("カスタムアイテムを使用しました!")
},
// ダメージイベント
EntityDamageEvent::class to { event, item ->
val e = event as EntityDamageEvent
e.damage *= 2.0 // ダメージ2倍
}
)
}
// ライフサイクルメソッド (Bukkitイベントではない) - 直接オーバーライド可能
override fun onTick(player: Player, item: ItemStack) {
// 定期実行ロジック
}
}
```
### 2. CustomComponent (コンポーネント) の作成
「ダブルジャンプ」のような、複数のアイテムに共通して付与できる機能を作成する場合に使用します。
#### 手順
1. `AbstractComponent` を継承し、`CustomComponent` インターフェースを実装するクラスを作成します。
2. `getEventHandlers` を実装してロジックを記述します。
3. `App.kt` で登録します。
#### 実装例
```kotlin
class MyComponent(plugin: App) : AbstractComponent(plugin, "my_component") {
override fun getEventHandlers(): Map<KClass<out Event>, ComponentEventHandler<*>> {
return mapOf(
PlayerToggleSneakEvent::class to { event, item ->
val e = event as PlayerToggleSneakEvent
if (e.isSneaking) {
e.player.sendMessage("スニークでコンポーネント発動!")
}
}
)
}
}
```
### 3. 設定管理 (Configuration)
`Config` オブジェクトを使用して設定値を管理します。
```kotlin
val cooldown = Config.myFeatureCooldown
```

View File

@ -4,6 +4,9 @@ import io.papermc.paper.datacomponent.DataComponentType
import net.hareworks.hcu.items.api.Tier import net.hareworks.hcu.items.api.Tier
import org.bukkit.NamespacedKey import org.bukkit.NamespacedKey
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
import org.bukkit.event.Event
import kotlin.reflect.KClass
import net.hareworks.hcu.items.events.ComponentEventHandler
interface CustomComponent { interface CustomComponent {
val key: NamespacedKey val key: NamespacedKey
@ -19,15 +22,12 @@ interface CustomComponent {
val dataComponentDependencies: List<DataComponentType.NonValued> val dataComponentDependencies: List<DataComponentType.NonValued>
get() = emptyList() get() = emptyList()
/**
* Returns a map of event classes to their handlers.
*/
fun getEventHandlers(): Map<KClass<out Event>, ComponentEventHandler<*>> = emptyMap()
fun apply(item: ItemStack, tier: Tier? = null) fun apply(item: ItemStack, tier: Tier? = null)
fun has(item: ItemStack): Boolean fun has(item: ItemStack): Boolean
fun remove(item: ItemStack) fun remove(item: ItemStack)
fun onInteract(event: org.bukkit.event.player.PlayerInteractEvent) {}
fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {}
fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) {}
fun onToggleSneak(player: org.bukkit.entity.Player, item: org.bukkit.inventory.ItemStack, event: org.bukkit.event.player.PlayerToggleSneakEvent) {}
fun onToggleGlide(player: org.bukkit.entity.Player, item: org.bukkit.inventory.ItemStack, event: org.bukkit.event.entity.EntityToggleGlideEvent) {}
fun onBlockBreak(event: org.bukkit.event.block.BlockBreakEvent) {}
fun onItemHeld(player: org.bukkit.entity.Player, item: org.bukkit.inventory.ItemStack, event: org.bukkit.event.player.PlayerItemHeldEvent) {}
} }

View File

@ -3,12 +3,20 @@ package net.hareworks.hcu.items.api.item
import net.hareworks.hcu.items.api.Tier import net.hareworks.hcu.items.api.Tier
import org.bukkit.NamespacedKey import org.bukkit.NamespacedKey
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
import org.bukkit.event.Event
import kotlin.reflect.KClass
import net.hareworks.hcu.items.events.ComponentEventHandler
import org.bukkit.persistence.PersistentDataType import org.bukkit.persistence.PersistentDataType
abstract class AbstractItem(override val id: String) : CustomItem { abstract class AbstractItem(override val id: String) : CustomItem {
override val maxTier: Int = 5 override open val maxTier: Int = 5
override val minTier: Int = 1 override open val minTier: Int = 1
/**
* Returns a map of event classes to their handlers.
*/
override fun getEventHandlers(): Map<KClass<out Event>, ComponentEventHandler<*>> = emptyMap()
override fun createItemStack(tier: Tier): ItemStack { override fun createItemStack(tier: Tier): ItemStack {
val validTier = if (tier.level < minTier) Tier.fromLevel(minTier) val validTier = if (tier.level < minTier) Tier.fromLevel(minTier)

View File

@ -2,21 +2,22 @@ package net.hareworks.hcu.items.api.item
import net.hareworks.hcu.items.api.Tier import net.hareworks.hcu.items.api.Tier
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
import org.bukkit.event.Event
import kotlin.reflect.KClass
import net.hareworks.hcu.items.events.ComponentEventHandler
interface CustomItem { interface CustomItem {
val id: String val id: String
val maxTier: Int val maxTier: Int
val minTier: Int val minTier: Int
/**
* Returns a map of event classes to their handlers.
*/
fun getEventHandlers(): Map<KClass<out Event>, ComponentEventHandler<*>> = emptyMap()
fun createItemStack(tier: Tier = Tier.ONE): ItemStack fun createItemStack(tier: Tier = Tier.ONE): ItemStack
fun onInteract(event: org.bukkit.event.player.PlayerInteractEvent) {}
fun onFish(event: org.bukkit.event.player.PlayerFishEvent) {}
fun onProjectileHit(event: org.bukkit.event.entity.ProjectileHitEvent) {}
fun onProjectileLaunch(event: org.bukkit.event.entity.ProjectileLaunchEvent) {}
fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {}
fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) {}
fun onItemHeld(event: org.bukkit.event.player.PlayerItemHeldEvent) {}
fun onFoodLevelChange(event: org.bukkit.event.entity.FoodLevelChangeEvent) {}
fun onTick(player: org.bukkit.entity.Player, item: ItemStack) {} fun onTick(player: org.bukkit.entity.Player, item: ItemStack) {}
} }

View File

@ -44,6 +44,17 @@ class BlinkComponent(private val plugin: App) : AbstractComponent(plugin, "blink
private val TRAIL_COLOR = TextColor.color(0x9400D3) // Dark Violet private val TRAIL_COLOR = TextColor.color(0x9400D3) // Dark Violet
} }
override fun getEventHandlers(): Map<kotlin.reflect.KClass<out org.bukkit.event.Event>, net.hareworks.hcu.items.events.ComponentEventHandler<*>> {
return mapOf(
EntityToggleGlideEvent::class to { event, item ->
val e = event as EntityToggleGlideEvent
if (e.entity is Player) {
handleToggleGlide(e.entity as Player, item, e)
}
}
)
}
override fun apply(item: ItemStack, tier: Tier?) { override fun apply(item: ItemStack, tier: Tier?) {
if (isBoots(item.type)) { if (isBoots(item.type)) {
super.apply(item, tier) super.apply(item, tier)
@ -85,7 +96,7 @@ class BlinkComponent(private val plugin: App) : AbstractComponent(plugin, "blink
} }
} }
override fun onToggleGlide(player: Player, item: ItemStack, event: EntityToggleGlideEvent) { private fun handleToggleGlide(player: Player, item: ItemStack, event: EntityToggleGlideEvent) {
// Only process if this item actually has this component // Only process if this item actually has this component
if (!has(item)) return if (!has(item)) return

View File

@ -43,6 +43,17 @@ class DoubleJumpComponent(private val plugin: App) : AbstractComponent(plugin, "
private val COOLDOWN_COLOR = NamedTextColor.GRAY private val COOLDOWN_COLOR = NamedTextColor.GRAY
} }
override fun getEventHandlers(): Map<kotlin.reflect.KClass<out org.bukkit.event.Event>, net.hareworks.hcu.items.events.ComponentEventHandler<*>> {
return mapOf(
EntityToggleGlideEvent::class to { event, item ->
val e = event as EntityToggleGlideEvent
if (e.entity is Player) {
handleToggleGlide(e.entity as Player, item, e)
}
}
)
}
override fun apply(item: ItemStack, tier: Tier?) { override fun apply(item: ItemStack, tier: Tier?) {
if (isBoots(item.type)) { if (isBoots(item.type)) {
super.apply(item, tier) super.apply(item, tier)
@ -80,7 +91,7 @@ class DoubleJumpComponent(private val plugin: App) : AbstractComponent(plugin, "
} }
} }
override fun onToggleGlide(player: Player, item: ItemStack, event: EntityToggleGlideEvent) { private fun handleToggleGlide(player: Player, item: ItemStack, event: EntityToggleGlideEvent) {
// Only process if this item actually has this component // Only process if this item actually has this component
if (!has(item)) return if (!has(item)) return

View File

@ -43,6 +43,18 @@ class GliderComponent(private val plugin: App) : AbstractComponent(plugin, "glid
var lastTickTime: Long = System.currentTimeMillis() var lastTickTime: Long = System.currentTimeMillis()
) )
override fun getEventHandlers(): Map<kotlin.reflect.KClass<out org.bukkit.event.Event>, net.hareworks.hcu.items.events.ComponentEventHandler<*>> {
return mapOf(
org.bukkit.event.player.PlayerToggleSneakEvent::class to { event, item ->
val e = event as org.bukkit.event.player.PlayerToggleSneakEvent
handleToggleSneak(e.player, item, e)
},
org.bukkit.event.entity.EntityDamageEvent::class to { event, _ ->
handleEntityDamage(event as org.bukkit.event.entity.EntityDamageEvent)
}
)
}
init { init {
// Global Ticker for active gliders // Global Ticker for active gliders
val interval = net.hareworks.hcu.items.config.Config.global.tickInterval val interval = net.hareworks.hcu.items.config.Config.global.tickInterval
@ -88,7 +100,7 @@ class GliderComponent(private val plugin: App) : AbstractComponent(plugin, "glid
} }
} }
override fun onToggleSneak(player: Player, item: ItemStack, event: org.bukkit.event.player.PlayerToggleSneakEvent) { private fun handleToggleSneak(player: Player, item: ItemStack, event: org.bukkit.event.player.PlayerToggleSneakEvent) {
// Only trigger when STARTING to sneak // Only trigger when STARTING to sneak
if (event.isSneaking) { if (event.isSneaking) {
if (activeGliders.containsKey(player.uniqueId)) { if (activeGliders.containsKey(player.uniqueId)) {
@ -321,7 +333,7 @@ class GliderComponent(private val plugin: App) : AbstractComponent(plugin, "glid
private fun getHungerInterval(tier: Tier): Int = 40 + (tier.level - 1) * 20 private fun getHungerInterval(tier: Tier): Int = 40 + (tier.level - 1) * 20
override fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) { private fun handleEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {
if (event.cause == org.bukkit.event.entity.EntityDamageEvent.DamageCause.FALL) { if (event.cause == org.bukkit.event.entity.EntityDamageEvent.DamageCause.FALL) {
val entity = event.entity val entity = event.entity
if (entity is Player && activeGliders.containsKey(entity.uniqueId)) { if (entity is Player && activeGliders.containsKey(entity.uniqueId)) {

View File

@ -36,6 +36,12 @@ class VeinMinerComponent(private val plugin: JavaPlugin) : ToolComponent {
override val maxTier: Int = 1 override val maxTier: Int = 1
override val minTier: Int = 1 override val minTier: Int = 1
override fun getEventHandlers(): Map<kotlin.reflect.KClass<out org.bukkit.event.Event>, net.hareworks.hcu.items.events.ComponentEventHandler<*>> {
return mapOf(
BlockBreakEvent::class to { event, _ -> handleBlockBreak(event as BlockBreakEvent) }
)
}
private val activeHighlights = mutableMapOf<UUID, HighlightSession>() private val activeHighlights = mutableMapOf<UUID, HighlightSession>()
companion object { companion object {
@ -146,7 +152,7 @@ class VeinMinerComponent(private val plugin: JavaPlugin) : ToolComponent {
} }
} }
override fun onBlockBreak(event: BlockBreakEvent) { private fun handleBlockBreak(event: BlockBreakEvent) {
if (event.isCancelled) return if (event.isCancelled) return
val player = event.player val player = event.player
val block = event.block val block = event.block

View File

@ -16,6 +16,15 @@ class GrapplingItem : AbstractItem("grappling_hook") {
override val maxTier: Int = 5 override val maxTier: Int = 5
override fun getEventHandlers(): Map<kotlin.reflect.KClass<out org.bukkit.event.Event>, net.hareworks.hcu.items.events.ComponentEventHandler<*>> {
return mapOf(
PlayerFishEvent::class to { event, _ -> handleFish(event as PlayerFishEvent) },
org.bukkit.event.entity.ProjectileLaunchEvent::class to { event, _ -> handleProjectileLaunch(event as org.bukkit.event.entity.ProjectileLaunchEvent) },
org.bukkit.event.entity.ProjectileHitEvent::class to { event, _ -> handleProjectileHit(event as org.bukkit.event.entity.ProjectileHitEvent) },
org.bukkit.event.entity.EntityDamageEvent::class to { event, _ -> handleEntityDamage(event as org.bukkit.event.entity.EntityDamageEvent) }
)
}
override fun buildItem(tier: Tier): ItemStack { override fun buildItem(tier: Tier): ItemStack {
val item = ItemStack(Material.FISHING_ROD) val item = ItemStack(Material.FISHING_ROD)
val meta = item.itemMeta ?: return item val meta = item.itemMeta ?: return item
@ -45,7 +54,7 @@ class GrapplingItem : AbstractItem("grappling_hook") {
return item return item
} }
override fun onFish(event: PlayerFishEvent) { private fun handleFish(event: PlayerFishEvent) {
val hook = event.hook val hook = event.hook
val player = event.player val player = event.player
@ -117,7 +126,7 @@ class GrapplingItem : AbstractItem("grappling_hook") {
} }
} }
override fun onProjectileLaunch(event: org.bukkit.event.entity.ProjectileLaunchEvent) { private fun handleProjectileLaunch(event: org.bukkit.event.entity.ProjectileLaunchEvent) {
val projectile = event.entity val projectile = event.entity
val shooter = projectile.shooter val shooter = projectile.shooter
@ -130,7 +139,7 @@ class GrapplingItem : AbstractItem("grappling_hook") {
} }
} }
override fun onProjectileHit(event: org.bukkit.event.entity.ProjectileHitEvent) { private fun handleProjectileHit(event: org.bukkit.event.entity.ProjectileHitEvent) {
if ((event.hitBlock != null || event.hitEntity != null) && event.hitBlock?.isCollidable() == true) { if ((event.hitBlock != null || event.hitEntity != null) && event.hitBlock?.isCollidable() == true) {
val hook = event.entity val hook = event.entity
if (hook is org.bukkit.entity.FishHook) { if (hook is org.bukkit.entity.FishHook) {
@ -180,7 +189,7 @@ class GrapplingItem : AbstractItem("grappling_hook") {
} }
} }
override fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) { private fun handleEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {
if (event.cause == org.bukkit.event.entity.EntityDamageEvent.DamageCause.FALL) { if (event.cause == org.bukkit.event.entity.EntityDamageEvent.DamageCause.FALL) {
val player = event.entity val player = event.entity
if (player is org.bukkit.entity.Player) { if (player is org.bukkit.entity.Player) {

View File

@ -18,6 +18,12 @@ class MagnetItem : AbstractItem("magnet_item") {
override val maxTier: Int = 3 override val maxTier: Int = 3
override fun getEventHandlers(): Map<kotlin.reflect.KClass<out org.bukkit.event.Event>, net.hareworks.hcu.items.events.ComponentEventHandler<*>> {
return mapOf(
org.bukkit.event.player.PlayerItemHeldEvent::class to { event, _ -> handleItemHeld(event as org.bukkit.event.player.PlayerItemHeldEvent) }
)
}
companion object { companion object {
val KEY_RADIUS = NamespacedKey("hcu_items", "magnet_radius") val KEY_RADIUS = NamespacedKey("hcu_items", "magnet_radius")
} }
@ -98,7 +104,7 @@ class MagnetItem : AbstractItem("magnet_item") {
} }
} }
override fun onItemHeld(event: org.bukkit.event.player.PlayerItemHeldEvent) { private fun handleItemHeld(event: org.bukkit.event.player.PlayerItemHeldEvent) {
val player = event.player val player = event.player
val item = player.inventory.getItem(event.previousSlot) ?: return val item = player.inventory.getItem(event.previousSlot) ?: return

View File

@ -8,6 +8,13 @@ import org.bukkit.Material
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
class TestItem : AbstractItem("test_sword") { class TestItem : AbstractItem("test_sword") {
override fun getEventHandlers(): Map<kotlin.reflect.KClass<out org.bukkit.event.Event>, net.hareworks.hcu.items.events.ComponentEventHandler<*>> {
return mapOf(
org.bukkit.event.player.PlayerInteractEvent::class to { event, _ -> handleInteract(event as org.bukkit.event.player.PlayerInteractEvent) }
)
}
override fun buildItem(tier: Tier): ItemStack { override fun buildItem(tier: Tier): ItemStack {
val item = ItemStack(Material.DIAMOND_SWORD) val item = ItemStack(Material.DIAMOND_SWORD)
val meta = item.itemMeta ?: return item val meta = item.itemMeta ?: return item
@ -22,7 +29,7 @@ class TestItem : AbstractItem("test_sword") {
return item return item
} }
override fun onInteract(event: org.bukkit.event.player.PlayerInteractEvent) { private fun handleInteract(event: org.bukkit.event.player.PlayerInteractEvent) {
val item = event.item val item = event.item
val tier = AbstractItem.getTier(item) val tier = AbstractItem.getTier(item)

View File

@ -0,0 +1,12 @@
package net.hareworks.hcu.items.events
import org.bukkit.event.Event
import org.bukkit.inventory.ItemStack
/**
* Functional interface for component event handlers.
*
* @param event The event being handled.
* @param item The specific item stack associated with this component/item execution.
*/
typealias ComponentEventHandler<T> = (T, ItemStack) -> Unit

View File

@ -0,0 +1,179 @@
package net.hareworks.hcu.items.events
import org.bukkit.entity.Player
import org.bukkit.event.Event
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.entity.EntityDamageEvent
import org.bukkit.event.entity.EntityToggleGlideEvent
import org.bukkit.event.entity.FoodLevelChangeEvent
import org.bukkit.event.entity.ProjectileHitEvent
import org.bukkit.event.entity.ProjectileLaunchEvent
import org.bukkit.event.player.PlayerFishEvent
import org.bukkit.event.player.PlayerInteractEvent
import org.bukkit.event.player.PlayerItemHeldEvent
import org.bukkit.event.player.PlayerMoveEvent
import org.bukkit.event.player.PlayerToggleSneakEvent
import org.bukkit.inventory.ItemStack
import kotlin.reflect.KClass
/**
* Strategy to extract relevant context (items) from an event.
*/
interface EventContextStrategy<T : Event> {
/**
* Resolves the items that should trigger components for this event.
*/
fun resolveItems(event: T): List<ItemStack>
/**
* Resolves the primary player involved in the event, if any.
* This is useful for context checks.
*/
fun resolvePlayer(event: T): Player?
}
object EventStrategyRegistry {
private val strategies = mutableMapOf<KClass<out Event>, EventContextStrategy<*>>()
fun <T : Event> register(eventClass: KClass<T>, strategy: EventContextStrategy<T>) {
strategies[eventClass] = strategy
}
@Suppress("UNCHECKED_CAST")
fun <T : Event> get(eventClass: KClass<T>): EventContextStrategy<T>? {
return strategies[eventClass] as? EventContextStrategy<T>
}
init {
registerDefaults()
}
private fun registerDefaults() {
// PlayerInteractEvent
register(PlayerInteractEvent::class, object : EventContextStrategy<PlayerInteractEvent> {
override fun resolveItems(event: PlayerInteractEvent): List<ItemStack> {
val items = mutableListOf<ItemStack>()
event.item?.let { items.add(it) }
return items
}
override fun resolvePlayer(event: PlayerInteractEvent): Player = event.player
})
// EntityDamageEvent
register(EntityDamageEvent::class, object : EventContextStrategy<EntityDamageEvent> {
override fun resolveItems(event: EntityDamageEvent): List<ItemStack> {
val entity = event.entity as? Player ?: return emptyList()
val items = mutableListOf<ItemStack>()
items.addAll(entity.inventory.armorContents.filterNotNull())
items.add(entity.inventory.itemInMainHand)
items.add(entity.inventory.itemInOffHand)
return items.filter { !it.type.isAir }
}
override fun resolvePlayer(event: EntityDamageEvent): Player? = event.entity as? Player
})
// BlockBreakEvent
register(BlockBreakEvent::class, object : EventContextStrategy<BlockBreakEvent> {
override fun resolveItems(event: BlockBreakEvent): List<ItemStack> {
val item = event.player.inventory.itemInMainHand
return if (item.type.isAir) emptyList() else listOf(item)
}
override fun resolvePlayer(event: BlockBreakEvent): Player = event.player
})
// PlayerToggleSneakEvent
register(PlayerToggleSneakEvent::class, object : EventContextStrategy<PlayerToggleSneakEvent> {
override fun resolveItems(event: PlayerToggleSneakEvent): List<ItemStack> {
val player = event.player
val items = mutableListOf<ItemStack>()
items.addAll(player.inventory.armorContents.filterNotNull())
items.add(player.inventory.itemInMainHand)
items.add(player.inventory.itemInOffHand)
return items.filter { !it.type.isAir }
}
override fun resolvePlayer(event: PlayerToggleSneakEvent): Player = event.player
})
// EntityToggleGlideEvent
register(EntityToggleGlideEvent::class, object : EventContextStrategy<EntityToggleGlideEvent> {
override fun resolveItems(event: EntityToggleGlideEvent): List<ItemStack> {
val player = event.entity as? Player ?: return emptyList()
val items = mutableListOf<ItemStack>()
items.addAll(player.inventory.armorContents.filterNotNull())
items.add(player.inventory.itemInMainHand)
items.add(player.inventory.itemInOffHand)
return items.filter { !it.type.isAir }
}
override fun resolvePlayer(event: EntityToggleGlideEvent): Player? = event.entity as? Player
})
// PlayerItemHeldEvent
register(PlayerItemHeldEvent::class, object : EventContextStrategy<PlayerItemHeldEvent> {
override fun resolveItems(event: PlayerItemHeldEvent): List<ItemStack> {
val item = event.player.inventory.getItem(event.previousSlot)
return if (item != null && !item.type.isAir) listOf(item) else emptyList()
}
override fun resolvePlayer(event: PlayerItemHeldEvent): Player = event.player
})
// PlayerFishEvent
register(PlayerFishEvent::class, object : EventContextStrategy<PlayerFishEvent> {
override fun resolveItems(event: PlayerFishEvent): List<ItemStack> {
val item = event.player.inventory.itemInMainHand
return if (item.type.isAir) emptyList() else listOf(item)
}
override fun resolvePlayer(event: PlayerFishEvent): Player = event.player
})
// ProjectileLaunchEvent
register(ProjectileLaunchEvent::class, object : EventContextStrategy<ProjectileLaunchEvent> {
override fun resolveItems(event: ProjectileLaunchEvent): List<ItemStack> {
val shooter = event.entity.shooter as? Player ?: return emptyList()
val item = shooter.inventory.itemInMainHand
return if (item.type.isAir) emptyList() else listOf(item)
}
override fun resolvePlayer(event: ProjectileLaunchEvent): Player? = event.entity.shooter as? Player
})
// ProjectileHitEvent
register(ProjectileHitEvent::class, object : EventContextStrategy<ProjectileHitEvent> {
override fun resolveItems(event: ProjectileHitEvent): List<ItemStack> {
// Requires scanning all registered items because the projectile might not be currently held
// But for strategy pattern, typically we want context from the event.
// Existing logic iterates ALL AbstractItems.
// To support that, we might need a "Global" strategy?
// For now, let's look at shooter's hand like Launch?
// The original logic: ItemRegistry.getAll().forEach { it.onProjectileHit(event) }
// This implies it's a "Global" event listener for all custom items.
// This Strategy pattern resolves relevant ItemStacks.
// If we return empty list, no specific item logic is called.
// But we want to call onProjectileHit for the Item TYPE that matches the projectile.
// This is tricky.
// Let's resolve the shooter's main hand for now as a best guess for "who did it".
val shooter = event.entity.shooter as? Player ?: return emptyList()
val item = shooter.inventory.itemInMainHand
return if (item.type.isAir) emptyList() else listOf(item)
}
override fun resolvePlayer(event: ProjectileHitEvent): Player? = event.entity.shooter as? Player
})
// PlayerMoveEvent
register(PlayerMoveEvent::class, object : EventContextStrategy<PlayerMoveEvent> {
override fun resolveItems(event: PlayerMoveEvent): List<ItemStack> {
val item = event.player.inventory.itemInMainHand
return if (item.type.isAir) emptyList() else listOf(item)
}
override fun resolvePlayer(event: PlayerMoveEvent): Player = event.player
})
// FoodLevelChangeEvent
register(FoodLevelChangeEvent::class, object : EventContextStrategy<FoodLevelChangeEvent> {
override fun resolveItems(event: FoodLevelChangeEvent): List<ItemStack> {
val player = event.entity as? Player ?: return emptyList()
// Original logic: entity.inventory.contents.filterNotNull()
return player.inventory.contents.filterNotNull()
}
override fun resolvePlayer(event: FoodLevelChangeEvent): Player? = event.entity as? Player
})
}
}

View File

@ -1,162 +1,152 @@
package net.hareworks.hcu.items.listeners package net.hareworks.hcu.items.listeners
import net.hareworks.hcu.items.api.component.CustomComponent import net.hareworks.hcu.items.api.component.CustomComponent
import net.hareworks.hcu.items.api.item.AbstractItem
import net.hareworks.hcu.items.registry.ComponentRegistry
import net.hareworks.hcu.items.registry.ItemRegistry
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.inventory.EquipmentSlot
import org.bukkit.inventory.ItemStack
import net.hareworks.hcu.items.api.component.EquippableComponent import net.hareworks.hcu.items.api.component.EquippableComponent
import net.hareworks.hcu.items.api.component.ToolComponent import net.hareworks.hcu.items.api.component.ToolComponent
import net.hareworks.hcu.items.api.item.AbstractItem
import net.hareworks.hcu.items.events.EventContextStrategy
import net.hareworks.hcu.items.events.EventStrategyRegistry
import net.hareworks.hcu.items.registry.ComponentRegistry
import net.hareworks.hcu.items.registry.ItemRegistry
import org.bukkit.event.Event
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.entity.ProjectileHitEvent
import org.bukkit.inventory.ItemStack
import org.bukkit.plugin.EventExecutor
import org.bukkit.plugin.Plugin import org.bukkit.plugin.Plugin
import org.bukkit.event.EventPriority
import kotlin.reflect.KClass
class EventListener(private val plugin: Plugin) : Listener { class EventListener(private val plugin: Plugin) : Listener {
// Track previously held items for ToolComponents to detect when they stop being held // Track previously held items for ToolComponents
// Key: Player UUID, Value: Pair(MainHandItem, OffHandItem)
private val lastHeldItems = java.util.concurrent.ConcurrentHashMap<java.util.UUID, Pair<ItemStack?, ItemStack?>>() private val lastHeldItems = java.util.concurrent.ConcurrentHashMap<java.util.UUID, Pair<ItemStack?, ItemStack?>>()
init { init {
// Use configurable tick interval (defaulting to 1L if something goes wrong, though Config handles defaults) // Register Dynamic Listeners
registerDynamicListeners()
// Start Ticker
val interval = net.hareworks.hcu.items.config.Config.global.tickInterval val interval = net.hareworks.hcu.items.config.Config.global.tickInterval
plugin.server.scheduler.runTaskTimer(plugin, Runnable { plugin.server.scheduler.runTaskTimer(plugin, Runnable {
tickComponents() tickComponents()
}, 1L, interval) }, 1L, interval)
} }
/**
* Special Case: ProjectileHitEvent
* This event is difficult to resolve context for (projectile might be far from shooter),
* so it's kept as a manual Global Dispatch to all AbstractItems.
*/
@EventHandler @EventHandler
fun onInteract(event: org.bukkit.event.player.PlayerInteractEvent) { fun onProjectileHit(event: ProjectileHitEvent) {
if (event.hand == EquipmentSlot.OFF_HAND) return val shooter = event.entity.shooter
if (shooter is org.bukkit.entity.Player) {
val item = event.item ?: return ItemRegistry.getAll().forEach { item ->
val handler = item.getEventHandlers()[ProjectileHitEvent::class]
dispatchToItem(item) { it.onInteract(event) } if (handler != null) {
dispatchToComponents(item) { it.onInteract(event) } @Suppress("UNCHECKED_CAST")
(handler as (ProjectileHitEvent, ItemStack) -> Unit).invoke(event, ItemStack(org.bukkit.Material.AIR))
} }
@EventHandler
fun onFish(event: org.bukkit.event.player.PlayerFishEvent) {
val player = event.player
val item = player.inventory.itemInMainHand
dispatchToItem(item) { it.onFish(event) }
// CustomComponent interface does not define onFish, so no dispatchToComponents here.
} }
@EventHandler
fun onProjectileHit(event: org.bukkit.event.entity.ProjectileHitEvent) {
val projectile = event.entity
val shooter = projectile.shooter
if (shooter is Player) {
// For ProjectileHitEvent, AbstractItems often need to check if the projectile belongs to them,
// which might not be the item currently held by the player.
// Therefore, we iterate through all registered AbstractItems.
ItemRegistry.getAll().forEach { it.onProjectileHit(event) }
// CustomComponent interface does not define onProjectileHit, so no dispatchToComponents here.
} }
} }
@EventHandler private fun registerDynamicListeners() {
fun onProjectileLaunch(event: org.bukkit.event.entity.ProjectileLaunchEvent) { val registeredEvents = mutableSetOf<KClass<out Event>>()
val projectile = event.entity
val shooter = projectile.shooter
if (shooter is Player) { // Collect all unique events monitored by Items or Components
val item = shooter.inventory.itemInMainHand ItemRegistry.getAll().forEach { item ->
dispatchToItem(item) { it.onProjectileLaunch(event) } registeredEvents.addAll(item.getEventHandlers().keys)
// CustomComponent interface does not define onProjectileLaunch, so no dispatchToComponents here. }
ComponentRegistry.getAll().forEach { component ->
registeredEvents.addAll(component.getEventHandlers().keys)
}
// Register a listener for each event type
for (eventClass in registeredEvents) {
// Skip ProjectileHitEvent as it is handled manually
if (eventClass == ProjectileHitEvent::class) continue
val strategy = EventStrategyRegistry.get(eventClass)
if (strategy == null) {
plugin.logger.warning("No strategy found for event ${eventClass.simpleName} but it is used by an item/component.")
continue
}
// Unchecked cast to pass valid types to generic helper
// We know strategy is Strategy<T> where T corresponds to eventClass
registerEvent(eventClass as KClass<Event>, strategy as EventContextStrategy<Event>)
plugin.logger.info("Registered dynamic listener for ${eventClass.simpleName}")
} }
} }
@EventHandler private fun <T : Event> registerEvent(eventClass: KClass<T>, strategy: EventContextStrategy<T>) {
fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) { val executor = EventExecutor { _, event ->
val entity = event.entity if (eventClass.isInstance(event)) {
if (entity is Player) { @Suppress("UNCHECKED_CAST")
val items = getEquipmentItems(entity) val castedEvent = event as T
dispatchWithStrategy(castedEvent, strategy)
}
}
plugin.server.pluginManager.registerEvent(
eventClass.java,
this, // Use this instance as the Listener owner
EventPriority.NORMAL,
executor,
plugin
)
}
private fun <T : Event> dispatchWithStrategy(event: T, strategy: EventContextStrategy<T>) {
val items = strategy.resolveItems(event)
// Avoid duplicate firing for the same component on the same item context
for (item in items) { for (item in items) {
dispatchToItem(item) { it.onEntityDamage(event) } dispatchToItem(event, item)
dispatchToComponents(item) { it.onEntityDamage(event) } dispatchToComponents(event, item)
}
}
private fun <T : Event> dispatchToItem(event: T, item: ItemStack) {
if (item.type.isAir) return
val id = AbstractItem.getId(item) ?: return
val abstractItem = ItemRegistry.get(id) ?: return
val handler = abstractItem.getEventHandlers()[event::class]
if (handler != null) {
@Suppress("UNCHECKED_CAST")
(handler as (T, ItemStack) -> Unit).invoke(event, item)
}
}
private fun <T : Event> dispatchToComponents(event: T, item: ItemStack) {
if (item.type.isAir) return
val meta = item.itemMeta ?: return
val pdc = meta.persistentDataContainer
val eventClass = event::class
for (key in pdc.keys) {
val component = ComponentRegistry.get(key) ?: continue
// Check Data Dependencies
val hasAllDependencies = component.dataComponentDependencies.all { dep ->
item.hasData(dep)
}
if (!hasAllDependencies) continue
val handler = component.getEventHandlers()[eventClass]
if (handler != null) {
@Suppress("UNCHECKED_CAST")
(handler as (T, ItemStack) -> Unit).invoke(event, item)
} }
} }
} }
@EventHandler // --- Tick Logic ---
fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) {
val player = event.player
val item = player.inventory.itemInMainHand
dispatchToItem(item) { it.onPlayerMove(event) }
dispatchToComponents(item) { it.onPlayerMove(event) }
}
@EventHandler
fun onToggleSneak(event: org.bukkit.event.player.PlayerToggleSneakEvent) {
val player = event.player
val items = getEquipmentItems(player)
for (item in items) {
// AbstractItem interface does not define onToggleSneak.
val handled = dispatchToComponentsWithDependencyCheck(
item = item,
isEventHandled = { event.isCancelled }
) { component ->
component.onToggleSneak(player, item, event)
}
if (handled) break
}
}
@EventHandler
fun onToggleGlide(event: org.bukkit.event.entity.EntityToggleGlideEvent) {
val entity = event.entity
if (entity is Player) {
val items = getEquipmentItems(entity)
for (item in items) {
val handled = dispatchToComponentsWithDependencyCheck(
item = item,
isEventHandled = { event.isCancelled }
) { component ->
component.onToggleGlide(entity, item, event)
}
if (handled) break
}
}
}
@EventHandler
fun onBlockBreak(event: org.bukkit.event.block.BlockBreakEvent) {
val player = event.player
val item = player.inventory.itemInMainHand
// Dispatch to components on the main hand item
dispatchToComponents(item) { it.onBlockBreak(event) }
}
@EventHandler
fun onItemHeld(event: org.bukkit.event.player.PlayerItemHeldEvent) {
val player = event.player
val item = player.inventory.getItem(event.previousSlot) ?: return
dispatchToItem(item) { it.onItemHeld(event) }
dispatchToComponents(item) { it.onItemHeld(player, item, event) }
}
@EventHandler
fun onFoodLevelChange(event: org.bukkit.event.entity.FoodLevelChangeEvent) {
val entity = event.entity
if (entity is Player) {
// インベントリ内の全アイテムをチェック
val items = entity.inventory.contents.filterNotNull()
for (item in items) {
dispatchToItem(item) { it.onFoodLevelChange(event) }
}
}
}
private fun tickComponents() { private fun tickComponents() {
for (player in plugin.server.onlinePlayers) { for (player in plugin.server.onlinePlayers) {
@ -171,7 +161,7 @@ class EventListener(private val plugin: Plugin) : Listener {
// Check Main Hand Change // Check Main Hand Change
if (lastMainHand != null && !lastMainHand.isSimilar(currentMainHand)) { if (lastMainHand != null && !lastMainHand.isSimilar(currentMainHand)) {
dispatchToComponents(lastMainHand) { component -> dispatchToComponentsTick(lastMainHand) { component ->
if (component is ToolComponent) { if (component is ToolComponent) {
component.onStopHolding(player, lastMainHand) component.onStopHolding(player, lastMainHand)
} }
@ -180,7 +170,7 @@ class EventListener(private val plugin: Plugin) : Listener {
// Check Off Hand Change // Check Off Hand Change
if (lastOffHand != null && !lastOffHand.isSimilar(currentOffHand)) { if (lastOffHand != null && !lastOffHand.isSimilar(currentOffHand)) {
dispatchToComponents(lastOffHand) { component -> dispatchToComponentsTick(lastOffHand) { component ->
if (component is ToolComponent) { if (component is ToolComponent) {
component.onStopHolding(player, lastOffHand) component.onStopHolding(player, lastOffHand)
} }
@ -192,21 +182,18 @@ class EventListener(private val plugin: Plugin) : Listener {
// --- Regular Tick Processing --- // --- Regular Tick Processing ---
// 1. Dispatch onTick to ALL CustomItems in inventory // 1. Dispatch onTick to ALL CustomItems
// inventory.contents typically covers storage, hotbar, armor, and offhand depending on implementation,
// but we iterate it to ensure we catch items anywhere in the inventory.
for (item in player.inventory.contents) { for (item in player.inventory.contents) {
if (item == null || item.type.isAir) continue if (item == null || item.type.isAir) continue
dispatchToItem(item) { it.onTick(player, item) } dispatchToItemTick(item) { it.onTick(player, item) }
} }
// 2. Dispatch to Components for specific slots (Armor, MainHand, OffHand) // 2. Dispatch to Components
// Note: dispatchToItem is skipped here because it was already done above for all items.
// Armor Tick // Armor Tick
for (item in player.inventory.armorContents) { for (item in player.inventory.armorContents) {
if (item == null || item.type.isAir) continue if (item == null || item.type.isAir) continue
dispatchToComponents(item) { component -> dispatchToComponentsTick(item) { component ->
if (component is EquippableComponent) { if (component is EquippableComponent) {
component.onTick(player, item) component.onTick(player, item)
} }
@ -215,7 +202,7 @@ class EventListener(private val plugin: Plugin) : Listener {
// ToolComponents in Main Hand // ToolComponents in Main Hand
if (!currentMainHand.type.isAir) { if (!currentMainHand.type.isAir) {
dispatchToComponents(currentMainHand) { component -> dispatchToComponentsTick(currentMainHand) { component ->
if (component is ToolComponent) { if (component is ToolComponent) {
component.onHoldTick(player, currentMainHand) component.onHoldTick(player, currentMainHand)
} }
@ -227,7 +214,7 @@ class EventListener(private val plugin: Plugin) : Listener {
// ToolComponents in Off Hand // ToolComponents in Off Hand
if (!currentOffHand.type.isAir) { if (!currentOffHand.type.isAir) {
dispatchToComponents(currentOffHand) { component -> dispatchToComponentsTick(currentOffHand) { component ->
if (component is ToolComponent) { if (component is ToolComponent) {
component.onHoldTick(player, currentOffHand) component.onHoldTick(player, currentOffHand)
} }
@ -239,69 +226,15 @@ class EventListener(private val plugin: Plugin) : Listener {
} }
} }
private fun getEquipmentItems(player: Player): List<ItemStack> { // Helpers for tick dispatching (legacy style interaction)
val equipment = player.equipment private fun dispatchToItemTick(item: ItemStack, action: (AbstractItem) -> Unit) {
val items = mutableListOf<ItemStack>()
items.addAll(equipment.armorContents.filterNotNull())
items.add(equipment.itemInMainHand)
items.add(equipment.itemInOffHand)
return items.filter { !it.type.isAir }
}
private fun dispatchToItem(item: ItemStack, action: (AbstractItem) -> Unit) {
if (item.type.isAir) return if (item.type.isAir) return
val id = AbstractItem.getId(item) ?: return val id = AbstractItem.getId(item) ?: return
val abstractItem = ItemRegistry.get(id) ?: return val abstractItem = ItemRegistry.get(id) ?: return
action(abstractItem) action(abstractItem)
} }
/** private fun dispatchToComponentsTick(item: ItemStack, action: (CustomComponent) -> Unit) {
* Dispatches to components on an item, but only if the item satisfies
* the component's dataComponentDependencies.
*
* This prevents multiple components with the same trigger conditions
* (e.g., GLIDER) from firing simultaneously when only one item
* actually has the required data component active.
*
* @param item The item to dispatch to
* @param isEventHandled Lambda to check if the event has been handled (e.g., cancelled)
* @param action The action to perform on each eligible component
* @return true if the event was handled by any component, false otherwise
*/
private fun dispatchToComponentsWithDependencyCheck(
item: ItemStack,
isEventHandled: () -> Boolean,
action: (CustomComponent) -> Unit
): Boolean {
if (item.type.isAir) return false
val meta = item.itemMeta ?: return false
val pdc = meta.persistentDataContainer
for (key in pdc.keys) {
// Stop if event is already handled by a previous component
if (isEventHandled()) return true
val component = ComponentRegistry.get(key) ?: continue
// Check if this item has all required data component dependencies
// This ensures the component only triggers on items that
// actually have the necessary data components active
val hasAllDependencies = component.dataComponentDependencies.all { dep ->
item.hasData(dep)
}
if (!hasAllDependencies) continue
action(component)
}
return isEventHandled()
}
/**
* Dispatches to all components on an item without checking dependencies.
* Use this for events that don't rely on data component triggers.
*/
private fun dispatchToComponents(item: ItemStack, action: (CustomComponent) -> Unit) {
if (item.type.isAir) return if (item.type.isAir) return
val meta = item.itemMeta ?: return val meta = item.itemMeta ?: return
val pdc = meta.persistentDataContainer val pdc = meta.persistentDataContainer