Skip to main content

banished_game/work/
professions.rs

1//! Professions (`Game`), workplaces (WorkPlaceComponent, the town manager's workplace registry
2//! `Game →`) and who takes a job when the player changes a profession's count. See docs/JOBS.md.
3//!
4//! The ProfessionUI spinners and the WorkPlaceUI buttons call the town manager directly in the
5//! original, between simulation iterations; the port queues them as [`Command`]s that the
6//! simulation applies before its next iteration, which is the same point in the game's Random
7//! stream.
8//!
9//! Original: `FUN_14007b890`, `FUN_14008ccb0`.
10// Game +0xb0; Game +0xf0 → +0x100
11
12use std::collections::BTreeMap;
13
14use crate::people::life::Home;
15use crate::random::Random;
16use crate::work::stock::StorageSearchMap;
17
18/// What the workplace searches read.
19///
20/// Original: `FUN_140115490`, `FUN_140115940`, `FUN_140115eb0`.
21pub trait Search {
22    /// Citizen `c`'s map cell.
23    fn cell(&self, c: usize) -> i32;
24    /// Profession `+0x4c` ([`Professions::works_anywhere`]).
25    fn anywhere(&self, profession: usize) -> bool;
26    /// The workplace's work priority (`Jobs` entry `+0x24`, lower first).
27    fn priority(&self, workplace: usize) -> i32;
28    /// `FUN_14011ada0`: the workplace's entrance can be reached from `cell`.
29    fn reach(&mut self, cell: i32, workplace: usize) -> bool;
30    /// The game's random generator (the reachability test and the hiring draw from it).
31    fn rng(&mut self) -> &mut Random;
32}
33
34/// `Profession` fields the simulation reads.
35///
36/// Original: `FUN_1400ae3e0`.
37#[derive(Debug, Clone, Default)]
38pub struct Profession {
39    /// `Game\Profession\Profession.rsc:<key>`.
40    pub reference: String,
41    // +0x30; +0x20
42    /// Name key (`ProfessionBuilder`), in the string table.
43    pub name: String,
44    /// The string table of the name, if not the default one.
45    pub string_table: Option<String>,
46    // +0x28; +0x30
47    /// The icon's sprite sheet (sprite).
48    pub sprite_sheet: Option<String>,
49    // +0x4c
50    /// Works anywhere (laborers and builders): `Jobs::TryList` offers it work outside its
51    /// workplace, and the job status icon stays off without one.
52    ///
53    /// Original: `FUN_1400fce80`.
54    pub works_anywhere: bool,
55    // +0x4d
56    /// With a workplace, the citizen does not catch diseases (the doctor).
57    ///
58    /// Original: `FUN_14011c060`.
59    pub immune_at_work: bool,
60    // +0x40
61    /// The death-message key of an accident at its work.
62    pub death: String,
63    // +0x54, +0x58; +0x54; +0x58
64    /// The accident odds `(years, factor)`: on average one accident per `years × factor` years
65    /// at work; none when the factor is not positive.
66    ///
67    /// Original: `FUN_1400ae760`.
68    pub accident: (f32, f32),
69}
70
71// +0x20/+0x28; +0x30; +0x38; Game +0xb0; +0x40; +0x48
72/// The profession manager (`Profession.rsc:resource`): the list of professions
73/// (`Profession.rsc:professionList`), the laborer and builder entries, and the child and student
74/// entries.
75///
76/// Original: `FUN_1400ae300`.
77#[derive(Debug, Clone, Default)]
78pub struct Professions {
79    /// Every profession.
80    pub list: Vec<Profession>,
81    /// The laborer's index in `list`.
82    pub laborer: usize,
83    /// The builder's index in `list`.
84    pub builder: usize,
85}
86
87impl Professions {
88    /// Whether a profession works anywhere (its description says so), not at one workplace.
89    pub fn works_anywhere(&self, p: usize) -> bool {
90        self.list.get(p).is_some_and(|p| p.works_anywhere)
91    }
92}
93
94// vtable 0x14016b160; +0x30
95/// WorkPlaceComponent plus the entity's WorkComponent most-workers value.
96///
97/// Original: `FUN_1401347b0`.
98#[derive(Debug, Clone)]
99pub struct WorkPlace {
100    // +0x48
101    /// The profession its workers take (builder while under construction).
102    pub profession: Option<usize>,
103    // +0x20
104    /// WorkPlaceDescription: the profession once built (none for houses and barns).
105    pub description_profession: Option<usize>,
106    // +0x50
107    /// Accepting workers (the constructor sets it).
108    pub enabled: bool,
109    // +0x58/+0x60
110    /// Citizen indices, in assignment order.
111    pub workers: Vec<usize>,
112    // +0x30
113    /// WorkComponent: most workers; 0 until first read, and every reader recomputes it from the
114    /// description while it is 0 (see [`WorkPlace::max`]).
115    pub max_workers: i32,
116    // WorkDescription +0x24, scaled when +0x34 is set.
117    /// The default number of workers, from the work description; scaled by the footprint's area
118    /// when the description says so.
119    ///
120    /// Original: `WorkComponent::MaxWorkers`, `FUN_140133a90`.
121    pub default_max: i32,
122    // +0xf0
123    /// The building's StatusIconComponent bits ([`icon`]).
124    pub status: u32,
125}
126
127/// The building status icons (`StatusIcons\BuildingIconSpriteSheet`): the bit of each.
128pub mod icon {
129    /// `BuildPaused`: switched off while being built.
130    pub const BUILD_PAUSED: u8 = 0;
131    /// `WorkDisabled`: switched off.
132    pub const WORK_DISABLED: u8 = 1;
133    /// `LimitProduction`: the stock is at its limit (or the building is full).
134    pub const LIMIT: u8 = 2;
135    /// `WorkUnavailable`: inputs are missing.
136    pub const NO_WORK: u8 = 3;
137    /// `WorkersUnavailable`: nobody works there.
138    pub const NO_WORKERS: u8 = 4;
139    /// `BuildDestroy`: marked to be torn down.
140    pub const DESTROY: u8 = 5;
141    /// A house without food, without firewood.
142    pub const NO_FOOD: u8 = 6;
143    /// The icon for no firewood.
144    pub const NO_FIREWOOD: u8 = 7;
145}
146
147impl WorkPlace {
148    /// A workplace with its profession and its default number of workers.
149    pub fn new(description_profession: Option<usize>, default_max: i32) -> WorkPlace {
150        WorkPlace {
151            profession: None,
152            description_profession,
153            enabled: true,
154            workers: Vec::new(),
155            max_workers: 0,
156            default_max,
157            status: 0,
158        }
159    }
160
161    /// Show or hide a status icon of the building.
162    ///
163    /// Original: `FUN_1401318c0` on the building's status icon component.
164    pub fn set_icon(&mut self, bit: u8, on: bool) {
165        crate::people::status::set_icon(&mut self.status, bit, on, false);
166    }
167
168    // +0x30; if +0x30 == 0 { +0x30 = MaxWorkers() }
169    /// WorkComponent as every reader sees it: `if == 0 { = MaxWorkers }`.
170    pub fn max(&self) -> i32 {
171        if self.max_workers == 0 {
172            self.default_max
173        } else {
174            self.max_workers
175        }
176    }
177}
178
179// +0x248; +0x238; +0x1f0
180/// The citizen fields assignment reads and writes (Citizen).
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub struct Worker {
183    // +0x208; desc+0x44 <= age
184    /// `desc <= age` and no school: the citizens every search counts.
185    pub adult: bool,
186    /// The profession.
187    pub profession: usize,
188    /// The workplace, if any.
189    pub workplace: Option<usize>,
190    /// The home, if any.
191    pub home: Option<usize>,
192}
193
194/// A player action on professions, applied by the simulation.
195#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
196pub enum Command {
197    /// ProfessionUI spinner `300 + profession` set to `count`.
198    ///
199    /// Original: `FUN_14007b890`.
200    SetCount {
201        /// The profession.
202        profession: usize,
203        /// The number of workers wanted.
204        count: i32,
205    },
206    /// WorkPlaceUI `0x1450`: one more worker of the workplace's profession.
207    ///
208    /// Original: `FUN_1400918a0`.
209    AddWorker {
210        /// The workplace.
211        entity: usize,
212    },
213    /// WorkPlaceUI `0x1451`: one fewer.
214    ///
215    /// Original: `FUN_140096650`.
216    RemoveWorker {
217        /// The workplace.
218        entity: usize,
219    },
220    /// WorkPlaceUI `0x1452/0x1453`: most workers ± 1.
221    ///
222    /// Original: `FUN_14008cae0`, `FUN_140134fd0`.
223    ChangeMaxWorkers {
224        /// The workplace.
225        entity: usize,
226        /// The change: +1 or −1.
227        delta: i32,
228    },
229    /// The pause button of a building's window (element 6000).
230    ///
231    /// Original: `FUN_1400b7370`, `FUN_1401350d0`.
232    SetEnabled {
233        /// The building.
234        entity: usize,
235        /// Whether it works.
236        enabled: bool,
237    },
238    // slot +0x58
239    /// CropUI's crop list: the field's crop; choosing the first one switches farming on.
240    ///
241    /// Original: `FUN_1400b7df0`.
242    SetCrop {
243        /// The field.
244        entity: usize,
245        /// The crop, or none.
246        crop: Option<usize>,
247    },
248    /// The field's buttons: 0 plant now, 1 harvest now, 2 farming on/off.
249    ///
250    /// Original: `FUN_140120190`.
251    FieldAction {
252        /// The field.
253        entity: usize,
254        /// The button: 0, 1 or 2.
255        action: i32,
256    },
257    // slot +0x48
258    /// PastureUI's slider: the most animals the pasture keeps.
259    ///
260    /// Original: `FUN_1400ba2f0`.
261    SetLimit {
262        /// The pasture.
263        entity: usize,
264        /// The most animals.
265        limit: i32,
266    },
267    /// Demolish a building (the Remove Buildings tool, or the button in its window).
268    ///
269    /// Original: `FUN_140109b40`, `FUN_140123470`.
270    Demolish {
271        /// The building.
272        entity: usize,
273    },
274    /// DestroyUI `0x1771`: the demolition is called off.
275    ///
276    /// Original: `FUN_1401238d0`.
277    CancelDemolition {
278        /// The building.
279        entity: usize,
280    },
281    /// RoadTool's apply: plan a road on a cell.
282    ///
283    /// Original: `FUN_14010a200`, `FUN_1400a1e30`.
284    PlanRoad {
285        /// The cell.
286        cell: i32,
287        /// The road kind.
288        kind: u8,
289        /// The cell type of the road.
290        ty: u8,
291        /// The raw material it is built from, if any.
292        material: Option<usize>,
293    },
294    /// Remove Roads: take a planned road back; a built one gets a removal marker (`marker`, the
295    /// tool's template).
296    ///
297    /// Original: `FUN_140108ab0`, `FUN_1400a1bd0`.
298    ClearRoad {
299        /// The cell.
300        cell: i32,
301        /// The removal marker's template.
302        marker: usize,
303    },
304    // slot +0x10
305    /// A StateMachine message: DestroyComponent's Remove Cancel (0 while torn down), DestroyUI
306    /// `0x1772` (0: rebuild the rubble) and `0x1773` (1: clear it); ImmigrationUI `0x9858` (7: take
307    /// the nomads in) and `0x9859` (8: turn them away).
308    ///
309    /// Original: `FUN_140123940`, `FUN_1401239a0`.
310    StateMessage {
311        /// The entity.
312        entity: usize,
313        /// The message.
314        message: i32,
315    },
316    // slot +0x70
317    /// ControlUI: the `kind` component's control `index`.
318    ///
319    /// Original: `FUN_140060150`.
320    Control {
321        /// The entity.
322        entity: usize,
323        /// The component kind.
324        kind: u32,
325        /// The control's index.
326        index: i32,
327    },
328    // +0x50
329    /// ConsumeProduceUI's product: ConsumeProduceComponent.
330    ///
331    /// Original: `FUN_14005fc90`.
332    SetRecipe {
333        /// The workplace.
334        entity: usize,
335        /// The product.
336        recipe: i32,
337    },
338    // +0x60
339    /// Upgrade a building to `template` (the upgrade button): it is demolished, and the new
340    /// building takes its place.
341    ///
342    /// Original: `FUN_14008c160`, then `FUN_140123470`.
343    Upgrade {
344        /// The building.
345        entity: usize,
346        /// The template of the new building.
347        template: usize,
348    },
349    /// TradeUI's trade button: sell (material, count) and buy (trade good, count).
350    ///
351    /// Original: `FUN_1400868f0`.
352    Exchange {
353        /// The trading post.
354        entity: usize,
355        /// What the town sells: `(raw material, count)`.
356        sell: Vec<(usize, i32)>,
357        /// What the town buys: `(trade good, count)`.
358        buy: Vec<(usize, i32)>,
359    },
360    /// TradeUI's settings on the TradeComponent.
361    Trade {
362        /// The trading post.
363        entity: usize,
364        /// The setting.
365        setting: crate::commerce::trade::TradeSetting,
366    },
367}
368
369/// The town manager's workplace and residence registries and every WorkPlaceComponent.
370#[derive(Debug, Clone, Default)]
371pub struct Workforce {
372    /// Entity → WorkPlaceComponent.
373    pub workplaces: BTreeMap<usize, WorkPlace>,
374    // +0x100
375    /// Profession → workplace entities, in registration order.
376    ///
377    /// Original: `FUN_14008ea70`.
378    pub registry: BTreeMap<usize, Vec<usize>>,
379    // +0x20/+0x28; FUN_14012f2b0 appends a residence when it is built.
380    /// The residences, in the order they were built.
381    pub residences: Vec<usize>,
382    /// Workplaces being torn down (their state machine in 3 to 5), kept by the sites' update: the
383    /// last worker leaving one shows no `WorkersUnavailable`.
384    ///
385    /// Original: `FUN_140123be0`.
386    pub demolishing: std::collections::BTreeSet<usize>,
387}
388
389impl Workforce {
390    /// Register `entity` under `profession` once.
391    ///
392    /// Original: `FUN_14008ea70`.
393    pub fn register(&mut self, entity: usize, profession: usize) {
394        let list = self.registry.entry(profession).or_default();
395        if !list.contains(&entity) {
396            list.push(entity);
397        }
398    }
399
400    /// Remove a workplace from a profession's list (every occurrence), keeping the order.
401    ///
402    /// Original: `FUN_140002cc0`.
403    pub fn unregister(&mut self, entity: usize, profession: usize) {
404        if let Some(list) = self.registry.get_mut(&profession) {
405            list.retain(|&e| e != entity);
406        }
407    }
408
409    /// Move citizen `c` to workplace `wp` (removing it from its old one, then appending to the new
410    /// one).
411    ///
412    /// Original: `FUN_140134cb0`, `FUN_140134ef0`, `FUN_1401173b0`.
413    pub fn set_workplace(&mut self, workers: &mut [Worker], c: usize, wp: Option<usize>) {
414        self.move_worker(c, workers[c].workplace, wp);
415        workers[c].workplace = wp;
416    }
417
418    /// The workplaces' side of [`Workforce::set_workplace`]: `c` leaves `old` and joins `new`
419    /// (not while it is being torn down). When the last worker leaves, the limit and work icons go,
420    /// and an enabled workplace of its own profession shows `WorkersUnavailable`; joining removes
421    /// that icon. The entity events 2 and 3 (work sounds and smoke) are only for presentation; the
422    /// app drives them from the workers.
423    ///
424    /// Original: `FUN_140123be0`; leaving `FUN_140134cb0`, joining `FUN_140134ef0`.
425    pub fn move_worker(&mut self, c: usize, old: Option<usize>, new: Option<usize>) {
426        if let Some(old) = old {
427            let demolishing = self.demolishing.contains(&old);
428            if let Some(w) = self.workplaces.get_mut(&old) {
429                w.workers.retain(|&x| x != c);
430                if w.workers.is_empty() {
431                    w.set_icon(icon::LIMIT, false);
432                    w.set_icon(icon::NO_WORK, false);
433                    if w.enabled
434                        && !demolishing
435                        && w.profession.is_some()
436                        && w.profession == w.description_profession
437                    {
438                        w.set_icon(icon::NO_WORKERS, true);
439                    }
440                }
441            }
442        }
443        if let Some(new) = new {
444            if let Some(w) = self.workplaces.get_mut(&new) {
445                w.set_icon(icon::NO_WORKERS, false);
446                w.workers.push(c);
447            }
448        }
449    }
450
451    /// The assignment tail shared by every search: change the workplace and profession only
452    /// when either differs.
453    fn assign(&mut self, workers: &mut [Worker], c: usize, profession: usize, wp: Option<usize>) {
454        if workers[c].profession != profession || workers[c].workplace != wp {
455            self.set_workplace(workers, c, wp);
456            workers[c].profession = profession;
457        }
458    }
459
460    /// `FUN_14008e940`: shuffle a profession's workplace list (`Rand(n)` per position, when it
461    /// has at least two).
462    fn shuffle_workplaces(&mut self, profession: usize, rng: &mut Random) {
463        if let Some(list) = self.registry.get_mut(&profession) {
464            shuffle(list, rng);
465        }
466    }
467
468    /// Shuffle, then the first enabled workplace with a free place.
469    ///
470    /// Original: `FUN_1400925d0`.
471    pub fn free_workplace(&mut self, profession: usize, rng: &mut Random) -> Option<usize> {
472        self.shuffle_workplaces(profession, rng);
473        let list = self.registry.get(&profession)?;
474        list.iter().copied().find(|e| {
475            self.workplaces
476                .get(e)
477                .is_some_and(|w| (w.workers.len() as i32) < w.max() && w.enabled)
478        })
479    }
480
481    /// Make one laborer take `profession` at `wp`. It looks, in this order, for: a laborer who
482    /// shares a home with someone of that profession, the laborer with a home nearest the
483    /// workplace, the household with the most laborers, then the first laborer.
484    ///
485    /// Original: `FUN_1400918a0` (and the loop body of `FUN_14007b890`); the searches
486    /// `FUN_1400920c0`, `FUN_140091ef0`, `FUN_140091ac0`, `FUN_140091980`.
487    #[allow(clippy::too_many_arguments)]
488    pub fn hire(
489        &mut self,
490        workers: &mut [Worker],
491        laborer: usize,
492        profession: usize,
493        wp: Option<usize>,
494        homes: &BTreeMap<usize, Home>,
495        map: &mut dyn StorageSearchMap,
496        rng: &mut Random,
497    ) -> Option<usize> {
498        if let Some(c) = self.family_member(workers, laborer, profession, wp, homes, map, rng) {
499            return Some(c);
500        }
501        if let Some(w) = wp {
502            if let Some(c) = self.nearest_laborer(workers, laborer, profession, w, map, rng) {
503                return Some(c);
504            }
505        }
506        if let Some(c) = self.largest_family(workers, laborer, profession, wp, homes, rng) {
507            return Some(c);
508        }
509        let c = workers
510            .iter()
511            .position(|w| w.adult && w.profession == laborer)?;
512        self.assign(workers, c, profession, wp);
513        Some(c)
514    }
515
516    /// `FUN_1400920c0`: a laborer in the household of a citizen who already has `profession`:
517    /// with a workplace, the household whose home entrance is nearest it (ties to the later
518    /// one); without, the first such laborer at once.
519    #[allow(clippy::too_many_arguments)]
520    fn family_member(
521        &mut self,
522        workers: &mut [Worker],
523        laborer: usize,
524        profession: usize,
525        wp: Option<usize>,
526        homes: &BTreeMap<usize, Home>,
527        map: &mut dyn StorageSearchMap,
528        rng: &mut Random,
529    ) -> Option<usize> {
530        let target = wp.map_or(-1, |w| map.entrance(w, rng));
531        let mut best = 0x7ffffff;
532        let mut found: Option<(usize, usize)> = None;
533        let free =
534            |workers: &[Worker], m: usize| workers[m].adult && workers[m].profession == laborer;
535        for c in 0..workers.len() {
536            if !(workers[c].adult && workers[c].profession == profession) {
537                continue;
538            }
539            let Some(home) = workers[c].home else {
540                continue;
541            };
542            let e = map.entrance(home, rng);
543            let d = if target < 0 {
544                best
545            } else {
546                let d = map.rank(e, target);
547                if d > best {
548                    continue;
549                }
550                d
551            };
552            let Some(h) = homes.get(&home) else { continue };
553            for (slot, family) in h.families.iter().enumerate() {
554                if let Some(&m) = family.iter().find(|&&m| free(workers, m)) {
555                    if wp.is_none() {
556                        self.assign(workers, m, profession, None);
557                        return Some(m);
558                    }
559                    found = Some((home, slot));
560                    best = d;
561                }
562            }
563        }
564        let (home, slot) = found?;
565        let m = *homes[&home].families[slot]
566            .iter()
567            .find(|&&m| free(workers, m))?;
568        self.assign(workers, m, profession, wp);
569        Some(m)
570    }
571
572    /// `FUN_140091ef0`: the homed laborer whose home entrance is nearest the workplace's
573    /// (path rank, ties to the later citizen).
574    fn nearest_laborer(
575        &mut self,
576        workers: &mut [Worker],
577        laborer: usize,
578        profession: usize,
579        wp: usize,
580        map: &mut dyn StorageSearchMap,
581        rng: &mut Random,
582    ) -> Option<usize> {
583        let target = map.entrance(wp, rng);
584        let mut best = 0x7ffffff;
585        let mut found = None;
586        for (i, w) in workers.iter().enumerate() {
587            if !(w.adult && w.profession == laborer) {
588                continue;
589            }
590            let Some(home) = w.home else { continue };
591            let e = map.entrance(home, rng);
592            let d = map.rank(e, target);
593            if d <= best {
594                best = d;
595                found = Some(i);
596            }
597        }
598        let c = found?;
599        self.assign(workers, c, profession, Some(wp));
600        Some(c)
601    }
602
603    /// `FUN_140091ac0`: shuffle the residences (`FUN_14008eb70`), then the first laborer of
604    /// the household with the most laborers (the first such household on ties).
605    fn largest_family(
606        &mut self,
607        workers: &mut [Worker],
608        laborer: usize,
609        profession: usize,
610        wp: Option<usize>,
611        homes: &BTreeMap<usize, Home>,
612        rng: &mut Random,
613    ) -> Option<usize> {
614        shuffle(&mut self.residences, rng);
615        let free =
616            |workers: &[Worker], m: usize| workers[m].adult && workers[m].profession == laborer;
617        let mut most = 0;
618        let mut found = None;
619        for &e in &self.residences {
620            let Some(h) = homes.get(&e) else { continue };
621            for (slot, family) in h.families.iter().enumerate() {
622                let n = family.iter().filter(|&&m| free(workers, m)).count();
623                if most < n {
624                    most = n;
625                    found = Some((e, slot));
626                }
627            }
628        }
629        let (e, slot) = found?;
630        let m = *homes[&e].families[slot]
631            .iter()
632            .find(|&&m| free(workers, m))?;
633        self.assign(workers, m, profession, wp);
634        Some(m)
635    }
636
637    /// One citizen of `profession` becomes a laborer: the first without a workplace, else the first
638    /// worker of the workplace with the most workers, else the first of that profession.
639    ///
640    /// Original: `FUN_140096650`.
641    pub fn fire(
642        &mut self,
643        workers: &mut [Worker],
644        laborer: usize,
645        profession: usize,
646    ) -> Option<usize> {
647        if let Some(c) = workers
648            .iter()
649            .position(|w| w.adult && w.profession == profession && w.workplace.is_none())
650        {
651            self.assign(workers, c, laborer, None);
652            return Some(c);
653        }
654        let mut most = 0;
655        let mut chosen = None;
656        for e in self.registry.get(&profession).into_iter().flatten() {
657            let n = self.workplaces.get(e).map_or(0, |w| w.workers.len());
658            if most < n {
659                most = n;
660                chosen = Some(*e);
661            }
662        }
663        if let Some(e) = chosen {
664            let c = self.workplaces[&e].workers[0];
665            self.assign(workers, c, laborer, None);
666            return Some(c);
667        }
668        let c = workers
669            .iter()
670            .position(|w| w.adult && w.profession == profession)?;
671        self.assign(workers, c, laborer, None);
672        Some(c)
673    }
674
675    /// Adults per profession.
676    ///
677    /// Original: `FUN_14007ba70` (counts).
678    pub fn counts(&self, workers: &[Worker], professions: usize) -> Vec<i32> {
679        let mut out = vec![0; professions];
680        for w in workers.iter().filter(|w| w.adult) {
681            if let Some(n) = out.get_mut(w.profession) {
682                *n += 1;
683            }
684        }
685        out
686    }
687
688    /// The most workers of every enabled workplace of a profession.
689    ///
690    /// Original: `FUN_14007ba70` ("of N").
691    pub fn capacity(&self, profession: usize) -> i32 {
692        self.registry
693            .get(&profession)
694            .into_iter()
695            .flatten()
696            .filter_map(|e| self.workplaces.get(e))
697            .filter(|w| w.enabled)
698            .map(|w| w.max())
699            .sum()
700    }
701
702    /// Move the profession's count toward `count`, one citizen at a time.
703    ///
704    /// Original: `FUN_14007b890`.
705    pub fn set_count(
706        &mut self,
707        workers: &mut [Worker],
708        professions: &Professions,
709        profession: usize,
710        count: i32,
711        homes: &BTreeMap<usize, Home>,
712        map: &mut dyn StorageSearchMap,
713        rng: &mut Random,
714    ) {
715        let current = self.counts(workers, professions.list.len())[profession];
716        for _ in current..count {
717            let wp = self.free_workplace(profession, rng);
718            self.hire(
719                workers,
720                professions.laborer,
721                profession,
722                wp,
723                homes,
724                map,
725                rng,
726            );
727        }
728        for _ in count..current {
729            self.fire(workers, professions.laborer, profession);
730        }
731    }
732
733    /// Set the most workers, then the first workers move to another workplace
734    /// ([`Workforce::find_workplace`], or none) until they fit.
735    ///
736    /// Original: `FUN_140134fd0`.
737    pub fn set_max_workers(
738        &mut self,
739        workers: &mut [Worker],
740        entity: usize,
741        max: i32,
742        search: &mut dyn Search,
743    ) {
744        let Some(w) = self.workplaces.get_mut(&entity) else {
745            return;
746        };
747        w.max_workers = max;
748        while max < self.workplaces[&entity].workers.len() as i32 {
749            let c = self.workplaces[&entity].workers[0];
750            let wp = self.find_workplace(c, workers[c].profession, search);
751            self.set_workplace(workers, c, wp);
752        }
753    }
754
755    /// Accept workers or not; a disabled workplace's workers move to another
756    /// ([`Workforce::find_workplace`], or none). Switched off it shows `BuildPaused` while being
757    /// `building` (states 0, 1, 8), else `WorkDisabled`, and neither on; switched off it drops the
758    /// limit, work and workers icons, on without workers of its own profession it shows
759    /// `WorkersUnavailable`.
760    ///
761    /// Original: `FUN_1401350d0`.
762    pub fn set_enabled(
763        &mut self,
764        workers: &mut [Worker],
765        entity: usize,
766        enabled: bool,
767        building: bool,
768        search: &mut dyn Search,
769    ) {
770        let Some(w) = self.workplaces.get_mut(&entity) else {
771            return;
772        };
773        w.enabled = enabled;
774        if !enabled {
775            let bit = if building {
776                icon::BUILD_PAUSED
777            } else {
778                icon::WORK_DISABLED
779            };
780            w.set_icon(bit, true);
781            for bit in [icon::LIMIT, icon::NO_WORK, icon::NO_WORKERS] {
782                w.set_icon(bit, false);
783            }
784        } else {
785            w.set_icon(icon::WORK_DISABLED, false);
786            w.set_icon(icon::BUILD_PAUSED, false);
787            if w.workers.is_empty()
788                && w.profession.is_some()
789                && w.profession == w.description_profession
790            {
791                w.set_icon(icon::NO_WORKERS, true);
792            }
793        }
794        if !enabled {
795            while let Some(&c) = self.workplaces.get(&entity).and_then(|w| w.workers.first()) {
796                let wp = self.find_workplace(c, workers[c].profession, search);
797                self.set_workplace(workers, c, wp);
798            }
799        }
800    }
801
802    /// A workplace of citizen `c`'s profession, enabled, with room and reachable: for a profession
803    /// that works anywhere, the one whose work comes first (by work priority), else the one with
804    /// the most room, the first of equals.
805    ///
806    /// Original: `FUN_140115940`, `FUN_140115490`, `FUN_140115450`; the priority sort is
807    /// `FUN_140115800`.
808    pub fn find_workplace(&self, c: usize, p: usize, search: &mut dyn Search) -> Option<usize> {
809        let list = self.registry.get(&p)?;
810        let anywhere = search.anywhere(p);
811        let cell = search.cell(c);
812        let mut best = None;
813        let mut most = 0;
814        let mut candidates = Vec::new();
815        for &e in list {
816            let Some(w) = self.workplaces.get(&e) else {
817                continue;
818            };
819            let free = w.max() - w.workers.len() as i32;
820            if free <= 0 || !w.enabled || !search.reach(cell, e) {
821                continue;
822            }
823            if anywhere {
824                candidates.push((e, search.priority(e)));
825            } else if most < free {
826                most = free;
827                best = Some(e);
828            }
829        }
830        if anywhere {
831            // `FUN_140030c00`: the CRT qsort by work priority (`FUN_140115800`); the first.
832            crate::behaviour::ai::crt_qsort_less(&mut candidates, |a, b| a.1 < b.1);
833            return candidates.first().map(|c| c.0);
834        }
835        best
836    }
837
838    /// Should citizen `c` look again? A profession that works anywhere when its workplace cannot be
839    /// reached or another with room has work that comes first; the others when the reachable
840    /// workplaces' room differs by more than one.
841    ///
842    /// Original: `FUN_140115eb0`.
843    pub fn should_move(
844        &self,
845        c: usize,
846        p: usize,
847        current: Option<usize>,
848        search: &mut dyn Search,
849    ) -> bool {
850        let list = self.registry.get(&p).cloned().unwrap_or_default();
851        let cell = search.cell(c);
852        if !search.anywhere(p) {
853            let (mut most, mut least) = (0, 0xffff);
854            for e in list {
855                let Some(w) = self.workplaces.get(&e) else {
856                    continue;
857                };
858                if !w.enabled || !search.reach(cell, e) {
859                    continue;
860                }
861                let free = w.max() - w.workers.len() as i32;
862                if most < free {
863                    most = free;
864                }
865                if free < least {
866                    least = free;
867                }
868            }
869            return 1 < most - least;
870        }
871        let Some(current) = current else {
872            return false;
873        };
874        let priority = search.priority(current);
875        if !search.reach(cell, current) {
876            return true;
877        }
878        list.into_iter().any(|e| {
879            e != current
880                && self
881                    .workplaces
882                    .get(&e)
883                    .is_some_and(|w| w.enabled && (w.workers.len() as i32) < w.max())
884                && search.priority(e) < priority
885        })
886    }
887
888    /// Every worker leaves.
889    ///
890    /// Original: `FUN_140134330`.
891    pub fn clear_workers(&mut self, workers: &mut [Worker], entity: usize) {
892        while let Some(&c) = self.workplaces.get(&entity).and_then(|w| w.workers.first()) {
893            self.set_workplace(workers, c, None);
894        }
895    }
896
897    /// Apply a UI command.
898    pub fn apply(
899        &mut self,
900        command: Command,
901        workers: &mut [Worker],
902        professions: &Professions,
903        homes: &BTreeMap<usize, Home>,
904        map: &mut dyn StorageSearchMap,
905        search: &mut dyn Search,
906    ) {
907        match command {
908            Command::SetCount { profession, count } => self.set_count(
909                workers,
910                professions,
911                profession,
912                count,
913                homes,
914                map,
915                search.rng(),
916            ),
917            Command::AddWorker { entity } => {
918                let Some(p) = self.workplaces.get(&entity).and_then(|w| w.profession) else {
919                    return;
920                };
921                let wp = self.free_workplace(p, search.rng());
922                self.hire(
923                    workers,
924                    professions.laborer,
925                    p,
926                    wp,
927                    homes,
928                    map,
929                    search.rng(),
930                );
931            }
932            Command::RemoveWorker { entity } => {
933                if let Some(p) = self.workplaces.get(&entity).and_then(|w| w.profession) {
934                    self.fire(workers, professions.laborer, p);
935                }
936            }
937            // The simulation answers this one with the building's state
938            // ([`crate::town::Sim::enable_workplace`]).
939            Command::SetEnabled { entity, enabled } => {
940                self.set_enabled(workers, entity, enabled, false, search)
941            }
942            Command::ChangeMaxWorkers { entity, delta } => {
943                if let Some(w) = self.workplaces.get(&entity) {
944                    let max = w.max() + delta;
945                    self.set_max_workers(workers, entity, max, search);
946                }
947            }
948            // Handled by the crop field, orchard and pasture (`crate::production::farm`, `crate::production::pasture`).
949            Command::SetCrop { .. }
950            | Command::FieldAction { .. }
951            | Command::SetLimit { .. }
952            | Command::Demolish { .. }
953            | Command::CancelDemolition { .. }
954            | Command::StateMessage { .. }
955            | Command::PlanRoad { .. }
956            | Command::ClearRoad { .. }
957            | Command::Control { .. }
958            | Command::SetRecipe { .. }
959            | Command::Upgrade { .. }
960            | Command::Exchange { .. }
961            | Command::Trade { .. } => {}
962        }
963    }
964}
965
966/// For each position `i`, swap with `Rand(n)` (no draws below two entries).
967///
968/// Original: `FUN_14008e940`, `FUN_14008eb70`.
969pub fn shuffle<T>(list: &mut [T], rng: &mut Random) {
970    let n = list.len();
971    if n < 2 {
972        return;
973    }
974    for i in 0..n {
975        let j = rng.rand_below(n as u32) as usize;
976        if i != j {
977            list.swap(i, j);
978        }
979    }
980}
981
982#[cfg(test)]
983mod tests;