feat: some more entity AI work

This commit is contained in:
Alexander Medvedev 2026-01-17 19:26:25 +01:00
parent ba13f8a6c7
commit 3fbefafe4d
15 changed files with 307 additions and 51 deletions

27
Cargo.lock generated
View File

@ -103,7 +103,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@ -114,7 +114,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"once_cell_polyfill", "once_cell_polyfill",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@ -391,11 +391,11 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]] [[package]]
name = "colored" name = "colored"
version = "3.0.0" version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -924,7 +924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2461,7 +2461,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2884,7 +2884,7 @@ dependencies = [
"fastrand", "fastrand",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -3449,7 +3449,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -3473,15 +3473,6 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.60.2" version = "0.60.2"

View File

@ -54,7 +54,7 @@ base64 = "0.22.1"
bitflags = "2.10.0" bitflags = "2.10.0"
cesu8 = "1.1" cesu8 = "1.1"
cfb8 = "0.8" cfb8 = "0.8"
colored = "3.0" colored = "3.1"
console-subscriber = { version = "0.5.0", default-features = false } console-subscriber = { version = "0.5.0", default-features = false }
crc-fast = "1.9.0" crc-fast = "1.9.0"
criterion = { version = "0.8", default-features = false } criterion = { version = "0.8", default-features = false }

View File

@ -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<CreeperEntity>,
}
impl CreeperIgniteGoal {
#[must_use]
pub fn new(creeper: Arc<CreeperEntity>) -> 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
}
}

View File

@ -120,18 +120,18 @@ impl Goal for MeleeAttackGoal {
fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async { Box::pin(async {
let mut target = mob.get_mob_entity().target.lock().await; 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 let mut navigator = mob.get_mob_entity().navigator.lock().await;
.test(mob.get_entity()) navigator.cancel();
.await
{
*target = None;
}
// TODO: set attacking to false and stop navigation
}) })
} }

View File

@ -3,6 +3,7 @@ use std::{any::TypeId, ops::BitOr, pin::Pin, ptr};
pub mod active_target; pub mod active_target;
pub mod ambient_stand; pub mod ambient_stand;
pub mod creeper_ignite;
pub mod goal_selector; pub mod goal_selector;
pub mod look_around; pub mod look_around;
pub mod look_at_entity; pub mod look_at_entity;

View File

@ -20,7 +20,7 @@ pub struct TrackTargetGoal {
can_navigate_flag: i32, can_navigate_flag: i32,
check_can_navigate_cooldown: i32, check_can_navigate_cooldown: i32,
time_without_visibility: i32, time_without_visibility: i32,
max_time_without_visibility: i32, // Default 60 max_time_without_visibility: i32,
} }
#[expect(dead_code)] #[expect(dead_code)]
@ -103,14 +103,12 @@ impl Goal for TrackTargetGoal {
let mob = mob.get_mob_entity(); let mob = mob.get_mob_entity();
let mob_target = mob.target.lock().await; 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() { let target = if mob_target.is_some() {
(*mob_target).clone() (*mob_target).clone()
} else { } else {
self.target.clone() self.target.clone()
}; };
// Drop the guard immediately after access to release the lock
drop(mob_target); drop(mob_target);
if target.is_none() { if target.is_none() {
@ -125,17 +123,14 @@ impl Goal for TrackTargetGoal {
self.can_navigate_flag = 0; self.can_navigate_flag = 0;
self.check_can_navigate_cooldown = 0; self.check_can_navigate_cooldown = 0;
self.time_without_visibility = 0; self.time_without_visibility = 0;
// No await needed here
}) })
} }
fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async { Box::pin(async {
let mob = mob.get_mob_entity(); let mob = mob.get_mob_entity();
let mut mob_target = mob.target.lock().await; let mut mob_target = mob.target.lock().await;
*mob_target = None; *mob_target = None;
self.target = None; self.target = None;
}) })
} }

View File

@ -37,8 +37,7 @@ impl Goal for ZombieAttackGoal {
fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async { Box::pin(async {
self.melee_attack_goal.stop(mob).await; self.melee_attack_goal.stop(mob).await;
// Assuming set_attacking is synchronous mob.get_mob_entity().set_attacking(false).await;
mob.get_mob_entity().set_attacking(false);
}) })
} }
@ -46,14 +45,12 @@ impl Goal for ZombieAttackGoal {
Box::pin(async { Box::pin(async {
self.melee_attack_goal.tick(mob).await; self.melee_attack_goal.tick(mob).await;
self.ticks += 1; self.ticks += 1;
// Note: Accessing cooldown/get_max_cooldown is assumed synchronous.
if self.ticks >= 5 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 { } else {
mob.get_mob_entity().set_attacking(false); mob.get_mob_entity().set_attacking(false).await;
} }
}) })
} }

View File

@ -75,7 +75,7 @@ impl Navigator {
// Now let's move // Now let's move
entity.entity.set_pos(goal.current_progress); entity.entity.set_pos(goal.current_progress);
entity.entity.send_pos().await; //entity.entity.send_pos().await;
} }
} }

View File

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

View File

@ -1,7 +1,10 @@
use std::sync::Arc; use std::sync::{Arc};
use pumpkin_data::entity::EntityType;
use crate::entity::{ use crate::entity::{
Entity, NBTStorage, Entity, NBTStorage,
ai::goal::active_target::ActiveTargetGoal,
mob::{Mob, MobEntity, zombie::ZombieEntity}, mob::{Mob, MobEntity, zombie::ZombieEntity},
}; };
@ -11,9 +14,25 @@ pub struct DrownedEntity {
impl DrownedEntity { impl DrownedEntity {
pub async fn make(entity: Entity) -> Arc<Self> { pub async fn make(entity: Entity) -> Arc<Self> {
Arc::new(Self { let entity = ZombieEntity::make(entity).await;
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
} }
} }

View File

@ -6,14 +6,18 @@ use crate::server::Server;
use crate::world::World; use crate::world::World;
use crossbeam::atomic::AtomicCell; use crossbeam::atomic::AtomicCell;
use pumpkin_data::damage::DamageType; 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::position::BlockPos;
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering::Relaxed; use std::sync::atomic::Ordering::Relaxed;
use std::sync::atomic::{AtomicI32, AtomicU8, Ordering};
use tokio::sync::Mutex; use tokio::sync::Mutex;
pub mod creeper;
pub mod drowned; pub mod drowned;
pub mod skeleton; pub mod skeleton;
pub mod zombie; pub mod zombie;
@ -27,9 +31,16 @@ pub struct MobEntity {
pub look_control: Mutex<LookControl>, pub look_control: Mutex<LookControl>,
pub position_target: AtomicCell<BlockPos>, pub position_target: AtomicCell<BlockPos>,
pub position_target_range: AtomicI32, pub position_target_range: AtomicI32,
mob_flags: AtomicU8,
} }
impl MobEntity { 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] #[must_use]
pub fn new(entity: Entity) -> Self { pub fn new(entity: Entity) -> Self {
Self { Self {
@ -41,6 +52,7 @@ impl MobEntity {
look_control: Mutex::new(LookControl::default()), look_control: Mutex::new(LookControl::default()),
position_target: AtomicCell::new(BlockPos::ZERO), position_target: AtomicCell::new(BlockPos::ZERO),
position_target_range: AtomicI32::new(-1), position_target_range: AtomicI32::new(-1),
mob_flags: AtomicU8::new(0),
} }
} }
pub fn is_in_position_target_range(&self) -> bool { pub fn is_in_position_target_range(&self) -> bool {
@ -57,8 +69,27 @@ impl MobEntity {
} }
} }
pub fn set_attacking(&self, _attacking: bool) { pub async fn set_attacking(&self, attacking: bool) {
// TODO: set to data tracker 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;
} }
} }

View File

@ -61,6 +61,7 @@ pub mod hunger;
pub mod item; pub mod item;
pub mod living; pub mod living;
pub mod mob; pub mod mob;
pub mod passive;
pub mod player; pub mod player;
pub mod projectile; pub mod projectile;
pub mod projectile_deflection; pub mod projectile_deflection;

View File

@ -0,0 +1 @@
pub mod wolf;

View File

@ -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<Self> {
let mob_entity = MobEntity::new(entity);
let wolf = Self { mob_entity };
let mob_arc = Arc::new(wolf);
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().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
}
}

View File

@ -11,7 +11,8 @@ use crate::{
armor_stand::ArmorStandEntity, end_crystal::EndCrystalEntity, painting::PaintingEntity, armor_stand::ArmorStandEntity, end_crystal::EndCrystalEntity, painting::PaintingEntity,
}, },
living::LivingEntity, living::LivingEntity,
mob::{drowned::DrownedEntity, zombie::ZombieEntity}, mob::{creeper::CreeperEntity, drowned::DrownedEntity, zombie::ZombieEntity},
passive::wolf::WolfEntity,
}, },
world::World, world::World,
}; };
@ -27,6 +28,8 @@ pub async fn from_type(
let mob: Arc<dyn EntityBase> = match entity_type.id { let mob: Arc<dyn EntityBase> = match entity_type.id {
id if id == EntityType::ZOMBIE.id => ZombieEntity::make(entity).await, id if id == EntityType::ZOMBIE.id => ZombieEntity::make(entity).await,
id if id == EntityType::DROWNED.id => DrownedEntity::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::ARMOR_STAND.id => Arc::new(ArmorStandEntity::new(entity)),
id if id == EntityType::PAINTING.id => Arc::new(PaintingEntity::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)), id if id == EntityType::END_CRYSTAL.id => Arc::new(EndCrystalEntity::new(entity)),