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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/main/kotlin/org/xodium/vanillaplus/VanillaPlusBootstrap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys
import io.papermc.paper.registry.tag.TagKey
import io.papermc.paper.tag.TagEntry
import net.kyori.adventure.key.Key
import org.xodium.vanillaplus.enchantments.NightVisionEnchantment
import org.xodium.vanillaplus.enchantments.PickupEnchantment
import org.xodium.vanillaplus.enchantments.ReplantEnchantment

Expand All @@ -23,7 +24,8 @@ internal class VanillaPlusBootstrap : PluginBootstrap {
const val INSTANCE = "vanillaplus"
val REPLANT = ReplantEnchantment.key
val PICKUP = PickupEnchantment.key
val ENCHANTS = setOf(REPLANT, PICKUP)
val NIGHT_VISION = NightVisionEnchantment.key
val ENCHANTS = setOf(REPLANT, PICKUP, NIGHT_VISION)
val TOOLS = TagKey.create(RegistryKey.ITEM, Key.key(INSTANCE, "tools"))
}

Expand All @@ -47,9 +49,20 @@ internal class VanillaPlusBootstrap : PluginBootstrap {
RegistryEvents.ENCHANTMENT.compose().newHandler { event ->
event.registry().apply {
register(REPLANT) {
ReplantEnchantment.builder(it).supportedItems(event.getOrCreateTag(ItemTypeTagKeys.HOES))
ReplantEnchantment
.builder(it)
.supportedItems(event.getOrCreateTag(ItemTypeTagKeys.HOES))
}
register(PICKUP) {
PickupEnchantment
.builder(it)
.supportedItems(event.getOrCreateTag(TOOLS))
}
register(NIGHT_VISION) {
NightVisionEnchantment
.builder(it)
.supportedItems(event.getOrCreateTag(ItemTypeTagKeys.HEAD_ARMOR))
}
register(PICKUP) { PickupEnchantment.builder(it).supportedItems(event.getOrCreateTag(TOOLS)) }
}
},
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.xodium.vanillaplus.enchantments

import io.papermc.paper.event.entity.EntityEquipmentChangedEvent
import io.papermc.paper.registry.data.EnchantmentRegistryEntry
import org.bukkit.entity.Player
import org.bukkit.inventory.EquipmentSlotGroup
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
import org.xodium.vanillaplus.interfaces.EnchantmentInterface
import org.xodium.vanillaplus.utils.ExtUtils.mm

/** Represents an object handling night vision enchantment implementation within the system. */
@Suppress("UnstableApiUsage")
internal object NightVisionEnchantment : EnchantmentInterface {
override fun builder(builder: EnchantmentRegistryEntry.Builder): EnchantmentRegistryEntry.Builder =
builder
.description(key.value().replaceFirstChar { it.uppercase() }.mm())
.anvilCost(2)
.maxLevel(1)
.weight(2)
.minimumCost(EnchantmentRegistryEntry.EnchantmentCost.of(25, 0))
.maximumCost(EnchantmentRegistryEntry.EnchantmentCost.of(75, 0))
.activeSlots(EquipmentSlotGroup.ARMOR)

/**
* Handles the equipment change event to apply or remove night vision effect based on the helmet enchantment.
* @param event The EntityEquipmentChangedEvent triggered when an entity's equipment changes.
*/
fun nightVision(event: EntityEquipmentChangedEvent) {
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

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

The equipment change event doesn't verify that the changed slot is actually the helmet slot. The EntityEquipmentChangedEvent is triggered for any equipment change (helmet, chestplate, leggings, boots, main hand, off hand), so this handler will execute and reapply/remove the night vision effect even when changing boots or holding a different item. Consider checking event.slot to ensure it's a helmet-related slot before processing.

Copilot uses AI. Check for mistakes.
val player = event.entity as? Player ?: return
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

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

The event handler doesn't check if the entity is a Player before casting. While the cast uses safe cast (as?), the function continues execution for non-Player entities unnecessarily. More importantly, the event is fired for all entity equipment changes, not just players. Consider adding an early return check if (event.entity !is Player) return before the cast for better performance and clarity.

Suggested change
val player = event.entity as? Player ?: return
if (event.entity !is Player) return
val player = event.entity as Player

Copilot uses AI. Check for mistakes.
val helmet = player.inventory.helmet

if (helmet != null && helmet.hasItemMeta() && helmet.itemMeta.hasEnchant(get())) {
player.addPotionEffect(PotionEffect(PotionEffectType.NIGHT_VISION, -1, 0, true, false, true))
} else {
player.activePotionEffects
.filter { it.type == PotionEffectType.NIGHT_VISION }
.forEach { if (it.duration == -1) player.removePotionEffect(PotionEffectType.NIGHT_VISION) }
Comment on lines +34 to +38
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

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

The night vision effect is applied with duration -1 (infinite), but the removal logic only removes effects with duration -1. This creates a potential issue where if the player receives a night vision effect from another source (e.g., potion, beacon), it won't be removed when they unequip the helmet. Additionally, if the player has a legitimate night vision effect from another source, it will be removed when they unequip the helmet. Consider tracking the source of the night vision effect or using a different approach to manage the enchantment's effect.

Suggested change
player.addPotionEffect(PotionEffect(PotionEffectType.NIGHT_VISION, -1, 0, true, false, true))
} else {
player.activePotionEffects
.filter { it.type == PotionEffectType.NIGHT_VISION }
.forEach { if (it.duration == -1) player.removePotionEffect(PotionEffectType.NIGHT_VISION) }
// Apply night vision with a unique combination of flags to identify the helmet's effect
player.addPotionEffect(PotionEffect(PotionEffectType.NIGHT_VISION, -1, 0, true, false, true))
} else {
// Remove only the night vision effect applied by the helmet (identified by flags)
player.activePotionEffects
.filter {
it.type == PotionEffectType.NIGHT_VISION &&
it.duration == -1 &&
it.isAmbient &&
!it.hasParticles &&
it.hasIcon
}
.forEach { player.removePotionEffect(PotionEffectType.NIGHT_VISION) }

Copilot uses AI. Check for mistakes.
}
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
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.event.block.BlockBreakEvent
import org.bukkit.inventory.EquipmentSlotGroup
import org.xodium.vanillaplus.VanillaPlusBootstrap.Companion.INSTANCE
import org.xodium.vanillaplus.interfaces.EnchantmentInterface
import org.xodium.vanillaplus.utils.ExtUtils.mm

/** Represents an object handling pickup enchantment implementation within the system. */
@Suppress("UnstableApiUsage")
internal object PickupEnchantment : EnchantmentInterface {
override val key: TypedKey<Enchantment> = TypedKey.create(RegistryKey.ENCHANTMENT, Key.key(INSTANCE, "pickup"))

override fun builder(builder: EnchantmentRegistryEntry.Builder): EnchantmentRegistryEntry.Builder =
builder
.description(key.value().replaceFirstChar { it.uppercase() }.mm())
// TODO: Adjust costs and levels as needed
.anvilCost(8)
.anvilCost(2)
.maxLevel(1)
.weight(5)
.minimumCost(EnchantmentRegistryEntry.EnchantmentCost.of(1, 10))
.maximumCost(EnchantmentRegistryEntry.EnchantmentCost.of(8, 20))
.weight(2)
.minimumCost(EnchantmentRegistryEntry.EnchantmentCost.of(25, 0))
.maximumCost(EnchantmentRegistryEntry.EnchantmentCost.of(75, 0))
.activeSlots(EquipmentSlotGroup.MAINHAND)

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
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.block.data.Ageable
import org.bukkit.enchantments.Enchantment
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.inventory.EquipmentSlotGroup
import org.xodium.vanillaplus.VanillaPlus.Companion.instance
import org.xodium.vanillaplus.VanillaPlusBootstrap.Companion.INSTANCE
import org.xodium.vanillaplus.interfaces.EnchantmentInterface
import org.xodium.vanillaplus.utils.ExtUtils.mm

/** Represents an object handling replant enchantment implementation within the system. */
@Suppress("UnstableApiUsage")
internal object ReplantEnchantment : EnchantmentInterface {
override val key: TypedKey<Enchantment> = TypedKey.create(RegistryKey.ENCHANTMENT, Key.key(INSTANCE, "replant"))

override fun builder(builder: EnchantmentRegistryEntry.Builder): EnchantmentRegistryEntry.Builder =
builder
.description(key.value().replaceFirstChar { it.uppercase() }.mm())
// TODO: Adjust costs and levels as needed
.anvilCost(8)
.anvilCost(2)
.maxLevel(1)
.weight(5)
.minimumCost(EnchantmentRegistryEntry.EnchantmentCost.of(1, 10))
.maximumCost(EnchantmentRegistryEntry.EnchantmentCost.of(8, 20))
.weight(2)
.minimumCost(EnchantmentRegistryEntry.EnchantmentCost.of(25, 0))
.maximumCost(EnchantmentRegistryEntry.EnchantmentCost.of(75, 0))
.activeSlots(EquipmentSlotGroup.MAINHAND)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,33 @@ 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 net.kyori.adventure.key.Key
import org.bukkit.enchantments.Enchantment
import org.xodium.vanillaplus.VanillaPlusBootstrap.Companion.INSTANCE

/** Represents a contract for enchantments within the system. */
@Suppress("UnstableApiUsage")
internal interface EnchantmentInterface {
/**
* The unique typed key that identifies this enchantment in the registry.
* The unique typed key identifies this enchantment in the registry.
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

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

Grammatical error in the documentation: 'The unique typed key identifies' should be 'The unique typed key that identifies'. The relative pronoun 'that' is missing.

Suggested change
* The unique typed key identifies this enchantment in the registry.
* The unique typed key that identifies this enchantment in the registry.

Copilot uses AI. Check for mistakes.
* @see TypedKey
* @see RegistryKey.ENCHANTMENT
*/
val key: TypedKey<Enchantment>
get() =
TypedKey.create(
RegistryKey.ENCHANTMENT,
Key.key(
INSTANCE,
this::class
.simpleName
?.removeSuffix("Enchantment")
?.split(Regex("(?=[A-Z])"))
?.filter { it.isNotEmpty() }
?.joinToString("_") { it.lowercase() }
.toString(),
Comment on lines +25 to +31
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

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

The default enchantment key implementation uses this::class.simpleName which could be null. The current code calls .toString() on a nullable type chain, which could result in the string "null" being used as the enchantment key name if simpleName is null. Consider adding a null check or using requireNotNull() to fail fast with a clear error message.

Suggested change
this::class
.simpleName
?.removeSuffix("Enchantment")
?.split(Regex("(?=[A-Z])"))
?.filter { it.isNotEmpty() }
?.joinToString("_") { it.lowercase() }
.toString(),
requireNotNull(this::class.simpleName) { "Enchantment class must have a non-null simpleName for key generation." }
.removeSuffix("Enchantment")
.split(Regex("(?=[A-Z])"))
.filter { it.isNotEmpty() }
.joinToString("_") { it.lowercase() },

Copilot uses AI. Check for mistakes.
),
)

/**
* Configures the properties of the enchantment using the provided builder.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.papermc.paper.command.brigadier.Commands
import io.papermc.paper.datacomponent.DataComponentTypes
import io.papermc.paper.datacomponent.item.ItemLore
import io.papermc.paper.datacomponent.item.ResolvableProfile
import io.papermc.paper.event.entity.EntityEquipmentChangedEvent
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder
import org.bukkit.Material
import org.bukkit.entity.Player
Expand All @@ -24,6 +25,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.NightVisionEnchantment
import org.xodium.vanillaplus.enchantments.PickupEnchantment
import org.xodium.vanillaplus.enchantments.ReplantEnchantment
import org.xodium.vanillaplus.interfaces.ModuleInterface
Expand Down Expand Up @@ -168,6 +170,13 @@ internal class PlayerModule(
PickupEnchantment.pickup(event)
}

Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

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

[nitpick] Missing documentation comment for the new event handler. The other event handlers in this module have KDoc comments explaining their purpose. Consider adding a KDoc comment to maintain consistency with the rest of the codebase.

Suggested change
/**
* Handles the event when a player's equipment changes, triggering the night vision enchantment logic.
* @param event The EntityEquipmentChangedEvent triggered when a player's equipment is updated.
*/

Copilot uses AI. Check for mistakes.
@EventHandler
fun on(event: EntityEquipmentChangedEvent) {
if (!enabled()) return

NightVisionEnchantment.nightVision(event)
}

/**
* Handles the interaction event where a player can convert their experience points into an experience bottle
* if specific conditions are met.
Expand Down
Loading