Initial Enchantment work

This commit is contained in:
Alexander Medvedev 2025-07-03 16:41:57 +02:00
parent fcf2700d65
commit 85007547c4
7 changed files with 2309 additions and 1 deletions

2109
assets/enchantments.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ mod block;
mod chunk_status; mod chunk_status;
mod composter_increase_chance; mod composter_increase_chance;
mod damage_type; mod damage_type;
mod enchantments;
mod entity_pose; mod entity_pose;
mod entity_status; mod entity_status;
mod entity_type; mod entity_type;
@ -72,6 +73,7 @@ pub fn main() {
"composter_increase_chance.rs", "composter_increase_chance.rs",
); );
write_generated_file(recipes::build(), "recipes.rs"); write_generated_file(recipes::build(), "recipes.rs");
write_generated_file(enchantments::build(), "enchantment.rs");
} }
pub fn array_to_tokenstream(array: &[String]) -> TokenStream { pub fn array_to_tokenstream(array: &[String]) -> TokenStream {
@ -99,7 +101,6 @@ pub fn write_generated_file(content: TokenStream, out_file: &str) {
// Doesn't matter if rustfmt is unavailable. // Doesn't matter if rustfmt is unavailable.
let _ = Command::new("rustfmt").arg(&path).output(); let _ = Command::new("rustfmt").arg(&path).output();
// Try to auto optimize using clippy. // Try to auto optimize using clippy.
// Doesn't matter if rustfmt is unavailable.
let _ = Command::new("cargo clippy --fix --allow-dirty") let _ = Command::new("cargo clippy --fix --allow-dirty")
.arg(&path) .arg(&path)
.output(); .output();

View File

@ -0,0 +1,119 @@
use std::{collections::HashMap, fs};
use heck::ToShoutySnakeCase;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use serde::Deserialize;
#[derive(Deserialize)]
struct Enchantment {
anvil_cost: u32,
supported_items: String,
max_level: i32,
slots: Vec<AttributeModifierSlot>, // TODO: add more
}
#[derive(Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum AttributeModifierSlot {
Any,
MainHand,
OffHand,
Hand,
Feet,
Legs,
Chest,
Head,
Armor,
Body,
Saddle,
}
impl AttributeModifierSlot {
fn to_tokens(&self) -> TokenStream {
match self {
AttributeModifierSlot::Any => quote! { AttributeModifierSlot::Any },
AttributeModifierSlot::MainHand => quote! { AttributeModifierSlot::MainHand },
AttributeModifierSlot::OffHand => quote! { AttributeModifierSlot::OffHand },
AttributeModifierSlot::Hand => quote! { AttributeModifierSlot::Hand },
AttributeModifierSlot::Feet => quote! { AttributeModifierSlot::Feet },
AttributeModifierSlot::Legs => quote! { AttributeModifierSlot::Legs },
AttributeModifierSlot::Chest => quote! { AttributeModifierSlot::Chest },
AttributeModifierSlot::Head => quote! { AttributeModifierSlot::Head },
AttributeModifierSlot::Armor => quote! { AttributeModifierSlot::Armor },
AttributeModifierSlot::Body => quote! { AttributeModifierSlot::Body },
AttributeModifierSlot::Saddle => quote! { AttributeModifierSlot::Saddle },
}
}
}
pub(crate) fn build() -> TokenStream {
println!("cargo:rerun-if-changed=../assets/enchantments.json");
let enchantments: HashMap<String, Enchantment> =
serde_json::from_str(&fs::read_to_string("../assets/enchantments.json").unwrap())
.expect("Failed to parse biome.json");
let mut variants = TokenStream::new();
let mut name_to_type = TokenStream::new();
for (name, enchantment) in enchantments.iter() {
let raw_name = name.strip_prefix("minecraft:").unwrap();
let format_name = format_ident!("{}", raw_name.to_shouty_snake_case());
let anvil_cost = enchantment.anvil_cost;
let supported_items = enchantment.supported_items.clone();
let max_level = enchantment.max_level;
let slots = enchantment.slots.clone();
let slots = slots.iter().map(|slot| slot.to_tokens());
variants.extend([quote! {
pub const #format_name: Enchantment = Enchantment {
name: #name,
anvil_cost: #anvil_cost,
supported_items: #supported_items,
max_level: #max_level,
slots: &[#(#slots),*]
};
}]);
name_to_type.extend(quote! { #name => Some(Self::#format_name), });
}
quote! {
#[derive(Debug, Clone)]
pub struct Enchantment {
pub name: &'static str,
pub anvil_cost: u32,
pub supported_items: &'static str,
pub max_level: i32,
pub slots: &'static [AttributeModifierSlot]
// TODO: add more
}
#[derive(Debug, Clone)]
pub enum AttributeModifierSlot {
Any,
MainHand,
OffHand,
Hand,
Feet,
Legs,
Chest,
Head,
Armor,
Body,
Saddle,
}
impl Enchantment {
#variants
pub fn from_name(name: &str) -> Option<Self> {
match name {
#name_to_type
_ => None
}
}
}
}
}

View File

@ -79,6 +79,11 @@ mod spawn_egg;
#[path = "generated/status_effect.rs"] #[path = "generated/status_effect.rs"]
mod status_effect; mod status_effect;
#[rustfmt::skip]
#[path = "generated/enchantment.rs"]
mod enchantment;
pub use enchantment::*;
pub mod entity { pub mod entity {
pub use super::entity_pose::*; pub use super::entity_pose::*;
pub use super::entity_status::*; pub use super::entity_status::*;

View File

@ -2,6 +2,7 @@ use std::{collections::HashMap, hash::Hash, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use bounded_num::{NotInBounds, Number}; use bounded_num::{NotInBounds, Number};
use pumpkin_data::Enchantment;
use pumpkin_data::damage::DamageType; use pumpkin_data::damage::DamageType;
use pumpkin_data::entity::EffectType; use pumpkin_data::entity::EffectType;
use pumpkin_data::particle::Particle; use pumpkin_data::particle::Particle;
@ -109,6 +110,7 @@ pub enum Arg<'a> {
SoundCategory(SoundCategory), SoundCategory(SoundCategory),
DamageType(DamageType), DamageType(DamageType),
Effect(EffectType), Effect(EffectType),
Enchantment(Enchantment),
} }
/// see [`crate::commands::tree::builder::argument`] and [`CommandTree::execute`]/[`crate::commands::tree::builder::NonLeafNodeBuilder::execute`] /// see [`crate::commands::tree::builder::argument`] and [`CommandTree::execute`]/[`crate::commands::tree::builder::NonLeafNodeBuilder::execute`]

View File

@ -0,0 +1,71 @@
use async_trait::async_trait;
use pumpkin_data::Enchantment;
use pumpkin_protocol::java::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders};
use crate::command::{
CommandSender,
args::{
Arg, ArgumentConsumer, ConsumedArgs, DefaultNameArgConsumer, FindArg,
GetClientSideArgParser,
},
dispatcher::CommandError,
tree::RawArgs,
};
use crate::server::Server;
pub struct EnchantmentArgumentConsumer;
impl GetClientSideArgParser for EnchantmentArgumentConsumer {
fn get_client_side_parser(&self) -> ArgumentType {
ArgumentType::Resource {
identifier: "enchantment",
}
}
fn get_client_side_suggestion_type_override(&self) -> Option<SuggestionProviders> {
None
}
}
#[async_trait]
impl ArgumentConsumer for EnchantmentArgumentConsumer {
async fn consume<'a>(
&'a self,
_sender: &CommandSender,
_server: &'a Server,
args: &mut RawArgs<'a>,
) -> Option<Arg<'a>> {
let name = args.pop()?;
// Create a static damage type first
let enchantment = Enchantment::from_name(name)?;
// Find matching static damage type from values array
Some(Arg::Enchantment(enchantment))
}
async fn suggest<'a>(
&'a self,
_sender: &CommandSender,
_server: &'a Server,
_input: &'a str,
) -> Result<Option<Vec<CommandSuggestion>>, CommandError> {
Ok(None)
}
}
impl DefaultNameArgConsumer for EnchantmentArgumentConsumer {
fn default_name(&self) -> &'static str {
"enchantment"
}
}
impl<'a> FindArg<'a> for EnchantmentArgumentConsumer {
type Data = &'a Enchantment;
fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Enchantment(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -1,4 +1,5 @@
pub mod damage_type; pub mod damage_type;
pub mod effect; pub mod effect;
pub mod enchantment;
pub mod item; pub mod item;
pub mod particle; pub mod particle;