mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-31 08:22:33 +00:00
feat: Initial advancement triggers
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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: _,
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)],
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("[")
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)],
|
||||
)))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
[],
|
||||
)))
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
[],
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1052,6 +1052,10 @@ impl Player {
|
||||
return;
|
||||
}
|
||||
|
||||
if damage >= 100.0 {
|
||||
self.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::DealtOverkillDamage).await;
|
||||
}
|
||||
|
||||
if let Some(enchantments) = item_stack
|
||||
.lock()
|
||||
.await
|
||||
@@ -2027,6 +2031,7 @@ impl Player {
|
||||
self.update_player_pose().await;
|
||||
self.breath_manager.tick(self).await;
|
||||
self.hunger_manager.tick(self).await;
|
||||
self.check_inventory_advancements().await;
|
||||
self.advancements.lock().await.flush_dirty(self, true);
|
||||
|
||||
// experience handling
|
||||
@@ -2514,9 +2519,18 @@ impl Player {
|
||||
self.unload_watched_chunks(¤t_world).await;
|
||||
|
||||
self.chunk_manager.lock().await.change_world(¤t_world.level, new_world.clone());
|
||||
// Update the entity's world reference for correct dimension-based operations
|
||||
self.living_entity.entity.set_world(new_world.clone());
|
||||
|
||||
if new_world.dimension == pumpkin_data::dimension::Dimension::THE_NETHER {
|
||||
self.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::EnterDimension {
|
||||
dimension: "the_nether".to_string(),
|
||||
}).await;
|
||||
} else if new_world.dimension == pumpkin_data::dimension::Dimension::THE_END {
|
||||
self.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::EnterDimension {
|
||||
dimension: "the_end".to_string(),
|
||||
}).await;
|
||||
}
|
||||
|
||||
let last_pos = self.living_entity.entity.last_pos.load();
|
||||
let death_dimension = ResourceLocation::from(self.world().dimension.minecraft_name);
|
||||
let death_location = BlockPos(Vector3::new(
|
||||
@@ -2842,6 +2856,10 @@ impl Player {
|
||||
}
|
||||
|
||||
async fn handle_killed(&self, death_msg: TextComponent) {
|
||||
self.trigger_advancement(
|
||||
crate::entity::player::advancement::trigger::AdvancementTrigger::PlayerKilled,
|
||||
)
|
||||
.await;
|
||||
self.set_client_loaded(false);
|
||||
let block_pos = self.position().to_block_pos();
|
||||
|
||||
@@ -4169,6 +4187,51 @@ impl Player {
|
||||
.into_source(server)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn has_advancement(
|
||||
&self,
|
||||
advancement: &'static pumpkin_data::advancement::Advancement,
|
||||
) -> bool {
|
||||
let advancements = self.advancements.lock().await;
|
||||
advancements
|
||||
.progress
|
||||
.map
|
||||
.get(advancement)
|
||||
.is_some_and(crate::entity::player::advancement::AdvancementProgress::is_done)
|
||||
}
|
||||
|
||||
pub async fn has_item_in_inventory(&self, item: &pumpkin_data::item::Item) -> bool {
|
||||
for slot in &self.inventory.main_inventory {
|
||||
let stack = slot.lock().await;
|
||||
if !stack.is_empty() && stack.item.id == item.id {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
let equipment = self.inventory.entity_equipment.lock().await;
|
||||
for slot_stack in equipment.equipment.values() {
|
||||
let stack = slot_stack.lock().await;
|
||||
if !stack.is_empty() && stack.item.id == item.id {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn trigger_advancement_criterion(
|
||||
&self,
|
||||
advancement: &'static pumpkin_data::advancement::Advancement,
|
||||
criterion: &str,
|
||||
) {
|
||||
let mut advancements = self.advancements.lock().await;
|
||||
advancements.award(advancement, criterion);
|
||||
}
|
||||
|
||||
pub async fn check_inventory_advancements(&self) {
|
||||
self.trigger_advancement(
|
||||
crate::entity::player::advancement::trigger::AdvancementTrigger::InventoryChanged,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Player {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod trigger;
|
||||
mod visibility_evaluator;
|
||||
|
||||
use crate::data::advancement_data::AdvancementManager;
|
||||
|
||||
740
pumpkin/src/entity/player/advancement/trigger.rs
Normal file
740
pumpkin/src/entity/player/advancement/trigger.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user