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"),
};
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();

View File

@@ -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: _,

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_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<Mutex<[HashMap<String, String>; 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)
});

View File

@@ -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,

View File

@@ -550,6 +550,13 @@ impl BlockBehaviour for SignBlock {
);
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() {
item.decrement(1);
}

View File

@@ -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<T: BlockBehaviour + BlockMetadata + 'static>(&mut self, block: T) {

View File

@@ -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<TargetSelector, TargetSelectorPars
let args_content = &arg[selector_type_end + 1..arg.len() - 1];
let mut filter_start = 0usize;
let mut curly_depth = 0;
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(
&mut selector,
&args_content[filter_start..i],
@@ -506,4 +529,43 @@ mod test {
assert!(s.includes_entities());
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::{
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<Self::Data, CommandError> {
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<Self::Data, CommandError> {
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);
}
}

View File

@@ -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<f64>),
/// 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<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.
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 {
#[must_use]
pub const fn new_all_of(predicates: Vec<Self>) -> 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)),
}
}

View File

@@ -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(())
}
}
}

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::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

View File

@@ -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,

View File

@@ -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)],
))
}

View File

@@ -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(

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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(),

View File

@@ -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()),

View File

@@ -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,
],
)
}
}

View File

@@ -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("[")

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)],
)))
}
})

View File

@@ -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,
[],
)))
};

View File

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

View File

@@ -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()),

View File

@@ -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 {

View File

@@ -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,
[],
)))
}

View File

@@ -133,16 +133,13 @@ async fn set_spawnpoint(sender: &CommandSender, target: &Arc<Player>, 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;

View File

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

View File

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

View File

@@ -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();

View File

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

View File

@@ -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(&current_world).await;
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());
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 {

View File

@@ -1,3 +1,4 @@
pub mod trigger;
mod visibility_evaluator;
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.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);

View File

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

View File

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