feat: Initial advancement triggers

This commit is contained in:
Alexander Medvedev 2026-07-17 16:10:43 +02:00
parent 55219f870f
commit 712bb96d3e
41 changed files with 1560 additions and 151 deletions

View File

@ -1520,15 +1520,14 @@ fn parse_geyser_entry(
_ => panic!("Expected NbtCompound in Geyser bedrock_mappings list"), _ => panic!("Expected NbtCompound in Geyser bedrock_mappings list"),
}; };
let bedrock_identifier = compound let raw_identifier = compound
.get_string("bedrock_identifier") .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:") .strip_prefix("minecraft:")
.unwrap_or_else(|| { .unwrap_or(raw_identifier)
compound
.get_string("bedrock_identifier")
.unwrap_or(java_block_name)
})
.to_string(); .to_string();
let mut properties = BTreeMap::new(); let mut properties = BTreeMap::new();

View File

@ -110,9 +110,12 @@ impl TextComponentBase {
TextContent::Text { text } => text.into_owned(), TextContent::Text { text } => text.into_owned(),
TextContent::Translate { TextContent::Translate {
translate, translate,
bedrock_translate: _, bedrock_translate,
with, 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 { TextContent::EntityNames {
selector, selector,
separator: _, separator: _,
@ -220,15 +223,11 @@ impl TextComponentBase {
TextContent::Text { text: t } => text.push_str(t), TextContent::Text { text: t } => text.push_str(t),
TextContent::Translate { TextContent::Translate {
translate, translate,
bedrock_translate: _, bedrock_translate,
with, with,
} => { } => {
// TODO let key = bedrock_translate.as_ref().unwrap_or(translate);
text.push_str(&get_translation_text( text.push_str(&get_translation_text(key.to_string(), locale, with.clone()));
translate.to_string(),
locale,
with.clone(),
));
} }
TextContent::EntityNames { selector, .. } => text.push_str(selector), TextContent::EntityNames { selector, .. } => text.push_str(selector),
TextContent::Keybind { keybind } => text.push_str(keybind), TextContent::Keybind { keybind } => text.push_str(keybind),
@ -261,9 +260,12 @@ impl TextComponentBase {
TextContent::Text { text } => text.into_owned(), TextContent::Text { text } => text.into_owned(),
TextContent::Translate { TextContent::Translate {
translate, translate,
bedrock_translate: _, bedrock_translate,
with, 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 { TextContent::EntityNames {
selector, selector,
separator: _, separator: _,

View File

@ -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_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_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 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. /// A character range representing a substitution placeholder within a translation string.
/// ///
@ -397,6 +398,21 @@ pub static TRANSLATIONS: LazyLock<Mutex<[HashMap<String, String>; Locale::COUNT]
for (key, value) in pumpkin_pl_pl { for (key, value) in pumpkin_pl_pl {
array[Locale::PlPl as usize].insert(format!("pumpkin:{key}"), value); 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) Mutex::new(array)
}); });

View File

@ -359,6 +359,11 @@ impl BlockBehaviour for BedBlock {
} }
args.player.sleep(bed_head_pos); args.player.sleep(bed_head_pos);
args.player
.trigger_advancement(
crate::entity::player::advancement::trigger::AdvancementTrigger::SleptInBed,
)
.await;
args.player args.player
.increment_stat( .increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom, pumpkin_data::statistic::StatisticCategory::Custom,

View File

@ -550,6 +550,13 @@ impl BlockBehaviour for SignBlock {
); );
if result == BlockActionResult::Success { if result == BlockActionResult::Success {
if pumpkin_item
.as_any()
.downcast_ref::<crate::item::items::glowing_ink_sac::GlowingInkSacItem>()
.is_some()
{
args.player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::GlowedSign).await;
}
if !args.player.has_infinite_materials() { if !args.player.has_infinite_materials() {
item.decrement(1); item.decrement(1);
} }

View File

@ -631,6 +631,14 @@ impl BlockRegistry {
) )
.await; .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))) Ok(Some((final_block_pos, new_state)))
} }
pub fn register<T: BlockBehaviour + BlockMetadata + 'static>(&mut self, block: T) { pub fn register<T: BlockBehaviour + BlockMetadata + 'static>(&mut self, block: T) {

View File

@ -121,6 +121,24 @@ impl FromStr for EntityFilter {
} }
Ok(Self::Sort(sort)) 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}")), _ => Err(format!("Unimplemented key: {key}")),
} }
} }
@ -268,8 +286,13 @@ fn parse_target_selector(arg: &str) -> Result<TargetSelector, TargetSelectorPars
let args_content = &arg[selector_type_end + 1..arg.len() - 1]; let args_content = &arg[selector_type_end + 1..arg.len() - 1];
let mut filter_start = 0usize; let mut filter_start = 0usize;
let mut curly_depth = 0;
for (i, c) in args_content.char_indices() { for (i, c) in args_content.char_indices() {
if c == ',' { if c == '{' {
curly_depth += 1;
} else if c == '}' {
curly_depth -= 1;
} else if c == ',' && curly_depth == 0 {
parse_selector_filter( parse_selector_filter(
&mut selector, &mut selector,
&args_content[filter_start..i], &args_content[filter_start..i],
@ -506,4 +529,43 @@ mod test {
assert!(s.includes_entities()); assert!(s.includes_entities());
assert_eq!(s.conditions.len(), 2); assert_eq!(s.conditions.len(), 2);
} }
#[test]
fn parse_advanced_selectors() {
// Test name and inverted name
let s = "@e[name=Alex,name=!Steve]"
.parse::<TargetSelector>()
.expect("should parse name selectors");
assert_eq!(s.conditions.len(), 2);
// Test tag selector
let s = "@e[tag=admin,tag=!vip]"
.parse::<TargetSelector>()
.expect("should parse tag selectors");
assert_eq!(s.conditions.len(), 2);
// Test team selector
let s = "@e[team=red,team=!blue]"
.parse::<TargetSelector>()
.expect("should parse team selectors");
assert_eq!(s.conditions.len(), 2);
// Test scores selector
let s = "@e[scores={kills=1..,deaths=..5}]"
.parse::<TargetSelector>()
.expect("should parse scores selectors");
assert_eq!(s.conditions.len(), 1);
// Test advancements selector
let s = "@e[advancements={story/mine_stone=true}]"
.parse::<TargetSelector>()
.expect("should parse advancements selectors");
assert_eq!(s.conditions.len(), 1);
// Test NBT selector
let s = "@e[nbt={OnGround:1b}]"
.parse::<TargetSelector>()
.expect("should parse NBT selectors");
assert_eq!(s.conditions.len(), 1);
}
} }

View File

@ -1,5 +1,7 @@
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::{ use pumpkin_data::{
data_component::DataComponent,
data_component_impl::DataComponentImpl,
item::Item, item::Item,
tag::{RegistryKey, get_tag_ids}, tag::{RegistryKey, get_tag_ids},
}; };
@ -52,28 +54,106 @@ impl DefaultNameArgConsumer for ItemArgumentConsumer {
} }
impl<'a> FindArg<'a> 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<Self::Data, CommandError> { fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) { match args.get(name) {
Some(Arg::Item(name)) => Item::from_registry_key(name).map_or_else( Some(Arg::Item(raw_name)) => {
|| { let item_id = raw_name.split('[').next().unwrap_or(raw_name);
if name.starts_with("minecraft:") { let item = Item::from_registry_key(item_id).ok_or_else(|| {
Err(CommandError::CommandFailed(TextComponent::translate_cross( if item_id.starts_with("minecraft:") {
CommandError::CommandFailed(TextComponent::translate_cross(
"argument.item.id.invalid", "argument.item.id.invalid",
"argument.item.id.invalid", "argument.item.id.invalid",
[TextComponent::text((*name).to_string())], [TextComponent::text((*item_id).to_string())],
))) ))
} else { } else {
Err(CommandError::CommandFailed(TextComponent::translate_cross( CommandError::CommandFailed(TextComponent::translate_cross(
"argument.item.id.invalid", "argument.item.id.invalid",
"argument.item.id.invalid", "argument.item.id.invalid",
[TextComponent::text("minecraft:".to_string() + *name)], [TextComponent::text("minecraft:".to_string() + item_id)],
))) ))
}
})?;
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))
} }
},
|item| Ok((*name, item)),
),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), _ => 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<Self::Data, CommandError> { fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) { match args.get(name) {
Some(Arg::Item(name)) => { Some(Arg::Item(raw_name)) => {
if *name == "*" { if *raw_name == "*" {
return Ok(ItemPredicate::Any); 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( Err(CommandError::CommandFailed(
TextComponent::translate_cross( TextComponent::translate_cross(
"argument.item.id.invalid", "argument.item.id.invalid",
"argument.item.id.invalid", "argument.item.id.invalid",
[TextComponent::text((*name).to_string())], [TextComponent::text((*clean_name).to_string())],
), ),
)) ))
} else { } else {
@ -155,7 +236,9 @@ impl<'a> FindArg<'a> for ItemPredicateArgumentConsumer {
TextComponent::translate_cross( TextComponent::translate_cross(
"argument.item.id.invalid", "argument.item.id.invalid",
"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);
}
}

View File

@ -8,14 +8,19 @@ use crate::command::context::command_source::CommandSource;
use crate::command::errors::command_syntax_error::CommandSyntaxError; use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::entity::EntityBase; use crate::entity::EntityBase;
use crate::entity::player::Player; use crate::entity::player::Player;
use crate::entity::player::advancement::AdvancementProgress;
use crate::world::World; use crate::world::World;
use pumpkin_data::Advancement;
use pumpkin_data::entity::EntityType; use pumpkin_data::entity::EntityType;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::tag::NbtTag;
use pumpkin_util::GameMode; use pumpkin_util::GameMode;
use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::bounds::{DoubleBounds, FloatDegreeBounds, IntBounds}; use pumpkin_util::math::bounds::{DoubleBounds, FloatDegreeBounds, IntBounds};
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::math::wrap_degrees; use pumpkin_util::math::wrap_degrees;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use uuid::Uuid; use uuid::Uuid;
@ -384,6 +389,20 @@ pub enum EntitySelectorPredicate {
Distance(DoubleBounds, Vector3<f64>), Distance(DoubleBounds, Vector3<f64>),
/// A predicate to check the entity type. This check can also be inverted. /// A predicate to check the entity type. This check can also be inverted.
EntityType(&'static EntityType, bool), 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<String, IntBounds>),
/// A predicate to check the player's advancements.
Advancements(HashMap<String, bool>),
/// 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. /// Used to combine sub-predicates.
AllOf(Vec<Self>), AllOf(Vec<Self>),
@ -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 { impl EntitySelectorPredicate {
#[must_use] #[must_use]
pub const fn new_all_of(predicates: Vec<Self>) -> Self { pub const fn new_all_of(predicates: Vec<Self>) -> Self {
Self::AllOf(predicates) Self::AllOf(predicates)
} }
#[allow(clippy::too_many_lines)]
pub fn test(&self, entity: &dyn EntityBase) -> bool { pub fn test(&self, entity: &dyn EntityBase) -> bool {
match self { match self {
Self::IsAlive => entity.get_entity().is_alive(), Self::IsAlive => entity.get_entity().is_alive(),
@ -443,6 +516,70 @@ impl EntitySelectorPredicate {
let actual_type = entity.get_entity().entity_type; let actual_type = entity.get_entity().entity_type;
(actual_type.id == expected_type.id) ^ invert (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)), Self::AllOf(predicates) => predicates.iter().all(|predicate| predicate.test(entity)),
} }
} }

View File

@ -311,9 +311,122 @@ impl EntitySelectorOption {
Err(TYPE_INVALID_ERROR_TYPE.create(parser.reader, TextComponent::text(string))) Err(TYPE_INVALID_ERROR_TYPE.create(parser.reader, TextComponent::text(string)))
} }
} }
_ => { Self::Name => {
tracing::warn!("Unimplemented entity selector option: {:?}", self); let start = parser.reader.cursor();
Err(UNKNOWN_OPTION_ERROR_TYPE.create_without_context(self.name_component())) 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(())
} }
} }
} }

View File

@ -602,7 +602,7 @@ const fn bedrock_param_type(arg: &ArgumentType) -> u32 {
} }
ArgumentType::String(StringProtoArgBehavior::GreedyPhrase) => arg_types::ARG_TYPE_RAW_TEXT, ArgumentType::String(StringProtoArgBehavior::GreedyPhrase) => arg_types::ARG_TYPE_RAW_TEXT,
ArgumentType::Message => arg_types::ARG_TYPE_MESSAGE, 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::ItemSlot | ArgumentType::ItemSlots => arg_types::ARG_TYPE_EQUIPMENT_SLOT,
ArgumentType::Component ArgumentType::Component
| ArgumentType::Style | ArgumentType::Style

View File

@ -66,11 +66,17 @@ impl CommandExecutor for BanListCommandExecutor {
) )
.await; .await;
} else { } else {
let bedrock_list_key = if self.ips && !self.players {
translation::bedrock::COMMANDS_BANLIST_IPS
} else {
translation::bedrock::COMMANDS_BANLIST_PLAYERS
};
source source
.send_feedback( .send_feedback(
TextComponent::translate_cross( TextComponent::translate_cross(
translation::java::COMMANDS_BANLIST_LIST, translation::java::COMMANDS_BANLIST_LIST,
translation::java::COMMANDS_BANLIST_LIST, bedrock_list_key,
[TextComponent::text(entries.len().to_string())], [TextComponent::text(entries.len().to_string())],
), ),
false, false,

View File

@ -69,8 +69,8 @@ impl CommandExecutor for AddExecutor {
if server.bossbars.lock().await.has_bossbar(&namespace) { if server.bossbars.lock().await.has_bossbar(&namespace) {
return Result::Err(CommandError::CommandFailed(TextComponent::translate_cross( return Result::Err(CommandError::CommandFailed(TextComponent::translate_cross(
"commands.bossbar.create.failed", translation::java::COMMANDS_BOSSBAR_CREATE_FAILED,
"commands.bossbar.create.failed", translation::bedrock::COMMANDS_BOSSBAR_ADD_FAILURE_EXISTS,
[TextComponent::text(namespace.clone())], [TextComponent::text(namespace.clone())],
))); )));
} }
@ -84,8 +84,8 @@ impl CommandExecutor for AddExecutor {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
"commands.bossbar.create.success", translation::java::COMMANDS_BOSSBAR_CREATE_SUCCESS,
"commands.bossbar.create.success", translation::bedrock::COMMANDS_BOSSBAR_ADD_SUCCESS,
[bossbar_prefix(bossbar.title.clone(), namespace.clone())], [bossbar_prefix(bossbar.title.clone(), namespace.clone())],
)) ))
.await; .await;
@ -119,8 +119,8 @@ impl CommandExecutor for GetExecutor {
CommandValueGet::Max => { CommandValueGet::Max => {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
"commands.bossbar.get.max", translation::java::COMMANDS_BOSSBAR_GET_MAX,
"commands.bossbar.get.max", translation::bedrock::COMMANDS_BOSSBAR_GET_MAX,
[ [
bossbar_prefix( bossbar_prefix(
bossbar.bossbar_data.title.clone(), bossbar.bossbar_data.title.clone(),
@ -136,8 +136,8 @@ impl CommandExecutor for GetExecutor {
CommandValueGet::Value => { CommandValueGet::Value => {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
"commands.bossbar.get.value", translation::java::COMMANDS_BOSSBAR_GET_VALUE,
"commands.bossbar.get.value", translation::bedrock::COMMANDS_BOSSBAR_GET_VALUE,
[ [
bossbar_prefix( bossbar_prefix(
bossbar.bossbar_data.title.clone(), bossbar.bossbar_data.title.clone(),
@ -150,15 +150,21 @@ impl CommandExecutor for GetExecutor {
Ok(bossbar.value) Ok(bossbar.value)
} }
CommandValueGet::Visible => { CommandValueGet::Visible => {
let state = if bossbar.visible { let (java_key, bedrock_key) = if bossbar.visible {
"commands.bossbar.get.visible.visible" (
translation::java::COMMANDS_BOSSBAR_SET_VISIBLE_SUCCESS_VISIBLE,
translation::bedrock::COMMANDS_BOSSBAR_GET_VISIBLE_TRUE,
)
} else { } else {
"commands.bossbar.get.visible.hidden" (
translation::java::COMMANDS_BOSSBAR_SET_VISIBLE_SUCCESS_HIDDEN,
translation::bedrock::COMMANDS_BOSSBAR_GET_VISIBLE_FALSE,
)
}; };
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
state, java_key,
state, bedrock_key,
[bossbar_prefix( [bossbar_prefix(
bossbar.bossbar_data.title.clone(), bossbar.bossbar_data.title.clone(),
namespace.clone(), namespace.clone(),
@ -187,8 +193,8 @@ impl CommandExecutor for ListExecutor {
if bossbars.is_empty() { if bossbars.is_empty() {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
"commands.bossbar.list.bars.none", translation::java::COMMANDS_BOSSBAR_LIST_BARS_NONE,
"commands.bossbar.list.bars.none", translation::bedrock::COMMANDS_BOSSBAR_LIST_NONE,
[], [],
)) ))
.await; .await;
@ -214,8 +220,8 @@ impl CommandExecutor for ListExecutor {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
"commands.bossbar.list.bars.some", translation::java::COMMANDS_BOSSBAR_LIST_BARS_SOME,
"commands.bossbar.list.bars.some", translation::bedrock::COMMANDS_BOSSBAR_LIST,
[ [
TextComponent::text(bossbars.len().to_string()), TextComponent::text(bossbars.len().to_string()),
bossbars_text, bossbars_text,
@ -250,8 +256,8 @@ impl CommandExecutor for RemoveExecutor {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
"commands.bossbar.remove.success", translation::java::COMMANDS_BOSSBAR_REMOVE_SUCCESS,
"commands.bossbar.remove.success", translation::bedrock::COMMANDS_BOSSBAR_REMOVE,
[bossbar_prefix( [bossbar_prefix(
bossbar.bossbar_data.title.clone(), bossbar.bossbar_data.title.clone(),
namespace.clone(), namespace.clone(),
@ -655,7 +661,7 @@ fn handle_bossbar_error(error: BossbarUpdateError) -> CommandError {
BossbarUpdateError::InvalidResourceLocation(location) => { BossbarUpdateError::InvalidResourceLocation(location) => {
CommandError::CommandFailed(TextComponent::translate_cross( CommandError::CommandFailed(TextComponent::translate_cross(
translation::java::COMMANDS_BOSSBAR_UNKNOWN, translation::java::COMMANDS_BOSSBAR_UNKNOWN,
translation::java::COMMANDS_BOSSBAR_UNKNOWN, translation::bedrock::COMMANDS_BOSSBAR_NOTFOUND,
[TextComponent::text(location)], [TextComponent::text(location)],
)) ))
} }

View File

@ -145,12 +145,12 @@ async fn clear_command_text_output(
match (targets, item_count == 0, max_count == 0) { match (targets, item_count == 0, max_count == 0) {
([target], true, _) => Err(TextComponent::translate_cross( ([target], true, _) => Err(TextComponent::translate_cross(
translation::java::CLEAR_FAILED_SINGLE, translation::java::CLEAR_FAILED_SINGLE,
translation::java::CLEAR_FAILED_SINGLE, translation::bedrock::COMMANDS_CLEAR_FAILURE,
[target.get_display_name().await], [target.get_display_name().await],
)), )),
(targets, true, _) => Err(TextComponent::translate_cross( (targets, true, _) => Err(TextComponent::translate_cross(
translation::java::CLEAR_FAILED_MULTIPLE, translation::java::CLEAR_FAILED_MULTIPLE,
translation::java::CLEAR_FAILED_MULTIPLE, translation::bedrock::COMMANDS_CLEAR_FAILURE,
[TextComponent::text(targets.len().to_string())], [TextComponent::text(targets.len().to_string())],
)), )),
([target], false, false) => Ok(TextComponent::translate_cross( ([target], false, false) => Ok(TextComponent::translate_cross(

View File

@ -24,17 +24,17 @@ const ARG_FILTER: &str = "filter";
const OVERLAP_ERROR: CommandErrorType<0> = CommandErrorType::new( const OVERLAP_ERROR: CommandErrorType<0> = CommandErrorType::new(
translation::java::COMMANDS_CLONE_OVERLAP, translation::java::COMMANDS_CLONE_OVERLAP,
translation::java::COMMANDS_CLONE_OVERLAP, translation::bedrock::COMMANDS_CLONE_NOOVERLAP,
); );
const TOOBIG_ERROR: CommandErrorType<2> = CommandErrorType::new( const TOOBIG_ERROR: CommandErrorType<2> = CommandErrorType::new(
translation::java::COMMANDS_CLONE_TOOBIG, translation::java::COMMANDS_CLONE_TOOBIG,
translation::java::COMMANDS_CLONE_TOOBIG, translation::bedrock::COMMANDS_CLONE_TOOMANYBLOCKS,
); );
const FAILED_ERROR: CommandErrorType<0> = CommandErrorType::new( const FAILED_ERROR: CommandErrorType<0> = CommandErrorType::new(
translation::java::COMMANDS_CLONE_FAILED, translation::java::COMMANDS_CLONE_FAILED,
translation::java::COMMANDS_CLONE_FAILED, translation::bedrock::COMMANDS_CLONE_FAILED,
); );
const NOT_LOADED_ERROR: CommandErrorType<0> = CommandErrorType::new( const NOT_LOADED_ERROR: CommandErrorType<0> = CommandErrorType::new(
@ -252,7 +252,7 @@ impl CommandExecutor for CloneExecutor {
.send_feedback( .send_feedback(
TextComponent::translate_cross( TextComponent::translate_cross(
translation::java::COMMANDS_CLONE_SUCCESS, translation::java::COMMANDS_CLONE_SUCCESS,
translation::java::COMMANDS_CLONE_SUCCESS, translation::bedrock::COMMANDS_CLONE_SUCCESS,
[TextComponent::text(count.to_string())], [TextComponent::text(count.to_string())],
), ),
true, true,

View File

@ -45,8 +45,8 @@ impl CommandExecutor for DefaultGamemodeExecutor {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
"commands.defaultgamemode.success", pumpkin_data::translation::java::COMMANDS_DEFAULTGAMEMODE_SUCCESS,
"commands.defaultgamemode.success", pumpkin_data::translation::bedrock::COMMANDS_DEFAULTGAMEMODE_SUCCESS,
[TextComponent::translate_cross( [TextComponent::translate_cross(
gamemode_string.clone(), gamemode_string.clone(),
gamemode_string, gamemode_string,

View File

@ -52,8 +52,8 @@ impl CommandExecutor for Executor {
} }
let msg = TextComponent::translate_cross( let msg = TextComponent::translate_cross(
"commands.deop.success", pumpkin_data::translation::java::COMMANDS_DEOP_SUCCESS,
"commands.deop.success", pumpkin_data::translation::bedrock::COMMANDS_DEOP_SUCCESS,
[TextComponent::text(profile.name.clone())], [TextComponent::text(profile.name.clone())],
); );
sender.send_message(msg).await; sender.send_message(msg).await;
@ -66,8 +66,8 @@ impl CommandExecutor for Executor {
if succeeded_deops == 0 { if succeeded_deops == 0 {
Err(CommandError::CommandFailed(TextComponent::translate_cross( Err(CommandError::CommandFailed(TextComponent::translate_cross(
"commands.deop.failed", pumpkin_data::translation::java::COMMANDS_DEOP_FAILED,
"commands.deop.failed", pumpkin_data::translation::bedrock::COMMANDS_DEOP_FAILED,
[], [],
))) )))
} else { } else {

View File

@ -65,8 +65,8 @@ impl CommandExecutor for DifficultySetExecutor {
.source .source
.send_feedback( .send_feedback(
TextComponent::translate_cross( TextComponent::translate_cross(
"commands.difficulty.success", pumpkin_data::translation::java::COMMANDS_DIFFICULTY_SUCCESS,
"commands.difficulty.success", pumpkin_data::translation::bedrock::COMMANDS_DIFFICULTY_SUCCESS,
[TextComponent::translate_cross( [TextComponent::translate_cross(
difficulty.translation_key(), difficulty.translation_key(),
difficulty.translation_key(), difficulty.translation_key(),

View File

@ -55,8 +55,8 @@ impl CommandExecutor for Executor {
if level > enchantment.max_level { if level > enchantment.max_level {
let msg = TextComponent::translate_cross( let msg = TextComponent::translate_cross(
"commands.enchant.failed.level", translation::java::COMMANDS_ENCHANT_FAILED_LEVEL,
"commands.enchant.failed.level", translation::bedrock::COMMANDS_ENCHANT_INVALIDLEVEL,
[ [
TextComponent::text(level.to_string()), TextComponent::text(level.to_string()),
TextComponent::text(enchantment.max_level.to_string()), TextComponent::text(enchantment.max_level.to_string()),
@ -87,8 +87,8 @@ impl CommandExecutor for Executor {
if item.is_empty() { if item.is_empty() {
if only_one { if only_one {
let msg = TextComponent::translate_cross( let msg = TextComponent::translate_cross(
"commands.enchant.failed.itemless", translation::java::COMMANDS_ENCHANT_FAILED_ITEMLESS,
"commands.enchant.failed.itemless", translation::bedrock::COMMANDS_ENCHANT_NOITEM,
[targets[0].get_display_name().await], [targets[0].get_display_name().await],
); );
return Err(CommandError::CommandFailed(msg)); return Err(CommandError::CommandFailed(msg));
@ -98,8 +98,8 @@ impl CommandExecutor for Executor {
if !enchantment.can_enchant(item.item) { if !enchantment.can_enchant(item.item) {
if only_one { if only_one {
let msg = TextComponent::translate_cross( let msg = TextComponent::translate_cross(
"commands.enchant.failed.incompatible", translation::java::COMMANDS_ENCHANT_FAILED_INCOMPATIBLE,
"commands.enchant.failed.incompatible", translation::bedrock::COMMANDS_ENCHANT_CANTENCHANT,
[item.item.translated_name()], [item.item.translated_name()],
); );
return Err(CommandError::CommandFailed(msg)); return Err(CommandError::CommandFailed(msg));
@ -112,8 +112,8 @@ impl CommandExecutor for Executor {
success += 1; success += 1;
} else if only_one { } else if only_one {
let msg = TextComponent::translate_cross( let msg = TextComponent::translate_cross(
"commands.enchant.failed.incompatible", translation::java::COMMANDS_ENCHANT_FAILED_INCOMPATIBLE,
"commands.enchant.failed.incompatible", translation::bedrock::COMMANDS_ENCHANT_CANTENCHANT,
[item.item.translated_name()], [item.item.translated_name()],
); );
return Err(CommandError::CommandFailed(msg)); return Err(CommandError::CommandFailed(msg));
@ -126,15 +126,15 @@ impl CommandExecutor for Executor {
if success == 0 { if success == 0 {
let msg = TextComponent::translate_cross( let msg = TextComponent::translate_cross(
translation::java::COMMANDS_ENCHANT_FAILED, translation::java::COMMANDS_ENCHANT_FAILED,
translation::java::COMMANDS_ENCHANT_FAILED, translation::bedrock::COMMANDS_ENCHANT_CANTENCHANT,
[], [TextComponent::text("")],
); );
return Err(CommandError::CommandFailed(msg)); return Err(CommandError::CommandFailed(msg));
} }
if only_one { if only_one {
let msg = TextComponent::translate_cross( let msg = TextComponent::translate_cross(
translation::java::COMMANDS_ENCHANT_SUCCESS_SINGLE, translation::java::COMMANDS_ENCHANT_SUCCESS_SINGLE,
translation::java::COMMANDS_ENCHANT_SUCCESS_SINGLE, translation::bedrock::COMMANDS_ENCHANT_SUCCESS,
[ [
enchantment.get_fullname(level), enchantment.get_fullname(level),
targets[0].get_display_name().await, targets[0].get_display_name().await,
@ -144,7 +144,7 @@ impl CommandExecutor for Executor {
} else { } else {
let msg = TextComponent::translate_cross( let msg = TextComponent::translate_cross(
translation::java::COMMANDS_ENCHANT_SUCCESS_MULTIPLE, translation::java::COMMANDS_ENCHANT_SUCCESS_MULTIPLE,
translation::java::COMMANDS_ENCHANT_SUCCESS_MULTIPLE, translation::bedrock::COMMANDS_ENCHANT_SUCCESS,
[ [
enchantment.get_fullname(level), enchantment.get_fullname(level),
TextComponent::text(targets.len().to_string()), TextComponent::text(targets.len().to_string()),

View File

@ -87,20 +87,36 @@ impl Executor {
Mode::Query => "query", 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 { if targets.len() > 1 {
TextComponent::translate_cross( TextComponent::translate_cross(
format!("commands.experience.{mode_str}.{type_str}.success.multiple"), 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()), TextComponent::text(targets.len().to_string()),
], ],
) )
} else { } else {
TextComponent::translate_cross( TextComponent::translate_cross(
format!("commands.experience.{mode_str}.{type_str}.success.single"), format!("commands.experience.{mode_str}.{type_str}.success.single"),
format!("commands.experience.{mode_str}.{type_str}.success.single"), bedrock_key,
[TextComponent::text(amount.to_string()), first_target_name], [
TextComponent::text(bedrock_amount.to_string()),
first_target_name,
],
) )
} }
} }

View File

@ -1,6 +1,5 @@
use pumpkin_data::data_component::DataComponent::MaxStackSize; use pumpkin_data::data_component::DataComponent::MaxStackSize;
use pumpkin_data::data_component_impl::{MaxStackSizeImpl, get}; use pumpkin_data::data_component_impl::{MaxStackSizeImpl, get};
use pumpkin_data::item_stack::ItemStack;
use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextComponent;
use pumpkin_util::text::hover::HoverEvent; use pumpkin_util::text::hover::HoverEvent;
@ -39,7 +38,8 @@ impl CommandExecutor for Executor {
Box::pin(async move { Box::pin(async move {
let targets = PlayersArgumentConsumer.find_arg_default_name(args)?; 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) { let item_count = match item_count_consumer().find_arg_default_name(args) {
Err(_) => 1, Err(_) => 1,
@ -71,7 +71,8 @@ impl CommandExecutor for Executor {
while remaining > 0 { while remaining > 0 {
let take = remaining.min(max_stack); 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; target.inventory().insert_stack_anywhere(&mut stack).await;
if !stack.is_empty() { if !stack.is_empty() {
target.drop_item(stack).await; target.drop_item(stack).await;
@ -82,8 +83,8 @@ impl CommandExecutor for Executor {
let msg = if targets.len() == 1 { let msg = if targets.len() == 1 {
TextComponent::translate_cross( TextComponent::translate_cross(
"commands.give.success.single", pumpkin_data::translation::java::COMMANDS_GIVE_SUCCESS_SINGLE,
"commands.give.success.single", pumpkin_data::translation::bedrock::COMMANDS_GIVE_SUCCESS,
[ [
TextComponent::text(item_count.to_string()), TextComponent::text(item_count.to_string()),
TextComponent::text("[") TextComponent::text("[")
@ -98,8 +99,8 @@ impl CommandExecutor for Executor {
) )
} else { } else {
TextComponent::translate_cross( TextComponent::translate_cross(
"commands.give.success.multiple", pumpkin_data::translation::java::COMMANDS_GIVE_SUCCESS_MULTIPLE,
"commands.give.success.multiple", pumpkin_data::translation::bedrock::COMMANDS_GIVE_SUCCESS,
[ [
TextComponent::text(item_count.to_string()), TextComponent::text(item_count.to_string()),
TextComponent::text("[") TextComponent::text("[")

View File

@ -1,5 +1,4 @@
use pumpkin_data::data_component_impl::EquipmentSlot; use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::translation; use pumpkin_data::translation;
use pumpkin_inventory::screen_handler::InventoryPlayer; use pumpkin_inventory::screen_handler::InventoryPlayer;
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; 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 pos = BlockPosArgumentConsumer::find_loaded_arg(args, ARG_POS, &world)?;
let (slot, slot_name) = SlotArgumentConsumer::find_arg(args, ARG_SLOT)?; 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) { let count = match count_consumer().find_arg_default_name(args) {
Ok(Ok(c)) => c, Ok(Ok(c)) => c,
Err(_) | Ok(Err(_)) => 1, 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; inventory.set_stack(slot, item_stack.clone()).await;
let msg = TextComponent::translate_cross( let msg = TextComponent::translate_cross(
@ -135,14 +136,16 @@ impl CommandExecutor for EntityReplaceExecutor {
Box::pin(async move { Box::pin(async move {
let targets = EntitiesArgumentConsumer.find_arg_default_name(args)?; let targets = EntitiesArgumentConsumer.find_arg_default_name(args)?;
let (mojang_slot, slot_name) = SlotArgumentConsumer::find_arg(args, ARG_SLOT)?; 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) { let count = match count_consumer().find_arg_default_name(args) {
Ok(Ok(c)) => c, Ok(Ok(c)) => c,
Err(_) | Ok(Err(_)) => 1, Err(_) | Ok(Err(_)) => 1,
}; };
let mut modified_count = 0; 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 { for target in targets {
if let Some(player) = target.get_player() { if let Some(player) = target.get_player() {

View File

@ -34,7 +34,8 @@ impl CommandExecutor for Executor {
return Err(InvalidConsumption(Some(ARG_TARGETS.into()))); 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()), Some(Arg::Msg(r)) => TextComponent::text(r.clone()),
_ => TextComponent::translate_cross( _ => TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_KICKED, translation::java::MULTIPLAYER_DISCONNECT_KICKED,
@ -45,9 +46,24 @@ impl CommandExecutor for Executor {
for target in targets { for target in targets {
target.kick(DisconnectReason::Kicked, reason.clone()).await; target.kick(DisconnectReason::Kicked, reason.clone()).await;
let mut msg = TextComponent::text("Kicked: ");
msg = msg.add_child(target.get_display_name().await); let feedback = if custom_reason.is_some() {
sender.send_message(msg.color_named(NamedColor::Blue)).await; 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) Ok(targets.len() as i32)

View File

@ -29,13 +29,13 @@ impl CommandExecutor for TargetsExecutor {
let msg = if target_count == 1 { let msg = if target_count == 1 {
TextComponent::translate_cross( TextComponent::translate_cross(
translation::java::COMMANDS_KILL_SUCCESS_SINGLE, translation::java::COMMANDS_KILL_SUCCESS_SINGLE,
translation::java::COMMANDS_KILL_SUCCESS_SINGLE, translation::bedrock::COMMANDS_KILL_SUCCESSFUL,
[targets[0].get_display_name().await], [targets[0].get_display_name().await],
) )
} else { } else {
TextComponent::translate_cross( TextComponent::translate_cross(
translation::java::COMMANDS_KILL_SUCCESS_MULTIPLE, translation::java::COMMANDS_KILL_SUCCESS_MULTIPLE,
translation::java::COMMANDS_KILL_SUCCESS_MULTIPLE, translation::bedrock::COMMANDS_KILL_SUCCESSFUL,
[TextComponent::text(target_count.to_string())], [TextComponent::text(target_count.to_string())],
) )
}; };
@ -60,7 +60,7 @@ impl CommandExecutor for SelfExecutor {
.send_feedback( .send_feedback(
TextComponent::translate_cross( TextComponent::translate_cross(
translation::java::COMMANDS_KILL_SUCCESS_SINGLE, translation::java::COMMANDS_KILL_SUCCESS_SINGLE,
translation::java::COMMANDS_KILL_SUCCESS_SINGLE, translation::bedrock::COMMANDS_KILL_SUCCESSFUL,
[target.get_display_name().await], [target.get_display_name().await],
), ),
true, true,

View File

@ -44,8 +44,8 @@ impl CommandExecutor for Executor {
lock.banned_players.remove(idx); lock.banned_players.remove(idx);
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
"commands.pardon.success", pumpkin_data::translation::java::COMMANDS_PARDON_SUCCESS,
"commands.pardon.success", pumpkin_data::translation::bedrock::COMMANDS_UNBAN_SUCCESS,
[TextComponent::text(target.name.clone())], [TextComponent::text(target.name.clone())],
)) ))
.await; .await;
@ -57,10 +57,13 @@ impl CommandExecutor for Executor {
lock.save(); lock.save();
Ok(successes) Ok(successes)
} else { } else {
let err_target = targets
.first()
.map_or_else(String::new, |first_target| first_target.name.clone());
Err(CommandError::CommandFailed(TextComponent::translate_cross( Err(CommandError::CommandFailed(TextComponent::translate_cross(
"commands.pardon.failed", pumpkin_data::translation::java::COMMANDS_PARDON_FAILED,
"commands.pardon.failed", pumpkin_data::translation::bedrock::COMMANDS_UNBAN_FAILED,
[], [TextComponent::text(err_target)],
))) )))
} }
}) })

View File

@ -32,8 +32,8 @@ impl CommandExecutor for Executor {
let Ok(ip) = IpAddr::from_str(target) else { let Ok(ip) = IpAddr::from_str(target) else {
return Err(CommandError::CommandFailed(TextComponent::translate_cross( return Err(CommandError::CommandFailed(TextComponent::translate_cross(
"commands.pardonip.invalid", pumpkin_data::translation::java::COMMANDS_PARDONIP_INVALID,
"commands.pardonip.invalid", pumpkin_data::translation::bedrock::COMMANDS_UNBANIP_INVALID,
[], [],
))); )));
}; };
@ -45,16 +45,16 @@ impl CommandExecutor for Executor {
lock.banned_ips.remove(idx); lock.banned_ips.remove(idx);
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
"commands.pardonip.success", pumpkin_data::translation::java::COMMANDS_PARDONIP_SUCCESS,
"commands.pardonip.success", pumpkin_data::translation::bedrock::COMMANDS_UNBANIP_SUCCESS,
[TextComponent::text(ip.to_string())], [TextComponent::text(ip.to_string())],
)) ))
.await; .await;
Ok(1) Ok(1)
} else { } else {
Err(CommandError::CommandFailed(TextComponent::translate_cross( Err(CommandError::CommandFailed(TextComponent::translate_cross(
"commands.pardonip.failed", pumpkin_data::translation::java::COMMANDS_PARDONIP_FAILED,
"commands.pardonip.failed", pumpkin_data::translation::java::COMMANDS_PARDONIP_FAILED,
[], [],
))) )))
}; };

View File

@ -74,9 +74,12 @@ impl CommandExecutor for Executor {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
"commands.particle.success", pumpkin_data::translation::java::COMMANDS_PARTICLE_SUCCESS,
"commands.particle.success", pumpkin_data::translation::bedrock::COMMANDS_PARTICLE_SUCCESS,
[TextComponent::text(format!("{particle:?}"))], [
TextComponent::text(format!("{particle:?}")),
TextComponent::text(count.to_string()),
],
)) ))
.await; .await;

View File

@ -142,8 +142,8 @@ impl CommandExecutor for Executor {
// Send appropriate message based on results // Send appropriate message based on results
if players_who_heard == 0 { if players_who_heard == 0 {
Err(CommandError::CommandFailed(TextComponent::translate_cross( Err(CommandError::CommandFailed(TextComponent::translate_cross(
"commands.playsound.failed", translation::java::COMMANDS_PLAYSOUND_FAILED,
"commands.playsound.failed", translation::java::COMMANDS_PLAYSOUND_FAILED,
[], [],
))) )))
} else { } else {
@ -152,7 +152,7 @@ impl CommandExecutor for Executor {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
translation::java::COMMANDS_PLAYSOUND_SUCCESS_SINGLE, translation::java::COMMANDS_PLAYSOUND_SUCCESS_SINGLE,
translation::java::COMMANDS_PLAYSOUND_SUCCESS_SINGLE, translation::bedrock::COMMANDS_PLAYSOUND_SUCCESS,
[ [
TextComponent::text(sound_name), TextComponent::text(sound_name),
targets[0].get_display_name().await, targets[0].get_display_name().await,
@ -163,7 +163,7 @@ impl CommandExecutor for Executor {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
translation::java::COMMANDS_PLAYSOUND_SUCCESS_MULTIPLE, translation::java::COMMANDS_PLAYSOUND_SUCCESS_MULTIPLE,
translation::java::COMMANDS_PLAYSOUND_SUCCESS_MULTIPLE, translation::bedrock::COMMANDS_PLAYSOUND_SUCCESS,
[ [
TextComponent::text(sound_name), TextComponent::text(sound_name),
TextComponent::text(players_who_heard.to_string()), TextComponent::text(players_who_heard.to_string()),

View File

@ -2,7 +2,7 @@ use crate::command::argument_builder::{ArgumentBuilder, command};
use crate::command::context::command_context::CommandContext; use crate::command::context::command_context::CommandContext;
use crate::command::node::dispatcher::CommandDispatcher; use crate::command::node::dispatcher::CommandDispatcher;
use crate::command::node::{CommandExecutor, CommandExecutorResult}; 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::PermissionLvl;
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
use pumpkin_util::text::click::ClickEvent; use pumpkin_util::text::click::ClickEvent;
@ -17,13 +17,13 @@ struct SeedCommandExecutor;
fn create_copy_on_click_text(content: String) -> TextComponent { fn create_copy_on_click_text(content: String) -> TextComponent {
TextComponent::translate_cross( TextComponent::translate_cross(
COMMANDS_SEED_SUCCESS, translation::java::COMMANDS_SEED_SUCCESS,
COMMANDS_SEED_SUCCESS, translation::bedrock::COMMANDS_SEED_SUCCESS,
[TextComponent::wrap_in_square_brackets( [TextComponent::wrap_in_square_brackets(
TextComponent::text(content.clone()) TextComponent::text(content.clone())
.hover_event(HoverEvent::show_text(TextComponent::translate_cross( .hover_event(HoverEvent::show_text(TextComponent::translate_cross(
CHAT_COPY_CLICK, translation::java::CHAT_COPY_CLICK,
CHAT_COPY_CLICK, translation::java::CHAT_COPY_CLICK,
[], [],
))) )))
.click_event(ClickEvent::CopyToClipboard { .click_event(ClickEvent::CopyToClipboard {

View File

@ -112,8 +112,8 @@ impl CommandExecutor for Executor {
if success { if success {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
"commands.setblock.success", pumpkin_data::translation::java::COMMANDS_SETBLOCK_SUCCESS,
"commands.setblock.success", pumpkin_data::translation::bedrock::COMMANDS_SETBLOCK_SUCCESS,
[ [
TextComponent::text(pos.0.x.to_string()), TextComponent::text(pos.0.x.to_string()),
TextComponent::text(pos.0.y.to_string()), TextComponent::text(pos.0.y.to_string()),
@ -124,8 +124,8 @@ impl CommandExecutor for Executor {
Ok(1) Ok(1)
} else { } else {
Err(CommandError::CommandFailed(TextComponent::translate_cross( Err(CommandError::CommandFailed(TextComponent::translate_cross(
"commands.setblock.failed", pumpkin_data::translation::java::COMMANDS_SETBLOCK_FAILED,
"commands.setblock.failed", pumpkin_data::translation::bedrock::COMMANDS_SETBLOCK_FAILED,
[], [],
))) )))
} }

View File

@ -133,16 +133,13 @@ async fn set_spawnpoint(sender: &CommandSender, target: &Arc<Player>, pos: Block
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
translation::java::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE_NEW, translation::java::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE,
translation::java::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE_NEW, translation::bedrock::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE,
[ [
TextComponent::text(target.gameprofile.name.clone()),
TextComponent::text(pos.0.x.to_string()), TextComponent::text(pos.0.x.to_string()),
TextComponent::text(pos.0.y.to_string()), TextComponent::text(pos.0.y.to_string()),
TextComponent::text(pos.0.z.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; .await;

View File

@ -66,10 +66,15 @@ impl CommandExecutor for QueryExecutor {
QueryMode::GameTime => level_time.query_gametime(), QueryMode::GameTime => level_time.query_gametime(),
QueryMode::Day => level_time.query_day(), 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 sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
translation::java::COMMANDS_TIME_QUERY, translation::java::COMMANDS_TIME_QUERY,
translation::java::COMMANDS_TIME_QUERY, bedrock_key,
[TextComponent::text(curr_time.to_string())], [TextComponent::text(curr_time.to_string())],
)) ))
.await; .await;

View File

@ -52,7 +52,7 @@ impl CommandExecutor for Executor {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
translation::java::COMMANDS_WEATHER_SET_CLEAR, translation::java::COMMANDS_WEATHER_SET_CLEAR,
translation::java::COMMANDS_WEATHER_SET_CLEAR, translation::bedrock::COMMANDS_WEATHER_CLEAR,
[], [],
)) ))
.await; .await;
@ -65,7 +65,7 @@ impl CommandExecutor for Executor {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
translation::java::COMMANDS_WEATHER_SET_RAIN, translation::java::COMMANDS_WEATHER_SET_RAIN,
translation::java::COMMANDS_WEATHER_SET_RAIN, translation::bedrock::COMMANDS_WEATHER_RAIN,
[], [],
)) ))
.await; .await;
@ -78,7 +78,7 @@ impl CommandExecutor for Executor {
sender sender
.send_message(TextComponent::translate_cross( .send_message(TextComponent::translate_cross(
translation::java::COMMANDS_WEATHER_SET_THUNDER, translation::java::COMMANDS_WEATHER_SET_THUNDER,
translation::java::COMMANDS_WEATHER_SET_THUNDER, translation::bedrock::COMMANDS_WEATHER_THUNDER,
[], [],
)) ))
.await; .await;

View File

@ -77,6 +77,14 @@ impl BreedGoal {
1, 1,
) )
.await; .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(); mob_entity.reset_love_ticks();

View File

@ -1485,6 +1485,38 @@ impl LivingEntity {
1, 1,
) )
.await; .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 killer_player
.increment_stat( .increment_stat(
@ -2212,6 +2244,8 @@ impl EntityBase for LivingEntity {
(effective_amount * 10.0) as i32, (effective_amount * 10.0) as i32,
) )
.await; .await;
player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::DeflectedDamage).await;
} }
if let Some(attacker_player) = cause.and_then(|c| c.get_player()) { 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() { 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: // Prefer modifying the exact stack that matches the consumed item:
// 1) selected hotbar (held_item) // 1) selected hotbar (held_item)
// 2) off-hand // 2) off-hand

View File

@ -1052,6 +1052,10 @@ impl Player {
return; return;
} }
if damage >= 100.0 {
self.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::DealtOverkillDamage).await;
}
if let Some(enchantments) = item_stack if let Some(enchantments) = item_stack
.lock() .lock()
.await .await
@ -2027,6 +2031,7 @@ impl Player {
self.update_player_pose().await; self.update_player_pose().await;
self.breath_manager.tick(self).await; self.breath_manager.tick(self).await;
self.hunger_manager.tick(self).await; self.hunger_manager.tick(self).await;
self.check_inventory_advancements().await;
self.advancements.lock().await.flush_dirty(self, true); self.advancements.lock().await.flush_dirty(self, true);
// experience handling // experience handling
@ -2514,9 +2519,18 @@ impl Player {
self.unload_watched_chunks(&current_world).await; self.unload_watched_chunks(&current_world).await;
self.chunk_manager.lock().await.change_world(&current_world.level, new_world.clone()); self.chunk_manager.lock().await.change_world(&current_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()); 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 last_pos = self.living_entity.entity.last_pos.load();
let death_dimension = ResourceLocation::from(self.world().dimension.minecraft_name); let death_dimension = ResourceLocation::from(self.world().dimension.minecraft_name);
let death_location = BlockPos(Vector3::new( let death_location = BlockPos(Vector3::new(
@ -2842,6 +2856,10 @@ impl Player {
} }
async fn handle_killed(&self, death_msg: TextComponent) { async fn handle_killed(&self, death_msg: TextComponent) {
self.trigger_advancement(
crate::entity::player::advancement::trigger::AdvancementTrigger::PlayerKilled,
)
.await;
self.set_client_loaded(false); self.set_client_loaded(false);
let block_pos = self.position().to_block_pos(); let block_pos = self.position().to_block_pos();
@ -4169,6 +4187,51 @@ impl Player {
.into_source(server) .into_source(server)
.await .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 { impl PartialEq for Player {

View File

@ -1,3 +1,4 @@
pub mod trigger;
mod visibility_evaluator; mod visibility_evaluator;
use crate::data::advancement_data::AdvancementManager; use crate::data::advancement_data::AdvancementManager;

View File

@ -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;
}
}
}
}
}

View File

@ -362,6 +362,14 @@ impl EntityBase for ArrowEntity {
self.shake_time.store(7, Ordering::Relaxed); self.shake_time.store(7, Ordering::Relaxed);
*self.last_block_pos.write().unwrap() = Some(pos); *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 // Stop the arrow
entity.velocity.store(Vector3::new(0.0, 0.0, 0.0)); entity.velocity.store(Vector3::new(0.0, 0.0, 0.0));
entity.set_pos(hit_pos); entity.set_pos(hit_pos);

View File

@ -127,6 +127,7 @@ impl ItemBehaviour for EnderEyeItem {
pitch, pitch,
); );
player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::LaunchedEyeOfEnder).await;
player.inventory.held_item().lock().await.decrement(1); player.inventory.held_item().lock().await.decrement(1);
}) })
} }

View File

@ -1853,6 +1853,13 @@ impl JavaClient {
ActionType::Interact | ActionType::InteractAt => { ActionType::Interact | ActionType::InteractAt => {
let held = player.inventory.held_item(); let held = player.inventory.held_item();
let mut stack = held.lock().await.clone(); 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; let interacted = event.target.interact(player, &mut stack).await;
if !interacted { if !interacted {
server server