Skip to main content

banished_game/town/
sites.rs

1//! Building sites and buildings: becoming a site or a building, marking them on the map, and destroying them.
2
3use super::*;
4
5impl Sim<'_, '_> {
6    /// `FUN_1401350d0` on `e`'s workplace: its workers move on when it is switched off.
7    pub(crate) fn enable_workplace(&mut self, e: usize, on: bool) {
8        // The state machine's pending state, else its state (`+0x52`, `+0x50`): 0, 1 or 8
9        // is being built.
10        let building = self.sites.get(&e).is_some_and(|s| {
11            let state = if s.machine.pending < 0 {
12                s.machine.state
13            } else {
14                s.machine.pending
15            } as u8;
16            state & 0xf6 == 0 && state != 9
17        });
18        self.with_search(|w, workers, search| w.set_enabled(workers, e, on, building, search));
19    }
20
21    /// `FUN_1401318c0` on `e`'s StatusIconComponent (its workplace's).
22    pub(crate) fn set_icon(&mut self, e: usize, bit: u8, on: bool) {
23        if let Some(w) = self.workforce.workplaces.get_mut(&e) {
24            w.set_icon(bit, on);
25        }
26    }
27
28    /// `FUN_14011d910`: the footprint is clear for its ClearAreaDescription: no road where
29    /// it clears cells (`FUN_1400a2d30`), then no natural resource of the filter's template
30    /// (`FUN_1400a2e00`) or, clearing resources and piles, no object at all (`FUN_1400a2f90`).
31    pub(crate) fn is_clear(&self, entity: usize) -> bool {
32        let p = &self.placed[entity];
33        let n = self.size as i32;
34        let (x, y) = p.cell;
35        let (w, h) = p.footprint;
36        let cells =
37            || (y..y + h).flat_map(move |cy| (x..x + w).map(move |cx| (cy * n + cx) as usize));
38        let desc = self
39            .sites
40            .get(&entity)
41            .and_then(|s| self.site_templates[s.template].clear.clone());
42        // `FUN_1400a2d30` (clearing cells): no road, planned road or road plan.
43        let roads_clear = !desc.as_ref().is_some_and(|d| d.clear_cells)
44            || cells().all(|i| {
45                let t = self.cells.types[i].0;
46                t != 0
47                    && t != 1
48                    && self.cells.road_kind[i] == crate::work::road::NONE
49                    && !self.pending_roads.contains_key(&(i as i32))
50            });
51        if !roads_clear {
52            return false;
53        }
54        match desc {
55            // `FUN_1400a2e00`: no natural resource (of the filter's template).
56            Some(d) if d.clear_resources && !d.clear_piles => cells().all(|i| {
57                self.cells.objects[i].is_none_or(|o| {
58                    let t = &self.templates[self.placed[o].template];
59                    t.natural.is_none()
60                        || d.reference.as_deref().is_some_and(|f| {
61                            !t.reference.eq_ignore_ascii_case(&f.replace('/', "\\"))
62                        })
63                })
64            }),
65            // `FUN_1400a2f90`: no object.
66            _ => cells().all(|i| self.cells.objects[i].is_none()),
67        }
68    }
69
70    /// [`crate::work::professions::Workforce::demolishing`] from the sites' state machines.
71    pub(crate) fn refresh_demolishing(&mut self) {
72        let ids: Vec<usize> = self.workforce.workplaces.keys().copied().collect();
73        for e in ids {
74            if self.sites.contains_key(&e) && self.is_demolishing(e) {
75                self.workforce.demolishing.insert(e);
76            } else {
77                self.workforce.demolishing.remove(&e);
78            }
79        }
80    }
81
82    /// `FUN_1401349f0` (WorkPlaceComponent::BecomeSite): the workers leave and builders work
83    /// here.
84    pub(crate) fn become_site(&mut self, jobs: &mut Jobs, e: usize) {
85        self.refresh_demolishing();
86        let builder = self.professions.builder;
87        let mut workers = self.workers();
88        self.workforce.clear_workers(&mut workers, e);
89        let id = self.sites[&e].work_id;
90        if let Some(old) = self.workforce.workplaces.get(&e).and_then(|w| w.profession) {
91            jobs.change_profession(id, old, builder);
92        }
93        if let Some(w) = self.workforce.workplaces.get_mut(&e) {
94            w.profession = Some(builder);
95            self.workforce.register(e, builder);
96            let max = self.build_point_count(e) as i32;
97            // Nobody to move: the workers were let go above.
98            self.set_workers(&workers);
99            self.with_search(|w, ws, search| w.set_max_workers(ws, e, max, search));
100            workers = self.workers();
101            // `FUN_1401350d0(1)`: the builders' profession is not its own, so no
102            // workers icon.
103            let w = self.workforce.workplaces.get_mut(&e).unwrap();
104            w.enabled = true;
105            w.set_icon(crate::work::professions::icon::WORK_DISABLED, false);
106            w.set_icon(crate::work::professions::icon::BUILD_PAUSED, false);
107        }
108        self.set_workers(&workers);
109    }
110
111    /// `FUN_140134b40` (WorkPlaceComponent::BecomeBuilding): back to its own profession.
112    pub(crate) fn become_building(&mut self, jobs: &mut Jobs, e: usize) {
113        let builder = self.professions.builder;
114        let mut workers = self.workers();
115        let template = self.sites[&e].template;
116        let id = self.sites[&e].work_id;
117        if let Some(w) = self.workforce.workplaces.get(&e) {
118            if w.profession == Some(builder) {
119                self.workforce.unregister(e, builder);
120                let desc = self.site_templates[template].profession;
121                let footprint = self.placed[e].footprint;
122                let max = self.site_templates[template].default_max_workers(footprint);
123                if let Some(p) = desc {
124                    jobs.change_profession(id, builder, p);
125                }
126                let w = self.workforce.workplaces.get_mut(&e).unwrap();
127                w.max_workers = max;
128                w.profession = desc;
129                self.workforce.clear_workers(&mut workers, e);
130            }
131            if let Some(p) = self.workforce.workplaces[&e].profession {
132                self.workforce.register(e, p);
133            }
134        }
135        self.set_workers(&workers);
136    }
137
138    /// `FUN_140134130`: register a work for the site's WorkComponent, replacing any.
139    pub(crate) fn set_work(
140        &mut self,
141        jobs: &mut Jobs,
142        entity: usize,
143        work: Work,
144        mask: u32,
145        keep: bool,
146    ) {
147        self.remove_work(jobs, entity);
148        let keep = if keep {
149            self.sites[&entity].work_priority
150        } else {
151            None
152        };
153        let id = jobs.add(work, mask, keep);
154        let priority = jobs.works[&id].priority;
155        let site = self.sites.get_mut(&entity).unwrap();
156        site.work_id = id;
157        site.work_priority = Some(priority);
158    }
159
160    /// `FUN_140134020`: keep the priority, remove the work; the limit and work icons go.
161    pub(crate) fn remove_work(&mut self, jobs: &mut Jobs, entity: usize) {
162        let id = self.sites[&entity].work_id;
163        if let Some(w) = jobs.works.get(&id) {
164            self.sites.get_mut(&entity).unwrap().work_priority = Some(w.priority);
165            jobs.remove(id, self);
166            self.set_icon(entity, crate::work::professions::icon::LIMIT, false);
167            self.set_icon(entity, crate::work::professions::icon::NO_WORK, false);
168        }
169    }
170
171    /// `ClearAreaComponent::Stamp` (`FUN_14011d740`): with the description's `+0x2c`, the
172    /// terrain under the map rectangle and one cell beyond is flattened (`FUN_1400a35b0`;
173    /// entity event 5 re-adds the decal, which follows the terrain here).
174    pub(crate) fn clear_area_stamp(&mut self, entity: usize) {
175        let Some(site) = self.sites.get(&entity) else {
176            return;
177        };
178        if !self.site_templates[site.template]
179            .clear
180            .as_ref()
181            .is_some_and(|c| c.stamp)
182        {
183            return;
184        }
185        let p = &self.placed[entity];
186        let (x, y) = p.cell;
187        let (w, h) = p.footprint;
188        self.flatten_requests.push([x, y, x + w, y + h]);
189    }
190
191    /// QuarryComponent's message (`FUN_1400554e0`): the pit goes to the floor between the
192    /// description's shallow and deep by the share of the ConsumeProduce lifetime produced,
193    /// and the quarry claims its steep cells.
194    pub(crate) fn quarry_message(&mut self, e: usize) {
195        let Some(q) = &self.templates[self.placed[e].template].quarry else {
196            return;
197        };
198        let Some(site) = self.sites.get(&e) else {
199            return;
200        };
201        let produced = site.produce.as_ref().map_or(0, |p| p.produced);
202        let lifetime = self.site_templates[site.template]
203            .produce
204            .as_ref()
205            .map_or(1, |p| p.lifetime);
206        let t = produced as f32 / lifetime as f32;
207        let floor = (1.0 - t) * q.shallow + t * q.deep;
208        self.terraform_requests
209            .push(crate::world::terraform::Terraform::Quarry {
210                entity: e,
211                floor,
212                claim: true,
213            });
214    }
215
216    /// DecalComponent event 1 (`FUN_140120c30`): alpha 1.
217    pub(crate) fn decal_opaque(&mut self, entity: usize) {
218        let d = self.decals.entry(entity).or_default();
219        if !d.opaque {
220            d.opaque = true;
221            *self.models_dirty = true;
222        }
223    }
224
225    pub(super) fn set_mesh(&mut self, entity: usize, mesh: i32) {
226        self.placed[entity].mesh = if mesh < 0 { usize::MAX } else { mesh as usize };
227    }
228
229    pub(super) fn meshes(&self, entity: usize) -> i32 {
230        let site = &self.sites[&entity];
231        let v = self.placed[entity].variant;
232        self.site_templates[site.template]
233            .meshes
234            .get(v)
235            .copied()
236            .unwrap_or(1) as i32
237    }
238
239    /// `FUN_14010d4c0`: the materials built in count as used (`+0xc`); the building's own
240    /// storage replaces the material store.
241    pub(super) fn finish_storage(&mut self, entity: usize) {
242        let site = &self.sites[&entity];
243        let items: Vec<(usize, i32)> = self.storages[site.storage]
244            .items
245            .iter()
246            .map(|i| (i.material, i.count))
247            .collect();
248        for (m, n) in items {
249            self.count_used(m, n);
250        }
251        let site = &self.sites[&entity];
252        let (w, h) = self.placed[entity].footprint;
253        let s = &mut self.storages[site.storage];
254        s.items.clear();
255        if let Some((accepts, capacity, per_cell)) = self.site_templates[site.template].storage {
256            s.accepts = accepts;
257            s.capacity = capacity * if per_cell { w * h } else { 1 };
258        } else {
259            s.accepts = 0;
260            s.capacity = 0;
261        }
262    }
263
264    /// `FUN_1400a30f0` through the MapComponent (entity flag 1).
265    pub(crate) fn stamp(&mut self, entity: usize) {
266        let p = &self.placed[entity];
267        let Some(map) = &self.templates[p.template].map else {
268            return;
269        };
270        if map.no_stamp {
271            return;
272        }
273        let rules = PlacementRules {
274            mask: 0,
275            masks: Vec::new(),
276            mask_chars: String::new(),
277            footprint: map.footprint.clone(),
278            width: 0,
279            height: 0,
280            cell_type: map.cell_type as u8,
281        };
282        let rect = Rect {
283            x0: p.cell.0,
284            y0: p.cell.1,
285            x1: p.cell.0 + p.footprint.0 - 1,
286            y1: p.cell.1 + p.footprint.1 - 1,
287        };
288        let degrees = p.yaw as i32;
289        crate::world::placement::stamp(self.cells, &rules, rect, degrees, entity);
290    }
291
292    /// `FUN_140129c20` → `FUN_1400a33b0`: the MapComponent's footprint is lifted. Stamped
293    /// cells (`#`, `_`, or all with an empty footprint) get their natural type back (ground
294    /// where water lies under another occupant), and every cell's object is cleared.
295    pub(crate) fn unstamp(&mut self, entity: usize) {
296        let p = &self.placed[entity];
297        let Some(map) = &self.templates[p.template].map else {
298            return;
299        };
300        let n = self.size as i32;
301        let (x0, y0) = p.cell;
302        let (w, h) = p.footprint;
303        let fp = map.footprint.as_bytes();
304        let degrees = p.yaw as i32;
305        for y in y0..y0 + h {
306            for x in x0..x0 + w {
307                let i = (n * y + x) as usize;
308                let stamped = fp.is_empty() || {
309                    let c = fp
310                        .get(crate::world::placement::footprint_index(
311                            x - x0,
312                            y - y0,
313                            w,
314                            h,
315                            degrees,
316                        ))
317                        .copied()
318                        .unwrap_or(b'.');
319                    c == b'#' || c == b'_'
320                };
321                if stamped {
322                    let mut ty = self.terrain.natural_cell_type(self.size, i);
323                    let water = (1u32 << (ty.0 & 0x1f)) & 0x1e0 != 0;
324                    if water && self.cells.occupants[i].is_some_and(|o| o != entity) {
325                        ty = crate::terrain::CellType(2);
326                    }
327                    self.cells.set_type(i, ty);
328                }
329                self.cells.objects[i] = None;
330            }
331        }
332    }
333
334    /// Activate a site placed since the last iteration.
335    ///
336    /// Original: `FUN_1400c9d70`, then the activation.
337    pub fn activate(&mut self, jobs: &mut Jobs, entity: usize) {
338        self.trade_activate(entity);
339        run(self, jobs, entity, false, false);
340    }
341
342    /// The sites' StateMachine updates: every site's bit-4 update with the iteration's time `dt`,
343    /// then the scheduled update of each site its bucket visited this iteration (`visits`, in
344    /// visiting order) with the time since its last visit.
345    ///
346    /// Original: `FUN_1400c9560`.
347    pub fn update_sites(&mut self, jobs: &mut Jobs, dt: i64, visits: &[(usize, i64)]) {
348        let ids: Vec<usize> = self.sites.keys().copied().collect();
349        self.refresh_demolishing();
350        self.site_dt = dt;
351        for e in &ids {
352            run(self, jobs, *e, false, true);
353        }
354        for &(e, elapsed) in visits {
355            if self.sites.get(&e).is_some_and(|s| !s.destroyed) {
356                self.site_dt = elapsed;
357                run(self, jobs, e, true, true);
358            }
359        }
360        self.site_dt = dt;
361        let destroyed: Vec<usize> = self
362            .sites
363            .iter()
364            .filter(|(_, s)| s.destroyed)
365            .map(|(&e, _)| e)
366            .collect();
367        for e in destroyed {
368            self.destroy_entity(jobs, e);
369        }
370    }
371
372    /// The entity manager's removal of a destroyed site entity (then each component's remove slot):
373    /// its work goes (citizens doing it abort), the ZoneComponent frees its cells and the entity is
374    /// gone.
375    ///
376    /// Original: `FUN_1400c8370`.
377    pub fn destroy_entity(&mut self, jobs: &mut Jobs, e: usize) {
378        if !self.sites.contains_key(&e) {
379            return;
380        }
381        self.remove_work(jobs, e);
382        // The components' removal slots (`FUN_14004b380`, `FUN_14012ccb0`).
383        self.happiness_leave(e);
384        // A torn-down building's WorkPlace and Residence components (they left the town's
385        // registries in state 3).
386        if self.workforce.workplaces.contains_key(&e) {
387            let mut workers = self.workers();
388            self.workforce.clear_workers(&mut workers, e);
389            self.set_workers(&workers);
390            if let Some(p) = self.workforce.workplaces.get(&e).and_then(|w| w.profession) {
391                self.workforce.unregister(e, p);
392            }
393            self.workforce.workplaces.remove(&e);
394        }
395        if self.homes.contains_key(&e) {
396            self.residence_leave(e);
397        }
398        if self.wildlife.pastures.contains_key(&e) {
399            self.pasture_leave(e);
400            self.wildlife.pastures.remove(&e);
401        }
402        // MultiObjectComponent's (`FUN_14012ac90`): its parts go too, lifting their stamps and
403        // their floors (`FUN_140124770`).
404        for part in self.sites[&e].parts.clone() {
405            self.unstamp(part);
406            let size = self.size;
407            self.cells.floors.remove(size, part);
408            self.placed[part].alive = false;
409        }
410        // TerraformComponent removal (`FUN_140057cb0`): the entity's, then its parts'.
411        let parts = self.sites[&e].parts.clone();
412        for x in std::iter::once(e).chain(parts) {
413            if self.templates[self.placed[x].template].terraform.is_some() {
414                self.terraform_requests
415                    .push(crate::world::terraform::Terraform::Undo(x));
416            }
417        }
418        self.sites.remove(&e);
419        self.decals.remove(&e);
420        self.site_list.retain(|&s| s != e);
421        let n = self.size as i32;
422        let p = &mut self.placed[e];
423        p.alive = false;
424        let (x, y) = p.cell;
425        let (w, h) = p.footprint;
426        for cy in y..y + h {
427            for cx in x..x + w {
428                let i = (cy * n + cx) as usize;
429                if self.cells.occupants[i] == Some(e) {
430                    self.cells.occupants[i] = None;
431                }
432            }
433        }
434        *self.models_dirty = true;
435    }
436}