Skip to main content

banished_game/behaviour/
ai.rs

1//! CitizenAIComponent: choosing what a citizen does next. See docs/CITIZEN-AI.md.
2//!
3//! Original: `FUN_140115040`.
4// vtable 0x140168890
5
6use crate::random::Random;
7use crate::scheduler::{seconds, ticks};
8
9/// Activities.
10///
11/// Original: `FUN_1401146c0`.
12pub mod activity {
13    /// Eat.
14    pub const EAT: u32 = 0;
15    /// Get warm.
16    pub const WARM: u32 = 1;
17    /// Emergency work, for example fighting a fire.
18    pub const EMERGENCY: u32 = 2;
19    /// Fetch clothing.
20    pub const CLOTHING: u32 = 3;
21    /// Fetch herbs.
22    pub const HERBS: u32 = 4;
23    /// Visit the doctor.
24    pub const DOCTOR: u32 = 5;
25    /// Stock the home with food.
26    pub const HOME_FOOD: u32 = 6;
27    /// Stock the home with firewood.
28    pub const HOME_FUEL: u32 = 7;
29    /// Fetch a tool.
30    pub const TOOL: u32 = 8;
31    /// Work.
32    pub const WORK: u32 = 9;
33    /// Go to school.
34    pub const SCHOOL: u32 = 10;
35    /// A child plays near its guardian.
36    pub const PLAY: u32 = 11;
37    /// Loiter near a building, then wait.
38    pub const IDLE: u32 = 12;
39    /// Drop off what the citizen carries.
40    pub const DROP_OFF: u32 = 13;
41    /// Eat, when very hungry.
42    pub const EAT_URGENT: u32 = 14;
43    /// Get warm, when very cold.
44    pub const WARM_URGENT: u32 = 15;
45}
46use activity::*;
47
48/// `DAT_1401788fc`, `DAT_140178934`, `DAT_140178954`: loiter timers (seconds).
49const LOITER_UNHAPPY: f32 = 75.0;
50const LOITER_SAD: f32 = 300.0;
51const LOITER_ANYWAY: f32 = 1800.0;
52/// `DAT_140178934`: carrying inventory this long raises drop-off priority.
53const CARRY_LONG: f32 = 300.0;
54
55/// The citizen values candidate building reads (`D` = CitizenDescription).
56#[derive(Debug, Clone, Copy, Default)]
57pub struct CitizenView {
58    // +0x68; +0x228
59    /// Whether it has a disease.
60    pub sick: bool,
61    /// Whether a doctor is assigned.
62    pub doctor: bool,
63    // +0x44; +0x48; +0x50; +0x54; +0x80
64    /// The hunger.
65    pub hunger: f32,
66    /// The cold.
67    pub cold: f32,
68    /// The health.
69    pub health: f32,
70    /// Bit 9 of the health events: herbs are not wanted (*inferred*).
71    pub health_flag: bool,
72    /// The happiness.
73    pub happiness: f32,
74    // +0x3c; +0x208; +0x238; +0x1e4; +0xac
75    /// The age.
76    pub age: f32,
77    /// Whether it attends school.
78    pub school: bool,
79    /// Whether it has a home.
80    pub home: bool,
81    /// The clothing level.
82    pub clothing: i32,
83    /// The uses left on its tool.
84    pub tool: i32,
85    // +0x22
86    /// Map byte set at home, work or here.
87    ///
88    /// Original: `FUN_14011b0b0`.
89    pub emergency: bool,
90    /// The home has food.
91    ///
92    /// Original: `FUN_14011abb0`, `FUN_14011acc0`.
93    pub home_food: bool,
94    /// The home has firewood.
95    pub home_fuel: bool,
96    // +0x250; +0xb0
97    /// The spouse's supply errand: 1 food, 2 firewood.
98    pub spouse_activity: Option<u32>,
99    /// Carried inventory count.
100    ///
101    /// Original: `FUN_14011b2b0`.
102    pub carrying: i32,
103}
104
105/// The CitizenDescription thresholds candidate building reads.
106#[derive(Debug, Clone, Copy, Default)]
107pub struct Thresholds {
108    // +0x44
109    /// The age at which a citizen is an adult.
110    pub adult_age: f32,
111    // +0x74
112    /// The health below which a citizen looks for herbs.
113    pub health_low: f32,
114    // +0xb4
115    /// The happiness below which a citizen gets restless after `LOITER_SAD` seconds.
116    pub happy_low: f32,
117    // +0xb8
118    /// The happiness below which a citizen gets restless after `LOITER_UNHAPPY` seconds.
119    pub happy_unhappy: f32,
120    // +0xc8
121    /// The hunger above which a citizen eats.
122    pub hungry: f32,
123    // +0xcc
124    /// The hunger above which a citizen eats at once.
125    pub hungry_urgent: f32,
126    // +0xe8
127    /// The cold above which a citizen warms up.
128    pub cold: f32,
129    // +0xec
130    /// The cold above which a citizen warms up at once.
131    pub cold_urgent: f32,
132}
133
134/// One candidate (12-byte entries: activity, priority, unused).
135pub type Candidate = (u32, i32);
136
137/// A citizen's AI: the cooldown of each activity, the current and queued activity, and the urgent
138/// flags.
139#[derive(Debug, Clone)]
140pub struct CitizenAi {
141    // +0x28; +0x2c
142    /// The current activity.
143    pub current: u32,
144    /// The queued activity, if any.
145    pub queued: Option<u32>,
146    // +0x30 / +0x31
147    /// Eating urgently.
148    pub eating: bool,
149    /// Warming up urgently.
150    pub warming: bool,
151    // +0x40..+0xb8
152    /// Per-activity cooldowns (ticks).
153    pub cooldowns: [i64; 16],
154    // +0xc0; +0xc8
155    /// The time since the citizen last loitered.
156    pub since_loiter: i64,
157    /// How long the citizen has carried goods (reset while it carries nothing).
158    pub carrying_time: i64,
159}
160
161impl Default for CitizenAi {
162    fn default() -> Self {
163        CitizenAi {
164            current: IDLE,
165            queued: None,
166            eating: false,
167            warming: false,
168            cooldowns: [0; 16],
169            since_loiter: 0,
170            carrying_time: 0,
171        }
172    }
173}
174
175/// Candidate activities in push order, and whether the list is shuffled and sorted (the doctor-only
176/// and urgent-only lists return before that).
177///
178/// Original: `FUN_140113170`.
179pub fn candidates(
180    ai: &CitizenAi,
181    c: &CitizenView,
182    d: &Thresholds,
183    urgent_only: bool,
184) -> (Vec<Candidate>, bool) {
185    let (list, early) = candidate_list(ai, c, d, urgent_only);
186    (list, !early)
187}
188
189fn candidate_list(
190    ai: &CitizenAi,
191    c: &CitizenView,
192    d: &Thresholds,
193    urgent_only: bool,
194) -> (Vec<Candidate>, bool) {
195    let mut out = Vec::new();
196    if c.sick && c.doctor {
197        out.push((DOCTOR, 6));
198        return (out, true);
199    }
200    if c.hunger > d.hungry_urgent {
201        out.push((EAT_URGENT, 7));
202    }
203    if c.cold > d.cold_urgent {
204        out.push((WARM_URGENT, 7));
205    }
206    if c.hunger > d.hungry {
207        out.push((EAT, 6));
208    }
209    if c.cold > d.cold {
210        out.push((WARM, 6));
211    }
212    if !c.sick {
213        if c.emergency {
214            out.push((EMERGENCY, 7));
215        }
216        if c.clothing == 0 {
217            out.push((CLOTHING, 3));
218        }
219        if c.health < d.health_low && !c.health_flag {
220            out.push((HERBS, 3));
221        }
222    } else {
223        out.push((DOCTOR, 6));
224    }
225    if urgent_only {
226        return (out, true);
227    }
228    let loiter = seconds(ai.since_loiter);
229    let restless = (c.happiness < d.happy_unhappy && loiter > LOITER_UNHAPPY)
230        || (c.happiness < d.happy_low && loiter > LOITER_SAD)
231        || loiter > LOITER_ANYWAY;
232    out.push((IDLE, if restless { 2 } else { 0 }));
233    if c.age < d.adult_age || c.school {
234        if !c.school {
235            if !c.sick {
236                out.push((PLAY, 0));
237            }
238            if !c.home_food {
239                out.push((HOME_FOOD, 4));
240            }
241            if c.home_fuel {
242                return (out, false);
243            }
244            out.push((HOME_FUEL, 4));
245        } else if !c.sick {
246            out.push((SCHOOL, 2));
247        }
248        return (out, false);
249    }
250    if c.home {
251        if !c.home_food && c.spouse_activity != Some(1) {
252            out.push((HOME_FOOD, if c.hunger > d.hungry_urgent { 6 } else { 4 }));
253        }
254        if !c.home_fuel && c.spouse_activity != Some(2) {
255            out.push((HOME_FUEL, if c.cold > d.cold_urgent { 6 } else { 4 }));
256        }
257    }
258    if c.sick {
259        return (out, false);
260    }
261    if c.tool < 1 {
262        out.push((TOOL, 5));
263    }
264    out.push((WORK, 2));
265    if c.carrying >= 1 {
266        let long = ticks(CARRY_LONG) < ai.carrying_time;
267        out.push((DROP_OFF, if long { 3 } else { 1 }));
268    }
269    (out, false)
270}
271
272/// Swap each position with `Rand(count)`, then sort.
273///
274/// Original: `FUN_140113170` (tail).
275pub fn shuffle_and_sort(list: &mut [Candidate], rng: &mut Random) {
276    let n = list.len() as u32;
277    for i in 0..list.len() {
278        let j = rng.rand_below(n) as usize;
279        if j != i {
280            list.swap(i, j);
281        }
282    }
283    if list.len() > 1 {
284        crt_qsort(list);
285    }
286}
287
288/// The MSVC CRT `qsort` inlined for candidates, ordering by priority, highest first (comparator
289/// `b.priority - a.priority`). Median of three, short sort (selection of the maximum) for 8 or
290/// fewer elements, smaller partition first.
291///
292/// Original: `FUN_140030770`.
293pub fn crt_qsort(a: &mut [Candidate]) {
294    crt_qsort_by(a, |a: &Candidate, b: &Candidate| b.1 - a.1);
295}
296
297/// The Microsoft C runtime's `qsort` with a `less` comparator, as the game's templated copies use
298/// it: the same algorithm as [`crt_qsort_by`], with `cmp(a, b) > 0` being `less(b, a)` and
299/// equality neither way.
300///
301/// Original: `FUN_14002c9d0`, `FUN_140030c00`, `FUN_1400220f0`, `FUN_14001f120`, `FUN_140015c10`,
302/// `FUN_140015f10`, `FUN_140016ce0`, `FUN_140016fe0`.
303pub fn crt_qsort_less<T>(a: &mut [T], less: impl Fn(&T, &T) -> bool) {
304    crt_qsort_by(a, |x, y| {
305        if less(y, x) {
306            1
307        } else if less(x, y) {
308            -1
309        } else {
310            0
311        }
312    });
313}
314
315/// The same with a three-way comparator.
316pub fn crt_qsort_by<T>(a: &mut [T], cmp: impl Fn(&T, &T) -> i32) {
317    const CUTOFF: usize = 8;
318    if a.len() < 2 {
319        return;
320    }
321    let mut stack: Vec<(usize, usize)> = Vec::new();
322    let (mut lo, mut hi) = (0usize, a.len() - 1);
323    loop {
324        let size = hi - lo + 1;
325        if size <= CUTOFF {
326            // shortsort
327            let mut h = hi;
328            while h > lo {
329                let mut max = lo;
330                for p in lo + 1..=h {
331                    if cmp(&a[p], &a[max]) > 0 {
332                        max = p;
333                    }
334                }
335                a.swap(max, h);
336                h -= 1;
337            }
338        } else {
339            let mut mid = lo + size / 2;
340            if cmp(&a[lo], &a[mid]) > 0 {
341                a.swap(lo, mid);
342            }
343            if cmp(&a[lo], &a[hi]) > 0 {
344                a.swap(lo, hi);
345            }
346            if cmp(&a[mid], &a[hi]) > 0 {
347                a.swap(mid, hi);
348            }
349            let (mut loguy, mut higuy) = (lo as isize, hi as isize);
350            let m = |mid: usize| mid as isize;
351            loop {
352                if m(mid) > loguy {
353                    loop {
354                        loguy += 1;
355                        if !(loguy < m(mid) && cmp(&a[loguy as usize], &a[mid]) <= 0) {
356                            break;
357                        }
358                    }
359                }
360                if m(mid) <= loguy {
361                    loop {
362                        loguy += 1;
363                        if !(loguy <= hi as isize && cmp(&a[loguy as usize], &a[mid]) <= 0) {
364                            break;
365                        }
366                    }
367                }
368                loop {
369                    higuy -= 1;
370                    if !(higuy > m(mid) && cmp(&a[higuy as usize], &a[mid]) > 0) {
371                        break;
372                    }
373                }
374                if higuy < loguy {
375                    break;
376                }
377                a.swap(loguy as usize, higuy as usize);
378                if m(mid) == higuy {
379                    mid = loguy as usize;
380                }
381            }
382            higuy += 1;
383            if m(mid) < higuy {
384                loop {
385                    higuy -= 1;
386                    if !(higuy > m(mid) && cmp(&a[higuy as usize], &a[mid]) == 0) {
387                        break;
388                    }
389                }
390            }
391            if m(mid) >= higuy {
392                loop {
393                    higuy -= 1;
394                    if !(higuy > lo as isize && cmp(&a[higuy as usize], &a[mid]) == 0) {
395                        break;
396                    }
397                }
398            }
399            if higuy - lo as isize >= hi as isize - loguy {
400                if (lo as isize) < higuy {
401                    stack.push((lo, higuy as usize));
402                }
403                if loguy < hi as isize {
404                    lo = loguy as usize;
405                    continue;
406                }
407            } else {
408                if loguy < hi as isize {
409                    stack.push((loguy as usize, hi));
410                }
411                if (lo as isize) < higuy {
412                    hi = higuy as usize;
413                    continue;
414                }
415            }
416        }
417        match stack.pop() {
418            Some((l, h)) => {
419                lo = l;
420                hi = h;
421            }
422            None => return,
423        }
424    }
425}
426
427impl CitizenAi {
428    /// Count cooldowns down, advance the loiter and carry clocks.
429    ///
430    /// Original: `FUN_1401148b0` (timers).
431    pub fn tick(&mut self, dt: i64, carrying: i32) {
432        for c in &mut self.cooldowns {
433            *c = (*c - dt).max(0);
434        }
435        self.since_loiter += dt;
436        self.carrying_time += dt;
437        if carrying == 0 {
438            self.carrying_time = 0;
439        }
440    }
441
442    // +0x60 >= 0
443    /// The scheduled update. `busy` is the character's "has actions" test; `run` dispatches an
444    /// activity (see [`CitizenAi::choose`]). Eating from a stocked home before an urgent choice and
445    /// the per-profession inventory size and model refresh are the caller's.
446    ///
447    /// Original: `FUN_14011a7b0`, `FUN_140114c70`, `FUN_140114d60`, `FUN_1401148b0`.
448    pub fn update(
449        &mut self,
450        dt: i64,
451        view: &CitizenView,
452        d: &Thresholds,
453        rng: &mut Random,
454        busy: impl Fn() -> bool,
455        mut run: impl FnMut(&mut CitizenAi, u32, &mut Random) -> bool,
456    ) {
457        self.tick(dt, view.carrying);
458        let urgent_running = (self.eating && self.current == EAT_URGENT)
459            || (self.warming && self.current == WARM_URGENT);
460        let in_urgent = self.current == EAT_URGENT || self.current == WARM_URGENT;
461        if view.hunger <= d.hungry_urgent
462            || urgent_running
463            || in_urgent
464            || self.cooldowns[EAT_URGENT as usize] > 0
465        {
466            if self.eating && view.hunger <= d.hungry_urgent {
467                self.eating = false;
468            }
469        } else {
470            self.choose(view, d, true, rng, &mut run);
471            self.eating = self.current == EAT_URGENT;
472        }
473        // `bVar4` is computed once, before the hunger choice.
474        let in_urgent = self.current == EAT_URGENT || self.current == WARM_URGENT;
475        if view.cold <= d.cold_urgent
476            || urgent_running
477            || in_urgent
478            || self.cooldowns[WARM_URGENT as usize] > 0
479        {
480            if self.warming && view.cold <= d.cold_urgent {
481                self.warming = false;
482            }
483        } else {
484            self.choose(view, d, true, rng, &mut run);
485            self.warming = self.current == WARM_URGENT;
486        }
487        if !self.eating && !self.warming && view.emergency && self.current != EMERGENCY {
488            self.choose(view, d, true, rng, &mut run);
489        }
490        if busy() {
491            return;
492        }
493        self.choose(view, d, false, rng, &mut run);
494    }
495
496    /// Run the queued activity, or try candidates in order until `run` leaves the character busy.
497    /// `run(activity)` is the dispatcher and returns whether the character now has actions queued.
498    ///
499    /// Original: `FUN_1401146c0`, `FUN_140113060`.
500    pub fn choose(
501        &mut self,
502        view: &CitizenView,
503        d: &Thresholds,
504        urgent_only: bool,
505        rng: &mut Random,
506        mut run: impl FnMut(&mut CitizenAi, u32, &mut Random) -> bool,
507    ) {
508        if let Some(q) = self.queued {
509            if self.dispatch(q, &mut run, rng) {
510                self.current = q;
511                self.queued = None;
512                return;
513            }
514        }
515        let (mut list, shuffle) = candidates(self, view, d, urgent_only);
516        if shuffle {
517            shuffle_and_sort(&mut list, rng);
518        }
519        for (activity, _) in list {
520            if self.dispatch(activity, &mut run, rng) {
521                self.current = activity;
522                break;
523            }
524        }
525    }
526
527    /// `FUN_1401146c0`: only activities whose cooldown has expired run.
528    fn dispatch(
529        &mut self,
530        activity: u32,
531        run: &mut impl FnMut(&mut CitizenAi, u32, &mut Random) -> bool,
532        rng: &mut Random,
533    ) -> bool {
534        if self.cooldowns[activity as usize] >= 1 {
535            return false;
536        }
537        run(self, activity, rng)
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn qsort_orders_by_priority_descending() {
547        let mut v: Vec<Candidate> = (0..20).map(|i| (i, (i as i32 * 7) % 5)).collect();
548        crt_qsort(&mut v);
549        assert!(v.windows(2).all(|w| w[0].1 >= w[1].1), "{v:?}");
550        let mut v: Vec<Candidate> = vec![(12, 0), (8, 5), (3, 3), (9, 2)];
551        crt_qsort(&mut v);
552        assert_eq!(v, vec![(8, 5), (3, 3), (9, 2), (12, 0)]);
553    }
554
555    #[test]
556    fn new_adult_candidates() {
557        let ai = CitizenAi::default();
558        let view = CitizenView {
559            age: 20.0,
560            health: 1.0,
561            ..Default::default()
562        };
563        let d = Thresholds {
564            adult_age: 10.0,
565            hungry: 50.0,
566            hungry_urgent: 75.0,
567            cold: 50.0,
568            cold_urgent: 75.0,
569            ..Default::default()
570        };
571        let (c, shuffle) = candidates(&ai, &view, &d, false);
572        assert!(shuffle);
573        assert_eq!(c, vec![(CLOTHING, 3), (IDLE, 0), (TOOL, 5), (WORK, 2)]);
574    }
575}