Skip to main content

banished_game/behaviour/
commute.rs

1//! The town's commute optimisation, run from StockManager::Update every 300 s of game time. See
2//! docs/COMMUTE.md.
3//!
4//! - Start: one cell pair (residence entrance, workplace entrance) per distinct pair of map
5//!   regions, each region pair cached with rank 0.
6//! - Step (every update): up to seven pairs, last first, get their path rank (`Map::PathRank` mode
7//!   0; 0 or 1 become the straight distance). With none left:
8//! - Homes: families in single-family residences, the longest commute first, move to the empty
9//!   residence that shortens theirs most, goods and all.
10//! - Jobs: every working adult is let go, then each workplace takes back as many as it had, the
11//!   workplace whose nearest candidate is furthest (weighted by how much of it is still to fill)
12//!   choosing first; the rest get the professions left over.
13//!
14//! Pointer orders in the original's trees (citizens, residences, professions) are taken as index
15//! order here.
16//!
17//! Original: `FUN_140091780`, `FUN_1400dc590`, `FUN_1400dc3d0`, `FUN_1400db770`, `FUN_1400d9880`.
18
19use std::cmp::Reverse;
20use std::collections::{BTreeMap, BTreeSet};
21
22use crate::people::citizens::Citizen;
23use crate::scheduler::ticks;
24use crate::town::Sim;
25
26/// `DAT_140178934`: seconds between runs.
27const PERIOD_SECONDS: f32 = 300.0;
28/// The cost of an unreachable or homeless commute (`0x1000000`).
29const FAR: i32 = 0x100_0000;
30/// The pairs ranked per update (`min(index, 6) + 1`).
31const STEP: i32 = 7;
32
33// +0x190; +0x1b8
34/// The optimisation's state: StockManager (the time since the last run) and the job object (its
35/// pairs, the next pair's index and the region-pair ranks).
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Commute {
38    /// The time since the last run.
39    pub timer: i64,
40    /// The pairs of cells to rank: a workplace entrance and a home.
41    pub pairs: Vec<(i32, i32)>,
42    /// The next pair to rank (−1 when done).
43    pub index: i32,
44    /// The rank of each pair of regions.
45    pub ranks: BTreeMap<(i32, i32), i32>,
46}
47
48impl Default for Commute {
49    fn default() -> Self {
50        Commute {
51            timer: 0,
52            pairs: Vec::new(),
53            index: -1,
54            ranks: BTreeMap::new(),
55        }
56    }
57}
58
59/// A workplace while the jobs are dealt (`0x28`-byte records): its entity, profession,
60/// entrance, places left and total (its workers at the start), and the candidates by score.
61struct Place {
62    entity: usize,
63    profession: Option<usize>,
64    entrance: i32,
65    left: i32,
66    total: i32,
67    candidates: BTreeSet<(i32, usize)>,
68}
69
70impl Place {
71    /// The queue priority: the best candidate's score times the share still to fill.
72    fn priority(&self, score: i32) -> i32 {
73        (score as f32 * (1.0 - (self.total - self.left) as f32 / self.total as f32)) as i32
74    }
75}
76
77impl Sim<'_, '_> {
78    /// Map cell `+0x18`: the region (port 0 is none, the original's −1).
79    fn region(&self, cell: i32) -> i32 {
80        match self.regions.get(cell as usize) {
81            Some(&r) if r != 0 => r as i32,
82            _ => -1,
83        }
84    }
85
86    fn region_key(&self, a: i32, b: i32) -> (i32, i32) {
87        let (ra, rb) = (self.region(a), self.region(b));
88        (ra.max(rb), ra.min(rb))
89    }
90
91    /// `(int)sqrtf(dy² + dx²)` in cells.
92    fn straight(&self, a: i32, b: i32) -> i32 {
93        let n = self.size as i32;
94        let (dy, dx) = (a / n - b / n, a % n - b % n);
95        ((dy * dy + dx * dx) as f32).sqrt() as i32
96    }
97
98    /// StockManager::Update's part: the clock, then a step, or a new start when idle.
99    pub fn update_commute(&mut self, job: &mut Commute, citizens: &[Citizen], dt: i64) {
100        job.timer += dt;
101        let period = ticks(PERIOD_SECONDS);
102        if job.timer < period {
103            return;
104        }
105        if !self.commute_step(job, citizens) {
106            self.commute_start(job);
107        } else if job.index < 0 && period < job.timer {
108            loop {
109                job.timer -= period;
110                if job.timer <= period {
111                    break;
112                }
113            }
114        }
115    }
116
117    /// `FUN_1400dc590` (only while idle).
118    fn commute_start(&mut self, job: &mut Commute) {
119        if 0 <= job.index {
120            return;
121        }
122        let residences = self.workforce.residences.clone();
123        let homes: Vec<i32> = residences.iter().map(|&r| self.entrance_cell(r)).collect();
124        job.ranks.clear();
125        job.pairs.clear();
126        for p in 0..self.professions.list.len() {
127            let places = self.workforce.registry.get(&p).cloned().unwrap_or_default();
128            for w in places {
129                let entrance = self.entrance_cell(w);
130                if self.region(entrance) < 0 {
131                    continue;
132                }
133                for &home in &homes {
134                    if self.region(home) < 0 {
135                        continue;
136                    }
137                    let key = self.region_key(entrance, home);
138                    if job.ranks.contains_key(&key) {
139                        continue;
140                    }
141                    job.pairs.push((entrance.max(home), entrance.min(home)));
142                    job.ranks.insert(key, 0);
143                }
144            }
145        }
146        job.index = job.pairs.len() as i32 - 1;
147    }
148
149    /// `FUN_1400dc3d0`: false while idle; with the last pair ranked, the homes and the jobs.
150    fn commute_step(&mut self, job: &mut Commute, citizens: &[Citizen]) -> bool {
151        if job.index < 0 {
152            return false;
153        }
154        for _ in 0..job.index.min(STEP - 1) + 1 {
155            let (a, b) = job.pairs[job.index as usize];
156            let mut rank = self.path_rank(a, b, false);
157            if (rank as u32) < 2 {
158                rank = self.straight(a, b);
159            }
160            let key = self.region_key(a, b);
161            job.ranks.insert(key, rank);
162            job.index -= 1;
163        }
164        if job.index < 0 {
165            self.commute_homes(citizens);
166            self.commute_jobs(job);
167            job.pairs.clear();
168        }
169        true
170    }
171
172    /// `FUN_1400dc130`: the commute of `family`'s working adults (no laborers, students or
173    /// unreachable workplaces) from `home`'s use point: the sum of the path ranks
174    /// (`FUN_140008830`).
175    fn family_cost(&mut self, family: usize, home: usize) -> i32 {
176        let from = self.point_cell(home);
177        let laborer = self.professions.laborer;
178        let members = self
179            .homes
180            .get(&family)
181            .map(|h| h.families[0].clone())
182            .unwrap_or_default();
183        let mut cost = 0i32;
184        for c in members {
185            let s = &self.states[c];
186            if !self.adult[c] || s.profession == laborer {
187                continue;
188            }
189            let Some(wp) = s.workplace else {
190                continue;
191            };
192            let to = self.entrance_cell(wp);
193            if self.reachable(from, to) {
194                cost = cost.wrapping_add(self.rank_or_distance(from, to));
195            }
196        }
197        cost
198    }
199
200    /// `FUN_1400db770`.
201    fn commute_homes(&mut self, citizens: &[Citizen]) {
202        let mut empty = BTreeSet::new();
203        // Longest commute first; equal ones by residence.
204        let mut occupied = BTreeSet::new();
205        for r in self.workforce.residences.clone() {
206            let Some(h) = self.homes.get(&r) else {
207                continue;
208            };
209            if 2 <= h.families.len() {
210                continue;
211            }
212            if h.families[0].is_empty() {
213                empty.insert(r);
214            } else {
215                let cost = self.family_cost(r, r);
216                occupied.insert((Reverse(cost), r));
217            }
218        }
219        if empty.is_empty() {
220            return;
221        }
222        for (Reverse(cost), from) in occupied {
223            let mut best = (cost, None);
224            for &to in &empty {
225                let c = self.family_cost(from, to);
226                if c < best.0 {
227                    best = (c, Some(to));
228                }
229            }
230            let Some(to) = best.1 else {
231                continue;
232            };
233            // `FUN_14011b8a0` for each member, over the live list.
234            let mut i = 0;
235            while let Some(&c) = self.homes.get(&from).and_then(|h| h.families[0].get(i)) {
236                self.move_home(citizens, c, to, 0);
237                i += 1;
238            }
239            self.move_goods(from, to);
240            empty.remove(&to);
241            if empty.is_empty() {
242                break;
243            }
244        }
245    }
246
247    /// The family's goods follow it, each item whole where it fits (`FUN_140132250`,
248    /// `FUN_1401324d0`), then leave the old home (`FUN_140132620`).
249    fn move_goods(&mut self, from: usize, to: usize) {
250        let (Some(a), Some(b)) = (
251            self.homes.get(&from).map(|h| h.storage),
252            self.homes.get(&to).map(|h| h.storage),
253        ) else {
254            return;
255        };
256        let items: Vec<(usize, i32)> = self.storages[a]
257            .items
258            .iter()
259            .map(|i| (i.material, i.count - i.reserved))
260            .collect();
261        let mut moved = Vec::new();
262        for (m, n) in items {
263            if self.storages[b].fits(self.materials, m, n) {
264                self.storages[b].add(m, n);
265                moved.push((m, n));
266            }
267        }
268        for (m, n) in moved {
269            self.storages[a].remove(m, n);
270        }
271    }
272
273    /// `FUN_14011b8a0`: citizen `c` (and its dependents) to `home`'s family `slot`.
274    fn move_home(&mut self, citizens: &[Citizen], c: usize, home: usize, slot: i32) {
275        let s = &self.states[c];
276        if s.home == Some(home) && s.home_slot == slot {
277            return;
278        }
279        if let Some(old) = s.home {
280            let old_slot = s.home_slot.max(0) as usize;
281            if let Some(h) = self.homes.get_mut(&old) {
282                if let Some(f) = h.families.get_mut(old_slot) {
283                    f.retain(|&x| x != c);
284                }
285                if h.occupants() == 0 {
286                    crate::people::status::set_icon(&mut h.status, 6, false, false);
287                    crate::people::status::set_icon(&mut h.status, 7, false, false);
288                }
289            }
290        }
291        self.states[c].home = Some(home);
292        self.states[c].home_slot = slot;
293        if let Some(f) = self
294            .homes
295            .get_mut(&home)
296            .and_then(|h| h.families.get_mut(slot.max(0) as usize))
297        {
298            f.push(c);
299            f.sort_by(|&a, &b| crate::people::life::family_order(citizens, a, b));
300        }
301        for d in self.states[c].dependents.clone() {
302            self.move_home(citizens, d, home, slot);
303        }
304    }
305
306    /// `FUN_1400db530`: the commute from `home` to a workplace entrance: far without a home or
307    /// a way; else the region pair's rank. A pair not yet ranked is ranked now (`FUN_140008830`)
308    /// but counts 0 this time.
309    fn job_score(&mut self, ranks: &mut BTreeMap<(i32, i32), i32>, home: i32, to: i32) -> i32 {
310        if home < 0 || !self.reachable(home, to) {
311            return FAR;
312        }
313        if self.region(home) < 0 || self.region(to) < 0 {
314            let d = self.straight(home, to);
315            return if d < 0 { FAR } else { d };
316        }
317        let key = self.region_key(home, to);
318        let score = match ranks.get(&key) {
319            Some(&r) => r,
320            None => {
321                let mut r = self.rank_or_distance(home, to);
322                if (r as u32) < 2 {
323                    r = self.straight(home, to);
324                }
325                ranks.insert(key, r);
326                0
327            }
328        };
329        if score < 0 {
330            FAR
331        } else {
332            score
333        }
334    }
335
336    /// A citizen's home use point, −1 without a home.
337    fn home_cell(&self, c: usize) -> i32 {
338        self.states[c].home.map_or(-1, |h| self.point_cell(h))
339    }
340
341    /// WorkPlaceComponent `+0x48`.
342    fn place_profession(&self, wp: usize) -> Option<usize> {
343        self.workforce
344            .workplaces
345            .get(&wp)
346            .and_then(|w| w.profession)
347    }
348
349    /// `FUN_1401173b0` and the profession, when either differs.
350    fn assign(
351        &mut self,
352        workers: &mut [crate::work::professions::Worker],
353        c: usize,
354        profession: usize,
355        wp: Option<usize>,
356    ) {
357        if workers[c].profession != profession || workers[c].workplace != wp {
358            self.workforce.set_workplace(workers, c, wp);
359            workers[c].profession = profession;
360        }
361    }
362
363    /// `FUN_1400d9880`.
364    fn commute_jobs(&mut self, job: &mut Commute) {
365        let laborer = self.professions.laborer;
366        let mut workers = self.workers();
367        let n = workers.len();
368        // A worker whose workplace takes another profession becomes a laborer.
369        for c in 0..n {
370            let Some(wp) = workers[c].workplace.filter(|_| workers[c].adult) else {
371                continue;
372            };
373            if self.place_profession(wp) != Some(workers[c].profession) {
374                self.assign(&mut workers, c, laborer, None);
375            }
376        }
377        // The workplaces with workers, by profession then registry order.
378        let mut places = Vec::new();
379        for p in 0..self.professions.list.len() {
380            for w in self.workforce.registry.get(&p).cloned().unwrap_or_default() {
381                let Some(place) = self.workforce.workplaces.get(&w) else {
382                    continue;
383                };
384                let count = place.workers.len() as i32;
385                let profession = place.profession;
386                if count == 0 {
387                    continue;
388                }
389                let entrance = self.entrance_cell(w);
390                places.push(Place {
391                    entity: w,
392                    profession,
393                    entrance,
394                    left: count,
395                    total: count,
396                    candidates: BTreeSet::new(),
397                });
398            }
399        }
400        // Every working adult is let go and scored against each workplace; the professions
401        // are counted.
402        let mut free = BTreeSet::new();
403        let mut quota: BTreeMap<usize, i32> = BTreeMap::new();
404        for c in 0..n {
405            if !workers[c].adult {
406                continue;
407            }
408            free.insert(c);
409            if let Some(wp) = workers[c].workplace {
410                if self.place_profession(wp) != Some(workers[c].profession) {
411                    self.assign(&mut workers, c, laborer, None);
412                }
413            }
414            *quota.entry(workers[c].profession).or_insert(0) += 1;
415            self.assign(&mut workers, c, laborer, None);
416            let home = self.home_cell(c);
417            for place in places.iter_mut() {
418                let score = self.job_score(&mut job.ranks, home, place.entrance);
419                place.candidates.insert((score, c));
420            }
421        }
422        // The queue: the highest priority first, then the first record.
423        let mut queue = BTreeSet::new();
424        for (i, place) in places.iter().enumerate() {
425            if let Some(&(score, _)) = place.candidates.first() {
426                queue.insert((Reverse(place.priority(score)), i));
427            }
428        }
429        while let Some(&(key, i)) = queue.first() {
430            queue.remove(&(key, i));
431            let Some(&(_, c)) = places[i].candidates.first() else {
432                continue;
433            };
434            let (entity, profession) = (places[i].entity, places[i].profession);
435            if let Some(p) = profession {
436                self.assign(&mut workers, c, p, Some(entity));
437            }
438            let key = profession.unwrap_or(usize::MAX);
439            if let Some(q) = quota.get_mut(&key) {
440                *q -= 1;
441                if *q == 0 {
442                    quota.remove(&key);
443                }
444            }
445            free.remove(&c);
446            let home = self.home_cell(c);
447            places[i].left -= 1;
448            for j in 0..places.len() {
449                if places[j].left == 0 {
450                    continue;
451                }
452                let score = self.job_score(&mut job.ranks, home, places[j].entrance);
453                let entry = if places[j].candidates.contains(&(score, c)) {
454                    Some((score, c))
455                } else {
456                    places[j].candidates.iter().copied().find(|e| e.1 == c)
457                };
458                let Some(entry) = entry else {
459                    continue;
460                };
461                if places[j].candidates.first() == Some(&entry) {
462                    let old = places[j].priority(entry.0);
463                    queue.remove(&(Reverse(old), j));
464                    places[j].candidates.remove(&entry);
465                    if let Some(&(best, _)) = places[j].candidates.first() {
466                        queue.insert((Reverse(places[j].priority(best)), j));
467                    }
468                } else {
469                    places[j].candidates.remove(&entry);
470                }
471            }
472        }
473        // The professions left over, the first citizens to the first professions.
474        while let Some((&p, _)) = quota.first_key_value() {
475            let Some(c) = free.pop_first() else {
476                break;
477            };
478            self.assign(&mut workers, c, p, None);
479            let q = quota.get_mut(&p).unwrap();
480            *q -= 1;
481            if *q == 0 {
482                quota.remove(&p);
483            }
484        }
485        self.set_workers(&workers);
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492
493    #[test]
494    fn priority_weights_the_share_left() {
495        let mut p = Place {
496            entity: 0,
497            profession: None,
498            entrance: 0,
499            left: 4,
500            total: 4,
501            candidates: BTreeSet::new(),
502        };
503        assert_eq!(p.priority(100), 100);
504        p.left = 1;
505        assert_eq!(p.priority(100), 25);
506        assert_eq!(p.priority(FAR), FAR / 4);
507    }
508}