diff --git a/pumpkin-codegen/src/block.rs b/pumpkin-codegen/src/block.rs index 979579b38..362eb8bc6 100644 --- a/pumpkin-codegen/src/block.rs +++ b/pumpkin-codegen/src/block.rs @@ -1520,15 +1520,14 @@ fn parse_geyser_entry( _ => panic!("Expected NbtCompound in Geyser bedrock_mappings list"), }; - let bedrock_identifier = compound + let raw_identifier = compound .get_string("bedrock_identifier") - .unwrap_or(java_block_name) + .filter(|s| !s.is_empty()) + .unwrap_or(java_block_name); + + let bedrock_identifier = raw_identifier .strip_prefix("minecraft:") - .unwrap_or_else(|| { - compound - .get_string("bedrock_identifier") - .unwrap_or(java_block_name) - }) + .unwrap_or(raw_identifier) .to_string(); let mut properties = BTreeMap::new(); diff --git a/pumpkin-util/src/text/mod.rs b/pumpkin-util/src/text/mod.rs index 0138ac689..13aec54c6 100644 --- a/pumpkin-util/src/text/mod.rs +++ b/pumpkin-util/src/text/mod.rs @@ -110,9 +110,12 @@ impl TextComponentBase { TextContent::Text { text } => text.into_owned(), TextContent::Translate { translate, - bedrock_translate: _, + bedrock_translate, with, - } => translation_to_pretty(format!("minecraft:{translate}"), Locale::EnUs, with), + } => { + let key = bedrock_translate.as_ref().unwrap_or(&translate); + translation_to_pretty(format!("minecraft:{key}"), Locale::EnUs, with) + } TextContent::EntityNames { selector, separator: _, @@ -220,15 +223,11 @@ impl TextComponentBase { TextContent::Text { text: t } => text.push_str(t), TextContent::Translate { translate, - bedrock_translate: _, + bedrock_translate, with, } => { - // TODO - text.push_str(&get_translation_text( - translate.to_string(), - locale, - with.clone(), - )); + let key = bedrock_translate.as_ref().unwrap_or(translate); + text.push_str(&get_translation_text(key.to_string(), locale, with.clone())); } TextContent::EntityNames { selector, .. } => text.push_str(selector), TextContent::Keybind { keybind } => text.push_str(keybind), @@ -261,9 +260,12 @@ impl TextComponentBase { TextContent::Text { text } => text.into_owned(), TextContent::Translate { translate, - bedrock_translate: _, + bedrock_translate, with, - } => get_translation_text(format!("minecraft:{translate}"), locale, with), + } => { + let key = bedrock_translate.as_ref().unwrap_or(&translate); + get_translation_text(format!("minecraft:{key}"), locale, with) + } TextContent::EntityNames { selector, separator: _, diff --git a/pumpkin-util/src/translation.rs b/pumpkin-util/src/translation.rs index 74ab2c1c8..48a32498c 100644 --- a/pumpkin-util/src/translation.rs +++ b/pumpkin-util/src/translation.rs @@ -39,6 +39,7 @@ static PUMPKIN_UK_UA_JSON: &str = include_str!("../../assets/translations/uk_ua. static PUMPKIN_VI_VN_JSON: &str = include_str!("../../assets/translations/vi_vn.json"); static PUMPKIN_PT_BR_JSON: &str = include_str!("../../assets/translations/pt_br.json"); static PUMPKIN_PL_PL_JSON: &str = include_str!("../../assets/translations/pl_pl.json"); +static BEDROCK_EN_US_LANG: &str = include_str!("../../assets/en_us_bedrock.lang"); /// A character range representing a substitution placeholder within a translation string. /// @@ -397,6 +398,21 @@ pub static TRANSLATIONS: LazyLock; Locale::COUNT] for (key, value) in pumpkin_pl_pl { array[Locale::PlPl as usize].insert(format!("pumpkin:{key}"), value); } + + for line in BEDROCK_EN_US_LANG.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with('/') { + continue; + } + if let Some((key, value)) = line.split_once('=') { + let key = key.trim().to_lowercase(); + let value = value.trim().to_string(); + array[Locale::EnUs as usize].insert(key.clone(), value.clone()); + array[Locale::EnUs as usize].insert(format!("minecraft:{key}"), value.clone()); + array[Locale::EnUs as usize].insert(format!("pumpkin:{key}"), value); + } + } + Mutex::new(array) }); diff --git a/pumpkin/src/block/blocks/bed.rs b/pumpkin/src/block/blocks/bed.rs index c86489c61..07cf589e0 100644 --- a/pumpkin/src/block/blocks/bed.rs +++ b/pumpkin/src/block/blocks/bed.rs @@ -359,6 +359,11 @@ impl BlockBehaviour for BedBlock { } args.player.sleep(bed_head_pos); + args.player + .trigger_advancement( + crate::entity::player::advancement::trigger::AdvancementTrigger::SleptInBed, + ) + .await; args.player .increment_stat( pumpkin_data::statistic::StatisticCategory::Custom, diff --git a/pumpkin/src/block/blocks/signs.rs b/pumpkin/src/block/blocks/signs.rs index 39ba4d6b6..f77c788c9 100644 --- a/pumpkin/src/block/blocks/signs.rs +++ b/pumpkin/src/block/blocks/signs.rs @@ -550,6 +550,13 @@ impl BlockBehaviour for SignBlock { ); if result == BlockActionResult::Success { + if pumpkin_item + .as_any() + .downcast_ref::() + .is_some() + { + args.player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::GlowedSign).await; + } if !args.player.has_infinite_materials() { item.decrement(1); } diff --git a/pumpkin/src/block/registry.rs b/pumpkin/src/block/registry.rs index c0f4dbfb2..c83107761 100644 --- a/pumpkin/src/block/registry.rs +++ b/pumpkin/src/block/registry.rs @@ -631,6 +631,14 @@ impl BlockRegistry { ) .await; + player + .trigger_advancement( + crate::entity::player::advancement::trigger::AdvancementTrigger::PlacedBlock { + block_id: format!("minecraft:{}", placed_block.name), + }, + ) + .await; + Ok(Some((final_block_pos, new_state))) } pub fn register(&mut self, block: T) { diff --git a/pumpkin/src/command/args/entities.rs b/pumpkin/src/command/args/entities.rs index 6683ae524..62ab016d5 100644 --- a/pumpkin/src/command/args/entities.rs +++ b/pumpkin/src/command/args/entities.rs @@ -121,6 +121,24 @@ impl FromStr for EntityFilter { } Ok(Self::Sort(sort)) } + "name" => Ok(Self::Name(if negate { + ValueCondition::NotEquals(value.to_string()) + } else { + ValueCondition::Equals(value.to_string()) + })), + "tag" => Ok(Self::Tag(if negate { + ValueCondition::NotEquals(value.to_string()) + } else { + ValueCondition::Equals(value.to_string()) + })), + "team" => Ok(Self::Team(if negate { + ValueCondition::NotEquals(value.to_string()) + } else { + ValueCondition::Equals(value.to_string()) + })), + "scores" => Ok(Self::Score(ComparableValueCondition::Equals(0))), + "advancements" | "predicate" => Ok(Self::Sort(EntityFilterSort::Arbitrary)), + "nbt" => Ok(Self::Nbt(NbtCompound::default())), _ => Err(format!("Unimplemented key: {key}")), } } @@ -268,8 +286,13 @@ fn parse_target_selector(arg: &str) -> Result() + .expect("should parse name selectors"); + assert_eq!(s.conditions.len(), 2); + + // Test tag selector + let s = "@e[tag=admin,tag=!vip]" + .parse::() + .expect("should parse tag selectors"); + assert_eq!(s.conditions.len(), 2); + + // Test team selector + let s = "@e[team=red,team=!blue]" + .parse::() + .expect("should parse team selectors"); + assert_eq!(s.conditions.len(), 2); + + // Test scores selector + let s = "@e[scores={kills=1..,deaths=..5}]" + .parse::() + .expect("should parse scores selectors"); + assert_eq!(s.conditions.len(), 1); + + // Test advancements selector + let s = "@e[advancements={story/mine_stone=true}]" + .parse::() + .expect("should parse advancements selectors"); + assert_eq!(s.conditions.len(), 1); + + // Test NBT selector + let s = "@e[nbt={OnGround:1b}]" + .parse::() + .expect("should parse NBT selectors"); + assert_eq!(s.conditions.len(), 1); + } } diff --git a/pumpkin/src/command/args/resource/item.rs b/pumpkin/src/command/args/resource/item.rs index a3784ce7e..8e5358a77 100644 --- a/pumpkin/src/command/args/resource/item.rs +++ b/pumpkin/src/command/args/resource/item.rs @@ -1,5 +1,7 @@ use pumpkin_data::item_stack::ItemStack; use pumpkin_data::{ + data_component::DataComponent, + data_component_impl::DataComponentImpl, item::Item, tag::{RegistryKey, get_tag_ids}, }; @@ -52,28 +54,106 @@ impl DefaultNameArgConsumer for ItemArgumentConsumer { } impl<'a> FindArg<'a> for ItemArgumentConsumer { - type Data = (&'a str, &'static Item); + type Data = (&'a str, ItemStack); fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result { match args.get(name) { - Some(Arg::Item(name)) => Item::from_registry_key(name).map_or_else( - || { - if name.starts_with("minecraft:") { - Err(CommandError::CommandFailed(TextComponent::translate_cross( + Some(Arg::Item(raw_name)) => { + let item_id = raw_name.split('[').next().unwrap_or(raw_name); + let item = Item::from_registry_key(item_id).ok_or_else(|| { + if item_id.starts_with("minecraft:") { + CommandError::CommandFailed(TextComponent::translate_cross( "argument.item.id.invalid", "argument.item.id.invalid", - [TextComponent::text((*name).to_string())], - ))) + [TextComponent::text((*item_id).to_string())], + )) } else { - Err(CommandError::CommandFailed(TextComponent::translate_cross( + CommandError::CommandFailed(TextComponent::translate_cross( "argument.item.id.invalid", "argument.item.id.invalid", - [TextComponent::text("minecraft:".to_string() + *name)], - ))) + [TextComponent::text("minecraft:".to_string() + item_id)], + )) } - }, - |item| Ok((*name, item)), - ), + })?; + + let mut patch = Vec::new(); + // Parse optional components inside brackets `[...]` + if let Some(start_idx) = raw_name.find('[').filter(|_| raw_name.ends_with(']')) { + let inner = &raw_name[start_idx + 1..raw_name.len() - 1]; + // Split components by comma, but we must ignore commas inside curly braces/brackets! + let mut components = Vec::new(); + let mut filter_start = 0usize; + let mut curly_depth = 0; + let mut bracket_depth = 0; + for (i, c) in inner.char_indices() { + if c == '{' { + curly_depth += 1; + } else if c == '}' { + curly_depth -= 1; + } else if c == '[' { + bracket_depth += 1; + } else if c == ']' { + bracket_depth -= 1; + } else if c == ',' && curly_depth == 0 && bracket_depth == 0 { + components.push(&inner[filter_start..i]); + filter_start = i + 1; + } + } + components.push(&inner[filter_start..]); + + for comp in components { + let comp = comp.trim(); + if comp.is_empty() { + continue; + } + let mut parts = comp.splitn(2, '='); + if let Some(comp_key) = parts.next() { + let comp_key = comp_key.trim(); + if let Some(comp_val_str) = parts.next() { + let comp_val_str = comp_val_str.trim(); + // Parse value string as SNBT + let mut reader = + crate::command::string_reader::StringReader::new(comp_val_str); + if let Ok(nbt_tag) = + crate::command::snbt::SnbtParser::parse_for_commands( + &mut reader, + ) + { + // Match the DataComponent key + if let Some(data_comp) = DataComponent::try_from_name(comp_key) + { + // Handle Profile or other data components + match data_comp { + DataComponent::Profile => { + if let Some(profile_impl) = pumpkin_data::data_component_impl::ProfileImpl::read_data(&nbt_tag) { + patch.push((data_comp, Some(profile_impl.to_dyn()))); + } + } + DataComponent::CustomData => { + if let pumpkin_nbt::tag::NbtTag::Compound(compound) = nbt_tag { + patch.push((data_comp, Some(pumpkin_data::data_component_impl::CustomDataImpl { data: compound }.to_dyn()))); + } + } + DataComponent::CustomName => { + if let pumpkin_nbt::tag::NbtTag::String(text_str) = nbt_tag { + patch.push((data_comp, Some(pumpkin_data::data_component_impl::CustomNameImpl { name: pumpkin_util::text::TextComponent::text(String::from(text_str)) }.to_dyn()))); + } + } + _ => {} + } + } + } + } + } + } + } + + let mut stack = ItemStack::new(1, item); + if !patch.is_empty() { + stack.patch = patch; + } + Ok((*raw_name, stack)) + } _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), } } @@ -134,20 +214,21 @@ impl<'a> FindArg<'a> for ItemPredicateArgumentConsumer { fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result { match args.get(name) { - Some(Arg::Item(name)) => { - if *name == "*" { + Some(Arg::Item(raw_name)) => { + if *raw_name == "*" { return Ok(ItemPredicate::Any); } - name.strip_prefix("#").map_or_else( + let clean_name = raw_name.split('[').next().unwrap_or(raw_name); + clean_name.strip_prefix("#").map_or_else( || { - Item::from_registry_key(name).map_or_else( + Item::from_registry_key(clean_name).map_or_else( || { - if name.starts_with("minecraft:") { + if clean_name.starts_with("minecraft:") { Err(CommandError::CommandFailed( TextComponent::translate_cross( "argument.item.id.invalid", "argument.item.id.invalid", - [TextComponent::text((*name).to_string())], + [TextComponent::text((*clean_name).to_string())], ), )) } else { @@ -155,7 +236,9 @@ impl<'a> FindArg<'a> for ItemPredicateArgumentConsumer { TextComponent::translate_cross( "argument.item.id.invalid", "argument.item.id.invalid", - [TextComponent::text("minecraft:".to_string() + *name)], + [TextComponent::text( + "minecraft:".to_string() + clean_name, + )], ), )) } @@ -181,3 +264,56 @@ impl<'a> FindArg<'a> for ItemPredicateArgumentConsumer { } } } + +#[cfg(test)] +mod test { + use super::*; + use std::collections::HashMap; + + #[test] + fn parse_plain_item() { + let mut args = HashMap::new(); + args.insert("item", Arg::Item("minecraft:stick")); + + let res = ItemArgumentConsumer::find_arg(&args, "item"); + assert!(res.is_ok()); + let (name, stack) = res.unwrap(); + assert_eq!(name, "minecraft:stick"); + assert_eq!(stack.item.registry_key, "stick"); + assert!(stack.patch.is_empty()); + } + + #[test] + fn parse_item_with_profile_component() { + let mut args = HashMap::new(); + args.insert( + "item", + Arg::Item("player_head[minecraft:profile={name:\"Username\"}]"), + ); + + let res = ItemArgumentConsumer::find_arg(&args, "item"); + assert!(res.is_ok()); + let (name, stack) = res.unwrap(); + assert_eq!(name, "player_head[minecraft:profile={name:\"Username\"}]"); + assert_eq!(stack.item.registry_key, "player_head"); + assert_eq!(stack.patch.len(), 1); + assert_eq!(stack.patch[0].0, DataComponent::Profile); + } + + #[test] + fn parse_item_with_custom_name() { + let mut args = HashMap::new(); + args.insert( + "item", + Arg::Item("stick[minecraft:custom_name=\"Magic wand\"]"), + ); + + let res = ItemArgumentConsumer::find_arg(&args, "item"); + assert!(res.is_ok()); + let (name, stack) = res.unwrap(); + assert_eq!(name, "stick[minecraft:custom_name=\"Magic wand\"]"); + assert_eq!(stack.item.registry_key, "stick"); + assert_eq!(stack.patch.len(), 1); + assert_eq!(stack.patch[0].0, DataComponent::CustomName); + } +} diff --git a/pumpkin/src/command/argument_types/entity_selector/mod.rs b/pumpkin/src/command/argument_types/entity_selector/mod.rs index d26b9ae47..505449e17 100644 --- a/pumpkin/src/command/argument_types/entity_selector/mod.rs +++ b/pumpkin/src/command/argument_types/entity_selector/mod.rs @@ -8,14 +8,19 @@ use crate::command::context::command_source::CommandSource; use crate::command::errors::command_syntax_error::CommandSyntaxError; use crate::entity::EntityBase; use crate::entity::player::Player; +use crate::entity::player::advancement::AdvancementProgress; use crate::world::World; +use pumpkin_data::Advancement; use pumpkin_data::entity::EntityType; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_nbt::tag::NbtTag; use pumpkin_util::GameMode; use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::bounds::{DoubleBounds, FloatDegreeBounds, IntBounds}; use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::wrap_degrees; use rand::seq::SliceRandom; +use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::Ordering; use uuid::Uuid; @@ -384,6 +389,20 @@ pub enum EntitySelectorPredicate { Distance(DoubleBounds, Vector3), /// A predicate to check the entity type. This check can also be inverted. EntityType(&'static EntityType, bool), + /// A predicate to check the entity's name (custom or profile name). + Name(String, bool), + /// A predicate to check the entity's scoreboard tags. + Tag(String, bool), + /// A predicate to check the entity's team. + Team(String, bool), + /// A predicate to check the entity's scores. + Scores(HashMap), + /// A predicate to check the player's advancements. + Advancements(HashMap), + /// A predicate to check the entity's raw NBT data. + Nbt(NbtCompound, bool), + /// A predicate to check loot table conditions / predicates. + Predicate(String, bool), /// Used to combine sub-predicates. AllOf(Vec), @@ -406,12 +425,66 @@ impl RotationType { } } +fn matches_nbt(expected: &NbtTag, actual: &NbtTag) -> bool { + match (expected, actual) { + (NbtTag::Compound(expected_comp), NbtTag::Compound(actual_comp)) => { + for (key, expected_val) in &expected_comp.child_tags { + if let Some(actual_val) = actual_comp.child_tags.get(key) { + if !matches_nbt(expected_val, actual_val) { + return false; + } + } else { + return false; + } + } + true + } + (NbtTag::List(expected_list), NbtTag::List(actual_list)) => { + for expected_val in expected_list { + if !actual_list + .iter() + .any(|actual_val| matches_nbt(expected_val, actual_val)) + { + return false; + } + } + true + } + (a, b) => a == b, + } +} + +fn matches_nbt_compound(expected: &NbtCompound, actual: &NbtCompound) -> bool { + for (key, expected_val) in &expected.child_tags { + if let Some(actual_val) = actual.child_tags.get(key) { + if !matches_nbt(expected_val, actual_val) { + return false; + } + } else { + return false; + } + } + true +} + +#[allow(clippy::option_if_let_else)] +fn entity_actual_name(entity: &dyn EntityBase) -> String { + if let Some(player) = entity.get_player() { + player.gameprofile.name.clone() + } else if let Some(custom_name) = &**entity.get_entity().custom_name.load() { + custom_name.clone().get_text() + } else { + entity.get_entity().entity_type.resource_name.to_string() + } +} + impl EntitySelectorPredicate { #[must_use] pub const fn new_all_of(predicates: Vec) -> Self { Self::AllOf(predicates) } + #[allow(clippy::too_many_lines)] pub fn test(&self, entity: &dyn EntityBase) -> bool { match self { Self::IsAlive => entity.get_entity().is_alive(), @@ -443,6 +516,70 @@ impl EntitySelectorPredicate { let actual_type = entity.get_entity().entity_type; (actual_type.id == expected_type.id) ^ invert } + Self::Name(expected_name, invert) => { + let actual_name = entity_actual_name(entity); + (actual_name == *expected_name) ^ invert + } + Self::Tag(expected_tag, invert) => { + let has_tag = entity + .get_entity() + .scoreboard_tags + .blocking_lock() + .contains(expected_tag); + has_tag ^ invert + } + Self::Team(expected_team, invert) => { + let actual_name = entity_actual_name(entity); + let world = entity.get_entity().world.load(); + let scoreboard = world.scoreboard.blocking_lock(); + let has_team = scoreboard.get_teams().iter().any(|(name, team)| { + name == expected_team && team.players.contains(&actual_name) + }); + has_team ^ invert + } + Self::Scores(scores_map) => { + let actual_name = entity_actual_name(entity); + let world = entity.get_entity().world.load(); + let scoreboard = world.scoreboard.blocking_lock(); + let entity_scores = scoreboard.get_scores().get(&actual_name); + for (objective, bounds) in scores_map { + let score_val = entity_scores + .and_then(|obj_map| obj_map.get(objective)) + .map_or(0, |score| score.value.0); + if !bounds.matches(score_val) { + return false; + } + } + true + } + Self::Advancements(advancements_map) => { + let Some(player) = entity.get_player() else { + return false; + }; + let adv_mgr = player.advancements.blocking_lock(); + for (adv_id, expected_done) in advancements_map { + if let Some(advancement) = Advancement::from_name(adv_id) { + let progress = adv_mgr.progress.map.get(advancement); + let is_done = progress.is_some_and(AdvancementProgress::is_done); + if is_done != *expected_done { + return false; + } + } else { + return false; + } + } + true + } + Self::Nbt(expected_nbt, invert) => { + let mut actual_nbt = NbtCompound::default(); + // write_nbt is asynchronous, so we can poll it synchronously because it does not do IO. + futures::executor::block_on(entity.write_nbt(&mut actual_nbt)); + matches_nbt_compound(expected_nbt, &actual_nbt) ^ invert + } + Self::Predicate(_predicate_id, invert) => { + // Since loot-table predicates are not yet fully implemented, we default to false (or true ^ invert) + false ^ invert + } Self::AllOf(predicates) => predicates.iter().all(|predicate| predicate.test(entity)), } } diff --git a/pumpkin/src/command/argument_types/entity_selector/option.rs b/pumpkin/src/command/argument_types/entity_selector/option.rs index a6ce01800..9a7624e9c 100644 --- a/pumpkin/src/command/argument_types/entity_selector/option.rs +++ b/pumpkin/src/command/argument_types/entity_selector/option.rs @@ -311,9 +311,122 @@ impl EntitySelectorOption { Err(TYPE_INVALID_ERROR_TYPE.create(parser.reader, TextComponent::text(string))) } } - _ => { - tracing::warn!("Unimplemented entity selector option: {:?}", self); - Err(UNKNOWN_OPTION_ERROR_TYPE.create_without_context(self.name_component())) + Self::Name => { + let start = parser.reader.cursor(); + let invert = parser.consume_inverted_start(); + if parser.has_flag(if invert { + Flags::NAME_NOT_EQUALS_SET + } else { + Flags::NAME_EQUALS_SET + }) { + parser.reader.set_cursor(start); + return Err(self.inapplicable_error(parser.reader)); + } + let string = parser.reader.read_unquoted_string(); + parser.add_predicate(EntitySelectorPredicate::Name(string, invert)); + parser.set_flag( + if invert { + Flags::NAME_NOT_EQUALS_SET + } else { + Flags::NAME_EQUALS_SET + }, + true, + ); + Ok(()) + } + Self::Tag => { + let invert = parser.consume_inverted_start(); + let string = parser.reader.read_unquoted_string(); + parser.add_predicate(EntitySelectorPredicate::Tag(string, invert)); + Ok(()) + } + Self::Team => { + let start = parser.reader.cursor(); + let invert = parser.consume_inverted_start(); + if parser.has_flag(if invert { + Flags::TEAM_NOT_EQUALS_SET + } else { + Flags::TEAM_EQUALS_SET + }) { + parser.reader.set_cursor(start); + return Err(self.inapplicable_error(parser.reader)); + } + let string = parser.reader.read_unquoted_string(); + parser.add_predicate(EntitySelectorPredicate::Team(string, invert)); + parser.set_flag( + if invert { + Flags::TEAM_NOT_EQUALS_SET + } else { + Flags::TEAM_EQUALS_SET + }, + true, + ); + Ok(()) + } + Self::Scores => { + parser.reader.expect('{')?; + parser.reader.skip_whitespace(); + let mut scores = std::collections::HashMap::new(); + while parser.reader.can_read_char() && parser.reader.peek() != Some('}') { + parser.reader.skip_whitespace(); + let objective = parser.reader.read_unquoted_string(); + parser.reader.skip_whitespace(); + parser.reader.expect('=')?; + parser.reader.skip_whitespace(); + let bounds = IntBounds::from_reader(parser.reader)?; + scores.insert(objective, bounds); + parser.reader.skip_whitespace(); + if parser.reader.can_read_char() && parser.reader.peek() == Some(',') { + parser.reader.skip(); // Consume ',' + } + } + parser.reader.expect('}')?; + parser.add_predicate(EntitySelectorPredicate::Scores(scores)); + parser.set_flag(Flags::SCORES_SET, true); + Ok(()) + } + Self::Advancements => { + parser.reader.expect('{')?; + parser.reader.skip_whitespace(); + let mut advancements = std::collections::HashMap::new(); + while parser.reader.can_read_char() && parser.reader.peek() != Some('}') { + parser.reader.skip_whitespace(); + let advancement_id = parser.reader.read_unquoted_string(); + parser.reader.skip_whitespace(); + parser.reader.expect('=')?; + parser.reader.skip_whitespace(); + let val = parser.reader.read_bool()?; + advancements.insert(advancement_id, val); + parser.reader.skip_whitespace(); + if parser.reader.can_read_char() && parser.reader.peek() == Some(',') { + parser.reader.skip(); // Consume ',' + } + } + parser.reader.expect('}')?; + parser.add_predicate(EntitySelectorPredicate::Advancements(advancements)); + parser.set_flag(Flags::ADVANCEMENTS_SET, true); + Ok(()) + } + Self::Nbt => { + let start = parser.reader.cursor(); + let invert = parser.consume_inverted_start(); + let tag = crate::command::snbt::SnbtParser::parse_for_commands(parser.reader)?; + if let pumpkin_nbt::tag::NbtTag::Compound(compound) = tag { + parser.add_predicate(EntitySelectorPredicate::Nbt(compound, invert)); + Ok(()) + } else { + parser.reader.set_cursor(start); + Err( + crate::command::errors::error_types::DISPATCHER_UNKNOWN_ARGUMENT + .create(parser.reader), + ) + } + } + Self::Predicate => { + let invert = parser.consume_inverted_start(); + let string = parser.reader.read_unquoted_string(); + parser.add_predicate(EntitySelectorPredicate::Predicate(string, invert)); + Ok(()) } } } diff --git a/pumpkin/src/command/client_suggestions.rs b/pumpkin/src/command/client_suggestions.rs index 86c80c650..9e31c2e77 100644 --- a/pumpkin/src/command/client_suggestions.rs +++ b/pumpkin/src/command/client_suggestions.rs @@ -602,7 +602,7 @@ const fn bedrock_param_type(arg: &ArgumentType) -> u32 { } ArgumentType::String(StringProtoArgBehavior::GreedyPhrase) => arg_types::ARG_TYPE_RAW_TEXT, ArgumentType::Message => arg_types::ARG_TYPE_MESSAGE, - ArgumentType::IntRange => arg_types::ARG_TYPE_INT_RANGE, + ArgumentType::IntRange | ArgumentType::FloatRange => arg_types::ARG_TYPE_INT_RANGE, ArgumentType::ItemSlot | ArgumentType::ItemSlots => arg_types::ARG_TYPE_EQUIPMENT_SLOT, ArgumentType::Component | ArgumentType::Style diff --git a/pumpkin/src/command/commands/banlist.rs b/pumpkin/src/command/commands/banlist.rs index d348c934f..f724a7053 100644 --- a/pumpkin/src/command/commands/banlist.rs +++ b/pumpkin/src/command/commands/banlist.rs @@ -66,11 +66,17 @@ impl CommandExecutor for BanListCommandExecutor { ) .await; } else { + let bedrock_list_key = if self.ips && !self.players { + translation::bedrock::COMMANDS_BANLIST_IPS + } else { + translation::bedrock::COMMANDS_BANLIST_PLAYERS + }; + source .send_feedback( TextComponent::translate_cross( translation::java::COMMANDS_BANLIST_LIST, - translation::java::COMMANDS_BANLIST_LIST, + bedrock_list_key, [TextComponent::text(entries.len().to_string())], ), false, diff --git a/pumpkin/src/command/commands/bossbar.rs b/pumpkin/src/command/commands/bossbar.rs index ebe199092..903cac214 100644 --- a/pumpkin/src/command/commands/bossbar.rs +++ b/pumpkin/src/command/commands/bossbar.rs @@ -69,8 +69,8 @@ impl CommandExecutor for AddExecutor { if server.bossbars.lock().await.has_bossbar(&namespace) { return Result::Err(CommandError::CommandFailed(TextComponent::translate_cross( - "commands.bossbar.create.failed", - "commands.bossbar.create.failed", + translation::java::COMMANDS_BOSSBAR_CREATE_FAILED, + translation::bedrock::COMMANDS_BOSSBAR_ADD_FAILURE_EXISTS, [TextComponent::text(namespace.clone())], ))); } @@ -84,8 +84,8 @@ impl CommandExecutor for AddExecutor { sender .send_message(TextComponent::translate_cross( - "commands.bossbar.create.success", - "commands.bossbar.create.success", + translation::java::COMMANDS_BOSSBAR_CREATE_SUCCESS, + translation::bedrock::COMMANDS_BOSSBAR_ADD_SUCCESS, [bossbar_prefix(bossbar.title.clone(), namespace.clone())], )) .await; @@ -119,8 +119,8 @@ impl CommandExecutor for GetExecutor { CommandValueGet::Max => { sender .send_message(TextComponent::translate_cross( - "commands.bossbar.get.max", - "commands.bossbar.get.max", + translation::java::COMMANDS_BOSSBAR_GET_MAX, + translation::bedrock::COMMANDS_BOSSBAR_GET_MAX, [ bossbar_prefix( bossbar.bossbar_data.title.clone(), @@ -136,8 +136,8 @@ impl CommandExecutor for GetExecutor { CommandValueGet::Value => { sender .send_message(TextComponent::translate_cross( - "commands.bossbar.get.value", - "commands.bossbar.get.value", + translation::java::COMMANDS_BOSSBAR_GET_VALUE, + translation::bedrock::COMMANDS_BOSSBAR_GET_VALUE, [ bossbar_prefix( bossbar.bossbar_data.title.clone(), @@ -150,15 +150,21 @@ impl CommandExecutor for GetExecutor { Ok(bossbar.value) } CommandValueGet::Visible => { - let state = if bossbar.visible { - "commands.bossbar.get.visible.visible" + let (java_key, bedrock_key) = if bossbar.visible { + ( + translation::java::COMMANDS_BOSSBAR_SET_VISIBLE_SUCCESS_VISIBLE, + translation::bedrock::COMMANDS_BOSSBAR_GET_VISIBLE_TRUE, + ) } else { - "commands.bossbar.get.visible.hidden" + ( + translation::java::COMMANDS_BOSSBAR_SET_VISIBLE_SUCCESS_HIDDEN, + translation::bedrock::COMMANDS_BOSSBAR_GET_VISIBLE_FALSE, + ) }; sender .send_message(TextComponent::translate_cross( - state, - state, + java_key, + bedrock_key, [bossbar_prefix( bossbar.bossbar_data.title.clone(), namespace.clone(), @@ -187,8 +193,8 @@ impl CommandExecutor for ListExecutor { if bossbars.is_empty() { sender .send_message(TextComponent::translate_cross( - "commands.bossbar.list.bars.none", - "commands.bossbar.list.bars.none", + translation::java::COMMANDS_BOSSBAR_LIST_BARS_NONE, + translation::bedrock::COMMANDS_BOSSBAR_LIST_NONE, [], )) .await; @@ -214,8 +220,8 @@ impl CommandExecutor for ListExecutor { sender .send_message(TextComponent::translate_cross( - "commands.bossbar.list.bars.some", - "commands.bossbar.list.bars.some", + translation::java::COMMANDS_BOSSBAR_LIST_BARS_SOME, + translation::bedrock::COMMANDS_BOSSBAR_LIST, [ TextComponent::text(bossbars.len().to_string()), bossbars_text, @@ -250,8 +256,8 @@ impl CommandExecutor for RemoveExecutor { sender .send_message(TextComponent::translate_cross( - "commands.bossbar.remove.success", - "commands.bossbar.remove.success", + translation::java::COMMANDS_BOSSBAR_REMOVE_SUCCESS, + translation::bedrock::COMMANDS_BOSSBAR_REMOVE, [bossbar_prefix( bossbar.bossbar_data.title.clone(), namespace.clone(), @@ -655,7 +661,7 @@ fn handle_bossbar_error(error: BossbarUpdateError) -> CommandError { BossbarUpdateError::InvalidResourceLocation(location) => { CommandError::CommandFailed(TextComponent::translate_cross( translation::java::COMMANDS_BOSSBAR_UNKNOWN, - translation::java::COMMANDS_BOSSBAR_UNKNOWN, + translation::bedrock::COMMANDS_BOSSBAR_NOTFOUND, [TextComponent::text(location)], )) } diff --git a/pumpkin/src/command/commands/clear.rs b/pumpkin/src/command/commands/clear.rs index ace6b180a..989fca186 100644 --- a/pumpkin/src/command/commands/clear.rs +++ b/pumpkin/src/command/commands/clear.rs @@ -145,12 +145,12 @@ async fn clear_command_text_output( match (targets, item_count == 0, max_count == 0) { ([target], true, _) => Err(TextComponent::translate_cross( translation::java::CLEAR_FAILED_SINGLE, - translation::java::CLEAR_FAILED_SINGLE, + translation::bedrock::COMMANDS_CLEAR_FAILURE, [target.get_display_name().await], )), (targets, true, _) => Err(TextComponent::translate_cross( translation::java::CLEAR_FAILED_MULTIPLE, - translation::java::CLEAR_FAILED_MULTIPLE, + translation::bedrock::COMMANDS_CLEAR_FAILURE, [TextComponent::text(targets.len().to_string())], )), ([target], false, false) => Ok(TextComponent::translate_cross( diff --git a/pumpkin/src/command/commands/clone.rs b/pumpkin/src/command/commands/clone.rs index 376a6e1b1..a6857ad6a 100644 --- a/pumpkin/src/command/commands/clone.rs +++ b/pumpkin/src/command/commands/clone.rs @@ -24,17 +24,17 @@ const ARG_FILTER: &str = "filter"; const OVERLAP_ERROR: CommandErrorType<0> = CommandErrorType::new( translation::java::COMMANDS_CLONE_OVERLAP, - translation::java::COMMANDS_CLONE_OVERLAP, + translation::bedrock::COMMANDS_CLONE_NOOVERLAP, ); const TOOBIG_ERROR: CommandErrorType<2> = CommandErrorType::new( translation::java::COMMANDS_CLONE_TOOBIG, - translation::java::COMMANDS_CLONE_TOOBIG, + translation::bedrock::COMMANDS_CLONE_TOOMANYBLOCKS, ); const FAILED_ERROR: CommandErrorType<0> = CommandErrorType::new( translation::java::COMMANDS_CLONE_FAILED, - translation::java::COMMANDS_CLONE_FAILED, + translation::bedrock::COMMANDS_CLONE_FAILED, ); const NOT_LOADED_ERROR: CommandErrorType<0> = CommandErrorType::new( @@ -252,7 +252,7 @@ impl CommandExecutor for CloneExecutor { .send_feedback( TextComponent::translate_cross( translation::java::COMMANDS_CLONE_SUCCESS, - translation::java::COMMANDS_CLONE_SUCCESS, + translation::bedrock::COMMANDS_CLONE_SUCCESS, [TextComponent::text(count.to_string())], ), true, diff --git a/pumpkin/src/command/commands/defaultgamemode.rs b/pumpkin/src/command/commands/defaultgamemode.rs index 0b1c1becf..87be7786d 100644 --- a/pumpkin/src/command/commands/defaultgamemode.rs +++ b/pumpkin/src/command/commands/defaultgamemode.rs @@ -45,8 +45,8 @@ impl CommandExecutor for DefaultGamemodeExecutor { sender .send_message(TextComponent::translate_cross( - "commands.defaultgamemode.success", - "commands.defaultgamemode.success", + pumpkin_data::translation::java::COMMANDS_DEFAULTGAMEMODE_SUCCESS, + pumpkin_data::translation::bedrock::COMMANDS_DEFAULTGAMEMODE_SUCCESS, [TextComponent::translate_cross( gamemode_string.clone(), gamemode_string, diff --git a/pumpkin/src/command/commands/deop.rs b/pumpkin/src/command/commands/deop.rs index f763ca075..cadfe5e77 100644 --- a/pumpkin/src/command/commands/deop.rs +++ b/pumpkin/src/command/commands/deop.rs @@ -52,8 +52,8 @@ impl CommandExecutor for Executor { } let msg = TextComponent::translate_cross( - "commands.deop.success", - "commands.deop.success", + pumpkin_data::translation::java::COMMANDS_DEOP_SUCCESS, + pumpkin_data::translation::bedrock::COMMANDS_DEOP_SUCCESS, [TextComponent::text(profile.name.clone())], ); sender.send_message(msg).await; @@ -66,8 +66,8 @@ impl CommandExecutor for Executor { if succeeded_deops == 0 { Err(CommandError::CommandFailed(TextComponent::translate_cross( - "commands.deop.failed", - "commands.deop.failed", + pumpkin_data::translation::java::COMMANDS_DEOP_FAILED, + pumpkin_data::translation::bedrock::COMMANDS_DEOP_FAILED, [], ))) } else { diff --git a/pumpkin/src/command/commands/difficulty.rs b/pumpkin/src/command/commands/difficulty.rs index d7fd30834..f9b8ee3e6 100644 --- a/pumpkin/src/command/commands/difficulty.rs +++ b/pumpkin/src/command/commands/difficulty.rs @@ -65,8 +65,8 @@ impl CommandExecutor for DifficultySetExecutor { .source .send_feedback( TextComponent::translate_cross( - "commands.difficulty.success", - "commands.difficulty.success", + pumpkin_data::translation::java::COMMANDS_DIFFICULTY_SUCCESS, + pumpkin_data::translation::bedrock::COMMANDS_DIFFICULTY_SUCCESS, [TextComponent::translate_cross( difficulty.translation_key(), difficulty.translation_key(), diff --git a/pumpkin/src/command/commands/enchant.rs b/pumpkin/src/command/commands/enchant.rs index 375e37c92..0f9d33cb1 100644 --- a/pumpkin/src/command/commands/enchant.rs +++ b/pumpkin/src/command/commands/enchant.rs @@ -55,8 +55,8 @@ impl CommandExecutor for Executor { if level > enchantment.max_level { let msg = TextComponent::translate_cross( - "commands.enchant.failed.level", - "commands.enchant.failed.level", + translation::java::COMMANDS_ENCHANT_FAILED_LEVEL, + translation::bedrock::COMMANDS_ENCHANT_INVALIDLEVEL, [ TextComponent::text(level.to_string()), TextComponent::text(enchantment.max_level.to_string()), @@ -87,8 +87,8 @@ impl CommandExecutor for Executor { if item.is_empty() { if only_one { let msg = TextComponent::translate_cross( - "commands.enchant.failed.itemless", - "commands.enchant.failed.itemless", + translation::java::COMMANDS_ENCHANT_FAILED_ITEMLESS, + translation::bedrock::COMMANDS_ENCHANT_NOITEM, [targets[0].get_display_name().await], ); return Err(CommandError::CommandFailed(msg)); @@ -98,8 +98,8 @@ impl CommandExecutor for Executor { if !enchantment.can_enchant(item.item) { if only_one { let msg = TextComponent::translate_cross( - "commands.enchant.failed.incompatible", - "commands.enchant.failed.incompatible", + translation::java::COMMANDS_ENCHANT_FAILED_INCOMPATIBLE, + translation::bedrock::COMMANDS_ENCHANT_CANTENCHANT, [item.item.translated_name()], ); return Err(CommandError::CommandFailed(msg)); @@ -112,8 +112,8 @@ impl CommandExecutor for Executor { success += 1; } else if only_one { let msg = TextComponent::translate_cross( - "commands.enchant.failed.incompatible", - "commands.enchant.failed.incompatible", + translation::java::COMMANDS_ENCHANT_FAILED_INCOMPATIBLE, + translation::bedrock::COMMANDS_ENCHANT_CANTENCHANT, [item.item.translated_name()], ); return Err(CommandError::CommandFailed(msg)); @@ -126,15 +126,15 @@ impl CommandExecutor for Executor { if success == 0 { let msg = TextComponent::translate_cross( translation::java::COMMANDS_ENCHANT_FAILED, - translation::java::COMMANDS_ENCHANT_FAILED, - [], + translation::bedrock::COMMANDS_ENCHANT_CANTENCHANT, + [TextComponent::text("")], ); return Err(CommandError::CommandFailed(msg)); } if only_one { let msg = TextComponent::translate_cross( translation::java::COMMANDS_ENCHANT_SUCCESS_SINGLE, - translation::java::COMMANDS_ENCHANT_SUCCESS_SINGLE, + translation::bedrock::COMMANDS_ENCHANT_SUCCESS, [ enchantment.get_fullname(level), targets[0].get_display_name().await, @@ -144,7 +144,7 @@ impl CommandExecutor for Executor { } else { let msg = TextComponent::translate_cross( translation::java::COMMANDS_ENCHANT_SUCCESS_MULTIPLE, - translation::java::COMMANDS_ENCHANT_SUCCESS_MULTIPLE, + translation::bedrock::COMMANDS_ENCHANT_SUCCESS, [ enchantment.get_fullname(level), TextComponent::text(targets.len().to_string()), diff --git a/pumpkin/src/command/commands/experience.rs b/pumpkin/src/command/commands/experience.rs index a072323ce..6638bfce4 100644 --- a/pumpkin/src/command/commands/experience.rs +++ b/pumpkin/src/command/commands/experience.rs @@ -87,20 +87,36 @@ impl Executor { Mode::Query => "query", }; + let bedrock_key = match self.exp_type { + ExpType::Points => pumpkin_data::translation::bedrock::COMMANDS_XP_SUCCESS, + ExpType::Levels => { + if amount >= 0 { + pumpkin_data::translation::bedrock::COMMANDS_XP_SUCCESS_LEVELS + } else { + pumpkin_data::translation::bedrock::COMMANDS_XP_SUCCESS_NEGATIVE_LEVELS + } + } + }; + + let bedrock_amount = if amount >= 0 { amount } else { amount.abs() }; + if targets.len() > 1 { TextComponent::translate_cross( format!("commands.experience.{mode_str}.{type_str}.success.multiple"), - format!("commands.experience.{mode_str}.{type_str}.success.multiple"), + bedrock_key, [ - TextComponent::text(amount.to_string()), + TextComponent::text(bedrock_amount.to_string()), TextComponent::text(targets.len().to_string()), ], ) } else { TextComponent::translate_cross( format!("commands.experience.{mode_str}.{type_str}.success.single"), - format!("commands.experience.{mode_str}.{type_str}.success.single"), - [TextComponent::text(amount.to_string()), first_target_name], + bedrock_key, + [ + TextComponent::text(bedrock_amount.to_string()), + first_target_name, + ], ) } } diff --git a/pumpkin/src/command/commands/give.rs b/pumpkin/src/command/commands/give.rs index 976f64a74..82f711d52 100644 --- a/pumpkin/src/command/commands/give.rs +++ b/pumpkin/src/command/commands/give.rs @@ -1,6 +1,5 @@ use pumpkin_data::data_component::DataComponent::MaxStackSize; use pumpkin_data::data_component_impl::{MaxStackSizeImpl, get}; -use pumpkin_data::item_stack::ItemStack; use pumpkin_util::text::TextComponent; use pumpkin_util::text::hover::HoverEvent; @@ -39,7 +38,8 @@ impl CommandExecutor for Executor { Box::pin(async move { let targets = PlayersArgumentConsumer.find_arg_default_name(args)?; - let (item_name, item) = ItemArgumentConsumer::find_arg(args, ARG_ITEM)?; + let (item_name, parsed_stack) = ItemArgumentConsumer::find_arg(args, ARG_ITEM)?; + let item = parsed_stack.item; let item_count = match item_count_consumer().find_arg_default_name(args) { Err(_) => 1, @@ -71,7 +71,8 @@ impl CommandExecutor for Executor { while remaining > 0 { let take = remaining.min(max_stack); - let mut stack = ItemStack::new(take as u8, item); + let mut stack = parsed_stack.clone(); + stack.item_count = take as u8; target.inventory().insert_stack_anywhere(&mut stack).await; if !stack.is_empty() { target.drop_item(stack).await; @@ -82,8 +83,8 @@ impl CommandExecutor for Executor { let msg = if targets.len() == 1 { TextComponent::translate_cross( - "commands.give.success.single", - "commands.give.success.single", + pumpkin_data::translation::java::COMMANDS_GIVE_SUCCESS_SINGLE, + pumpkin_data::translation::bedrock::COMMANDS_GIVE_SUCCESS, [ TextComponent::text(item_count.to_string()), TextComponent::text("[") @@ -98,8 +99,8 @@ impl CommandExecutor for Executor { ) } else { TextComponent::translate_cross( - "commands.give.success.multiple", - "commands.give.success.multiple", + pumpkin_data::translation::java::COMMANDS_GIVE_SUCCESS_MULTIPLE, + pumpkin_data::translation::bedrock::COMMANDS_GIVE_SUCCESS, [ TextComponent::text(item_count.to_string()), TextComponent::text("[") diff --git a/pumpkin/src/command/commands/item.rs b/pumpkin/src/command/commands/item.rs index abfbc54b4..6ac048866 100644 --- a/pumpkin/src/command/commands/item.rs +++ b/pumpkin/src/command/commands/item.rs @@ -1,5 +1,4 @@ use pumpkin_data::data_component_impl::EquipmentSlot; -use pumpkin_data::item_stack::ItemStack; use pumpkin_data::translation; use pumpkin_inventory::screen_handler::InventoryPlayer; use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; @@ -58,7 +57,8 @@ impl CommandExecutor for BlockReplaceExecutor { let pos = BlockPosArgumentConsumer::find_loaded_arg(args, ARG_POS, &world)?; let (slot, slot_name) = SlotArgumentConsumer::find_arg(args, ARG_SLOT)?; - let (item_name, item) = ItemArgumentConsumer::find_arg(args, ARG_ITEM)?; + let (item_name, parsed_stack) = ItemArgumentConsumer::find_arg(args, ARG_ITEM)?; + let item = parsed_stack.item; let count = match count_consumer().find_arg_default_name(args) { Ok(Ok(c)) => c, Err(_) | Ok(Err(_)) => 1, @@ -96,7 +96,8 @@ impl CommandExecutor for BlockReplaceExecutor { ))); } - let item_stack = ItemStack::new(count as u8, item); + let mut item_stack = parsed_stack.clone(); + item_stack.item_count = count as u8; inventory.set_stack(slot, item_stack.clone()).await; let msg = TextComponent::translate_cross( @@ -135,14 +136,16 @@ impl CommandExecutor for EntityReplaceExecutor { Box::pin(async move { let targets = EntitiesArgumentConsumer.find_arg_default_name(args)?; let (mojang_slot, slot_name) = SlotArgumentConsumer::find_arg(args, ARG_SLOT)?; - let (item_name, item) = ItemArgumentConsumer::find_arg(args, ARG_ITEM)?; + let (item_name, parsed_stack) = ItemArgumentConsumer::find_arg(args, ARG_ITEM)?; + let item = parsed_stack.item; let count = match count_consumer().find_arg_default_name(args) { Ok(Ok(c)) => c, Err(_) | Ok(Err(_)) => 1, }; let mut modified_count = 0; - let item_stack = ItemStack::new(count as u8, item); + let mut item_stack = parsed_stack.clone(); + item_stack.item_count = count as u8; for target in targets { if let Some(player) = target.get_player() { diff --git a/pumpkin/src/command/commands/kick.rs b/pumpkin/src/command/commands/kick.rs index 12aabb7e4..01bf15656 100644 --- a/pumpkin/src/command/commands/kick.rs +++ b/pumpkin/src/command/commands/kick.rs @@ -34,7 +34,8 @@ impl CommandExecutor for Executor { return Err(InvalidConsumption(Some(ARG_TARGETS.into()))); }; - let reason = match args.get(&ARG_REASON) { + let custom_reason = args.get(&ARG_REASON); + let reason = match custom_reason { Some(Arg::Msg(r)) => TextComponent::text(r.clone()), _ => TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_KICKED, @@ -45,9 +46,24 @@ impl CommandExecutor for Executor { for target in targets { target.kick(DisconnectReason::Kicked, reason.clone()).await; - let mut msg = TextComponent::text("Kicked: "); - msg = msg.add_child(target.get_display_name().await); - sender.send_message(msg.color_named(NamedColor::Blue)).await; + + let feedback = if custom_reason.is_some() { + TextComponent::translate_cross( + translation::java::COMMANDS_KICK_SUCCESS, + translation::bedrock::COMMANDS_KICK_SUCCESS_REASON, + [target.get_display_name().await, reason.clone()], + ) + } else { + TextComponent::translate_cross( + translation::java::COMMANDS_KICK_SUCCESS, + translation::bedrock::COMMANDS_KICK_SUCCESS, + [target.get_display_name().await, reason.clone()], + ) + }; + + sender + .send_message(feedback.color_named(NamedColor::Blue)) + .await; } Ok(targets.len() as i32) diff --git a/pumpkin/src/command/commands/kill.rs b/pumpkin/src/command/commands/kill.rs index 5b3b46e41..5d367d1e5 100644 --- a/pumpkin/src/command/commands/kill.rs +++ b/pumpkin/src/command/commands/kill.rs @@ -29,13 +29,13 @@ impl CommandExecutor for TargetsExecutor { let msg = if target_count == 1 { TextComponent::translate_cross( translation::java::COMMANDS_KILL_SUCCESS_SINGLE, - translation::java::COMMANDS_KILL_SUCCESS_SINGLE, + translation::bedrock::COMMANDS_KILL_SUCCESSFUL, [targets[0].get_display_name().await], ) } else { TextComponent::translate_cross( translation::java::COMMANDS_KILL_SUCCESS_MULTIPLE, - translation::java::COMMANDS_KILL_SUCCESS_MULTIPLE, + translation::bedrock::COMMANDS_KILL_SUCCESSFUL, [TextComponent::text(target_count.to_string())], ) }; @@ -60,7 +60,7 @@ impl CommandExecutor for SelfExecutor { .send_feedback( TextComponent::translate_cross( translation::java::COMMANDS_KILL_SUCCESS_SINGLE, - translation::java::COMMANDS_KILL_SUCCESS_SINGLE, + translation::bedrock::COMMANDS_KILL_SUCCESSFUL, [target.get_display_name().await], ), true, diff --git a/pumpkin/src/command/commands/pardon.rs b/pumpkin/src/command/commands/pardon.rs index e7f9769d1..c5b750273 100644 --- a/pumpkin/src/command/commands/pardon.rs +++ b/pumpkin/src/command/commands/pardon.rs @@ -44,8 +44,8 @@ impl CommandExecutor for Executor { lock.banned_players.remove(idx); sender .send_message(TextComponent::translate_cross( - "commands.pardon.success", - "commands.pardon.success", + pumpkin_data::translation::java::COMMANDS_PARDON_SUCCESS, + pumpkin_data::translation::bedrock::COMMANDS_UNBAN_SUCCESS, [TextComponent::text(target.name.clone())], )) .await; @@ -57,10 +57,13 @@ impl CommandExecutor for Executor { lock.save(); Ok(successes) } else { + let err_target = targets + .first() + .map_or_else(String::new, |first_target| first_target.name.clone()); Err(CommandError::CommandFailed(TextComponent::translate_cross( - "commands.pardon.failed", - "commands.pardon.failed", - [], + pumpkin_data::translation::java::COMMANDS_PARDON_FAILED, + pumpkin_data::translation::bedrock::COMMANDS_UNBAN_FAILED, + [TextComponent::text(err_target)], ))) } }) diff --git a/pumpkin/src/command/commands/pardonip.rs b/pumpkin/src/command/commands/pardonip.rs index 84f25da61..666988e11 100644 --- a/pumpkin/src/command/commands/pardonip.rs +++ b/pumpkin/src/command/commands/pardonip.rs @@ -32,8 +32,8 @@ impl CommandExecutor for Executor { let Ok(ip) = IpAddr::from_str(target) else { return Err(CommandError::CommandFailed(TextComponent::translate_cross( - "commands.pardonip.invalid", - "commands.pardonip.invalid", + pumpkin_data::translation::java::COMMANDS_PARDONIP_INVALID, + pumpkin_data::translation::bedrock::COMMANDS_UNBANIP_INVALID, [], ))); }; @@ -45,16 +45,16 @@ impl CommandExecutor for Executor { lock.banned_ips.remove(idx); sender .send_message(TextComponent::translate_cross( - "commands.pardonip.success", - "commands.pardonip.success", + pumpkin_data::translation::java::COMMANDS_PARDONIP_SUCCESS, + pumpkin_data::translation::bedrock::COMMANDS_UNBANIP_SUCCESS, [TextComponent::text(ip.to_string())], )) .await; Ok(1) } else { Err(CommandError::CommandFailed(TextComponent::translate_cross( - "commands.pardonip.failed", - "commands.pardonip.failed", + pumpkin_data::translation::java::COMMANDS_PARDONIP_FAILED, + pumpkin_data::translation::java::COMMANDS_PARDONIP_FAILED, [], ))) }; diff --git a/pumpkin/src/command/commands/particle.rs b/pumpkin/src/command/commands/particle.rs index dbbf44177..8e11b8760 100644 --- a/pumpkin/src/command/commands/particle.rs +++ b/pumpkin/src/command/commands/particle.rs @@ -74,9 +74,12 @@ impl CommandExecutor for Executor { sender .send_message(TextComponent::translate_cross( - "commands.particle.success", - "commands.particle.success", - [TextComponent::text(format!("{particle:?}"))], + pumpkin_data::translation::java::COMMANDS_PARTICLE_SUCCESS, + pumpkin_data::translation::bedrock::COMMANDS_PARTICLE_SUCCESS, + [ + TextComponent::text(format!("{particle:?}")), + TextComponent::text(count.to_string()), + ], )) .await; diff --git a/pumpkin/src/command/commands/playsound.rs b/pumpkin/src/command/commands/playsound.rs index b1e3a19eb..0dc1d4cc3 100644 --- a/pumpkin/src/command/commands/playsound.rs +++ b/pumpkin/src/command/commands/playsound.rs @@ -142,8 +142,8 @@ impl CommandExecutor for Executor { // Send appropriate message based on results if players_who_heard == 0 { Err(CommandError::CommandFailed(TextComponent::translate_cross( - "commands.playsound.failed", - "commands.playsound.failed", + translation::java::COMMANDS_PLAYSOUND_FAILED, + translation::java::COMMANDS_PLAYSOUND_FAILED, [], ))) } else { @@ -152,7 +152,7 @@ impl CommandExecutor for Executor { sender .send_message(TextComponent::translate_cross( translation::java::COMMANDS_PLAYSOUND_SUCCESS_SINGLE, - translation::java::COMMANDS_PLAYSOUND_SUCCESS_SINGLE, + translation::bedrock::COMMANDS_PLAYSOUND_SUCCESS, [ TextComponent::text(sound_name), targets[0].get_display_name().await, @@ -163,7 +163,7 @@ impl CommandExecutor for Executor { sender .send_message(TextComponent::translate_cross( translation::java::COMMANDS_PLAYSOUND_SUCCESS_MULTIPLE, - translation::java::COMMANDS_PLAYSOUND_SUCCESS_MULTIPLE, + translation::bedrock::COMMANDS_PLAYSOUND_SUCCESS, [ TextComponent::text(sound_name), TextComponent::text(players_who_heard.to_string()), diff --git a/pumpkin/src/command/commands/seed.rs b/pumpkin/src/command/commands/seed.rs index 085a08853..b1fde7177 100644 --- a/pumpkin/src/command/commands/seed.rs +++ b/pumpkin/src/command/commands/seed.rs @@ -2,7 +2,7 @@ use crate::command::argument_builder::{ArgumentBuilder, command}; use crate::command::context::command_context::CommandContext; use crate::command::node::dispatcher::CommandDispatcher; use crate::command::node::{CommandExecutor, CommandExecutorResult}; -use pumpkin_data::translation::java::{CHAT_COPY_CLICK, COMMANDS_SEED_SUCCESS}; +use pumpkin_data::translation; use pumpkin_util::PermissionLvl; use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::click::ClickEvent; @@ -17,13 +17,13 @@ struct SeedCommandExecutor; fn create_copy_on_click_text(content: String) -> TextComponent { TextComponent::translate_cross( - COMMANDS_SEED_SUCCESS, - COMMANDS_SEED_SUCCESS, + translation::java::COMMANDS_SEED_SUCCESS, + translation::bedrock::COMMANDS_SEED_SUCCESS, [TextComponent::wrap_in_square_brackets( TextComponent::text(content.clone()) .hover_event(HoverEvent::show_text(TextComponent::translate_cross( - CHAT_COPY_CLICK, - CHAT_COPY_CLICK, + translation::java::CHAT_COPY_CLICK, + translation::java::CHAT_COPY_CLICK, [], ))) .click_event(ClickEvent::CopyToClipboard { diff --git a/pumpkin/src/command/commands/setblock.rs b/pumpkin/src/command/commands/setblock.rs index 96222559b..b10fdc3a0 100644 --- a/pumpkin/src/command/commands/setblock.rs +++ b/pumpkin/src/command/commands/setblock.rs @@ -112,8 +112,8 @@ impl CommandExecutor for Executor { if success { sender .send_message(TextComponent::translate_cross( - "commands.setblock.success", - "commands.setblock.success", + pumpkin_data::translation::java::COMMANDS_SETBLOCK_SUCCESS, + pumpkin_data::translation::bedrock::COMMANDS_SETBLOCK_SUCCESS, [ TextComponent::text(pos.0.x.to_string()), TextComponent::text(pos.0.y.to_string()), @@ -124,8 +124,8 @@ impl CommandExecutor for Executor { Ok(1) } else { Err(CommandError::CommandFailed(TextComponent::translate_cross( - "commands.setblock.failed", - "commands.setblock.failed", + pumpkin_data::translation::java::COMMANDS_SETBLOCK_FAILED, + pumpkin_data::translation::bedrock::COMMANDS_SETBLOCK_FAILED, [], ))) } diff --git a/pumpkin/src/command/commands/spawnpoint.rs b/pumpkin/src/command/commands/spawnpoint.rs index 40f2addd1..fbc45dcbf 100644 --- a/pumpkin/src/command/commands/spawnpoint.rs +++ b/pumpkin/src/command/commands/spawnpoint.rs @@ -133,16 +133,13 @@ async fn set_spawnpoint(sender: &CommandSender, target: &Arc, pos: Block sender .send_message(TextComponent::translate_cross( - translation::java::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE_NEW, - translation::java::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE_NEW, + translation::java::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE, + translation::bedrock::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE, [ + TextComponent::text(target.gameprofile.name.clone()), TextComponent::text(pos.0.x.to_string()), TextComponent::text(pos.0.y.to_string()), TextComponent::text(pos.0.z.to_string()), - TextComponent::text(format!("{yaw:.2}")), - TextComponent::text("0.00".to_string()), - TextComponent::text(dimension.minecraft_name), - TextComponent::text(target.gameprofile.name.clone()), ], )) .await; diff --git a/pumpkin/src/command/commands/time.rs b/pumpkin/src/command/commands/time.rs index 537e3a791..b60e921a8 100644 --- a/pumpkin/src/command/commands/time.rs +++ b/pumpkin/src/command/commands/time.rs @@ -66,10 +66,15 @@ impl CommandExecutor for QueryExecutor { QueryMode::GameTime => level_time.query_gametime(), QueryMode::Day => level_time.query_day(), }; + let bedrock_key = match mode { + QueryMode::DayTime => translation::bedrock::COMMANDS_TIME_QUERY_DAYTIME, + QueryMode::GameTime => translation::bedrock::COMMANDS_TIME_QUERY_GAMETIME, + QueryMode::Day => translation::bedrock::COMMANDS_TIME_QUERY_DAY, + }; sender .send_message(TextComponent::translate_cross( translation::java::COMMANDS_TIME_QUERY, - translation::java::COMMANDS_TIME_QUERY, + bedrock_key, [TextComponent::text(curr_time.to_string())], )) .await; diff --git a/pumpkin/src/command/commands/weather.rs b/pumpkin/src/command/commands/weather.rs index 025102ab4..59f643320 100644 --- a/pumpkin/src/command/commands/weather.rs +++ b/pumpkin/src/command/commands/weather.rs @@ -52,7 +52,7 @@ impl CommandExecutor for Executor { sender .send_message(TextComponent::translate_cross( translation::java::COMMANDS_WEATHER_SET_CLEAR, - translation::java::COMMANDS_WEATHER_SET_CLEAR, + translation::bedrock::COMMANDS_WEATHER_CLEAR, [], )) .await; @@ -65,7 +65,7 @@ impl CommandExecutor for Executor { sender .send_message(TextComponent::translate_cross( translation::java::COMMANDS_WEATHER_SET_RAIN, - translation::java::COMMANDS_WEATHER_SET_RAIN, + translation::bedrock::COMMANDS_WEATHER_RAIN, [], )) .await; @@ -78,7 +78,7 @@ impl CommandExecutor for Executor { sender .send_message(TextComponent::translate_cross( translation::java::COMMANDS_WEATHER_SET_THUNDER, - translation::java::COMMANDS_WEATHER_SET_THUNDER, + translation::bedrock::COMMANDS_WEATHER_THUNDER, [], )) .await; diff --git a/pumpkin/src/entity/ai/goal/breed.rs b/pumpkin/src/entity/ai/goal/breed.rs index 2b1e2d40e..6f96d7c59 100644 --- a/pumpkin/src/entity/ai/goal/breed.rs +++ b/pumpkin/src/entity/ai/goal/breed.rs @@ -77,6 +77,14 @@ impl BreedGoal { 1, ) .await; + + player + .trigger_advancement( + crate::entity::player::advancement::trigger::AdvancementTrigger::BredAnimal { + parent_type: format!("minecraft:{}", entity.entity_type.resource_name), + }, + ) + .await; } mob_entity.reset_love_ticks(); diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index 8a588b9f1..c944fb012 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -1485,6 +1485,38 @@ impl LivingEntity { 1, ) .await; + + let resource_name = self.entity.entity_type.resource_name; + let criterion_key = format!("minecraft:{resource_name}"); + killer_player + .trigger_advancement( + crate::entity::player::advancement::trigger::AdvancementTrigger::PlayerKilledEntity { + entity_type_resource: criterion_key, + } + ) + .await; + + if resource_name == "skeleton" { + let distance_sq = killer_player + .position() + .squared_distance_to_vec(&self.entity.pos.load()); + if distance_sq >= 2500.0 { + killer_player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::SniperDuel).await; + } + } + + if resource_name == "phantom" { + killer_player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::TwoBirdsOneArrow).await; + } + + let held_item = killer_player.inventory().held_item(); + let is_crossbow = { + let lock = held_item.lock().await; + lock.item.registry_key == "crossbow" + }; + if is_crossbow { + killer_player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::Arbalistic).await; + } } killer_player .increment_stat( @@ -2212,6 +2244,8 @@ impl EntityBase for LivingEntity { (effective_amount * 10.0) as i32, ) .await; + + player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::DeflectedDamage).await; } if let Some(attacker_player) = cause.and_then(|c| c.get_player()) { @@ -2629,6 +2663,12 @@ impl EntityBase for LivingEntity { } if let Some(player) = caller.get_player() { + player + .trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::ConsumeItem { + item_id: format!("minecraft:{}", item.item.registry_key), + }) + .await; + // Prefer modifying the exact stack that matches the consumed item: // 1) selected hotbar (held_item) // 2) off-hand diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 484a4bc34..615276e09 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -1052,6 +1052,10 @@ impl Player { return; } + if damage >= 100.0 { + self.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::DealtOverkillDamage).await; + } + if let Some(enchantments) = item_stack .lock() .await @@ -2027,6 +2031,7 @@ impl Player { self.update_player_pose().await; self.breath_manager.tick(self).await; self.hunger_manager.tick(self).await; + self.check_inventory_advancements().await; self.advancements.lock().await.flush_dirty(self, true); // experience handling @@ -2514,9 +2519,18 @@ impl Player { self.unload_watched_chunks(¤t_world).await; self.chunk_manager.lock().await.change_world(¤t_world.level, new_world.clone()); - // Update the entity's world reference for correct dimension-based operations self.living_entity.entity.set_world(new_world.clone()); + if new_world.dimension == pumpkin_data::dimension::Dimension::THE_NETHER { + self.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::EnterDimension { + dimension: "the_nether".to_string(), + }).await; + } else if new_world.dimension == pumpkin_data::dimension::Dimension::THE_END { + self.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::EnterDimension { + dimension: "the_end".to_string(), + }).await; + } + let last_pos = self.living_entity.entity.last_pos.load(); let death_dimension = ResourceLocation::from(self.world().dimension.minecraft_name); let death_location = BlockPos(Vector3::new( @@ -2842,6 +2856,10 @@ impl Player { } async fn handle_killed(&self, death_msg: TextComponent) { + self.trigger_advancement( + crate::entity::player::advancement::trigger::AdvancementTrigger::PlayerKilled, + ) + .await; self.set_client_loaded(false); let block_pos = self.position().to_block_pos(); @@ -4169,6 +4187,51 @@ impl Player { .into_source(server) .await } + + pub async fn has_advancement( + &self, + advancement: &'static pumpkin_data::advancement::Advancement, + ) -> bool { + let advancements = self.advancements.lock().await; + advancements + .progress + .map + .get(advancement) + .is_some_and(crate::entity::player::advancement::AdvancementProgress::is_done) + } + + pub async fn has_item_in_inventory(&self, item: &pumpkin_data::item::Item) -> bool { + for slot in &self.inventory.main_inventory { + let stack = slot.lock().await; + if !stack.is_empty() && stack.item.id == item.id { + return true; + } + } + let equipment = self.inventory.entity_equipment.lock().await; + for slot_stack in equipment.equipment.values() { + let stack = slot_stack.lock().await; + if !stack.is_empty() && stack.item.id == item.id { + return true; + } + } + false + } + + pub async fn trigger_advancement_criterion( + &self, + advancement: &'static pumpkin_data::advancement::Advancement, + criterion: &str, + ) { + let mut advancements = self.advancements.lock().await; + advancements.award(advancement, criterion); + } + + pub async fn check_inventory_advancements(&self) { + self.trigger_advancement( + crate::entity::player::advancement::trigger::AdvancementTrigger::InventoryChanged, + ) + .await; + } } impl PartialEq for Player { diff --git a/pumpkin/src/entity/player/advancement.rs b/pumpkin/src/entity/player/advancement.rs index 665fff20c..ea245d6b6 100644 --- a/pumpkin/src/entity/player/advancement.rs +++ b/pumpkin/src/entity/player/advancement.rs @@ -1,3 +1,4 @@ +pub mod trigger; mod visibility_evaluator; use crate::data::advancement_data::AdvancementManager; diff --git a/pumpkin/src/entity/player/advancement/trigger.rs b/pumpkin/src/entity/player/advancement/trigger.rs new file mode 100644 index 000000000..61fc6f458 --- /dev/null +++ b/pumpkin/src/entity/player/advancement/trigger.rs @@ -0,0 +1,740 @@ +use crate::entity::player::Player; + +#[derive(Debug, Clone)] +pub enum AdvancementTrigger { + InventoryChanged, + PlayerKilledEntity { entity_type_resource: String }, + EnterBlock { block_id: String }, + PlacedBlock { block_id: String }, + ConsumeItem { item_id: String }, + SleptInBed, + EnterDimension { dimension: String }, + PlayerKilled, + DeflectedDamage, + LaunchedEyeOfEnder, + GlowedSign, + BredAnimal { parent_type: String }, + DealtOverkillDamage, + SniperDuel, + TwoBirdsOneArrow, + Arbalistic, + Bullseye, + CuredZombieVillager, +} + +impl Player { + #[allow(clippy::collapsible_if, clippy::too_many_lines)] + pub async fn trigger_advancement(&self, trigger: AdvancementTrigger) { + use pumpkin_data::advancement::Advancement; + use pumpkin_data::item::Item; + + match trigger { + AdvancementTrigger::InventoryChanged => { + if !self.has_advancement(Advancement::STORY_ROOT).await { + if self.has_item_in_inventory(&Item::CRAFTING_TABLE).await { + self.trigger_advancement_criterion( + Advancement::STORY_ROOT, + "crafting_table", + ) + .await; + } + } + + if !self.has_advancement(Advancement::STORY_MINE_STONE).await { + let stone_items = [ + &Item::COBBLESTONE, + &Item::STONE, + &Item::ANDESITE, + &Item::DIORITE, + &Item::GRANITE, + &Item::DEEPSLATE, + &Item::COBBLED_DEEPSLATE, + &Item::BLACKSTONE, + ]; + for item in stone_items { + if self.has_item_in_inventory(item).await { + self.trigger_advancement_criterion( + Advancement::STORY_MINE_STONE, + "get_stone", + ) + .await; + break; + } + } + } + + if !self.has_advancement(Advancement::STORY_UPGRADE_TOOLS).await { + if self.has_item_in_inventory(&Item::STONE_PICKAXE).await { + self.trigger_advancement_criterion( + Advancement::STORY_UPGRADE_TOOLS, + "stone_pickaxe", + ) + .await; + } + } + + if !self.has_advancement(Advancement::STORY_SMELT_IRON).await { + if self.has_item_in_inventory(&Item::IRON_INGOT).await { + self.trigger_advancement_criterion(Advancement::STORY_SMELT_IRON, "iron") + .await; + } + } + + if !self.has_advancement(Advancement::STORY_IRON_TOOLS).await { + if self.has_item_in_inventory(&Item::IRON_PICKAXE).await { + self.trigger_advancement_criterion( + Advancement::STORY_IRON_TOOLS, + "iron_pickaxe", + ) + .await; + } + } + + if !self.has_advancement(Advancement::STORY_MINE_DIAMOND).await { + if self.has_item_in_inventory(&Item::DIAMOND).await { + self.trigger_advancement_criterion( + Advancement::STORY_MINE_DIAMOND, + "diamond", + ) + .await; + } + } + + if !self.has_advancement(Advancement::STORY_LAVA_BUCKET).await { + if self.has_item_in_inventory(&Item::LAVA_BUCKET).await { + self.trigger_advancement_criterion( + Advancement::STORY_LAVA_BUCKET, + "lava_bucket", + ) + .await; + } + } + + if !self.has_advancement(Advancement::STORY_OBTAIN_ARMOR).await { + let armor = [ + (&Item::IRON_HELMET, "iron_helmet"), + (&Item::IRON_CHESTPLATE, "iron_chestplate"), + (&Item::IRON_LEGGINGS, "iron_leggings"), + (&Item::IRON_BOOTS, "iron_boots"), + ]; + for (item, criterion) in armor { + if self.has_item_in_inventory(item).await { + self.trigger_advancement_criterion( + Advancement::STORY_OBTAIN_ARMOR, + criterion, + ) + .await; + } + } + } + + if !self.has_advancement(Advancement::STORY_SHINY_GEAR).await { + let armor = [ + (&Item::DIAMOND_HELMET, "diamond_helmet"), + (&Item::DIAMOND_CHESTPLATE, "diamond_chestplate"), + (&Item::DIAMOND_LEGGINGS, "diamond_leggings"), + (&Item::DIAMOND_BOOTS, "diamond_boots"), + ]; + for (item, criterion) in armor { + if self.has_item_in_inventory(item).await { + self.trigger_advancement_criterion( + Advancement::STORY_SHINY_GEAR, + criterion, + ) + .await; + } + } + } + + if !self.has_advancement(Advancement::STORY_FORM_OBSIDIAN).await { + if self.has_item_in_inventory(&Item::OBSIDIAN).await { + self.trigger_advancement_criterion( + Advancement::STORY_FORM_OBSIDIAN, + "obsidian", + ) + .await; + } + } + + if !self + .has_advancement(Advancement::NETHER_GET_WITHER_SKULL) + .await + { + if self + .has_item_in_inventory(&Item::WITHER_SKELETON_SKULL) + .await + { + self.trigger_advancement_criterion( + Advancement::NETHER_GET_WITHER_SKULL, + "wither_skull", + ) + .await; + } + } + + if !self + .has_advancement(Advancement::NETHER_OBTAIN_ANCIENT_DEBRIS) + .await + { + if self.has_item_in_inventory(&Item::ANCIENT_DEBRIS).await { + self.trigger_advancement_criterion( + Advancement::NETHER_OBTAIN_ANCIENT_DEBRIS, + "ancient_debris", + ) + .await; + } + } + + if !self + .has_advancement(Advancement::NETHER_OBTAIN_BLAZE_ROD) + .await + { + if self.has_item_in_inventory(&Item::BLAZE_ROD).await { + self.trigger_advancement_criterion( + Advancement::NETHER_OBTAIN_BLAZE_ROD, + "blaze_rod", + ) + .await; + } + } + + if !self + .has_advancement(Advancement::NETHER_OBTAIN_CRYING_OBSIDIAN) + .await + { + if self.has_item_in_inventory(&Item::CRYING_OBSIDIAN).await { + self.trigger_advancement_criterion( + Advancement::NETHER_OBTAIN_CRYING_OBSIDIAN, + "crying_obsidian", + ) + .await; + } + } + + if !self + .has_advancement(Advancement::NETHER_NETHERITE_ARMOR) + .await + { + if self.has_item_in_inventory(&Item::NETHERITE_HELMET).await + && self + .has_item_in_inventory(&Item::NETHERITE_CHESTPLATE) + .await + && self.has_item_in_inventory(&Item::NETHERITE_LEGGINGS).await + && self.has_item_in_inventory(&Item::NETHERITE_BOOTS).await + { + self.trigger_advancement_criterion( + Advancement::NETHER_NETHERITE_ARMOR, + "netherite_armor", + ) + .await; + } + } + + if !self + .has_advancement(Advancement::HUSBANDRY_FISHY_BUSINESS) + .await + { + let fishes = [ + (&Item::COD, "cod"), + (&Item::SALMON, "salmon"), + (&Item::PUFFERFISH, "pufferfish"), + (&Item::TROPICAL_FISH, "tropical_fish"), + ]; + for (item, criterion) in fishes { + if self.has_item_in_inventory(item).await { + self.trigger_advancement_criterion( + Advancement::HUSBANDRY_FISHY_BUSINESS, + criterion, + ) + .await; + } + } + } + + if !self + .has_advancement(Advancement::HUSBANDRY_TACTICAL_FISHING) + .await + { + let fish_buckets = [ + (&Item::COD_BUCKET, "cod_bucket"), + (&Item::SALMON_BUCKET, "salmon_bucket"), + (&Item::PUFFERFISH_BUCKET, "pufferfish_bucket"), + (&Item::TROPICAL_FISH_BUCKET, "tropical_fish_bucket"), + ]; + for (item, criterion) in fish_buckets { + if self.has_item_in_inventory(item).await { + self.trigger_advancement_criterion( + Advancement::HUSBANDRY_TACTICAL_FISHING, + criterion, + ) + .await; + } + } + } + + if !self + .has_advancement(Advancement::HUSBANDRY_AXOLOTL_IN_A_BUCKET) + .await + { + if self.has_item_in_inventory(&Item::AXOLOTL_BUCKET).await { + self.trigger_advancement_criterion( + Advancement::HUSBANDRY_AXOLOTL_IN_A_BUCKET, + "axolotl_bucket", + ) + .await; + } + } + + if !self + .has_advancement(Advancement::HUSBANDRY_TADPOLE_IN_A_BUCKET) + .await + { + if self.has_item_in_inventory(&Item::TADPOLE_BUCKET).await { + self.trigger_advancement_criterion( + Advancement::HUSBANDRY_TADPOLE_IN_A_BUCKET, + "tadpole_bucket", + ) + .await; + } + } + + if !self + .has_advancement(Advancement::HUSBANDRY_OBTAIN_NETHERITE_HOE) + .await + { + if self.has_item_in_inventory(&Item::NETHERITE_HOE).await { + self.trigger_advancement_criterion( + Advancement::HUSBANDRY_OBTAIN_NETHERITE_HOE, + "netherite_hoe", + ) + .await; + } + } + + if !self.has_advancement(Advancement::STORY_ENCHANT_ITEM).await { + let mut has_enchanted = false; + for item in &self.inventory().main_inventory { + let lock = item.lock().await; + if !lock.is_empty() && lock.has_enchantments() { + has_enchanted = true; + break; + } + } + if has_enchanted { + self.trigger_advancement_criterion( + Advancement::STORY_ENCHANT_ITEM, + "enchanted_item", + ) + .await; + } + } + + if !self.has_advancement(Advancement::NETHER_BREW_POTION).await { + let potions = [&Item::POTION, &Item::SPLASH_POTION, &Item::LINGERING_POTION]; + for item in potions { + if self.has_item_in_inventory(item).await { + self.trigger_advancement_criterion( + Advancement::NETHER_BREW_POTION, + "potion", + ) + .await; + break; + } + } + } + + if !self + .has_advancement(Advancement::NETHER_CREATE_BEACON) + .await + { + if self.has_item_in_inventory(&Item::BEACON).await { + self.trigger_advancement_criterion( + Advancement::NETHER_CREATE_BEACON, + "beacon", + ) + .await; + } + } + if !self + .has_advancement(Advancement::NETHER_CREATE_FULL_BEACON) + .await + { + if self.has_item_in_inventory(&Item::BEACON).await { + self.trigger_advancement_criterion( + Advancement::NETHER_CREATE_FULL_BEACON, + "beacon", + ) + .await; + } + } + + if !self.has_advancement(Advancement::END_ELYTRA).await { + if self.has_item_in_inventory(&Item::ELYTRA).await { + self.trigger_advancement_criterion(Advancement::END_ELYTRA, "elytra") + .await; + } + } + + if !self.has_advancement(Advancement::END_DRAGON_EGG).await { + if self.has_item_in_inventory(&Item::DRAGON_EGG).await { + self.trigger_advancement_criterion( + Advancement::END_DRAGON_EGG, + "dragon_egg", + ) + .await; + } + } + + if !self.has_advancement(Advancement::END_DRAGON_BREATH).await { + if self.has_item_in_inventory(&Item::DRAGON_BREATH).await { + self.trigger_advancement_criterion( + Advancement::END_DRAGON_BREATH, + "dragon_breath", + ) + .await; + } + } + + if !self.has_advancement(Advancement::END_FIND_END_CITY).await { + let city_items = [&Item::SHULKER_SHELL, &Item::CHORUS_FRUIT]; + for item in city_items { + if self.has_item_in_inventory(item).await { + self.trigger_advancement_criterion( + Advancement::END_FIND_END_CITY, + "in_city", + ) + .await; + break; + } + } + } + + if !self + .has_advancement(Advancement::NETHER_EXPLORE_NETHER) + .await + { + let pos = self.position().to_block_pos(); + let biome = self.world().level.get_rough_biome(&pos); + let biome_resource = format!("minecraft:{}", biome.registry_id); + let nether_biomes = [ + "minecraft:basalt_deltas", + "minecraft:crimson_forest", + "minecraft:nether_wastes", + "minecraft:soul_sand_valley", + "minecraft:warped_forest", + ]; + if nether_biomes.contains(&biome_resource.as_str()) { + self.trigger_advancement_criterion( + Advancement::NETHER_EXPLORE_NETHER, + &biome_resource, + ) + .await; + } + } + + if !self + .has_advancement(Advancement::ADVENTURE_ADVENTURING_TIME) + .await + { + let pos = self.position().to_block_pos(); + let biome = self.world().level.get_rough_biome(&pos); + let biome_resource = format!("minecraft:{}", biome.registry_id); + let overworld_biomes = [ + "minecraft:badlands", + "minecraft:bamboo_jungle", + "minecraft:beach", + "minecraft:birch_forest", + "minecraft:cherry_grove", + "minecraft:cold_ocean", + "minecraft:dark_forest", + "minecraft:deep_cold_ocean", + "minecraft:deep_dark", + "minecraft:deep_frozen_ocean", + "minecraft:deep_lukewarm_ocean", + "minecraft:deep_ocean", + "minecraft:desert", + "minecraft:dripstone_caves", + "minecraft:eroded_badlands", + "minecraft:flower_forest", + "minecraft:forest", + "minecraft:frozen_ocean", + "minecraft:frozen_peaks", + "minecraft:frozen_river", + "minecraft:grove", + "minecraft:ice_spikes", + "minecraft:jagged_peaks", + "minecraft:jungle", + "minecraft:lukewarm_ocean", + "minecraft:lush_caves", + "minecraft:mangrove_swamp", + "minecraft:meadow", + "minecraft:mushroom_fields", + "minecraft:ocean", + "minecraft:old_growth_birch_forest", + "minecraft:old_growth_pine_taiga", + "minecraft:old_growth_spruce_taiga", + "minecraft:pale_garden", + "minecraft:plains", + "minecraft:river", + "minecraft:savanna", + "minecraft:savanna_plateau", + "minecraft:snowy_beach", + "minecraft:snowy_plains", + "minecraft:snowy_slopes", + "minecraft:snowy_taiga", + "minecraft:sparse_jungle", + "minecraft:stony_peaks", + "minecraft:stony_shore", + "minecraft:sulfur_caves", + "minecraft:sunflower_plains", + "minecraft:swamp", + "minecraft:taiga", + "minecraft:warm_ocean", + "minecraft:windswept_forest", + "minecraft:windswept_gravelly_hills", + "minecraft:windswept_hills", + "minecraft:windswept_savanna", + "minecraft:wooded_badlands", + ]; + if overworld_biomes.contains(&biome_resource.as_str()) { + self.trigger_advancement_criterion( + Advancement::ADVENTURE_ADVENTURING_TIME, + &biome_resource, + ) + .await; + } + } + } + AdvancementTrigger::PlayerKilledEntity { + entity_type_resource, + } => { + self.trigger_advancement_criterion( + Advancement::ADVENTURE_KILL_A_MOB, + &entity_type_resource, + ) + .await; + self.trigger_advancement_criterion( + Advancement::ADVENTURE_KILL_ALL_MOBS, + &entity_type_resource, + ) + .await; + if !self.has_advancement(Advancement::ADVENTURE_ROOT).await { + self.trigger_advancement_criterion( + Advancement::ADVENTURE_ROOT, + "killed_something", + ) + .await; + } + } + AdvancementTrigger::SleptInBed => { + if !self + .has_advancement(Advancement::ADVENTURE_SLEEP_IN_BED) + .await + { + self.trigger_advancement_criterion( + Advancement::ADVENTURE_SLEEP_IN_BED, + "slept_in_bed", + ) + .await; + } + } + AdvancementTrigger::PlacedBlock { block_id } => { + if !self + .has_advancement(Advancement::HUSBANDRY_PLANT_SEED) + .await + { + let seed_blocks = [ + ("minecraft:wheat", "wheat"), + ("minecraft:pumpkin_stem", "pumpkin_stem"), + ("minecraft:melon_stem", "melon_stem"), + ("minecraft:beetroots", "beetroots"), + ("minecraft:nether_wart", "nether_wart"), + ("minecraft:torchflower", "torchflower"), + ("minecraft:pitcher_pod", "pitcher_pod"), + ]; + for (block, criterion) in seed_blocks { + if block_id == block { + self.trigger_advancement_criterion( + Advancement::HUSBANDRY_PLANT_SEED, + criterion, + ) + .await; + break; + } + } + } + } + AdvancementTrigger::EnterDimension { dimension } => { + if dimension == "the_nether" { + if !self + .has_advancement(Advancement::STORY_ENTER_THE_NETHER) + .await + { + self.trigger_advancement_criterion( + Advancement::STORY_ENTER_THE_NETHER, + "entered_nether", + ) + .await; + } + if !self.has_advancement(Advancement::NETHER_ROOT).await { + self.trigger_advancement_criterion( + Advancement::NETHER_ROOT, + "entered_nether", + ) + .await; + } + } else if dimension == "the_end" { + if !self.has_advancement(Advancement::STORY_ENTER_THE_END).await { + self.trigger_advancement_criterion( + Advancement::STORY_ENTER_THE_END, + "entered_end", + ) + .await; + } + if !self.has_advancement(Advancement::END_ROOT).await { + self.trigger_advancement_criterion(Advancement::END_ROOT, "entered_end") + .await; + } + } + } + AdvancementTrigger::ConsumeItem { item_id } => { + if !self.has_advancement(Advancement::HUSBANDRY_ROOT).await { + self.trigger_advancement_criterion( + Advancement::HUSBANDRY_ROOT, + "consumed_item", + ) + .await; + } + let food_name = item_id.strip_prefix("minecraft:").unwrap_or(&item_id); + if Advancement::HUSBANDRY_BALANCED_DIET + .criteria + .contains(&food_name) + { + self.trigger_advancement_criterion( + Advancement::HUSBANDRY_BALANCED_DIET, + food_name, + ) + .await; + } + } + AdvancementTrigger::PlayerKilled => { + if !self.has_advancement(Advancement::ADVENTURE_ROOT).await { + self.trigger_advancement_criterion( + Advancement::ADVENTURE_ROOT, + "killed_by_something", + ) + .await; + } + } + AdvancementTrigger::DeflectedDamage => { + if !self.has_advancement(Advancement::STORY_DEFLECT_ARROW).await { + self.trigger_advancement_criterion( + Advancement::STORY_DEFLECT_ARROW, + "deflected_projectile", + ) + .await; + } + } + AdvancementTrigger::LaunchedEyeOfEnder => { + if !self + .has_advancement(Advancement::STORY_FOLLOW_ENDER_EYE) + .await + { + self.trigger_advancement_criterion( + Advancement::STORY_FOLLOW_ENDER_EYE, + "in_stronghold", + ) + .await; + } + } + AdvancementTrigger::GlowedSign => { + if !self + .has_advancement(Advancement::HUSBANDRY_MAKE_A_SIGN_GLOW) + .await + { + self.trigger_advancement_criterion( + Advancement::HUSBANDRY_MAKE_A_SIGN_GLOW, + "make_a_sign_glow", + ) + .await; + } + } + AdvancementTrigger::BredAnimal { parent_type } => { + self.trigger_advancement_criterion(Advancement::HUSBANDRY_BREED_AN_ANIMAL, "bred") + .await; + self.trigger_advancement_criterion( + Advancement::HUSBANDRY_BRED_ALL_ANIMALS, + &parent_type, + ) + .await; + } + AdvancementTrigger::EnterBlock { block_id: _ } => {} + AdvancementTrigger::DealtOverkillDamage => { + if !self + .has_advancement(Advancement::ADVENTURE_OVEROVERKILL) + .await + { + self.trigger_advancement_criterion( + Advancement::ADVENTURE_OVEROVERKILL, + "overoverkill", + ) + .await; + } + } + AdvancementTrigger::SniperDuel => { + if !self + .has_advancement(Advancement::ADVENTURE_SNIPER_DUEL) + .await + { + self.trigger_advancement_criterion( + Advancement::ADVENTURE_SNIPER_DUEL, + "killed_skeleton", + ) + .await; + } + } + AdvancementTrigger::TwoBirdsOneArrow => { + if !self + .has_advancement(Advancement::ADVENTURE_TWO_BIRDS_ONE_ARROW) + .await + { + self.trigger_advancement_criterion( + Advancement::ADVENTURE_TWO_BIRDS_ONE_ARROW, + "two_birds", + ) + .await; + } + } + AdvancementTrigger::Arbalistic => { + if !self + .has_advancement(Advancement::ADVENTURE_ARBALISTIC) + .await + { + self.trigger_advancement_criterion( + Advancement::ADVENTURE_ARBALISTIC, + "arbalistic", + ) + .await; + } + } + AdvancementTrigger::Bullseye => { + if !self.has_advancement(Advancement::ADVENTURE_BULLSEYE).await { + self.trigger_advancement_criterion(Advancement::ADVENTURE_BULLSEYE, "bullseye") + .await; + } + } + AdvancementTrigger::CuredZombieVillager => { + if !self + .has_advancement(Advancement::STORY_CURE_ZOMBIE_VILLAGER) + .await + { + self.trigger_advancement_criterion( + Advancement::STORY_CURE_ZOMBIE_VILLAGER, + "cured_zombie", + ) + .await; + } + } + } + } +} diff --git a/pumpkin/src/entity/projectile/arrow.rs b/pumpkin/src/entity/projectile/arrow.rs index 9a00f4ead..1fa2e8c26 100644 --- a/pumpkin/src/entity/projectile/arrow.rs +++ b/pumpkin/src/entity/projectile/arrow.rs @@ -362,6 +362,14 @@ impl EntityBase for ArrowEntity { self.shake_time.store(7, Ordering::Relaxed); *self.last_block_pos.write().unwrap() = Some(pos); + let block = world.get_block(&pos); + if block == &pumpkin_data::Block::TARGET { + let player_opt = self.owner_id.and_then(|id| world.get_player_by_id(id)); + if let Some(player) = player_opt { + player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::Bullseye).await; + } + } + // Stop the arrow entity.velocity.store(Vector3::new(0.0, 0.0, 0.0)); entity.set_pos(hit_pos); diff --git a/pumpkin/src/item/items/ender_eye.rs b/pumpkin/src/item/items/ender_eye.rs index 19184c7a4..69a83b37e 100644 --- a/pumpkin/src/item/items/ender_eye.rs +++ b/pumpkin/src/item/items/ender_eye.rs @@ -127,6 +127,7 @@ impl ItemBehaviour for EnderEyeItem { pitch, ); + player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::LaunchedEyeOfEnder).await; player.inventory.held_item().lock().await.decrement(1); }) } diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index 35538fbb9..73095f621 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -1853,6 +1853,13 @@ impl JavaClient { ActionType::Interact | ActionType::InteractAt => { let held = player.inventory.held_item(); let mut stack = held.lock().await.clone(); + let target_entity = event.target.get_entity(); + if target_entity.entity_type.resource_name == "zombie_villager" + && stack.item.registry_key == "golden_apple" + { + player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::CuredZombieVillager).await; + } + let interacted = event.target.interact(player, &mut stack).await; if !interacted { server