Skip to main content

steel_core/entity/ai/goal/
selector.rs

1use std::fmt;
2use std::ops::BitOr;
3
4use crate::entity::PathfinderMob;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum GoalControl {
8    Move,
9    Look,
10    Jump,
11    Target,
12}
13
14impl GoalControl {
15    const ALL: [Self; 4] = [Self::Move, Self::Look, Self::Jump, Self::Target];
16}
17
18#[derive(Clone, Copy, Default, PartialEq, Eq)]
19pub struct GoalControls(u8);
20
21impl GoalControls {
22    pub const EMPTY: Self = Self(0);
23    pub const MOVE: Self = Self(1 << 0);
24    pub const LOOK: Self = Self(1 << 1);
25    pub const JUMP: Self = Self(1 << 2);
26    pub const TARGET: Self = Self(1 << 3);
27
28    #[must_use]
29    pub const fn from_control(control: GoalControl) -> Self {
30        match control {
31            GoalControl::Move => Self::MOVE,
32            GoalControl::Look => Self::LOOK,
33            GoalControl::Jump => Self::JUMP,
34            GoalControl::Target => Self::TARGET,
35        }
36    }
37
38    #[must_use]
39    pub const fn contains(self, control: GoalControl) -> bool {
40        self.0 & Self::from_control(control).0 != 0
41    }
42
43    #[must_use]
44    pub const fn intersects(self, other: Self) -> bool {
45        self.0 & other.0 != 0
46    }
47
48    pub const fn insert(&mut self, control: GoalControl) {
49        self.0 |= Self::from_control(control).0;
50    }
51
52    pub const fn remove(&mut self, control: GoalControl) {
53        self.0 &= !Self::from_control(control).0;
54    }
55
56    pub fn iter(self) -> impl Iterator<Item = GoalControl> {
57        GoalControl::ALL
58            .into_iter()
59            .filter(move |control| self.contains(*control))
60    }
61}
62
63impl fmt::Debug for GoalControls {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        f.debug_set().entries(self.iter()).finish()
66    }
67}
68
69impl BitOr for GoalControls {
70    type Output = Self;
71
72    fn bitor(self, rhs: Self) -> Self::Output {
73        Self(self.0 | rhs.0)
74    }
75}
76
77pub trait Goal: Send {
78    fn controls(&self) -> GoalControls;
79
80    fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool;
81
82    fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
83        self.can_use(mob)
84    }
85
86    fn is_interruptable(&self) -> bool {
87        true
88    }
89
90    fn is_panic_goal(&self) -> bool {
91        false
92    }
93
94    fn start(&mut self, _mob: &dyn PathfinderMob) {}
95
96    fn stop(&mut self, _mob: &dyn PathfinderMob) {}
97
98    fn requires_update_every_tick(&self) -> bool {
99        false
100    }
101
102    fn tick(&mut self, _mob: &dyn PathfinderMob) {}
103}
104
105struct WrappedGoal {
106    priority: i32,
107    goal: Box<dyn Goal>,
108    running: bool,
109}
110
111impl WrappedGoal {
112    fn new(priority: i32, goal: Box<dyn Goal>) -> Self {
113        Self {
114            priority,
115            goal,
116            running: false,
117        }
118    }
119
120    const fn is_running(&self) -> bool {
121        self.running
122    }
123
124    fn controls(&self) -> GoalControls {
125        self.goal.controls()
126    }
127
128    fn can_be_replaced_by(&self, candidate_priority: i32) -> bool {
129        self.goal.is_interruptable() && candidate_priority < self.priority
130    }
131
132    fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
133        self.goal.can_use(mob)
134    }
135
136    fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
137        self.goal.can_continue_to_use(mob)
138    }
139
140    fn start(&mut self, mob: &dyn PathfinderMob) {
141        if self.running {
142            return;
143        }
144        self.running = true;
145        self.goal.start(mob);
146    }
147
148    fn stop(&mut self, mob: &dyn PathfinderMob) {
149        if !self.running {
150            return;
151        }
152        self.running = false;
153        self.goal.stop(mob);
154    }
155
156    fn tick(&mut self, mob: &dyn PathfinderMob) {
157        self.goal.tick(mob);
158    }
159
160    fn requires_update_every_tick(&self) -> bool {
161        self.goal.requires_update_every_tick()
162    }
163
164    fn is_panic_goal(&self) -> bool {
165        self.goal.is_panic_goal()
166    }
167}
168
169pub struct GoalSelector {
170    available_goals: Vec<WrappedGoal>,
171    disabled_controls: GoalControls,
172}
173
174impl GoalSelector {
175    #[must_use]
176    pub const fn new() -> Self {
177        Self {
178            available_goals: Vec::new(),
179            disabled_controls: GoalControls::EMPTY,
180        }
181    }
182
183    pub fn add_goal<G>(&mut self, priority: i32, goal: G)
184    where
185        G: Goal + 'static,
186    {
187        self.available_goals
188            .push(WrappedGoal::new(priority, Box::new(goal)));
189    }
190
191    pub fn tick(&mut self, mob: &dyn PathfinderMob) {
192        for index in 0..self.available_goals.len() {
193            let should_stop = {
194                let disabled_controls = self.disabled_controls;
195                let goal = &mut self.available_goals[index];
196                goal.is_running()
197                    && (goal.controls().intersects(disabled_controls)
198                        || !goal.can_continue_to_use(mob))
199            };
200            if should_stop {
201                self.available_goals[index].stop(mob);
202            }
203        }
204
205        for index in 0..self.available_goals.len() {
206            if !self.can_start_goal(index) {
207                continue;
208            }
209            if !self.available_goals[index].can_use(mob) {
210                continue;
211            }
212
213            let controls = self.available_goals[index].controls();
214            for control in controls.iter() {
215                if let Some(current_index) = self.running_goal_index_for(control) {
216                    self.available_goals[current_index].stop(mob);
217                }
218            }
219            self.available_goals[index].start(mob);
220        }
221
222        self.tick_running_goals(mob, true);
223    }
224
225    pub fn tick_running_goals(
226        &mut self,
227        mob: &dyn PathfinderMob,
228        force_tick_all_running_goals: bool,
229    ) {
230        for goal in &mut self.available_goals {
231            if goal.is_running()
232                && (force_tick_all_running_goals || goal.requires_update_every_tick())
233            {
234                goal.tick(mob);
235            }
236        }
237    }
238
239    pub const fn disable_control(&mut self, control: GoalControl) {
240        self.disabled_controls.insert(control);
241    }
242
243    pub const fn enable_control(&mut self, control: GoalControl) {
244        self.disabled_controls.remove(control);
245    }
246
247    pub const fn set_control(&mut self, control: GoalControl, enabled: bool) {
248        if enabled {
249            self.enable_control(control);
250        } else {
251            self.disable_control(control);
252        }
253    }
254
255    #[must_use]
256    pub fn running_goal_count(&self) -> usize {
257        self.available_goals
258            .iter()
259            .filter(|goal| goal.is_running())
260            .count()
261    }
262
263    #[must_use]
264    pub const fn available_goal_count(&self) -> usize {
265        self.available_goals.len()
266    }
267
268    #[must_use]
269    pub(crate) fn has_running_panic_goal(&self) -> bool {
270        self.available_goals
271            .iter()
272            .any(|goal| goal.is_running() && goal.is_panic_goal())
273    }
274
275    #[cfg(test)]
276    #[must_use]
277    pub(crate) fn available_goal_priorities(&self) -> Vec<i32> {
278        self.available_goals
279            .iter()
280            .map(|goal| goal.priority)
281            .collect()
282    }
283
284    #[cfg(test)]
285    #[must_use]
286    pub(crate) const fn is_control_disabled(&self, control: GoalControl) -> bool {
287        self.disabled_controls.contains(control)
288    }
289
290    fn can_start_goal(&self, index: usize) -> bool {
291        let goal = &self.available_goals[index];
292        !goal.is_running()
293            && !goal.controls().intersects(self.disabled_controls)
294            && self.goal_can_be_replaced_for_all_controls(index)
295    }
296
297    fn goal_can_be_replaced_for_all_controls(&self, candidate_index: usize) -> bool {
298        let candidate = &self.available_goals[candidate_index];
299        for control in candidate.controls().iter() {
300            if let Some(current_index) = self.running_goal_index_for(control)
301                && !self.available_goals[current_index].can_be_replaced_by(candidate.priority)
302            {
303                return false;
304            }
305        }
306        true
307    }
308
309    fn running_goal_index_for(&self, control: GoalControl) -> Option<usize> {
310        self.available_goals
311            .iter()
312            .position(|goal| goal.is_running() && goal.controls().contains(control))
313    }
314
315    #[cfg(test)]
316    fn is_priority_running(&self, priority: i32) -> bool {
317        self.available_goals
318            .iter()
319            .any(|goal| goal.priority == priority && goal.is_running())
320    }
321}
322
323impl Default for GoalSelector {
324    fn default() -> Self {
325        Self::new()
326    }
327}
328
329impl fmt::Debug for GoalSelector {
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        f.debug_struct("GoalSelector")
332            .field("available_goals", &self.available_goals.len())
333            .field("running_goals", &self.running_goal_count())
334            .field("disabled_controls", &self.disabled_controls)
335            .finish()
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use std::sync::Weak;
342    use std::sync::atomic::{AtomicUsize, Ordering};
343
344    use glam::DVec3;
345    use steel_registry::entity_type::EntityTypeRef;
346    use steel_registry::{init_vanilla_registry, vanilla_entities};
347    use steel_utils::locks::SyncMutex;
348
349    use super::*;
350    use crate::entity::{
351        Entity, EntityBase, LivingEntity, LivingEntityBase, Mob, MobBase, PathfinderMob,
352    };
353
354    struct TestPathfinderMob {
355        base: EntityBase,
356        living_base: LivingEntityBase,
357        mob_base: MobBase,
358        mob_flags: SyncMutex<i8>,
359        health: SyncMutex<f32>,
360    }
361
362    impl TestPathfinderMob {
363        fn new() -> Self {
364            init_vanilla_registry();
365            Self {
366                base: EntityBase::new(
367                    1,
368                    DVec3::ZERO,
369                    vanilla_entities::PIG.dimensions,
370                    Weak::new(),
371                ),
372                living_base: LivingEntityBase::new(&vanilla_entities::PIG),
373                mob_base: MobBase::new(),
374                mob_flags: SyncMutex::new(0),
375                health: SyncMutex::new(10.0),
376            }
377        }
378    }
379
380    crate::entity::impl_test_downcast_type!(TestPathfinderMob);
381
382    impl Entity for TestPathfinderMob {
383        fn base(&self) -> &EntityBase {
384            &self.base
385        }
386
387        fn entity_type(&self) -> EntityTypeRef {
388            &vanilla_entities::PIG
389        }
390    }
391
392    impl LivingEntity for TestPathfinderMob {
393        fn living_base(&self) -> &LivingEntityBase {
394            &self.living_base
395        }
396
397        fn get_health(&self) -> f32 {
398            *self.health.lock()
399        }
400
401        fn set_health(&self, health: f32) {
402            *self.health.lock() = health;
403        }
404    }
405
406    impl Mob for TestPathfinderMob {
407        fn mob_base(&self) -> &MobBase {
408            &self.mob_base
409        }
410
411        fn mob_flags(&self) -> i8 {
412            *self.mob_flags.lock()
413        }
414
415        fn set_mob_flags(&self, flags: i8) {
416            *self.mob_flags.lock() = flags;
417        }
418    }
419
420    impl PathfinderMob for TestPathfinderMob {}
421
422    struct StaticGoal {
423        controls: GoalControls,
424        can_use: bool,
425        can_continue: bool,
426        interruptable: bool,
427        requires_update_every_tick: bool,
428        tick_count: Option<&'static AtomicUsize>,
429        can_use_once: bool,
430        panic_goal: bool,
431    }
432
433    impl StaticGoal {
434        const fn new(controls: GoalControls) -> Self {
435            Self {
436                controls,
437                can_use: true,
438                can_continue: true,
439                interruptable: true,
440                requires_update_every_tick: false,
441                tick_count: None,
442                can_use_once: false,
443                panic_goal: false,
444            }
445        }
446
447        const fn non_interruptable(mut self) -> Self {
448            self.interruptable = false;
449            self
450        }
451
452        const fn with_can_continue(mut self, can_continue: bool) -> Self {
453            self.can_continue = can_continue;
454            self
455        }
456
457        const fn with_can_use_once(mut self) -> Self {
458            self.can_use_once = true;
459            self
460        }
461
462        const fn with_update_every_tick(mut self) -> Self {
463            self.requires_update_every_tick = true;
464            self
465        }
466
467        const fn with_tick_counter(mut self, tick_count: &'static AtomicUsize) -> Self {
468            self.tick_count = Some(tick_count);
469            self
470        }
471
472        const fn with_panic_goal(mut self) -> Self {
473            self.panic_goal = true;
474            self
475        }
476    }
477
478    impl Goal for StaticGoal {
479        fn controls(&self) -> GoalControls {
480            self.controls
481        }
482
483        fn can_use(&mut self, _mob: &dyn PathfinderMob) -> bool {
484            if self.can_use_once {
485                if !self.can_use {
486                    return false;
487                }
488                self.can_use = false;
489                return true;
490            }
491            self.can_use
492        }
493
494        fn can_continue_to_use(&mut self, _mob: &dyn PathfinderMob) -> bool {
495            self.can_continue
496        }
497
498        fn is_interruptable(&self) -> bool {
499            self.interruptable
500        }
501
502        fn is_panic_goal(&self) -> bool {
503            self.panic_goal
504        }
505
506        fn requires_update_every_tick(&self) -> bool {
507            self.requires_update_every_tick
508        }
509
510        fn tick(&mut self, _mob: &dyn PathfinderMob) {
511            if let Some(tick_count) = self.tick_count {
512                tick_count.fetch_add(1, Ordering::Relaxed);
513            }
514        }
515    }
516
517    static RUNNING_TICK_COUNT: AtomicUsize = AtomicUsize::new(0);
518
519    #[test]
520    fn lower_priority_goal_replaces_running_goal_for_same_control() {
521        let mob = TestPathfinderMob::new();
522        let mut selector = GoalSelector::new();
523        selector.add_goal(5, StaticGoal::new(GoalControls::MOVE));
524        selector.tick(&mob);
525
526        selector.add_goal(3, StaticGoal::new(GoalControls::MOVE));
527        selector.tick(&mob);
528
529        assert_eq!(selector.running_goal_count(), 1);
530        assert!(selector.is_priority_running(3));
531    }
532
533    #[test]
534    fn non_interruptable_goal_blocks_replacement() {
535        let mob = TestPathfinderMob::new();
536        let mut selector = GoalSelector::new();
537        selector.add_goal(5, StaticGoal::new(GoalControls::MOVE).non_interruptable());
538        selector.tick(&mob);
539
540        selector.add_goal(3, StaticGoal::new(GoalControls::MOVE));
541        selector.tick(&mob);
542
543        assert_eq!(selector.running_goal_count(), 1);
544        assert!(selector.is_priority_running(5));
545    }
546
547    #[test]
548    fn disabled_control_stops_running_goal() {
549        let mob = TestPathfinderMob::new();
550        let mut selector = GoalSelector::new();
551        selector.add_goal(5, StaticGoal::new(GoalControls::MOVE));
552        selector.tick(&mob);
553
554        selector.disable_control(GoalControl::Move);
555        selector.tick(&mob);
556
557        assert_eq!(selector.running_goal_count(), 0);
558    }
559
560    #[test]
561    fn tick_running_goals_respects_requires_update_every_tick() {
562        RUNNING_TICK_COUNT.store(0, Ordering::Relaxed);
563        let mob = TestPathfinderMob::new();
564        let mut selector = GoalSelector::new();
565        selector.add_goal(
566            5,
567            StaticGoal::new(GoalControls::MOVE)
568                .with_update_every_tick()
569                .with_tick_counter(&RUNNING_TICK_COUNT),
570        );
571        selector.tick(&mob);
572
573        selector.tick_running_goals(&mob, false);
574
575        assert_eq!(RUNNING_TICK_COUNT.load(Ordering::Relaxed), 2);
576    }
577
578    #[test]
579    fn cleanup_stops_goal_that_can_no_longer_continue() {
580        let mob = TestPathfinderMob::new();
581        let mut selector = GoalSelector::new();
582        selector.add_goal(
583            5,
584            StaticGoal::new(GoalControls::MOVE)
585                .with_can_continue(false)
586                .with_can_use_once(),
587        );
588
589        selector.tick(&mob);
590        selector.tick(&mob);
591
592        assert_eq!(selector.running_goal_count(), 0);
593    }
594
595    #[test]
596    fn running_panic_goal_is_visible_to_pathfinder_mob() {
597        let mob = TestPathfinderMob::new();
598        mob.mob_base()
599            .goal_selector()
600            .lock()
601            .add_goal(1, StaticGoal::new(GoalControls::MOVE).with_panic_goal());
602
603        assert!(!mob.is_panicking());
604
605        mob.mob_base().goal_selector().lock().tick(&mob);
606
607        assert!(
608            mob.mob_base()
609                .goal_selector()
610                .lock()
611                .has_running_panic_goal()
612        );
613        assert!(mob.is_panicking());
614    }
615}