From 3fbefafe4db4523eeb93ba4d35948af93c03dba7 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sat, 17 Jan 2026 19:26:25 +0100 Subject: [PATCH] feat: some more entity AI work --- Cargo.lock | 27 ++---- Cargo.toml | 2 +- pumpkin/src/entity/ai/goal/creeper_ignite.rs | 94 ++++++++++++++++++++ pumpkin/src/entity/ai/goal/melee_attack.rs | 20 ++--- pumpkin/src/entity/ai/goal/mod.rs | 1 + pumpkin/src/entity/ai/goal/track_target.rs | 7 +- pumpkin/src/entity/ai/goal/zombie_attack.rs | 11 +-- pumpkin/src/entity/ai/path/mod.rs | 2 +- pumpkin/src/entity/mob/creeper.rs | 77 ++++++++++++++++ pumpkin/src/entity/mob/drowned.rs | 27 +++++- pumpkin/src/entity/mob/mod.rs | 37 +++++++- pumpkin/src/entity/mod.rs | 1 + pumpkin/src/entity/passive/mod.rs | 1 + pumpkin/src/entity/passive/wolf.rs | 46 ++++++++++ pumpkin/src/entity/type.rs | 5 +- 15 files changed, 307 insertions(+), 51 deletions(-) create mode 100644 pumpkin/src/entity/ai/goal/creeper_ignite.rs create mode 100644 pumpkin/src/entity/mob/creeper.rs create mode 100644 pumpkin/src/entity/passive/mod.rs create mode 100644 pumpkin/src/entity/passive/wolf.rs diff --git a/Cargo.lock b/Cargo.lock index 1c308b9bb..f535f97a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -114,7 +114,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -391,11 +391,11 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -924,7 +924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2461,7 +2461,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2884,7 +2884,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3449,7 +3449,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3473,15 +3473,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" diff --git a/Cargo.toml b/Cargo.toml index 6ec2341be..b0ac285c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ base64 = "0.22.1" bitflags = "2.10.0" cesu8 = "1.1" cfb8 = "0.8" -colored = "3.0" +colored = "3.1" console-subscriber = { version = "0.5.0", default-features = false } crc-fast = "1.9.0" criterion = { version = "0.8", default-features = false } diff --git a/pumpkin/src/entity/ai/goal/creeper_ignite.rs b/pumpkin/src/entity/ai/goal/creeper_ignite.rs new file mode 100644 index 000000000..fc381eb27 --- /dev/null +++ b/pumpkin/src/entity/ai/goal/creeper_ignite.rs @@ -0,0 +1,94 @@ +use std::sync::Arc; +use std::sync::atomic::Ordering; + +use super::{Controls, Goal}; +use crate::entity::ai::goal::GoalFuture; +use crate::entity::mob::Mob; +use crate::entity::mob::creeper::CreeperEntity; + +pub struct CreeperIgniteGoal { + goal_control: Controls, + creeper: Arc, +} + +impl CreeperIgniteGoal { + #[must_use] + pub fn new(creeper: Arc) -> Self { + Self { + goal_control: Controls::MOVE, + creeper, + } + } +} + +impl Goal for CreeperIgniteGoal { + fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let creeper = mob.get_mob_entity(); + let target_lock = creeper.target.lock().await; + + if self.creeper.fuse_speed.load(Ordering::Relaxed) > 0 { + return true; + } + + if let Some(target) = target_lock.as_ref() { + let dist_sq = mob + .get_entity() + .pos + .load() + .squared_distance_to_vec(target.get_entity().pos.load()); + return dist_sq < 9.0; + } + + false + }) + } + + fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + let mut navigator = mob.get_mob_entity().navigator.lock().await; + navigator.cancel(); + + // TODO + }) + } + + fn stop<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + // TODO + }) + } + + fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + let target_lock = mob.get_mob_entity().target.lock().await; + + let Some(target) = target_lock.as_ref() else { + self.creeper.set_fuse_speed(-1).await; + return; + }; + + let dist_sq = mob + .get_entity() + .pos + .load() + .squared_distance_to_vec(target.get_entity().pos.load()); + + if dist_sq > 49.0 { + self.creeper.set_fuse_speed(-1).await; + } + // TODO: Check line of sight + else { + self.creeper.set_fuse_speed(1).await; + } + }) + } + + fn should_run_every_tick(&self) -> bool { + true + } + + fn controls(&self) -> Controls { + self.goal_control + } +} diff --git a/pumpkin/src/entity/ai/goal/melee_attack.rs b/pumpkin/src/entity/ai/goal/melee_attack.rs index 874e5d7b5..2c3e10eae 100644 --- a/pumpkin/src/entity/ai/goal/melee_attack.rs +++ b/pumpkin/src/entity/ai/goal/melee_attack.rs @@ -120,18 +120,18 @@ impl Goal for MeleeAttackGoal { fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { Box::pin(async { let mut target = mob.get_mob_entity().target.lock().await; - if target.is_none() { - return; + + if let Some(entity) = target.as_deref() { + if !EntityPredicate::ExceptCreativeOrSpectator + .test(entity.get_entity()) + .await + { + *target = None; + } } - if !EntityPredicate::ExceptCreativeOrSpectator - .test(mob.get_entity()) - .await - { - *target = None; - } - - // TODO: set attacking to false and stop navigation + let mut navigator = mob.get_mob_entity().navigator.lock().await; + navigator.cancel(); }) } diff --git a/pumpkin/src/entity/ai/goal/mod.rs b/pumpkin/src/entity/ai/goal/mod.rs index 340b1662c..130bdf9e0 100644 --- a/pumpkin/src/entity/ai/goal/mod.rs +++ b/pumpkin/src/entity/ai/goal/mod.rs @@ -3,6 +3,7 @@ use std::{any::TypeId, ops::BitOr, pin::Pin, ptr}; pub mod active_target; pub mod ambient_stand; +pub mod creeper_ignite; pub mod goal_selector; pub mod look_around; pub mod look_at_entity; diff --git a/pumpkin/src/entity/ai/goal/track_target.rs b/pumpkin/src/entity/ai/goal/track_target.rs index fbfe13df8..5d1ff6a56 100644 --- a/pumpkin/src/entity/ai/goal/track_target.rs +++ b/pumpkin/src/entity/ai/goal/track_target.rs @@ -20,7 +20,7 @@ pub struct TrackTargetGoal { can_navigate_flag: i32, check_can_navigate_cooldown: i32, time_without_visibility: i32, - max_time_without_visibility: i32, // Default 60 + max_time_without_visibility: i32, } #[expect(dead_code)] @@ -103,14 +103,12 @@ impl Goal for TrackTargetGoal { let mob = mob.get_mob_entity(); let mob_target = mob.target.lock().await; - // We need to decide which target to use for the check let target = if mob_target.is_some() { (*mob_target).clone() } else { self.target.clone() }; - // Drop the guard immediately after access to release the lock drop(mob_target); if target.is_none() { @@ -125,17 +123,14 @@ impl Goal for TrackTargetGoal { self.can_navigate_flag = 0; self.check_can_navigate_cooldown = 0; self.time_without_visibility = 0; - // No await needed here }) } fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { Box::pin(async { let mob = mob.get_mob_entity(); - let mut mob_target = mob.target.lock().await; *mob_target = None; - self.target = None; }) } diff --git a/pumpkin/src/entity/ai/goal/zombie_attack.rs b/pumpkin/src/entity/ai/goal/zombie_attack.rs index e774cee87..72d19d4ec 100644 --- a/pumpkin/src/entity/ai/goal/zombie_attack.rs +++ b/pumpkin/src/entity/ai/goal/zombie_attack.rs @@ -37,8 +37,7 @@ impl Goal for ZombieAttackGoal { fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { Box::pin(async { self.melee_attack_goal.stop(mob).await; - // Assuming set_attacking is synchronous - mob.get_mob_entity().set_attacking(false); + mob.get_mob_entity().set_attacking(false).await; }) } @@ -46,14 +45,12 @@ impl Goal for ZombieAttackGoal { Box::pin(async { self.melee_attack_goal.tick(mob).await; self.ticks += 1; - - // Note: Accessing cooldown/get_max_cooldown is assumed synchronous. if self.ticks >= 5 - && self.melee_attack_goal.cooldown < self.melee_attack_goal.get_max_cooldown() + && self.melee_attack_goal.cooldown < self.melee_attack_goal.get_max_cooldown() / 2 { - mob.get_mob_entity().set_attacking(true); + mob.get_mob_entity().set_attacking(true).await; } else { - mob.get_mob_entity().set_attacking(false); + mob.get_mob_entity().set_attacking(false).await; } }) } diff --git a/pumpkin/src/entity/ai/path/mod.rs b/pumpkin/src/entity/ai/path/mod.rs index a5c63d782..079de322d 100644 --- a/pumpkin/src/entity/ai/path/mod.rs +++ b/pumpkin/src/entity/ai/path/mod.rs @@ -75,7 +75,7 @@ impl Navigator { // Now let's move entity.entity.set_pos(goal.current_progress); - entity.entity.send_pos().await; + //entity.entity.send_pos().await; } } diff --git a/pumpkin/src/entity/mob/creeper.rs b/pumpkin/src/entity/mob/creeper.rs new file mode 100644 index 000000000..5f2e0f4e0 --- /dev/null +++ b/pumpkin/src/entity/mob/creeper.rs @@ -0,0 +1,77 @@ +use std::sync::{ + Arc, Weak, + atomic::{AtomicI32, Ordering}, +}; + +use pumpkin_data::{entity::EntityType, meta_data_type::MetaDataType, tracked_data::TrackedData}; +use pumpkin_protocol::java::client::play::Metadata; + +use crate::entity::{ + Entity, NBTStorage, + ai::goal::{ + active_target::ActiveTargetGoal, creeper_ignite::CreeperIgniteGoal, + look_around::LookAroundGoal, look_at_entity::LookAtEntityGoal, + }, + mob::{Mob, MobEntity}, +}; + +pub struct CreeperEntity { + pub mob_entity: MobEntity, + pub fuse_speed: AtomicI32, +} + +impl CreeperEntity { + pub async fn make(entity: Entity) -> Arc { + let mob_entity = MobEntity::new(entity); + let entity = Self { + mob_entity, + fuse_speed: AtomicI32::new(-1), + }; + let mob_arc = Arc::new(entity); + let mob_weak: Weak = { + let mob_arc: Arc = mob_arc.clone(); + Arc::downgrade(&mob_arc) + }; + + { + let mut goal_selector = mob_arc.mob_entity.goals_selector.lock().await; + let mut target_selector = mob_arc.mob_entity.target_selector.lock().await; + + goal_selector.add_goal(2, Box::new(CreeperIgniteGoal::new(mob_arc.clone()))); + goal_selector.add_goal( + 6, + LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0), + ); + goal_selector.add_goal(6, Box::new(LookAroundGoal::default())); + + target_selector.add_goal( + 1, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true), + ); + }; + + mob_arc + } + + pub async fn set_fuse_speed(&self, speed: i32) { + dbg!(speed); + self.fuse_speed.store(speed, Ordering::Relaxed); + self.mob_entity + .living_entity + .entity + .send_meta_data(&[Metadata::new( + TrackedData::DATA_FUSE_SPEED, + MetaDataType::Integer, + speed, + )]) + .await + } +} + +impl NBTStorage for CreeperEntity {} + +impl Mob for CreeperEntity { + fn get_mob_entity(&self) -> &MobEntity { + &self.mob_entity + } +} diff --git a/pumpkin/src/entity/mob/drowned.rs b/pumpkin/src/entity/mob/drowned.rs index dba4c281c..c039de477 100644 --- a/pumpkin/src/entity/mob/drowned.rs +++ b/pumpkin/src/entity/mob/drowned.rs @@ -1,7 +1,10 @@ -use std::sync::Arc; +use std::sync::{Arc}; + +use pumpkin_data::entity::EntityType; use crate::entity::{ Entity, NBTStorage, + ai::goal::active_target::ActiveTargetGoal, mob::{Mob, MobEntity, zombie::ZombieEntity}, }; @@ -11,9 +14,25 @@ pub struct DrownedEntity { impl DrownedEntity { pub async fn make(entity: Entity) -> Arc { - Arc::new(Self { - entity: ZombieEntity::make(entity).await, - }) + let entity = ZombieEntity::make(entity).await; + let zombie = Self { entity }; + let mob_arc = Arc::new(zombie); + + { + let mut target_selector = mob_arc.entity.mob_entity.target_selector.lock().await; + + // TODO + target_selector.add_goal( + 2, + ActiveTargetGoal::with_default( + &mob_arc.entity.mob_entity, + &EntityType::PLAYER, + true, + ), + ); + }; + + mob_arc } } diff --git a/pumpkin/src/entity/mob/mod.rs b/pumpkin/src/entity/mob/mod.rs index 829f0ece2..8dc5bd274 100644 --- a/pumpkin/src/entity/mob/mod.rs +++ b/pumpkin/src/entity/mob/mod.rs @@ -6,14 +6,18 @@ use crate::server::Server; use crate::world::World; use crossbeam::atomic::AtomicCell; use pumpkin_data::damage::DamageType; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_protocol::java::client::play::Metadata; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; use std::pin::Pin; use std::sync::Arc; -use std::sync::atomic::AtomicI32; use std::sync::atomic::Ordering::Relaxed; +use std::sync::atomic::{AtomicI32, AtomicU8, Ordering}; use tokio::sync::Mutex; +pub mod creeper; pub mod drowned; pub mod skeleton; pub mod zombie; @@ -27,9 +31,16 @@ pub struct MobEntity { pub look_control: Mutex, pub position_target: AtomicCell, pub position_target_range: AtomicI32, + mob_flags: AtomicU8, } impl MobEntity { + #[expect(dead_code)] + const AI_DISABLED_FLAG: u8 = 1; + #[expect(dead_code)] + const LEFT_HANDED_FLAG: u8 = 2; + const ATTACKING_FLAG: u8 = 4; + #[must_use] pub fn new(entity: Entity) -> Self { Self { @@ -41,6 +52,7 @@ impl MobEntity { look_control: Mutex::new(LookControl::default()), position_target: AtomicCell::new(BlockPos::ZERO), position_target_range: AtomicI32::new(-1), + mob_flags: AtomicU8::new(0), } } pub fn is_in_position_target_range(&self) -> bool { @@ -57,8 +69,27 @@ impl MobEntity { } } - pub fn set_attacking(&self, _attacking: bool) { - // TODO: set to data tracker + pub async fn set_attacking(&self, attacking: bool) { + self.set_mob_flag(Self::ATTACKING_FLAG, attacking).await + } + + async fn set_mob_flag(&self, flag: u8, value: bool) { + let index = flag; + let mut b = self.mob_flags.load(Ordering::Relaxed); + if value { + b |= index; + } else { + b &= !index; + } + self.mob_flags.store(b, Ordering::Relaxed); + self.living_entity + .entity + .send_meta_data(&[Metadata::new( + TrackedData::DATA_MOB_FLAGS, + MetaDataType::Byte, + b, + )]) + .await; } } diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 61dee6703..c35a22cc0 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -61,6 +61,7 @@ pub mod hunger; pub mod item; pub mod living; pub mod mob; +pub mod passive; pub mod player; pub mod projectile; pub mod projectile_deflection; diff --git a/pumpkin/src/entity/passive/mod.rs b/pumpkin/src/entity/passive/mod.rs new file mode 100644 index 000000000..296f1c40c --- /dev/null +++ b/pumpkin/src/entity/passive/mod.rs @@ -0,0 +1 @@ +pub mod wolf; diff --git a/pumpkin/src/entity/passive/wolf.rs b/pumpkin/src/entity/passive/wolf.rs new file mode 100644 index 000000000..121ac849b --- /dev/null +++ b/pumpkin/src/entity/passive/wolf.rs @@ -0,0 +1,46 @@ +use std::sync::{Arc, Weak}; + +use pumpkin_data::entity::EntityType; + +use crate::entity::{ + Entity, NBTStorage, + ai::goal::{look_around::LookAroundGoal, look_at_entity::LookAtEntityGoal}, + mob::{Mob, MobEntity}, +}; + +pub struct WolfEntity { + pub mob_entity: MobEntity, +} + +impl WolfEntity { + pub async fn make(entity: Entity) -> Arc { + let mob_entity = MobEntity::new(entity); + let wolf = Self { mob_entity }; + let mob_arc = Arc::new(wolf); + let mob_weak: Weak = { + let mob_arc: Arc = mob_arc.clone(); + Arc::downgrade(&mob_arc) + }; + + { + let mut goal_selector = mob_arc.mob_entity.goals_selector.lock().await; + + // TODO + goal_selector.add_goal( + 8, + LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0), + ); + goal_selector.add_goal(8, Box::new(LookAroundGoal::default())); + }; + + mob_arc + } +} + +impl NBTStorage for WolfEntity {} + +impl Mob for WolfEntity { + fn get_mob_entity(&self) -> &MobEntity { + &self.mob_entity + } +} diff --git a/pumpkin/src/entity/type.rs b/pumpkin/src/entity/type.rs index 6cdd43ed5..ddaaf8bbd 100644 --- a/pumpkin/src/entity/type.rs +++ b/pumpkin/src/entity/type.rs @@ -11,7 +11,8 @@ use crate::{ armor_stand::ArmorStandEntity, end_crystal::EndCrystalEntity, painting::PaintingEntity, }, living::LivingEntity, - mob::{drowned::DrownedEntity, zombie::ZombieEntity}, + mob::{creeper::CreeperEntity, drowned::DrownedEntity, zombie::ZombieEntity}, + passive::wolf::WolfEntity, }, world::World, }; @@ -27,6 +28,8 @@ pub async fn from_type( let mob: Arc = match entity_type.id { id if id == EntityType::ZOMBIE.id => ZombieEntity::make(entity).await, id if id == EntityType::DROWNED.id => DrownedEntity::make(entity).await, + id if id == EntityType::CREEPER.id => CreeperEntity::make(entity).await, + id if id == EntityType::WOLF.id => WolfEntity::make(entity).await, id if id == EntityType::ARMOR_STAND.id => Arc::new(ArmorStandEntity::new(entity)), id if id == EntityType::PAINTING.id => Arc::new(PaintingEntity::new(entity)), id if id == EntityType::END_CRYSTAL.id => Arc::new(EndCrystalEntity::new(entity)),