diff --git a/docs/creating_new_content.md b/docs/creating_new_content.md index 99566fb..d6db43b 100644 --- a/docs/creating_new_content.md +++ b/docs/creating_new_content.md @@ -1,119 +1,98 @@ # 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. -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. 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) +### 1. Creating an AbstractItem (Custom Item) Use this when creating an item with completely unique behavior. -### Steps +#### Steps 1. Create a new class that inherits from the `AbstractItem` class. 2. Specify a unique ID in the constructor. -3. Implement the `buildItem` method to define the item's appearance (Material, Name, Lore, etc.). -4. Override necessary event handlers (`onInteract`, `onPlayerMove`, etc.). +3. Implement the `buildItem` method to define the item's appearance. +4. **Implement `getEventHandlers`** to define which events this item handles. 5. Register by calling `ItemRegistry.register(YourItem())` in `App.kt`'s `onEnable`. +#### Example + ```kotlin class MyCustomItem : AbstractItem("my_custom_item") { + + // Define item appearance override fun buildItem(tier: Tier): ItemStack { return ItemStack(Material.DIAMOND_SWORD).apply { - // Meta configuration + // ... meta ... } } - override fun onInteract(event: PlayerInteractEvent) { - event.player.sendMessage("Used custom item!") + // Register Event Handlers + override fun getEventHandlers(): Map, 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`). -2. Specify a unique component ID in the constructor. -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). -4. Override necessary event handlers. -5. Register by calling `ComponentRegistry.register(YourComponent(this))` in `App.kt`'s `onEnable`. +1. Create a class that inherits from `AbstractComponent` and implements `CustomComponent`. +2. Implement `getEventHandlers` to define event logic. +3. Register in `App.kt`. + +#### Example ```kotlin class MyComponent(plugin: App) : AbstractComponent(plugin, "my_component") { - override fun onInteract(event: PlayerInteractEvent) { - event.player.sendMessage("Component interaction!") + + override fun getEventHandlers(): Map, 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`. - -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: +Use the `Config` object to manage values. ```kotlin -import net.hareworks.hcu.items.config.Config - -// 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 ... - } -} +val cooldown = Config.myFeatureCooldown ``` \ No newline at end of file diff --git a/docs/creating_new_content_ja.md b/docs/creating_new_content_ja.md new file mode 100644 index 0000000..4c3e931 --- /dev/null +++ b/docs/creating_new_content_ja.md @@ -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, 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, 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 +``` diff --git a/src/main/kotlin/net/hareworks/hcu/items/api/component/CustomComponent.kt b/src/main/kotlin/net/hareworks/hcu/items/api/component/CustomComponent.kt index 4d43783..9572a85 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/api/component/CustomComponent.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/api/component/CustomComponent.kt @@ -4,6 +4,9 @@ import io.papermc.paper.datacomponent.DataComponentType import net.hareworks.hcu.items.api.Tier import org.bukkit.NamespacedKey import org.bukkit.inventory.ItemStack +import org.bukkit.event.Event +import kotlin.reflect.KClass +import net.hareworks.hcu.items.events.ComponentEventHandler interface CustomComponent { val key: NamespacedKey @@ -19,15 +22,12 @@ interface CustomComponent { val dataComponentDependencies: List get() = emptyList() + /** + * Returns a map of event classes to their handlers. + */ + fun getEventHandlers(): Map, ComponentEventHandler<*>> = emptyMap() + fun apply(item: ItemStack, tier: Tier? = null) fun has(item: ItemStack): Boolean 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) {} } diff --git a/src/main/kotlin/net/hareworks/hcu/items/api/item/AbstractItem.kt b/src/main/kotlin/net/hareworks/hcu/items/api/item/AbstractItem.kt index 96d7043..be2754a 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/api/item/AbstractItem.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/api/item/AbstractItem.kt @@ -3,12 +3,20 @@ package net.hareworks.hcu.items.api.item import net.hareworks.hcu.items.api.Tier import org.bukkit.NamespacedKey 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 abstract class AbstractItem(override val id: String) : CustomItem { - override val maxTier: Int = 5 - override val minTier: Int = 1 + override open val maxTier: Int = 5 + override open val minTier: Int = 1 + + /** + * Returns a map of event classes to their handlers. + */ + override fun getEventHandlers(): Map, ComponentEventHandler<*>> = emptyMap() override fun createItemStack(tier: Tier): ItemStack { val validTier = if (tier.level < minTier) Tier.fromLevel(minTier) diff --git a/src/main/kotlin/net/hareworks/hcu/items/api/item/CustomItem.kt b/src/main/kotlin/net/hareworks/hcu/items/api/item/CustomItem.kt index 15917d6..3235bcd 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/api/item/CustomItem.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/api/item/CustomItem.kt @@ -2,21 +2,22 @@ package net.hareworks.hcu.items.api.item import net.hareworks.hcu.items.api.Tier import org.bukkit.inventory.ItemStack +import org.bukkit.event.Event +import kotlin.reflect.KClass +import net.hareworks.hcu.items.events.ComponentEventHandler interface CustomItem { val id: String val maxTier: Int val minTier: Int + + /** + * Returns a map of event classes to their handlers. + */ + fun getEventHandlers(): Map, ComponentEventHandler<*>> = emptyMap() + 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) {} } diff --git a/src/main/kotlin/net/hareworks/hcu/items/content/components/BlinkComponent.kt b/src/main/kotlin/net/hareworks/hcu/items/content/components/BlinkComponent.kt index ffed388..58fedb5 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/content/components/BlinkComponent.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/content/components/BlinkComponent.kt @@ -44,6 +44,17 @@ class BlinkComponent(private val plugin: App) : AbstractComponent(plugin, "blink private val TRAIL_COLOR = TextColor.color(0x9400D3) // Dark Violet } + override fun getEventHandlers(): Map, 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?) { if (isBoots(item.type)) { 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 if (!has(item)) return diff --git a/src/main/kotlin/net/hareworks/hcu/items/content/components/DoubleJumpComponent.kt b/src/main/kotlin/net/hareworks/hcu/items/content/components/DoubleJumpComponent.kt index 08893bd..8028491 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/content/components/DoubleJumpComponent.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/content/components/DoubleJumpComponent.kt @@ -43,6 +43,17 @@ class DoubleJumpComponent(private val plugin: App) : AbstractComponent(plugin, " private val COOLDOWN_COLOR = NamedTextColor.GRAY } + override fun getEventHandlers(): Map, 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?) { if (isBoots(item.type)) { 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 if (!has(item)) return diff --git a/src/main/kotlin/net/hareworks/hcu/items/content/components/GliderComponent.kt b/src/main/kotlin/net/hareworks/hcu/items/content/components/GliderComponent.kt index 31ed8f3..6685d35 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/content/components/GliderComponent.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/content/components/GliderComponent.kt @@ -43,6 +43,18 @@ class GliderComponent(private val plugin: App) : AbstractComponent(plugin, "glid var lastTickTime: Long = System.currentTimeMillis() ) + override fun getEventHandlers(): Map, 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 { // Global Ticker for active gliders 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 if (event.isSneaking) { 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 - 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) { val entity = event.entity if (entity is Player && activeGliders.containsKey(entity.uniqueId)) { diff --git a/src/main/kotlin/net/hareworks/hcu/items/content/components/VeinMinerComponent.kt b/src/main/kotlin/net/hareworks/hcu/items/content/components/VeinMinerComponent.kt index 80bb21b..bcebf73 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/content/components/VeinMinerComponent.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/content/components/VeinMinerComponent.kt @@ -36,6 +36,12 @@ class VeinMinerComponent(private val plugin: JavaPlugin) : ToolComponent { override val maxTier: Int = 1 override val minTier: Int = 1 + override fun getEventHandlers(): Map, net.hareworks.hcu.items.events.ComponentEventHandler<*>> { + return mapOf( + BlockBreakEvent::class to { event, _ -> handleBlockBreak(event as BlockBreakEvent) } + ) + } + private val activeHighlights = mutableMapOf() 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 val player = event.player val block = event.block diff --git a/src/main/kotlin/net/hareworks/hcu/items/content/items/GrapplingItem.kt b/src/main/kotlin/net/hareworks/hcu/items/content/items/GrapplingItem.kt index a519181..1cb83f0 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/content/items/GrapplingItem.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/content/items/GrapplingItem.kt @@ -15,6 +15,15 @@ import kotlin.time.Duration.Companion.seconds class GrapplingItem : AbstractItem("grappling_hook") { override val maxTier: Int = 5 + + override fun getEventHandlers(): Map, 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 { val item = ItemStack(Material.FISHING_ROD) @@ -45,7 +54,7 @@ class GrapplingItem : AbstractItem("grappling_hook") { return item } - override fun onFish(event: PlayerFishEvent) { + private fun handleFish(event: PlayerFishEvent) { val hook = event.hook 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 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) { val hook = event.entity 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) { val player = event.entity if (player is org.bukkit.entity.Player) { diff --git a/src/main/kotlin/net/hareworks/hcu/items/content/items/MagnetItem.kt b/src/main/kotlin/net/hareworks/hcu/items/content/items/MagnetItem.kt index 09452e5..45a2adf 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/content/items/MagnetItem.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/content/items/MagnetItem.kt @@ -18,6 +18,12 @@ class MagnetItem : AbstractItem("magnet_item") { override val maxTier: Int = 3 + override fun getEventHandlers(): Map, 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 { 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 item = player.inventory.getItem(event.previousSlot) ?: return diff --git a/src/main/kotlin/net/hareworks/hcu/items/content/items/TestItem.kt b/src/main/kotlin/net/hareworks/hcu/items/content/items/TestItem.kt index 4e7eaf1..8246ad9 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/content/items/TestItem.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/content/items/TestItem.kt @@ -8,6 +8,13 @@ import org.bukkit.Material import org.bukkit.inventory.ItemStack class TestItem : AbstractItem("test_sword") { + + override fun getEventHandlers(): Map, 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 { val item = ItemStack(Material.DIAMOND_SWORD) val meta = item.itemMeta ?: return item @@ -22,7 +29,7 @@ class TestItem : AbstractItem("test_sword") { 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 tier = AbstractItem.getTier(item) diff --git a/src/main/kotlin/net/hareworks/hcu/items/events/ComponentEventHandler.kt b/src/main/kotlin/net/hareworks/hcu/items/events/ComponentEventHandler.kt new file mode 100644 index 0000000..8716c7e --- /dev/null +++ b/src/main/kotlin/net/hareworks/hcu/items/events/ComponentEventHandler.kt @@ -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, ItemStack) -> Unit diff --git a/src/main/kotlin/net/hareworks/hcu/items/events/EventContextStrategy.kt b/src/main/kotlin/net/hareworks/hcu/items/events/EventContextStrategy.kt new file mode 100644 index 0000000..93fc90f --- /dev/null +++ b/src/main/kotlin/net/hareworks/hcu/items/events/EventContextStrategy.kt @@ -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 { + /** + * Resolves the items that should trigger components for this event. + */ + fun resolveItems(event: T): List + + /** + * 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, EventContextStrategy<*>>() + + fun register(eventClass: KClass, strategy: EventContextStrategy) { + strategies[eventClass] = strategy + } + + @Suppress("UNCHECKED_CAST") + fun get(eventClass: KClass): EventContextStrategy? { + return strategies[eventClass] as? EventContextStrategy + } + + init { + registerDefaults() + } + + private fun registerDefaults() { + // PlayerInteractEvent + register(PlayerInteractEvent::class, object : EventContextStrategy { + override fun resolveItems(event: PlayerInteractEvent): List { + val items = mutableListOf() + event.item?.let { items.add(it) } + return items + } + override fun resolvePlayer(event: PlayerInteractEvent): Player = event.player + }) + + // EntityDamageEvent + register(EntityDamageEvent::class, object : EventContextStrategy { + override fun resolveItems(event: EntityDamageEvent): List { + val entity = event.entity as? Player ?: return emptyList() + val items = mutableListOf() + 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 { + override fun resolveItems(event: BlockBreakEvent): List { + 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 { + override fun resolveItems(event: PlayerToggleSneakEvent): List { + val player = event.player + val items = mutableListOf() + 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 { + override fun resolveItems(event: EntityToggleGlideEvent): List { + val player = event.entity as? Player ?: return emptyList() + val items = mutableListOf() + 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 { + override fun resolveItems(event: PlayerItemHeldEvent): List { + 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 { + override fun resolveItems(event: PlayerFishEvent): List { + 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 { + override fun resolveItems(event: ProjectileLaunchEvent): List { + 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 { + override fun resolveItems(event: ProjectileHitEvent): List { + // 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 { + override fun resolveItems(event: PlayerMoveEvent): List { + 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 { + override fun resolveItems(event: FoodLevelChangeEvent): List { + 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 + }) + } +} diff --git a/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt b/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt index bd29f51..e13e059 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt @@ -1,163 +1,153 @@ package net.hareworks.hcu.items.listeners 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.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.event.EventPriority +import kotlin.reflect.KClass class EventListener(private val plugin: Plugin) : Listener { - // Track previously held items for ToolComponents to detect when they stop being held - // Key: Player UUID, Value: Pair(MainHandItem, OffHandItem) + // Track previously held items for ToolComponents private val lastHeldItems = java.util.concurrent.ConcurrentHashMap>() 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 plugin.server.scheduler.runTaskTimer(plugin, Runnable { tickComponents() }, 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 - fun onInteract(event: org.bukkit.event.player.PlayerInteractEvent) { - if (event.hand == EquipmentSlot.OFF_HAND) return - - val item = event.item ?: return - - dispatchToItem(item) { it.onInteract(event) } - dispatchToComponents(item) { it.onInteract(event) } - } - - @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. + fun onProjectileHit(event: ProjectileHitEvent) { + val shooter = event.entity.shooter + if (shooter is org.bukkit.entity.Player) { + ItemRegistry.getAll().forEach { item -> + val handler = item.getEventHandlers()[ProjectileHitEvent::class] + if (handler != null) { + @Suppress("UNCHECKED_CAST") + (handler as (ProjectileHitEvent, ItemStack) -> Unit).invoke(event, ItemStack(org.bukkit.Material.AIR)) + } + } } } - @EventHandler - fun onProjectileLaunch(event: org.bukkit.event.entity.ProjectileLaunchEvent) { - val projectile = event.entity - val shooter = projectile.shooter - - if (shooter is Player) { - val item = shooter.inventory.itemInMainHand - dispatchToItem(item) { it.onProjectileLaunch(event) } - // CustomComponent interface does not define onProjectileLaunch, so no dispatchToComponents here. + private fun registerDynamicListeners() { + val registeredEvents = mutableSetOf>() + + // Collect all unique events monitored by Items or Components + ItemRegistry.getAll().forEach { item -> + registeredEvents.addAll(item.getEventHandlers().keys) + } + 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 where T corresponds to eventClass + registerEvent(eventClass as KClass, strategy as EventContextStrategy) + plugin.logger.info("Registered dynamic listener for ${eventClass.simpleName}") } } - @EventHandler - fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) { - val entity = event.entity - if (entity is Player) { - val items = getEquipmentItems(entity) - for (item in items) { - dispatchToItem(item) { it.onEntityDamage(event) } - dispatchToComponents(item) { it.onEntityDamage(event) } + private fun registerEvent(eventClass: KClass, strategy: EventContextStrategy) { + val executor = EventExecutor { _, event -> + if (eventClass.isInstance(event)) { + @Suppress("UNCHECKED_CAST") + 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 + ) } - @EventHandler - 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) + private fun dispatchWithStrategy(event: T, strategy: EventContextStrategy) { + val items = strategy.resolveItems(event) + // Avoid duplicate firing for the same component on the same item context 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 + dispatchToItem(event, item) + dispatchToComponents(event, item) } } - @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 - } + private fun 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) } } - @EventHandler - fun onBlockBreak(event: org.bukkit.event.block.BlockBreakEvent) { - val player = event.player - val item = player.inventory.itemInMainHand + private fun dispatchToComponents(event: T, item: ItemStack) { + if (item.type.isAir) return + val meta = item.itemMeta ?: return + val pdc = meta.persistentDataContainer - // Dispatch to components on the main hand item - dispatchToComponents(item) { it.onBlockBreak(event) } - } + val eventClass = event::class - @EventHandler - fun onItemHeld(event: org.bukkit.event.player.PlayerItemHeldEvent) { - val player = event.player - val item = player.inventory.getItem(event.previousSlot) ?: return + 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 - 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) } + val handler = component.getEventHandlers()[eventClass] + if (handler != null) { + @Suppress("UNCHECKED_CAST") + (handler as (T, ItemStack) -> Unit).invoke(event, item) } } } + // --- Tick Logic --- + private fun tickComponents() { for (player in plugin.server.onlinePlayers) { val uuid = player.uniqueId @@ -171,7 +161,7 @@ class EventListener(private val plugin: Plugin) : Listener { // Check Main Hand Change if (lastMainHand != null && !lastMainHand.isSimilar(currentMainHand)) { - dispatchToComponents(lastMainHand) { component -> + dispatchToComponentsTick(lastMainHand) { component -> if (component is ToolComponent) { component.onStopHolding(player, lastMainHand) } @@ -180,7 +170,7 @@ class EventListener(private val plugin: Plugin) : Listener { // Check Off Hand Change if (lastOffHand != null && !lastOffHand.isSimilar(currentOffHand)) { - dispatchToComponents(lastOffHand) { component -> + dispatchToComponentsTick(lastOffHand) { component -> if (component is ToolComponent) { component.onStopHolding(player, lastOffHand) } @@ -192,21 +182,18 @@ class EventListener(private val plugin: Plugin) : Listener { // --- Regular Tick Processing --- - // 1. Dispatch onTick to ALL CustomItems in inventory - // 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. + // 1. Dispatch onTick to ALL CustomItems for (item in player.inventory.contents) { 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) - // Note: dispatchToItem is skipped here because it was already done above for all items. + // 2. Dispatch to Components // Armor Tick for (item in player.inventory.armorContents) { if (item == null || item.type.isAir) continue - dispatchToComponents(item) { component -> + dispatchToComponentsTick(item) { component -> if (component is EquippableComponent) { component.onTick(player, item) } @@ -215,7 +202,7 @@ class EventListener(private val plugin: Plugin) : Listener { // ToolComponents in Main Hand if (!currentMainHand.type.isAir) { - dispatchToComponents(currentMainHand) { component -> + dispatchToComponentsTick(currentMainHand) { component -> if (component is ToolComponent) { component.onHoldTick(player, currentMainHand) } @@ -227,7 +214,7 @@ class EventListener(private val plugin: Plugin) : Listener { // ToolComponents in Off Hand if (!currentOffHand.type.isAir) { - dispatchToComponents(currentOffHand) { component -> + dispatchToComponentsTick(currentOffHand) { component -> if (component is ToolComponent) { component.onHoldTick(player, currentOffHand) } @@ -239,69 +226,15 @@ class EventListener(private val plugin: Plugin) : Listener { } } - private fun getEquipmentItems(player: Player): List { - val equipment = player.equipment - val items = mutableListOf() - 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) { + // Helpers for tick dispatching (legacy style interaction) + private fun dispatchToItemTick(item: ItemStack, action: (AbstractItem) -> Unit) { if (item.type.isAir) return val id = AbstractItem.getId(item) ?: return val abstractItem = ItemRegistry.get(id) ?: return action(abstractItem) } - /** - * 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) { + private fun dispatchToComponentsTick(item: ItemStack, action: (CustomComponent) -> Unit) { if (item.type.isAir) return val meta = item.itemMeta ?: return val pdc = meta.persistentDataContainer