steel_core/entity/ai/goal/
selector.rs1use 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}