-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat/PlayerModule/ReplantEnchantment #312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| @file:Suppress("ktlint:standard:no-wildcard-imports") | ||
|
|
||
| package org.xodium.vanillaplus | ||
|
|
||
| import io.papermc.paper.plugin.bootstrap.BootstrapContext | ||
| import io.papermc.paper.plugin.bootstrap.PluginBootstrap | ||
| import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents | ||
| import io.papermc.paper.registry.RegistryKey | ||
| import io.papermc.paper.registry.event.RegistryEvents | ||
| import io.papermc.paper.registry.keys.tags.EnchantmentTagKeys | ||
| import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys | ||
| import org.xodium.vanillaplus.enchantments.ReplantEnchantment | ||
|
|
||
| /** Main bootstrap class of the plugin. */ | ||
| @Suppress("UnstableApiUsage", "Unused") | ||
| internal class VanillaPlusBootstrap : PluginBootstrap { | ||
| companion object { | ||
| const val INSTANCE = "vanillaplus" | ||
| val REPLANT = ReplantEnchantment.key | ||
| val ENCHANTS = setOf(REPLANT) | ||
| } | ||
|
|
||
| override fun bootstrap(ctx: BootstrapContext) { | ||
| ctx.lifecycleManager.apply { | ||
| registerEventHandler( | ||
| RegistryEvents.ENCHANTMENT.compose().newHandler { event -> | ||
| val hoeTag = event.getOrCreateTag(ItemTypeTagKeys.HOES) | ||
| event.registry().apply { | ||
| register(REPLANT) { ReplantEnchantment.init(it).supportedItems(hoeTag) } | ||
| } | ||
| }, | ||
| ) | ||
| registerEventHandler(LifecycleEvents.TAGS.postFlatten(RegistryKey.ENCHANTMENT)) { event -> | ||
| event.registrar().apply { | ||
| addToTag(EnchantmentTagKeys.TRADEABLE, ENCHANTS) | ||
| addToTag(EnchantmentTagKeys.NON_TREASURE, ENCHANTS) | ||
| addToTag(EnchantmentTagKeys.IN_ENCHANTING_TABLE, ENCHANTS) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package org.xodium.vanillaplus.enchantments | ||
|
|
||
| import io.papermc.paper.registry.RegistryKey | ||
| import io.papermc.paper.registry.TypedKey | ||
| import io.papermc.paper.registry.data.EnchantmentRegistryEntry | ||
| import net.kyori.adventure.key.Key | ||
| import org.bukkit.enchantments.Enchantment | ||
| import org.bukkit.inventory.EquipmentSlotGroup | ||
| import org.xodium.vanillaplus.VanillaPlusBootstrap.Companion.INSTANCE | ||
| import org.xodium.vanillaplus.VanillaPlusBootstrap.Companion.REPLANT | ||
| import org.xodium.vanillaplus.interfaces.EnchantmentInterface | ||
| import org.xodium.vanillaplus.utils.ExtUtils.mm | ||
|
|
||
| @Suppress("UnstableApiUsage") | ||
| internal object ReplantEnchantment : EnchantmentInterface { | ||
| override val key: TypedKey<Enchantment> = TypedKey.create(RegistryKey.ENCHANTMENT, Key.key(INSTANCE, "replant")) | ||
|
|
||
| override fun init(builder: EnchantmentRegistryEntry.Builder): EnchantmentRegistryEntry.Builder = | ||
| builder | ||
| .description(REPLANT.value().replaceFirstChar { it.uppercase() }.mm()) | ||
| .anvilCost(8) | ||
| .maxLevel(1) | ||
| .weight(5) | ||
| .minimumCost(EnchantmentRegistryEntry.EnchantmentCost.of(1, 10)) | ||
| .maximumCost(EnchantmentRegistryEntry.EnchantmentCost.of(8, 20)) | ||
| .activeSlots(EquipmentSlotGroup.MAINHAND) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package org.xodium.vanillaplus.interfaces | ||
|
|
||
| import io.papermc.paper.registry.RegistryAccess | ||
| import io.papermc.paper.registry.RegistryKey | ||
| import io.papermc.paper.registry.TypedKey | ||
| import io.papermc.paper.registry.data.EnchantmentRegistryEntry | ||
| import org.bukkit.enchantments.Enchantment | ||
|
|
||
| /** Represents a contract for enchantments within the system. */ | ||
| @Suppress("UnstableApiUsage") | ||
| internal interface EnchantmentInterface { | ||
| /** | ||
| * The unique typed key that identifies this enchantment in the registry. | ||
| * @see TypedKey | ||
| * @see RegistryKey.ENCHANTMENT | ||
| */ | ||
| val key: TypedKey<Enchantment> | ||
|
|
||
| /** | ||
| * Initializes the Drift enchantment. | ||
| * @param builder The builder used to define the enchantment properties. | ||
| * @return The builder for method chaining. | ||
| */ | ||
| fun init(builder: EnchantmentRegistryEntry.Builder): EnchantmentRegistryEntry.Builder | ||
|
|
||
| /** | ||
| * Retrieves the enchantment from the registry. | ||
| * @return The [Enchantment] instance corresponding to the key. | ||
| * @throws NoSuchElementException if the enchantment is not found in the registry. | ||
| */ | ||
| fun get(): Enchantment = RegistryAccess.registryAccess().getRegistry(RegistryKey.ENCHANTMENT).getOrThrow(key) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,9 +7,12 @@ import io.papermc.paper.datacomponent.item.ItemLore | |||||||||||||||||||||||||||
| import io.papermc.paper.datacomponent.item.ResolvableProfile | ||||||||||||||||||||||||||||
| import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder | ||||||||||||||||||||||||||||
| import org.bukkit.Material | ||||||||||||||||||||||||||||
| import org.bukkit.block.Block | ||||||||||||||||||||||||||||
| import org.bukkit.block.data.Ageable | ||||||||||||||||||||||||||||
| import org.bukkit.entity.Player | ||||||||||||||||||||||||||||
| import org.bukkit.event.EventHandler | ||||||||||||||||||||||||||||
| import org.bukkit.event.EventPriority | ||||||||||||||||||||||||||||
| import org.bukkit.event.block.BlockBreakEvent | ||||||||||||||||||||||||||||
| import org.bukkit.event.entity.PlayerDeathEvent | ||||||||||||||||||||||||||||
| import org.bukkit.event.inventory.ClickType | ||||||||||||||||||||||||||||
| import org.bukkit.event.inventory.InventoryClickEvent | ||||||||||||||||||||||||||||
|
|
@@ -23,6 +26,7 @@ import org.bukkit.permissions.Permission | |||||||||||||||||||||||||||
| import org.bukkit.permissions.PermissionDefault | ||||||||||||||||||||||||||||
| import org.xodium.vanillaplus.VanillaPlus.Companion.instance | ||||||||||||||||||||||||||||
| import org.xodium.vanillaplus.data.CommandData | ||||||||||||||||||||||||||||
| import org.xodium.vanillaplus.enchantments.ReplantEnchantment | ||||||||||||||||||||||||||||
| import org.xodium.vanillaplus.interfaces.ModuleInterface | ||||||||||||||||||||||||||||
| import org.xodium.vanillaplus.pdcs.PlayerPDC.nickname | ||||||||||||||||||||||||||||
| import org.xodium.vanillaplus.utils.ExtUtils.mm | ||||||||||||||||||||||||||||
|
|
@@ -76,11 +80,15 @@ internal class PlayerModule( | |||||||||||||||||||||||||||
| @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) | ||||||||||||||||||||||||||||
| fun on(event: PlayerJoinEvent) { | ||||||||||||||||||||||||||||
| if (!enabled()) return | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| val player = event.player | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| player.displayName(player.nickname?.mm()) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (config.i18n.playerJoinMsg.isEmpty()) return | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| event.joinMessage(null) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| instance.server.onlinePlayers | ||||||||||||||||||||||||||||
| .filter { it.uniqueId != player.uniqueId } | ||||||||||||||||||||||||||||
| .forEach { | ||||||||||||||||||||||||||||
|
|
@@ -95,13 +103,16 @@ internal class PlayerModule( | |||||||||||||||||||||||||||
| @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) | ||||||||||||||||||||||||||||
| fun on(event: PlayerQuitEvent) { | ||||||||||||||||||||||||||||
| if (!enabled() || config.i18n.playerQuitMsg.isEmpty()) return | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| event.quitMessage(config.i18n.playerQuitMsg.mm(Placeholder.component("player", event.player.displayName()))) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) | ||||||||||||||||||||||||||||
| fun on(event: PlayerDeathEvent) { | ||||||||||||||||||||||||||||
| if (!enabled()) return | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| val killer = event.entity.killer ?: return | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (Math.random() < config.skullDropChance) { | ||||||||||||||||||||||||||||
| event.entity.world.dropItemNaturally( | ||||||||||||||||||||||||||||
| event.entity.location, | ||||||||||||||||||||||||||||
|
|
@@ -116,6 +127,7 @@ internal class PlayerModule( | |||||||||||||||||||||||||||
| @EventHandler | ||||||||||||||||||||||||||||
| fun on(event: PlayerAdvancementDoneEvent) { | ||||||||||||||||||||||||||||
| if (!enabled() || config.i18n.playerAdvancementDoneMsg.isEmpty()) return | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| event.message( | ||||||||||||||||||||||||||||
| config.i18n.playerAdvancementDoneMsg.mm( | ||||||||||||||||||||||||||||
| Placeholder.component("player", event.player.displayName()), | ||||||||||||||||||||||||||||
|
|
@@ -133,7 +145,9 @@ internal class PlayerModule( | |||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| event.isCancelled = true | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| instance.server.scheduler.runTask( | ||||||||||||||||||||||||||||
| instance, | ||||||||||||||||||||||||||||
| Runnable { event.whoClicked.openInventory(event.whoClicked.enderChest) }, | ||||||||||||||||||||||||||||
|
|
@@ -143,9 +157,43 @@ internal class PlayerModule( | |||||||||||||||||||||||||||
| @EventHandler(ignoreCancelled = true) | ||||||||||||||||||||||||||||
| fun on(event: PlayerInteractEvent) { | ||||||||||||||||||||||||||||
| if (!enabled()) return | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| xpToBottle(event) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @EventHandler | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
| @EventHandler | |
| @EventHandler(ignoreCancelled = true) |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code checks event.player.inventory.itemInMainHand but doesn't verify if this was actually the item used to break the block. Players can break blocks with their off-hand as well. Consider checking both main hand and off-hand, or use a more reliable method to determine which hand was used for breaking the block.
| replant(event.block, event.player.inventory.itemInMainHand) | |
| val mainHand = event.player.inventory.itemInMainHand | |
| val offHand = event.player.inventory.itemInOffHand | |
| // Prefer main hand if both have the enchantment, but check both | |
| if (mainHand.hasItemMeta() && mainHand.itemMeta.hasEnchant(ReplantEnchantment.get())) { | |
| replant(event.block, mainHand) | |
| } else if (offHand.hasItemMeta() && offHand.itemMeta.hasEnchant(ReplantEnchantment.get())) { | |
| replant(event.block, offHand) | |
| } |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The enchantment check doesn't verify that the tool is a hoe. While the enchantment is registered to only appear on hoes (in VanillaPlusBootstrap.kt), players could theoretically use commands or other means to add this enchantment to non-hoe items. Consider adding a check to ensure the tool is a hoe before attempting to replant, or document that this is intentionally allowed for any tool with the enchantment.
| if (ageable.age < ageable.maximumAge) return | |
| if (ageable.age < ageable.maximumAge) return | |
| // Only allow hoes to trigger replant | |
| if (tool.type !in setOf( | |
| Material.WOODEN_HOE, | |
| Material.STONE_HOE, | |
| Material.IRON_HOE, | |
| Material.GOLDEN_HOE, | |
| Material.DIAMOND_HOE, | |
| Material.NETHERITE_HOE | |
| )) return |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The function checks if the tool has the Replant enchantment on a hoe, but doesn't verify that the block being broken is actually a crop that should be replanted. While the cast to Ageable will return null for non-ageable blocks, some ageable blocks like saplings or stems might not be appropriate for this enchantment. Consider adding validation to ensure only appropriate crop blocks (e.g., wheat, carrots, potatoes, beetroots, etc.) are replanted, or document that all ageable blocks are intentionally supported.
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential race condition in the replanting logic. The code schedules a task to run 2 ticks later that sets block.type = blockType, but it doesn't verify that the block is actually AIR after the break event completes. If another plugin or event modifies the block during the delay, or if the BlockBreakEvent gets cancelled after this handler runs, this could overwrite that change or place a crop in an unexpected location. Consider checking if (block.type == Material.AIR) before setting the block type in the delayed task.
| block.type = blockType | |
| block.blockData = ageable.apply { age = 0 } | |
| if (block.type == Material.AIR) { | |
| block.type = blockType | |
| block.blockData = ageable.apply { age = 0 } | |
| } |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Using a 2-tick delay for replanting may be fragile. The block break event processes synchronously, and the drops/block state changes happen during event processing. A 2-tick delay might not be sufficient in all cases (e.g., with lag or other plugins modifying the block). Consider using a 1-tick delay (which is typically sufficient for the block to be fully processed) or adding validation that the block is AIR before replanting to ensure the break was successful.
| val blockType = block.type | |
| block.type = blockType | |
| block.blockData = ageable.apply { age = 0 } | |
| }, | |
| 2, | |
| // Only replant if the block is AIR (i.e., was actually broken) | |
| if (block.type == Material.AIR) { | |
| block.type = block.type // restore the crop type | |
| block.blockData = ageable.apply { age = 0 } | |
| } | |
| }, | |
| 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation refers to "Drift enchantment" but this interface is generic and used for all enchantments, including the "Replant" enchantment. The documentation should be updated to be generic (e.g., "Initializes the enchantment.")