Skip to main content

banished_game/town/
clearing.rs

1//! Clearing an area (`ClearArea`): removing trees, rocks and other resources before building.
2
3use super::*;
4
5impl Sim<'_, '_> {
6    /// The entity on a cell as the job code reads it: occupant (`+0x00`), else object (`+0x08`).
7    pub(super) fn entity_on(&self, cell: i32) -> Option<usize> {
8        self.cells.entity_at(cell as usize)
9    }
10
11    /// `FUN_14011ce30`'s registration: count the footprint's objects into a category mask
12    /// (bit 0, plus logs/stone/iron from their raw materials), keep `+0x48` = `FUN_14011d800`
13    /// and register a ClearArea work.
14    pub(super) fn register_clear(&mut self, jobs: &mut Jobs, e: usize) {
15        let site = &self.sites[&e];
16        let Some(clear) = self.site_templates[site.template].clear.clone() else {
17            return;
18        };
19        let n = self.size as i32;
20        let (x, y) = self.placed[e].cell;
21        let (w, h) = self.placed[e].footprint;
22        let mut mask = 1u32;
23        for cy in y..y + h {
24            for cx in x..x + w {
25                let Some(o) = self.cells.objects[(cy * n + cx) as usize] else {
26                    continue;
27                };
28                let t = &self.templates[self.placed[o].template];
29                if t.natural.is_none() {
30                    continue;
31                }
32                if let Some(m) = t.pile.and_then(|p| self.templates[p].pile_material) {
33                    mask |= clear_mask(self.materials[m].flags);
34                }
35            }
36        }
37        let total = self.clear_remaining(e);
38        if let Some(site) = self.sites.get_mut(&e) {
39            site.clear_total = total;
40        }
41        let work = Work::new(
42            e,
43            self.rect_cell(e),
44            self.professions.laborer,
45            WorkKind::Clear(ClearWork {
46                filter: clear.reference.clone(),
47                clear_resources: clear.clear_resources,
48                clear_cells: clear.clear_cells,
49                clear_piles: clear.clear_piles,
50                ..Default::default()
51            }),
52        );
53        self.set_work(jobs, e, work, mask, clear.keep_priority);
54    }
55
56    /// WorkComponent `+0x30` (most workers): the WorkPlaceComponent's, else `FUN_140133a90`
57    /// since nothing else sets it.
58    pub(crate) fn work_max(&self, e: usize) -> i32 {
59        match self.workforce.workplaces.get(&e) {
60            Some(w) => w.max(),
61            None => self.sites.get(&e).map_or(0, |s| {
62                self.site_templates[s.template].default_max_workers(self.placed[e].footprint)
63            }),
64        }
65    }
66
67    /// The footprint cells holding another object.
68    ///
69    /// Original: `FUN_14011d800`.
70    pub fn clear_remaining(&self, e: usize) -> i32 {
71        clear_remaining(&self.cells.objects, self.size as i32, &self.placed[e], e)
72    }
73
74    /// `FUN_1400eafe0`: drop resource claims whose citizen lost the task; a lost cell claim
75    /// returns before anything is dropped.
76    pub(super) fn clear_refresh(&mut self, work: &mut Work) {
77        let id = work.id;
78        let WorkKind::Clear(cl) = &mut work.kind else {
79            return;
80        };
81        let lost: Vec<usize> = cl
82            .resources
83            .iter()
84            .filter(|(_, &c)| self.characters[c].stack.holds_task(id).is_none())
85            .map(|(&r, _)| r)
86            .collect();
87        if cl
88            .cells
89            .values()
90            .any(|&c| self.characters[c].stack.holds_task(id).is_none())
91        {
92            return;
93        }
94        for r in lost {
95            cl.resources.remove(&r);
96        }
97    }
98
99    /// `FUN_1400eb3b0`: ClearArea's slot `+0x20`.
100    pub(super) fn clear_try(&mut self, work: &mut Work, c: usize, time: i64) -> u8 {
101        let e = work.entity;
102        let max = self.work_max(e);
103        let WorkKind::Clear(cl) = &work.kind else {
104            return 0;
105        };
106        if max <= (cl.resources.len() + cl.cells.len()) as i32 {
107            return 1;
108        }
109        if self
110            .workforce
111            .workplaces
112            .get(&e)
113            .is_some_and(|w| !w.enabled)
114        {
115            return 1;
116        }
117        // `FUN_1400a50d0`: the footprint's objects (and, clearing cells, its road cells with
118        // nothing on them), then `FUN_1400a2c90`: squared distance to the citizen, CRT qsort.
119        let n = self.size as i32;
120        let (x, y) = self.placed[e].cell;
121        let (w, h) = self.placed[e].footprint;
122        let from = self.citizen_cell(c);
123        let clear_cells = cl.clear_cells;
124        let mut entries: Vec<(i32, i32, Option<usize>)> = Vec::new();
125        for cy in y..y + h {
126            for cx in x..x + w {
127                let cell = cy * n + cx;
128                let i = cell as usize;
129                let (dy, dx) = (cell / n - from / n, cell % n - from % n);
130                if let Some(o) = self.cells.objects[i] {
131                    entries.push((cell, dy * dy + dx * dx, Some(o)));
132                }
133                if clear_cells {
134                    let t = self.cells.types[i].0;
135                    let road = t == 0
136                        || t == 1
137                        || self.cells.road_kind[i] != crate::work::road::NONE
138                        || self.pending_roads.contains_key(&cell);
139                    if road {
140                        entries.push((cell, dy * dy + dx * dx, None));
141                    }
142                }
143            }
144        }
145        crate::behaviour::ai::crt_qsort_by(&mut entries, |a, b| a.1 - b.1);
146        let (resources, filter, clear_piles) =
147            (cl.clear_resources, cl.filter.clone(), cl.clear_piles);
148        if resources {
149            for &(cell, _, o) in &entries {
150                let Some(o) = o else { continue };
151                let WorkKind::Clear(cl) = &work.kind else {
152                    return 0;
153                };
154                if cl.resources.contains_key(&o) {
155                    continue;
156                }
157                let t = &self.templates[self.placed[o].template];
158                if let Some(f) = &filter {
159                    if !t.reference.eq_ignore_ascii_case(&f.replace('/', "\\")) {
160                        continue;
161                    }
162                }
163                if t.natural.is_none() {
164                    continue;
165                }
166                let Some(nb) = nearest_neighbour(self.size, &self.cells.types, cell, from, 0xe07)
167                else {
168                    continue;
169                };
170                if !self.reachable(nb, from) {
171                    continue;
172                }
173                if !self.patience(work, c, nb, from, time) {
174                    return 0;
175                }
176                if self.claimed.contains(&o) {
177                    continue;
178                }
179                let point = self.ground_at(nb);
180                self.removal_sequence(c, work, o, point, false);
181                self.took_job(c, work);
182                if let WorkKind::Clear(cl) = &mut work.kind {
183                    cl.resources.insert(o, c);
184                }
185                return 2;
186            }
187        }
188        // The road cells (`+0x71`): laid back to ground (`FUN_1400c54a0` with type 2, the
189        // task's parameter the cell).
190        if clear_cells {
191            for &(cell, _, o) in &entries {
192                if o.is_some() {
193                    continue;
194                }
195                let WorkKind::Clear(cl) = &work.kind else {
196                    return 0;
197                };
198                if cl.cells.contains_key(&cell) || !self.reachable(cell, from) {
199                    continue;
200                }
201                let target = self.ground_at(cell);
202                self.road_clear_sequence(c, work, target, cell);
203                self.took_job(c, work);
204                if let WorkKind::Clear(cl) = &mut work.kind {
205                    cl.cells.insert(cell, c);
206                }
207                return 2;
208            }
209        }
210        if clear_piles {
211            // The piles' own Pickup works (`FUN_1400fdaf0` without a second try), in order.
212            self.offers = entries
213                .iter()
214                .filter_map(|&(_, _, o)| o.and_then(|o| self.piles.get(&o)).map(|p| p.work))
215                .filter(|&id| id >= 0)
216                .collect();
217            if !self.offers.is_empty() {
218                return NESTED;
219            }
220        }
221        1
222    }
223
224    /// `FUN_1400c7380`: walk next to a natural resource, work it with its tool, let it fall
225    /// (resources with the animation flag), work it again, remove it and report.
226    pub(crate) fn removal_sequence(
227        &mut self,
228        c: usize,
229        work: &mut Work,
230        resource: usize,
231        point: [f32; 3],
232        auto_pickup: bool,
233    ) {
234        let t = &self.templates[self.placed[resource].template];
235        let Some(nr) = t.natural.clone() else {
236            return;
237        };
238        let facing = crate::behaviour::character::work_facing(self.facing_centre(resource), point);
239        self.clear_citizen(c, Some(work));
240        self.on_empty_think(c);
241        let tool = nr.value as i32;
242        let clip = if (nr.value) < 11 { tool + 9 } else { 8 } as usize;
243        self.push(
244            c,
245            Action::CompleteTask {
246                work: work.id,
247                param: 0,
248            },
249        );
250        self.push(c, Action::ShowTool(-1));
251        self.push(
252            c,
253            Action::RemoveResource {
254                entity: resource,
255                flag: auto_pickup,
256            },
257        );
258        if nr.flag {
259            self.push(c, self.perform(c, facing, clip, seconds(nr.time_b), false));
260            self.push(
261                c,
262                Action::AnimateResource {
263                    entity: resource,
264                    anim: 1,
265                    wait: false,
266                },
267            );
268            self.push(
269                c,
270                Action::AnimateResource {
271                    entity: resource,
272                    anim: 0,
273                    wait: true,
274                },
275            );
276        }
277        self.push(c, self.perform(c, facing, clip, seconds(nr.time_a), true));
278        let walk = match tool {
279            0 | 1 | 4..=7 => 2,
280            2 | 3 => 3,
281            _ => 1,
282        };
283        let m = MoveTo::new(point, walk, 0, WALKABLE, 0, -1.0, self.rng);
284        self.push(c, Action::move_to(m));
285        self.push(c, Action::ShowTool(tool));
286        self.claimed.insert(resource);
287    }
288
289    /// A removal sequence was aborted.
290    ///
291    /// Original: `FUN_1400cff70`.
292    pub fn unclaim(&mut self, resource: usize) {
293        self.claimed.remove(&resource);
294    }
295
296    /// A resource with the animation flag plays clip `anim`.
297    ///
298    /// Original: `FUN_14012b910`.
299    pub fn animate_resource(&mut self, resource: usize, anim: usize) {
300        let p = &mut self.placed[resource];
301        let t = &self.templates[p.template];
302        if t.natural.as_ref().is_some_and(|n| n.flag)
303            && t.animations
304                .get(p.variant)
305                .and_then(Option::as_ref)
306                .is_some_and(|g| g.clips.get(anim).is_some())
307        {
308            p.animation = Some(crate::behaviour::animation::Playback::new(anim, 0));
309        }
310    }
311
312    /// The start of `RemoveNaturalResource`: queue the resource for removal; unless
313    /// another profession should have harvested it, leave a pile of `yield × min(growth, 1)` units
314    /// (the educated yield for educated citizens), turned `RandTol(2) × 90°`. `priority` is the
315    /// priority of a work at the resource's cell.
316    ///
317    /// Original: `FUN_1400cffe0`, `FUN_1400fe220`, `FUN_14012b5c0`.
318    pub fn remove_resource(
319        &mut self,
320        c: usize,
321        resource: usize,
322        educated: bool,
323        priority: Option<i32>,
324        auto_pickup: bool,
325    ) {
326        self.claimed.remove(&resource);
327        if !self.placed[resource].alive {
328            return;
329        }
330        let template = self.placed[resource].template;
331        let Some(nr) = self.templates[template].natural.clone() else {
332            return;
333        };
334        // `+0x2d`: as a crop field set it (else kept), and not kept when a related profession
335        // other than the laborer should harvest it.
336        let mut keep = self.keep_piles.remove(&resource).unwrap_or(true);
337        if let Some(r) = nr.related.as_deref() {
338            let r = r.replace('/', "\\");
339            let related = self
340                .professions
341                .list
342                .iter()
343                .position(|p| p.reference.eq_ignore_ascii_case(&r));
344            if let Some(p) = related {
345                if p != self.professions.laborer && self.states[c].profession != p {
346                    keep = false;
347                }
348            }
349        }
350        // `FUN_1400c8370`: the entity leaves at the end of the iteration (the port removes it
351        // on the next animation pass).
352        let growth = self.placed[resource].growth;
353        let cell = self.placed[resource].cell;
354        {
355            let p = &mut self.placed[resource];
356            p.animation = None;
357            p.removal_requested = true;
358        }
359        *self.models_dirty = true;
360        if !keep {
361            return;
362        }
363        let Some(pile_t) = self.templates[template].pile else {
364            return;
365        };
366        let pt = &self.templates[pile_t];
367        let base = if educated {
368            pt.pile_yield.1
369        } else {
370            pt.pile_yield.0
371        };
372        let amount = (base as f32 * growth.min(1.0)) as i32;
373        if pt.pile_material.is_none() || amount <= 0 {
374            return;
375        }
376        let yaw = self.rng.rand_tol(2) as f32 * 90.0;
377        let pile = self.spawn_pile(pile_t, cell, amount, yaw, priority);
378        // `FUN_14008e3c0`: made, for the citizen's workplace (`+0x248`) when the action's
379        // `+0x30` is set.
380        if let Some(m) = self.templates[pile_t].pile_material {
381            let workplace = self.states[c].workplace;
382            self.count_made(workplace, auto_pickup, m, amount);
383        }
384        // `FUN_140045170` once the pile's work is registered ([`Sim::activate_piles`]).
385        if auto_pickup {
386            if let (Some(wp), Some(p)) = (self.states[c].workplace, self.piles.get_mut(&pile)) {
387                p.auto_pickup = Some(wp);
388            }
389        }
390    }
391
392    /// `FUN_14011d230`: every other entity on the footprint's cells that is not already going
393    /// goes now (`FUN_1400c8370`): its work and its stamp are lifted at once and it is removed
394    /// with the entities queued this iteration. A pile goes with its work.
395    pub(super) fn clear_instantly(&mut self, jobs: &mut Jobs, e: usize) {
396        let n = self.size as i32;
397        let (x, y) = self.placed[e].cell;
398        let (w, h) = self.placed[e].footprint;
399        for cy in y..y + h {
400            for cx in x..x + w {
401                let Some(o) = self.cells.objects[(cy * n + cx) as usize] else {
402                    continue;
403                };
404                if o == e || !self.placed[o].alive || self.placed[o].removal_requested {
405                    continue;
406                }
407                if self.piles.contains_key(&o) {
408                    self.destroy_pile(jobs, o);
409                    continue;
410                }
411                if self.templates[self.placed[o].template].natural.is_some() {
412                    // The natural resource's removal (`FUN_140125a30` → `FUN_1400a33b0`) frees
413                    // its cell now; the growth map follows when the entity goes.
414                    let (ox, oy) = self.placed[o].cell;
415                    let i = (oy * n + ox) as usize;
416                    if self.cells.objects[i] == Some(o) {
417                        self.cells.objects[i] = None;
418                        self.cells
419                            .set_type(i, self.terrain.natural_cell_type(self.size, i));
420                    }
421                } else {
422                    self.deactivate(jobs, o);
423                }
424                let p = &mut self.placed[o];
425                p.animation = None;
426                p.removal_requested = true;
427                *self.models_dirty = true;
428            }
429        }
430    }
431}