Pumpkin/crates/pumpkin-plugin-api/src/enchantment.rs
2026-08-16 22:28:01 +02:00

378 lines
12 KiB
Rust

//! Plugin custom enchantment registration and builder utilities.
//!
//! This module provides a fluent, type-safe API for defining, registering, and querying
//! custom enchantments as well as applying custom enchantments to [`ItemStack`](crate::ItemStack)s.
//!
//! # Examples
//!
//! ## Defining and Registering a Custom Enchantment
//! ```rust,ignore
//! use pumpkin_plugin_api::{
//! enchantment::{AttributeModifierSlot, EnchantmentBuilder},
//! text::TextComponent,
//! Server,
//! };
//!
//! fn register_enchantments(server: &Server) {
//! let manager = server.get_enchantment_manager();
//!
//! manager.register(
//! EnchantmentBuilder::new("my_plugin:lifesteal", TextComponent::text("Life Steal"))
//! .max_level(3)
//! .anvil_cost(4)
//! .supported_items("#minecraft:enchantable/weapon")
//! .weight(2)
//! .slots([AttributeModifierSlot::MainHand])
//! .exclusive_with("custom:poison_touch")
//! ).expect("failed to register custom enchantment");
//! }
//! ```
//!
//! ## Applying Custom Enchantments to an [`ItemStack`](crate::ItemStack)
//! ```rust,ignore
//! use pumpkin_plugin_api::ItemStack;
//!
//! fn give_sword() -> ItemStack {
//! let mut sword = ItemStack::new("minecraft:diamond_sword", 1);
//! sword.add_custom_enchantment("my_plugin:lifesteal", 2);
//! assert!(sword.has_custom_enchantment("my_plugin:lifesteal"));
//! assert_eq!(sword.get_custom_enchantment_level("my_plugin:lifesteal"), Some(2));
//! sword
//! }
//! ```
pub use crate::wit::pumpkin::plugin::enchantments::{
AttributeModifierSlot, CustomEnchantment, Enchantment, EnchantmentManager,
};
pub use crate::wit::pumpkin::plugin::item_stack::CustomEnchantmentValue;
use crate::{Context, Server, TextComponent};
use std::fmt;
/// Errors that can occur when building or registering a custom enchantment.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EnchantmentError {
/// The enchantment identifier was empty.
EmptyId,
/// The enchantment maximum level was invalid (must be >= 1).
InvalidMaxLevel,
/// Registration with the server enchantment manager failed.
RegistrationFailed(String),
}
impl fmt::Display for EnchantmentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyId => write!(f, "enchantment identifier cannot be empty"),
Self::InvalidMaxLevel => write!(f, "enchantment max level must be at least 1"),
Self::RegistrationFailed(msg) => {
write!(f, "failed to register enchantment: {msg}")
}
}
}
}
impl std::error::Error for EnchantmentError {}
/// Fluent builder for constructing and validating a [`CustomEnchantment`].
pub struct EnchantmentBuilder {
id: String,
description: TextComponent,
max_level: u32,
anvil_cost: u32,
supported_items: String,
weight: u32,
slots: Vec<AttributeModifierSlot>,
exclusive_set: Vec<String>,
}
impl EnchantmentBuilder {
/// Creates a new enchantment builder with the given unique identifier and description.
///
/// # Example
/// ```rust,ignore
/// let builder = EnchantmentBuilder::new("my_plugin:lifesteal", TextComponent::text("Life Steal"));
/// ```
#[must_use]
pub fn new(id: impl Into<String>, description: impl Into<TextComponent>) -> Self {
Self {
id: id.into(),
description: description.into(),
max_level: 1,
anvil_cost: 4,
supported_items: "#minecraft:enchantable/weapon".into(),
weight: 5,
slots: vec![AttributeModifierSlot::MainHand],
exclusive_set: Vec::new(),
}
}
/// Sets the maximum level of the enchantment (default: 1).
#[must_use]
pub const fn max_level(mut self, max_level: u32) -> Self {
self.max_level = max_level;
self
}
/// Sets the base anvil cost multiplier (default: 4).
#[must_use]
pub const fn anvil_cost(mut self, anvil_cost: u32) -> Self {
self.anvil_cost = anvil_cost;
self
}
/// Sets the supported items pattern or tag (e.g. `"#minecraft:enchantable/weapon"`).
#[must_use]
pub fn supported_items(mut self, items: impl Into<String>) -> Self {
self.supported_items = items.into();
self
}
/// Sets the weight / rarity of the enchantment (1..=10, higher = more common, default: 5).
#[must_use]
pub const fn weight(mut self, weight: u32) -> Self {
self.weight = weight;
self
}
/// Adds a single active equipment slot for this enchantment.
#[must_use]
pub fn slot(mut self, slot: AttributeModifierSlot) -> Self {
self.slots.push(slot);
self
}
/// Replaces the active equipment slots for this enchantment.
#[must_use]
pub fn slots(mut self, slots: impl IntoIterator<Item = AttributeModifierSlot>) -> Self {
self.slots = slots.into_iter().collect();
self
}
/// Adds an exclusive / conflicting enchantment ID.
#[must_use]
pub fn exclusive_with(mut self, id: impl Into<String>) -> Self {
self.exclusive_set.push(id.into());
self
}
/// Replaces the exclusive / conflicting enchantment list.
#[must_use]
pub fn exclusive_set(mut self, set: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.exclusive_set = set.into_iter().map(Into::into).collect();
self
}
/// Validates the enchantment parameters.
///
/// # Errors
/// Returns [`EnchantmentError`] if validation fails.
pub fn validate(&self) -> Result<(), EnchantmentError> {
if self.id.trim().is_empty() {
return Err(EnchantmentError::EmptyId);
}
if self.max_level == 0 {
return Err(EnchantmentError::InvalidMaxLevel);
}
Ok(())
}
/// Builds the validated [`CustomEnchantment`] record.
///
/// # Errors
/// Returns [`EnchantmentError`] if validation fails.
pub fn build(self) -> Result<CustomEnchantment, EnchantmentError> {
self.validate()?;
Ok(CustomEnchantment {
id: self.id,
description: self.description,
max_level: self.max_level,
anvil_cost: self.anvil_cost,
supported_items: self.supported_items,
weight: self.weight,
slots: self.slots,
exclusive_set: self.exclusive_set,
})
}
/// Registers this custom enchantment directly with the provided [`EnchantmentManager`].
///
/// # Errors
/// Returns [`EnchantmentError`] if validation or registration fails.
pub fn register(self, manager: &EnchantmentManager) -> Result<(), EnchantmentError> {
manager.register(self)
}
/// Registers this custom enchantment with the server.
///
/// # Errors
/// Returns [`EnchantmentError`] if validation or registration fails.
pub fn register_to_server(self, server: &Server) -> Result<(), EnchantmentError> {
let manager = server.get_enchantment_manager();
manager.register(self)
}
/// Registers this custom enchantment with the plugin context.
///
/// # Errors
/// Returns [`EnchantmentError`] if validation or registration fails.
pub fn register_to_context(self, context: &Context) -> Result<(), EnchantmentError> {
let manager = context.get_enchantment_manager();
manager.register(self)
}
}
/// Trait for enchantment types that can be registered with an [`EnchantmentManager`].
pub trait RegistrableEnchantment {
/// Registers this enchantment with the provided enchantment manager.
///
/// # Errors
/// Returns [`EnchantmentError`] if validation or registration fails.
fn register(self, manager: &EnchantmentManager) -> Result<(), EnchantmentError>;
}
impl RegistrableEnchantment for EnchantmentBuilder {
fn register(self, manager: &EnchantmentManager) -> Result<(), EnchantmentError> {
let enchantment = self.build()?;
manager
.register_enchantment(enchantment)
.map_err(EnchantmentError::RegistrationFailed)
}
}
impl RegistrableEnchantment for CustomEnchantment {
fn register(self, manager: &EnchantmentManager) -> Result<(), EnchantmentError> {
manager
.register_enchantment(self)
.map_err(EnchantmentError::RegistrationFailed)
}
}
impl EnchantmentManager {
/// Registers a custom enchantment with the server.
///
/// Accepts [`EnchantmentBuilder`] or a [`CustomEnchantment`].
///
/// # Errors
/// Returns [`EnchantmentError`] if registration or validation fails.
pub fn register(
&self,
enchantment: impl RegistrableEnchantment,
) -> Result<(), EnchantmentError> {
enchantment.register(self)
}
/// Gets an enchantment definition by its ID (custom or vanilla).
#[must_use]
pub fn get(&self, id: &str) -> Option<CustomEnchantment> {
self.get_enchantment(id)
}
/// Checks if an enchantment ID is registered on the server.
#[must_use]
pub fn has(&self, id: &str) -> bool {
self.has_enchantment(id)
}
/// Returns all registered enchantment IDs (custom and vanilla).
#[must_use]
pub fn get_all_ids(&self) -> Vec<String> {
self.get_all_enchantment_ids()
}
}
impl Context {
/// Returns the global enchantment manager for registering and querying custom enchantments.
#[must_use]
pub fn get_enchantment_manager(&self) -> EnchantmentManager {
self.get_server().get_enchantment_manager()
}
/// Registers a custom enchantment with the server.
///
/// # Errors
/// Returns [`EnchantmentError`] if validation or registration fails.
pub fn register_enchantment(
&self,
enchantment: impl RegistrableEnchantment,
) -> Result<(), EnchantmentError> {
self.get_enchantment_manager().register(enchantment)
}
/// Gets an enchantment definition by its ID.
#[must_use]
pub fn get_enchantment(&self, id: &str) -> Option<CustomEnchantment> {
self.get_server().get_enchantment(id)
}
}
impl Server {
/// Registers a custom enchantment with the server.
///
/// # Errors
/// Returns [`EnchantmentError`] if validation or registration fails.
pub fn register_enchantment(
&self,
enchantment: impl RegistrableEnchantment,
) -> Result<(), EnchantmentError> {
self.get_enchantment_manager().register(enchantment)
}
}
/// Converts a positive integer to a standard Roman numeral representation (e.g. `1 -> "I"`, `3 -> "III"`, `5 -> "V"`).
#[must_use]
pub fn to_roman_numeral(level: u32) -> String {
match level {
1 => "I".into(),
2 => "II".into(),
3 => "III".into(),
4 => "IV".into(),
5 => "V".into(),
6 => "VI".into(),
7 => "VII".into(),
8 => "VIII".into(),
9 => "IX".into(),
10 => "X".into(),
_ => level.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enchantment_builder_defaults() {
let dummy_text: TextComponent = unsafe { std::mem::zeroed() };
let builder = EnchantmentBuilder::new("custom:freeze", dummy_text);
assert_eq!(builder.max_level, 1);
assert_eq!(builder.anvil_cost, 4);
assert_eq!(builder.weight, 5);
assert_eq!(builder.slots, vec![AttributeModifierSlot::MainHand]);
std::mem::forget(builder);
}
#[test]
fn enchantment_builder_validation() {
let dummy_text: TextComponent = unsafe { std::mem::zeroed() };
let builder = EnchantmentBuilder::new("", dummy_text);
assert_eq!(builder.validate(), Err(EnchantmentError::EmptyId));
std::mem::forget(builder);
let dummy_text: TextComponent = unsafe { std::mem::zeroed() };
let builder = EnchantmentBuilder::new("custom:poison", dummy_text).max_level(0);
assert_eq!(builder.validate(), Err(EnchantmentError::InvalidMaxLevel));
std::mem::forget(builder);
}
#[test]
fn roman_numerals() {
assert_eq!(to_roman_numeral(1), "I");
assert_eq!(to_roman_numeral(2), "II");
assert_eq!(to_roman_numeral(3), "III");
assert_eq!(to_roman_numeral(4), "IV");
assert_eq!(to_roman_numeral(5), "V");
assert_eq!(to_roman_numeral(10), "X");
assert_eq!(to_roman_numeral(255), "255");
}
}