Skip to main content

banished_path/
sectors.rs

1//! The sector graph, and the "path rank" distance that storage, job and resource searches use.
2//!
3//! Running a full A* search for every candidate storage or job would be slow. So the game cuts
4//! the map into 8 × 8-cell sectors. The walkable cells of each sector split into connected
5//! areas, and each area becomes a node at the mean of its cells. Nodes link to the nodes just
6//! outside their sector. A distance is an A* search over the nodes, with Euclidean costs
7//! truncated to integers, and it is cached for 120 seconds of game time.
8//!
9//! When the map changes, [`SectorGraph::mark`] marks the sector, and the graph rebuilds the
10//! marked sectors before the next search.
11
12// Original: FUN_1400a4ce0 (path rank); sectors FUN_1400adb40, areas FUN_1400ad3f0 /
13// FUN_1400acfa0, nodes FUN_1400ace20, cell node (cell +0x18) FUN_1400acb40, links
14// FUN_1400acc20 / FUN_1400ac950, marking FUN_1400ae160, lazy rebuild FUN_1400adf60, node A*
15// FUN_14001e630, summed by FUN_1400ae070, cache FUN_1400ab000 / FUN_1400ab1e0.
16// See docs/CITIZEN-AI.md.
17
18use std::cmp::Reverse;
19use std::collections::{BTreeMap, BTreeSet, VecDeque};
20
21use crate::pathfind::WALKABLE;
22use banished_terrain::CellType;
23
24const SECTOR: i32 = 8;
25/// `DAT_140178910`: cached distances live 120 s.
26const CACHE_LIFETIME: i64 = 120_000_000;
27/// `FUN_14001d600`: cache ring size.
28const CACHE_SIZE: usize = 0x20000;
29/// `FUN_14001e560`: longest node path.
30const MAX_NODES: usize = 0x2000;
31
32fn walk(t: CellType) -> bool {
33    (1u32 << (t.0 & 0x1f)) & WALKABLE != 0
34}
35
36/// `(int) sqrtf((float) (dx² + dy²))`.
37fn length(a: (i32, i32), b: (i32, i32)) -> i32 {
38    let (dx, dy) = (a.0 - b.0, a.1 - b.1);
39    ((dx * dx + dy * dy) as f32).sqrt() as i32
40}
41
42#[derive(Debug, Clone, Default)]
43struct Node {
44    /// `+0`, `+4`: position (mean cell); `+8`: sector.
45    pos: (i32, i32),
46    #[allow(dead_code)]
47    sector: i32,
48    /// `+0x10`: linked nodes; `+0x20`: cells just outside the sector next to this area.
49    links: Vec<i32>,
50    border: Vec<i32>,
51}
52
53#[derive(Debug, Clone, Default)]
54struct Sector {
55    x: i32,
56    y: i32,
57    nodes: Vec<i32>,
58    node_dirty: bool,
59    link_dirty: bool,
60}
61
62#[derive(Clone, Copy, Default)]
63struct Search {
64    parent: i32,
65    open: i32,
66    closed: i32,
67    g: i32,
68    key: (i32, i32),
69}
70
71/// `FUN_1400ab000`'s cache: a FIFO ring keyed by node pair.
72#[derive(Debug, Clone)]
73struct Cache {
74    time: i64,
75    head: usize,
76    tail: usize,
77    /// `(key, value, time)` or `None` for a cleared slot.
78    ring: Vec<Option<((i32, i32), i32, i64)>>,
79    index: BTreeMap<(i32, i32), usize>,
80}
81
82/// The sector graph: the map cut into 8 × 8-cell sectors, the walkable areas of each sector as
83/// nodes, and links between neighbouring areas. It gives the "path rank" distance that storage,
84/// job and resource searches use, and caches it.
85pub struct SectorGraph {
86    n: i32,
87    w: i32,
88    h: i32,
89    nodes: Vec<Node>,
90    sectors: Vec<Sector>,
91    dirty_nodes: Vec<usize>,
92    dirty_links: Vec<usize>,
93    free: Vec<i32>,
94    // +0x18
95    /// Cell: node of each cell (-1 none).
96    pub cell_node: Vec<i32>,
97    search: Vec<Search>,
98    search_id: i32,
99    open: BTreeSet<((i32, Reverse<i32>), i32)>,
100    counter: i32,
101    cache: Cache,
102}
103
104impl SectorGraph {
105    /// The graph of a map: every sector is built, then linked, in index order.
106    ///
107    /// Original: `FUN_1400adb40`, then `FUN_1400adf60`.
108    pub fn new(size: usize, types: &[CellType]) -> SectorGraph {
109        let n = size as i32;
110        let (w, h) = (n / SECTOR, n / SECTOR);
111        let mut sectors = Vec::new();
112        for y in 0..h {
113            for x in 0..w {
114                sectors.push(Sector {
115                    x,
116                    y,
117                    nodes: Vec::new(),
118                    node_dirty: true,
119                    link_dirty: true,
120                });
121            }
122        }
123        let count = sectors.len();
124        let mut g = SectorGraph {
125            n,
126            w,
127            h,
128            nodes: Vec::new(),
129            sectors,
130            dirty_nodes: (0..count).collect(),
131            dirty_links: (0..count).collect(),
132            free: Vec::new(),
133            cell_node: vec![-1; size * size],
134            search: Vec::new(),
135            search_id: 1,
136            open: BTreeSet::new(),
137            counter: 0,
138            cache: Cache {
139                time: 0,
140                head: 0,
141                tail: 0,
142                ring: vec![None; CACHE_SIZE],
143                index: BTreeMap::new(),
144            },
145        };
146        g.rebuild(types);
147        g
148    }
149
150    /// A cell changed; rebuild its sector and relink the 3 × 3 around it.
151    ///
152    /// Original: `FUN_1400ae160`.
153    pub fn mark(&mut self, x: i32, y: i32) {
154        let s = ((y / SECTOR) * self.w + x / SECTOR) as usize;
155        if self.sectors[s].node_dirty {
156            return;
157        }
158        self.sectors[s].node_dirty = true;
159        self.dirty_nodes.push(s);
160        let (sx, sy) = (self.sectors[s].x, self.sectors[s].y);
161        for yy in sy - 1..=sy + 1 {
162            if yy < 0 || yy >= self.h {
163                continue;
164            }
165            for xx in sx - 1..=sx + 1 {
166                if xx < 0 || xx >= self.w {
167                    continue;
168                }
169                let t = (yy * self.w + xx) as usize;
170                if !self.sectors[t].link_dirty {
171                    self.sectors[t].link_dirty = true;
172                    self.dirty_links.push(t);
173                }
174            }
175        }
176    }
177
178    /// Rebuild and relink the sectors that [`mark`](Self::mark) marked.
179    ///
180    /// Original: `FUN_1400adf60`.
181    pub fn rebuild(&mut self, types: &[CellType]) {
182        if self.dirty_nodes.is_empty() && self.dirty_links.is_empty() {
183            return;
184        }
185        for s in std::mem::take(&mut self.dirty_nodes) {
186            self.build_sector(s, types);
187            self.sectors[s].node_dirty = false;
188        }
189        for s in std::mem::take(&mut self.dirty_links) {
190            self.link_sector(s);
191            self.sectors[s].link_dirty = false;
192        }
193        self.search = vec![Search::default(); self.nodes.len()];
194    }
195
196    /// `FUN_1400ad570`.
197    fn build_sector(&mut self, s: usize, types: &[CellType]) {
198        // `FUN_1400ad6d0`: free the sector's nodes.
199        for id in std::mem::take(&mut self.sectors[s].nodes) {
200            let node = &mut self.nodes[id as usize];
201            *node = Node {
202                pos: (-1, -1),
203                sector: -1,
204                ..Default::default()
205            };
206            self.free.push(id);
207        }
208        let (x0, y0) = (self.sectors[s].x * SECTOR, self.sectors[s].y * SECTOR);
209        let labels = self.label(x0, y0, types);
210        let count = labels.iter().copied().max().unwrap_or(0);
211        for label in 1..=count {
212            let id = self.create_node(s, x0, y0, &labels, label);
213            self.sectors[s].nodes.push(id);
214        }
215        // `FUN_1400acb40`.
216        for ly in 0..SECTOR {
217            for lx in 0..SECTOR {
218                let label = labels[(ly * SECTOR + lx) as usize];
219                let cell = ((y0 + ly) * self.n + x0 + lx) as usize;
220                self.cell_node[cell] = if label >= 1 {
221                    self.sectors[s].nodes[label as usize - 1]
222                } else {
223                    -1
224                };
225            }
226        }
227    }
228
229    /// `FUN_1400ad3f0` / `FUN_1400acfa0`: label the sector's connected walkable areas (rows
230    /// outer) with an 8-connected FIFO scanline fill.
231    fn label(&self, x0: i32, y0: i32, types: &[CellType]) -> Vec<i32> {
232        let mut labels = vec![0i32; (SECTOR * SECTOR) as usize];
233        let at = |lx: i32, ly: i32| types[((y0 + ly) * self.n + x0 + lx) as usize];
234        let mut next = 1;
235        for ly in 0..SECTOR {
236            for lx in 0..SECTOR {
237                if !walk(at(lx, ly)) || labels[(ly * SECTOR + lx) as usize] != 0 {
238                    continue;
239                }
240                let mut queue = VecDeque::from([(lx, ly)]);
241                while let Some((qx, qy)) = queue.pop_front() {
242                    if labels[(qy * SECTOR + qx) as usize] != 0 || !walk(at(qx, qy)) {
243                        continue;
244                    }
245                    let mut left = qx;
246                    while left - 1 >= 0 && walk(at(left - 1, qy)) {
247                        left -= 1;
248                    }
249                    let mut right = qx;
250                    while right + 1 <= SECTOR - 1 && walk(at(right + 1, qy)) {
251                        right += 1;
252                    }
253                    for u in left..=right {
254                        labels[(qy * SECTOR + u) as usize] = next;
255                    }
256                    if left - 1 >= 0 {
257                        left -= 1;
258                    }
259                    if right + 1 <= SECTOR - 1 {
260                        right += 1;
261                    }
262                    for (ok, row) in [(qy > 0, qy - 1), (qy < SECTOR - 1, qy + 1)] {
263                        if !ok {
264                            continue;
265                        }
266                        for u in left..=right {
267                            if labels[(row * SECTOR + u) as usize] == 0 && walk(at(u, row)) {
268                                queue.push_back((u, row));
269                            }
270                        }
271                    }
272                }
273                next += 1;
274            }
275        }
276        labels
277    }
278
279    /// `FUN_1400ace20`: a node for one labelled area.
280    fn create_node(&mut self, s: usize, x0: i32, y0: i32, labels: &[i32], label: i32) -> i32 {
281        // `FUN_1400ad820`: reuse the most recently freed id.
282        let id = match self.free.pop() {
283            Some(id) => id,
284            None => {
285                self.nodes.push(Node::default());
286                self.nodes.len() as i32 - 1
287            }
288        };
289        let (mut sx, mut sy, mut count) = (0, 0, 0);
290        let mut border = Vec::new();
291        for ly in 0..SECTOR {
292            for lx in 0..SECTOR {
293                if labels[(ly * SECTOR + lx) as usize] != label {
294                    continue;
295                }
296                let (gx, gy) = (lx + x0, ly + y0);
297                count += 1;
298                sx += gx;
299                sy += gy;
300                if lx == 0 || ly == 0 || lx == SECTOR - 1 || ly == SECTOR - 1 {
301                    // `FUN_1400acc20`: neighbours outside the sector.
302                    for dy in -1..=1 {
303                        for dx in -1..=1 {
304                            let (cx, cy) = (gx + dx, gy + dy);
305                            if cx < 0 || cy < 0 || cx >= self.n || cy >= self.n {
306                                continue;
307                            }
308                            let outside =
309                                cx < x0 || cy < y0 || cx > x0 + SECTOR - 1 || cy > y0 + SECTOR - 1;
310                            let cell = cy * self.n + cx;
311                            if outside && !border.contains(&cell) {
312                                border.push(cell);
313                            }
314                        }
315                    }
316                }
317            }
318        }
319        self.nodes[id as usize] = Node {
320            pos: (sx / count, sy / count),
321            sector: s as i32,
322            links: Vec::new(),
323            border,
324        };
325        id
326    }
327
328    /// `FUN_1400ac950`.
329    fn link_sector(&mut self, s: usize) {
330        for &id in &self.sectors[s].nodes.clone() {
331            let border = self.nodes[id as usize].border.clone();
332            let mut links = Vec::new();
333            for cell in border {
334                let other = self.cell_node[cell as usize];
335                if other >= 0 && !links.contains(&other) {
336                    links.push(other);
337                }
338            }
339            self.nodes[id as usize].links = links;
340        }
341    }
342
343    /// The node of `cell`, else the first node found on rings 1–3 around it.
344    ///
345    /// Original: `FUN_1400a2570`.
346    pub fn node_near(&self, cell: i32) -> i32 {
347        let n = self.n;
348        let id = self.cell_node[cell as usize];
349        if id >= 0 {
350            return id;
351        }
352        let (x, y) = (cell % n, cell / n);
353        let node = |cx: i32, cy: i32| -> Option<i32> {
354            (cx >= 0 && cy >= 0 && cx < n && cy < n)
355                .then(|| self.cell_node[(cy * n + cx) as usize])
356                .filter(|&v| v >= 0)
357        };
358        for r in 1..=3 {
359            for t in x - r..=x + r {
360                let row = y + (t - x);
361                if row >= 0 && row < n {
362                    if let Some(v) = node(x - r, row).or_else(|| node(x + r, row)) {
363                        return v;
364                    }
365                }
366                if t >= 0 && t < n {
367                    if let Some(v) = node(t, y - r).or_else(|| node(t, y + r)) {
368                        return v;
369                    }
370                }
371            }
372        }
373        -1
374    }
375
376    fn push(&mut self, node: i32, parent: i32, g: i32, f: i32) {
377        let s = self.search[node as usize];
378        if s.open == self.search_id {
379            self.open.remove(&((s.key.0, Reverse(s.key.1)), node));
380        }
381        let counter = self.counter;
382        self.counter += 1;
383        let s = &mut self.search[node as usize];
384        s.closed = s.closed.wrapping_sub(1);
385        s.parent = parent;
386        s.open = self.search_id;
387        s.g = g;
388        s.key = (f, counter);
389        self.open.insert(((f, Reverse(counter)), node));
390    }
391
392    /// `FUN_14001e560` / `FUN_14001e630`: node path from `a` to `b`.
393    fn node_path(&mut self, a: i32, b: i32) -> Option<Vec<i32>> {
394        self.search_id = self.search_id.wrapping_add(1);
395        if self.search.len() < self.nodes.len() {
396            self.search.resize(self.nodes.len(), Search::default());
397        }
398        self.open.clear();
399        self.counter = 0;
400        let h = length(self.nodes[b as usize].pos, self.nodes[a as usize].pos);
401        self.push(a, -1, 0, h);
402        let mut found = false;
403        while let Some(&first) = self.open.first() {
404            let cur = first.1;
405            if cur == b {
406                found = true;
407                break;
408            }
409            self.open.remove(&first);
410            let id = self.search_id;
411            let s = &mut self.search[cur as usize];
412            s.open = s.open.wrapping_sub(1);
413            s.closed = id;
414            let g_cur = s.g;
415            let pos = self.nodes[cur as usize].pos;
416            for k in 0..self.nodes[cur as usize].links.len() {
417                let nb = self.nodes[cur as usize].links[k];
418                if nb < 0 {
419                    continue;
420                }
421                let npos = self.nodes[nb as usize].pos;
422                let g = length(npos, pos) + g_cur;
423                let s = &mut self.search[nb as usize];
424                if s.closed == id {
425                    if s.g <= g {
426                        continue;
427                    }
428                    s.closed = s.closed.wrapping_sub(1);
429                }
430                if s.open != id || g < s.g {
431                    let f = length(self.nodes[b as usize].pos, npos) + g;
432                    self.push(nb, cur, g, f);
433                }
434            }
435        }
436        if !found {
437            return None;
438        }
439        let mut path = Vec::new();
440        let mut c = b;
441        while c != -1 {
442            path.push(c);
443            c = self.search[c as usize].parent;
444        }
445        path.reverse();
446        path.truncate(MAX_NODES);
447        Some(path)
448    }
449
450    /// `FUN_1400ae070`: summed hop lengths of the node path (0 for fewer than 2 nodes).
451    fn node_distance(&mut self, a: i32, b: i32, types: &[CellType]) -> i32 {
452        self.rebuild(types);
453        let Some(path) = self.node_path(a, b) else {
454            return 0;
455        };
456        path.windows(2)
457            .map(|w| length(self.nodes[w[1] as usize].pos, self.nodes[w[0] as usize].pos))
458            .sum()
459    }
460
461    /// The path-rank distance from cell `a` to cell `b`, or -1 when `b` cannot be reached.
462    /// `reachable` is the caller's region test (both cells in the same walkable region); `store`
463    /// keeps a computed distance in the cache.
464    ///
465    /// Original: `FUN_1400a4ce0`.
466    pub fn distance(
467        &mut self,
468        a: i32,
469        b: i32,
470        reachable: bool,
471        store: bool,
472        types: &[CellType],
473    ) -> i32 {
474        if !reachable {
475            return -1;
476        }
477        let na = self.node_near(a);
478        let nb = self.node_near(b);
479        if na < 0 || nb < 0 {
480            return -1;
481        }
482        if na == nb {
483            return 1;
484        }
485        let key = (na.max(nb), na.min(nb));
486        if let Some(&slot) = self.cache.index.get(&key) {
487            if let Some((_, v, _)) = self.cache.ring[slot] {
488                return v;
489            }
490        }
491        let d = self.node_distance(na, nb, types);
492        if store {
493            self.cache_store(key, d);
494        }
495        d
496    }
497
498    /// `FUN_1400ab000`.
499    fn cache_store(&mut self, key: (i32, i32), value: i32) {
500        if let Some(&slot) = self.cache.index.get(&key) {
501            if let Some(e) = &mut self.cache.ring[slot] {
502                e.1 = value;
503                return;
504            }
505        }
506        let slot = self.cache.head;
507        self.cache.head = (slot + 1) % CACHE_SIZE;
508        if self.cache.head == self.cache.tail {
509            self.evict_oldest();
510        }
511        self.cache.ring[slot] = Some((key, value, self.cache.time));
512        self.cache.index.insert(key, slot);
513    }
514
515    fn evict_oldest(&mut self) {
516        let t = self.cache.tail;
517        if let Some((k, _, _)) = self.cache.ring[t].take() {
518            if self.cache.index.get(&k) == Some(&t) {
519                self.cache.index.remove(&k);
520            }
521        }
522        self.cache.tail = (t + 1) % CACHE_SIZE;
523    }
524
525    /// Age the cache by `dt` ticks, and drop the entries older than 120 seconds. The game calls it
526    /// once per simulation iteration.
527    ///
528    /// Original: `FUN_1400ab1e0`.
529    pub fn age_cache(&mut self, dt: i64) {
530        self.cache.time += dt;
531        loop {
532            let t = self.cache.tail;
533            let entry_time = self.cache.ring[t].map_or(0, |e| e.2);
534            if !(CACHE_LIFETIME < self.cache.time - entry_time) {
535                return;
536            }
537            if t == self.cache.head {
538                return;
539            }
540            self.evict_oldest();
541        }
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548
549    #[test]
550    fn open_map_distances() {
551        let n = 32;
552        let types = vec![CellType(2); n * n];
553        let mut g = SectorGraph::new(n, &types);
554        // One node per sector at its centre.
555        assert_eq!(g.cell_node[0], g.cell_node[7 * n + 7]);
556        assert_ne!(g.cell_node[0], g.cell_node[8]);
557        assert_eq!(g.distance(0, 1, true, false, &types), 1);
558        let d = g.distance(0, (n * n - 1) as i32, true, true, &types);
559        assert!(d > 0);
560        assert_eq!(g.distance(0, (n * n - 1) as i32, true, false, &types), d);
561    }
562}