mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
* chore(bedrock): Update most of the packets to mirror the official docs more closely. * Fix typo * Fix CCreativeContent * Undo unrelated `registry.rs` change * Update WIT to merged PR --------- Signed-off-by: Demetrius Kanios <demetrius@kanios.net>
505 lines
16 KiB
Rust
505 lines
16 KiB
Rust
use std::sync::{
|
|
Arc, Weak,
|
|
atomic::{AtomicBool, AtomicU8, Ordering},
|
|
};
|
|
|
|
use pumpkin_data::entity::{EntityStatus, EntityType};
|
|
use pumpkin_data::item::Item;
|
|
use pumpkin_data::item_stack::ItemStack;
|
|
use pumpkin_data::tag::{self, Taggable};
|
|
use pumpkin_nbt::compound::NbtCompound;
|
|
use pumpkin_protocol::bedrock::server::actor_event::ActorEventID;
|
|
use pumpkin_protocol::codec::var_int::VarInt;
|
|
use pumpkin_protocol::java::client::play::Metadata;
|
|
use rand::RngExt;
|
|
use uuid::Uuid;
|
|
|
|
use crate::entity::{
|
|
Entity, EntityBase, EntityBaseFuture, NbtFuture,
|
|
ai::goal::{
|
|
active_target::ActiveTargetGoal, avoid_entity::AvoidEntityGoal, breed::BreedGoal,
|
|
escape_danger::EscapeDangerGoal, follow_owner::FollowOwnerGoal,
|
|
follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal,
|
|
look_at_entity::LookAtEntityGoal, swim::SwimGoal, tempt::TemptGoal,
|
|
wander_around::WanderAroundGoal,
|
|
},
|
|
mob::{Mob, MobEntity},
|
|
passive::{
|
|
animal::Animal,
|
|
tamable::{TamableAnimal, TamableData},
|
|
},
|
|
player::Player,
|
|
};
|
|
|
|
const TEMPT_ITEMS: &[&Item] = &[&Item::COD, &Item::SALMON];
|
|
|
|
fn get_dye_color_from_item(item: &Item) -> Option<u8> {
|
|
let key = item.registry_key;
|
|
if key.contains("white") {
|
|
Some(0)
|
|
} else if key.contains("orange") {
|
|
Some(1)
|
|
} else if key.contains("magenta") {
|
|
Some(2)
|
|
} else if key.contains("light_blue") {
|
|
Some(3)
|
|
} else if key.contains("yellow") {
|
|
Some(4)
|
|
} else if key.contains("lime") {
|
|
Some(5)
|
|
} else if key.contains("pink") {
|
|
Some(6)
|
|
} else if key.contains("light_gray") {
|
|
Some(8)
|
|
} else if key.contains("gray") {
|
|
Some(7)
|
|
} else if key.contains("cyan") {
|
|
Some(9)
|
|
} else if key.contains("purple") {
|
|
Some(10)
|
|
} else if key.contains("blue") {
|
|
Some(11)
|
|
} else if key.contains("brown") {
|
|
Some(12)
|
|
} else if key.contains("green") {
|
|
Some(13)
|
|
} else if key.contains("red") {
|
|
Some(14)
|
|
} else if key.contains("black") {
|
|
Some(15)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub struct CatEntity {
|
|
pub mob_entity: MobEntity,
|
|
pub variant: AtomicU8,
|
|
pub sound_variant: AtomicU8,
|
|
pub collar_color: AtomicU8,
|
|
pub tamable_data: TamableData,
|
|
pub is_lying: AtomicBool,
|
|
pub relax_state_one: AtomicBool,
|
|
}
|
|
|
|
impl CatEntity {
|
|
pub fn new(entity: Entity) -> Arc<Self> {
|
|
let mob_entity = MobEntity::new(entity);
|
|
let cat = Self {
|
|
mob_entity,
|
|
variant: AtomicU8::new(1), // Default to black
|
|
sound_variant: AtomicU8::new(0), // Default to classic
|
|
collar_color: AtomicU8::new(14), // Default to red
|
|
tamable_data: TamableData::default(),
|
|
is_lying: AtomicBool::new(false),
|
|
relax_state_one: AtomicBool::new(false),
|
|
};
|
|
let mob_arc = Arc::new(cat);
|
|
let mob_weak: Weak<dyn Mob> = {
|
|
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
|
Arc::downgrade(&mob_arc)
|
|
};
|
|
|
|
{
|
|
let mut goal_selector = mob_arc
|
|
.mob_entity
|
|
.goals_selector
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
|
|
// Goal 1: SwimGoal (FloatGoal)
|
|
goal_selector.add_goal(1, Box::new(SwimGoal::default()));
|
|
// Goal 1: TamableAnimalPanicGoal (EscapeDangerGoal)
|
|
goal_selector.add_goal(1, EscapeDangerGoal::new(1.5));
|
|
// Goal 4: CatTemptGoal
|
|
goal_selector.add_goal(4, Box::new(TemptGoal::new(0.6, TEMPT_ITEMS)));
|
|
// Goal 4: CatAvoidEntityGoal (when untamed)
|
|
goal_selector.add_goal(
|
|
4,
|
|
Box::new(AvoidEntityGoal::new(&EntityType::PLAYER, 16.0, 0.8, 1.33)),
|
|
);
|
|
// Goal 5: BreedGoal
|
|
goal_selector.add_goal(5, BreedGoal::new(0.8));
|
|
// Goal 6: FollowOwnerGoal
|
|
goal_selector.add_goal(6, FollowOwnerGoal::new(1.0, 10.0, 5.0));
|
|
// Goal 9: FollowParentGoal
|
|
goal_selector.add_goal(9, Box::new(FollowParentGoal::new(0.8)));
|
|
// Goal 11: WanderAroundGoal
|
|
goal_selector.add_goal(11, Box::new(WanderAroundGoal::new(0.8)));
|
|
// Goal 12: LookAtPlayerGoal
|
|
goal_selector.add_goal(
|
|
12,
|
|
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 10.0),
|
|
);
|
|
// Goal 12: RandomLookAroundGoal
|
|
goal_selector.add_goal(12, Box::new(RandomLookAroundGoal::default()));
|
|
};
|
|
|
|
{
|
|
let mut target_selector = mob_arc
|
|
.mob_entity
|
|
.target_selector
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
|
|
// Target Goal 1: NonTameRandomTargetGoal for Rabbit and Turtle
|
|
target_selector.add_goal(
|
|
1,
|
|
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::RABBIT, false),
|
|
);
|
|
target_selector.add_goal(
|
|
1,
|
|
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::TURTLE, false),
|
|
);
|
|
};
|
|
|
|
mob_arc
|
|
}
|
|
|
|
pub fn get_tame_flags(&self) -> u8 {
|
|
let mut flags = 0u8;
|
|
if self.is_in_sitting_pose() {
|
|
flags |= 0x01;
|
|
}
|
|
if self.is_tame() {
|
|
flags |= 0x04;
|
|
}
|
|
flags
|
|
}
|
|
|
|
pub fn is_sitting(&self) -> bool {
|
|
self.is_in_sitting_pose()
|
|
}
|
|
|
|
pub fn is_lying(&self) -> bool {
|
|
self.is_lying.load(Ordering::Relaxed)
|
|
}
|
|
|
|
pub fn is_relax_state_one(&self) -> bool {
|
|
self.relax_state_one.load(Ordering::Relaxed)
|
|
}
|
|
|
|
pub fn get_collar_color(&self) -> u8 {
|
|
self.collar_color.load(Ordering::Relaxed)
|
|
}
|
|
|
|
pub fn set_collar_color(&self, color: u8) {
|
|
self.collar_color.store(color, Ordering::Relaxed);
|
|
let entity = self.get_entity();
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::CAT_COLLAR_COLOR,
|
|
VarInt(color as i32),
|
|
)],
|
|
None,
|
|
);
|
|
}
|
|
|
|
pub fn set_sitting(&self, sitting: bool) {
|
|
self.set_in_sitting_pose(sitting);
|
|
let entity = self.get_entity();
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::TAMEABLE_FLAGS,
|
|
self.get_tame_flags(),
|
|
)],
|
|
None,
|
|
);
|
|
}
|
|
|
|
pub fn set_tame(&self, tame: bool, owner: Option<Uuid>) {
|
|
self.tamable_data.is_tame.store(tame, Ordering::Relaxed);
|
|
self.set_owner(owner);
|
|
let entity = self.get_entity();
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::TAMEABLE_FLAGS,
|
|
self.get_tame_flags(),
|
|
)],
|
|
None,
|
|
);
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::OWNER_UUID,
|
|
owner,
|
|
)],
|
|
None,
|
|
);
|
|
}
|
|
|
|
pub fn set_variant(&self, variant: u8) {
|
|
self.variant.store(variant, Ordering::Relaxed);
|
|
let entity = self.get_entity();
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::CAT_VARIANT,
|
|
VarInt(variant as i32),
|
|
)],
|
|
None,
|
|
);
|
|
}
|
|
|
|
pub fn set_lying(&self, lying: bool) {
|
|
self.is_lying.store(lying, Ordering::Relaxed);
|
|
let entity = self.get_entity();
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::IS_LYING,
|
|
lying,
|
|
)],
|
|
None,
|
|
);
|
|
}
|
|
|
|
pub fn set_relax_state_one(&self, relax: bool) {
|
|
self.relax_state_one.store(relax, Ordering::Relaxed);
|
|
let entity = self.get_entity();
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::RELAX_STATE_ONE,
|
|
relax,
|
|
)],
|
|
None,
|
|
);
|
|
}
|
|
|
|
pub fn play_eating_sound(&self) {
|
|
let mob_entity = self.get_mob_entity();
|
|
let entity = &mob_entity.living_entity.entity;
|
|
let world = entity.world.load();
|
|
world.play_sound(
|
|
pumpkin_data::sound::Sound::EntityCatEat,
|
|
pumpkin_data::sound::SoundCategory::Neutral,
|
|
&entity.pos.load(),
|
|
);
|
|
}
|
|
}
|
|
|
|
impl Animal for CatEntity {
|
|
fn is_food(&self, item_stack: &ItemStack) -> bool {
|
|
let item = item_stack.get_item();
|
|
item.has_tag(&tag::Item::MINECRAFT_CAT_FOOD) || item == &Item::COD || item == &Item::SALMON
|
|
}
|
|
}
|
|
|
|
impl TamableAnimal for CatEntity {
|
|
fn get_tamable_data(&self) -> &TamableData {
|
|
&self.tamable_data
|
|
}
|
|
}
|
|
|
|
impl Mob for CatEntity {
|
|
fn as_animal(&self) -> Option<&dyn Animal> {
|
|
Some(self)
|
|
}
|
|
|
|
fn as_tamable(&self) -> Option<&dyn TamableAnimal> {
|
|
Some(self)
|
|
}
|
|
|
|
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
|
Box::pin(async {
|
|
let variant_str = match self.variant.load(Ordering::Relaxed) {
|
|
0 => "minecraft:all_black",
|
|
1 => "minecraft:black",
|
|
2 => "minecraft:british_shorthair",
|
|
3 => "minecraft:calico",
|
|
4 => "minecraft:jellie",
|
|
5 => "minecraft:persian",
|
|
6 => "minecraft:ragdoll",
|
|
7 => "minecraft:red",
|
|
8 => "minecraft:siamese",
|
|
10 => "minecraft:white",
|
|
_ => "minecraft:tabby",
|
|
};
|
|
nbt.put_string("variant", variant_str.to_string());
|
|
nbt.put_string("sound_variant", "minecraft:classic".to_string());
|
|
nbt.put_byte(
|
|
"CollarColor",
|
|
self.collar_color.load(Ordering::Relaxed) as i8,
|
|
);
|
|
})
|
|
}
|
|
|
|
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
|
Box::pin(async {
|
|
if let Some(variant_str) = nbt.get_string("variant") {
|
|
let variant = match variant_str
|
|
.strip_prefix("minecraft:")
|
|
.unwrap_or(variant_str)
|
|
{
|
|
"all_black" => 0,
|
|
"black" => 1,
|
|
"british_shorthair" => 2,
|
|
"calico" => 3,
|
|
"jellie" => 4,
|
|
"persian" => 5,
|
|
"ragdoll" => 6,
|
|
"red" => 7,
|
|
"siamese" => 8,
|
|
"white" => 10,
|
|
_ => 9,
|
|
};
|
|
self.variant.store(variant, Ordering::Relaxed);
|
|
}
|
|
if let Some(collar) = nbt.get_byte("CollarColor") {
|
|
self.collar_color.store(collar as u8, Ordering::Relaxed);
|
|
} else if let Some(collar_int) = nbt.get_int("CollarColor") {
|
|
self.collar_color.store(collar_int as u8, Ordering::Relaxed);
|
|
}
|
|
})
|
|
}
|
|
|
|
fn get_mob_entity(&self) -> &MobEntity {
|
|
&self.mob_entity
|
|
}
|
|
|
|
fn mob_set_variant_name(&self, name: &str) {
|
|
let variant = match name.strip_prefix("minecraft:").unwrap_or(name) {
|
|
"all_black" => 0,
|
|
"black" => 1,
|
|
"british_shorthair" => 2,
|
|
"calico" => 3,
|
|
"jellie" => 4,
|
|
"persian" => 5,
|
|
"ragdoll" => 6,
|
|
"red" => 7,
|
|
"siamese" => 8,
|
|
"white" => 10,
|
|
_ => 9,
|
|
};
|
|
self.variant.store(variant, Ordering::Relaxed);
|
|
}
|
|
|
|
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
|
|
Box::pin(async move {
|
|
let entity = self.get_entity();
|
|
let is_baby = entity.age.load(Ordering::Relaxed) < 0;
|
|
if is_baby {
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::BABY_ID,
|
|
true,
|
|
)],
|
|
None,
|
|
);
|
|
}
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::TAMEABLE_FLAGS,
|
|
self.get_tame_flags(),
|
|
)],
|
|
None,
|
|
);
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::OWNER_UUID,
|
|
self.get_owner(),
|
|
)],
|
|
None,
|
|
);
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::CAT_VARIANT,
|
|
VarInt(self.variant.load(Ordering::Relaxed) as i32),
|
|
)],
|
|
None,
|
|
);
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::IS_LYING,
|
|
self.is_lying.load(Ordering::Relaxed),
|
|
)],
|
|
None,
|
|
);
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::RELAX_STATE_ONE,
|
|
self.relax_state_one.load(Ordering::Relaxed),
|
|
)],
|
|
None,
|
|
);
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::CAT_COLLAR_COLOR,
|
|
VarInt(self.collar_color.load(Ordering::Relaxed) as i32),
|
|
)],
|
|
None,
|
|
);
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
pumpkin_data::tracked_data::cat::SOUND_VARIANT,
|
|
VarInt(self.sound_variant.load(Ordering::Relaxed) as i32),
|
|
)],
|
|
None,
|
|
);
|
|
})
|
|
}
|
|
|
|
fn mob_interact<'a>(
|
|
&'a self,
|
|
player: &'a Arc<Player>,
|
|
item_stack: &'a mut ItemStack,
|
|
) -> EntityBaseFuture<'a, bool> {
|
|
Box::pin(async move {
|
|
let item = item_stack.get_item();
|
|
let is_food = self.is_food(item_stack);
|
|
|
|
if self.is_tame() {
|
|
if self.get_owner_uuid() == Some(player.gameprofile.id) {
|
|
if item.has_tag(&tag::Item::MINECRAFT_CAT_COLLAR_DYES)
|
|
|| item.has_tag(&tag::Item::C_DYES)
|
|
{
|
|
if let Some(color) = get_dye_color_from_item(item)
|
|
&& color != self.get_collar_color()
|
|
{
|
|
self.set_collar_color(color);
|
|
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
|
|
return true;
|
|
}
|
|
} else if is_food
|
|
&& self.mob_entity.living_entity.health.load()
|
|
< self.mob_entity.living_entity.get_max_health()
|
|
{
|
|
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
|
|
self.mob_entity.living_entity.heal(2.0);
|
|
self.play_eating_sound();
|
|
return true;
|
|
}
|
|
|
|
let parent_interaction = self.mob_entity.mob_interact(player, item_stack).await;
|
|
if !parent_interaction {
|
|
self.set_sitting(!self.is_sitting());
|
|
return true;
|
|
}
|
|
return parent_interaction;
|
|
}
|
|
} else if is_food {
|
|
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
|
|
self.play_eating_sound();
|
|
|
|
let mut rng = rand::rng();
|
|
if rng.random_range(0..3) == 0 {
|
|
self.set_tame(true, Some(player.gameprofile.id));
|
|
self.set_sitting(true);
|
|
self.get_entity().world.load().send_entity_status(
|
|
self.get_entity(),
|
|
EntityStatus::TamingSucceeded,
|
|
Some(ActorEventID::TamingSucceeded),
|
|
);
|
|
} else {
|
|
self.get_entity().world.load().send_entity_status(
|
|
self.get_entity(),
|
|
EntityStatus::TamingFailed,
|
|
Some(ActorEventID::TamingFailed),
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
self.mob_entity.mob_interact(player, item_stack).await
|
|
})
|
|
}
|
|
}
|