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;