Skip to main content

banished_game/work/
jobs.rs

1//! The job manager (`Game`). See docs/JOBS.md.
2//!
3//! Every `Work` gets a sequential id (from 1) and a priority (sequential unless the owner keeps
4//! one). The manager indexes works by id, by priority, by category (`+ (c − 1)·0x10`, categories
5//! 1..14 from the registration mask's bits) and by profession (the work's slot). All of them are
6//! trees keyed by id, so iteration is in id order.
7//!
8//! The AI's work (9) and emergency (2) activities ask the manager for a job:
9//!
10//! - [`Jobs::assign_work`]: the citizen's own profession's works, then [`Jobs::assign_emergency`],
11//!   then the laborers' works;
12//! - [`Jobs::assign_emergency`]: category lists chosen from low stock (`Rand(n)` on the game
13//!   stream), each tried by distance;
14//! - `try_list` sorts candidate works by priority or path rank, ties by id, and `offer` offers them
15//!   in order.
16//!
17//! A work's virtual slots are supplied by a [`WorkHost`]: try to assign (0 no, 1 not now, 2 taken),
18//! a second try, a task completed, a task failed, cancel every worker, refresh.
19//!
20//! Original: `FUN_1400fe650`, `FUN_1400fcd00`, `FUN_1400fca20`, `FUN_1400fce80`, `FUN_1400fd4d0`,
21//! `FUN_1400fd160`, `FUN_1400fd030`.
22// Game +0x118; +0x00; +0x04; +0x30; +0x10; +0x120; +0x40 + (c − 1)·0x10; +0x50; +0x20; +0x28;
23// +0x38; +0x40; +0x48
24
25use std::collections::{BTreeMap, BTreeSet};
26
27use crate::random::Random;
28
29/// Categories 1..14 (the registration mask's bits 1..14).
30pub const CATEGORIES: usize = 14;
31/// Construction (`BuildStructure` and `GatherRawMaterials`, mask `0x2001`).
32pub const CONSTRUCTION: u32 = 13;
33/// Emergency (fire) work, the only category a citizen with an emergency tries.
34pub const EMERGENCY: u32 = 14;
35
36/// The kinds of work the port has, with their own state.
37#[derive(Debug, Clone)]
38pub enum WorkKind {
39    // vtable 0x140167840
40    /// `GatherRawMaterials`.
41    ///
42    /// Original: `FUN_1400f1be0`.
43    Gather(crate::work::construction::Gather),
44    // vtable 0x1401674c0
45    /// `BuildStructure`.
46    ///
47    /// Original: `FUN_1400eae90`.
48    Build(crate::work::construction::BuildWork),
49    // vtable 0x140167540
50    /// `ClearArea`.
51    ///
52    /// Original: `FUN_1400ebe30`.
53    Clear(crate::work::construction::ClearWork),
54    // vtable 0x140167c30
55    /// `Pickup`: carry a pile to storage. A pile makes it when it is activated.
56    ///
57    /// Original: made by `FUN_14012e6a0`.
58    Pickup(crate::work::construction::PickupWork),
59    // vtable 0x140167640; +0x90
60    /// `ConsumeProduce`: a workplace's production.
61    ///
62    /// Original: `FUN_1400ee6d0`; made by slot `FUN_14011e1d0`.
63    Produce(crate::work::construction::ProduceWork),
64    // vtable 0x140167ab0
65    /// `LocationWork`: staff stand at a school, chapel or doctor.
66    Location(crate::people::services::LocationWork),
67    // vtable 0x1401679c0
68    /// The work of gatherers and herbalists.
69    ///
70    /// Original: made by `FUN_14004bac0`.
71    Herbalist(crate::production::herbalist::HerbalistWork),
72    // vtable 0x1401677c0
73    /// A forester's work, with the same fields.
74    ///
75    /// Original: made by `FUN_140124c50`.
76    Forester(crate::production::herbalist::HerbalistWork),
77    // vtable 0x140167a40
78    /// A hunter's work, with the gatherer's fields.
79    ///
80    /// Original: made by `FUN_140127b70`.
81    Hunt(crate::production::herbalist::HerbalistWork),
82    // vtable 0x140167cb0
83    /// `PlantArea`.
84    ///
85    /// Original: `FUN_1400f9b70`.
86    PlantArea(crate::production::farm::AreaWork),
87    // vtable 0x140167db0
88    /// `TendArea`: the workers, as the gatherer's.
89    TendArea(crate::production::herbalist::HerbalistWork),
90    // vtable 0x1401678c0
91    /// The harvest work.
92    ///
93    /// Original: `FUN_1400f2990`.
94    Harvest(crate::production::farm::AreaWork),
95    // vtable 0x140167d30
96    /// `PlantOrchard`: `need` marks the slots being planted.
97    ///
98    /// Original: `FUN_1400fa610`.
99    PlantOrchard(crate::production::farm::AreaWork),
100    // vtable 0x140167940
101    /// Fruit picking.
102    ///
103    /// Original: `FUN_1400f3530`.
104    PickFruit(crate::production::farm::AreaWork),
105    // vtable 0x140167bc0; +0x48
106    /// PastureComponent's work: the ranchers tending, as the gatherer's.
107    ///
108    /// Original: `FUN_1400f8250`.
109    Pasture(crate::production::herbalist::HerbalistWork),
110    // vtable 0x1401675c0; +0x58
111    /// `ClearStorage`: an empty house's goods go to the town's storage; the haulers.
112    ///
113    /// Original: `FUN_1400ec920`.
114    ClearStorage(crate::production::herbalist::HerbalistWork),
115    // vtable 0x1401676c0; +0x50
116    /// `FireWork`: firefighters carrying buckets to a burning building; the workers.
117    ///
118    /// Original: `FUN_1400ef0a0`.
119    Fire(crate::production::herbalist::HerbalistWork),
120    // vtable 0x140167e30
121    /// `TradeWork`: haulers keeping the trading post's stock.
122    Trade(crate::commerce::trade::TradeWork),
123    // vtable 0x140167430
124    /// `BuildRoad`: one cell of road.
125    Road(crate::work::road::RoadWork),
126    // vtable 0x140167b40; +0x90
127    /// MarketWork (made by MarketComponent's slot).
128    Market(crate::commerce::market::MarketWork),
129    // vtable 0x140167740
130    /// `FishingWork`.
131    ///
132    /// Original: `FUN_1400eff90`.
133    Fishing(crate::production::fishing::FishingWork),
134    /// A test stand-in.
135    Test(i32),
136}
137
138// vtable 0x140167e30
139/// `Work`.
140///
141/// Original: `FUN_1400fc7b0`.
142#[derive(Debug, Clone)]
143pub struct Work {
144    // +0x10
145    /// The owning entity.
146    pub entity: usize,
147    // +0x20; +0x24
148    /// The id.
149    pub id: i32,
150    /// The priority.
151    pub priority: i32,
152    // +0x28
153    /// The owner's map-rectangle centre cell.
154    pub cell: i32,
155    // +0x2c
156    /// The manager pass in which it last answered "not now".
157    pub failed_pass: i32,
158    // +0x30, +0x38, +0x40
159    /// The clock of the distance patience: how long a far work waits before a citizen takes it.
160    ///
161    /// Original: used by `FUN_1400fc3c0`.
162    pub patience: i64,
163    /// The clock when the patience was last brought up to date.
164    pub patience_base: i64,
165    /// How many times a citizen found the work too far.
166    pub far_count: i32,
167    // +0x50
168    /// The profession whose citizens do this work.
169    pub profession: usize,
170    /// What kind of work it is.
171    pub kind: WorkKind,
172}
173
174impl Work {
175    /// `cell` is `(y + h/2)·n + x + w/2` of the owner's map rectangle.
176    ///
177    /// Original: `FUN_1400fc7b0`.
178    pub fn new(entity: usize, cell: i32, profession: usize, kind: WorkKind) -> Work {
179        Work {
180            entity,
181            id: -1,
182            priority: 0,
183            cell,
184            failed_pass: 0,
185            patience: 0,
186            patience_base: 0,
187            far_count: 0,
188            profession,
189            kind,
190        }
191    }
192}
193
194/// The citizen and world queries and the work slots the manager calls.
195pub trait WorkHost {
196    /// Citizen `+0x1f0`, `+0x248`, the character's cell, and `FUN_14011b0b0`.
197    fn profession(&self, citizen: usize) -> usize;
198    /// The citizen's workplace.
199    fn workplace(&self, citizen: usize) -> Option<usize>;
200    /// The citizen's map cell.
201    fn cell(&self, citizen: usize) -> i32;
202    /// Whether there is an emergency near the citizen.
203    fn has_emergency(&self, citizen: usize) -> bool;
204    /// Profession `+0x4c`.
205    fn works_anywhere(&self, profession: usize) -> bool;
206    /// `FUN_1400a4ce0(map, a, b, 1)`, with the callers' fallback `n + (int)sqrtf(d²)`.
207    fn rank(&mut self, a: i32, b: i32) -> i32;
208    /// The stock manager's `(count, limit)` per slot (`Game +0xf0 → +0x170`).
209    fn stock(&self, slot: usize) -> (i32, i32);
210    /// The laborer and builder professions (`Game +0xb0 → +0x30/+0x38`).
211    fn laborer(&self) -> usize;
212    /// The builder profession.
213    fn builder(&self) -> usize;
214    /// The game's random numbers.
215    fn rng(&mut self) -> &mut Random;
216
217    /// Slot `+0x48`: refresh (drop workers that no longer hold this work's task).
218    fn refresh(&mut self, work: &mut Work);
219    /// Slot `+0x20`: 0 cannot, 1 not now, 2 assigned. `time` is the manager's clock `+0x130`.
220    fn try_assign(&mut self, work: &mut Work, citizen: usize, time: i64) -> u8;
221    /// Slot `+0x28` (the base returns false).
222    fn try_assign_again(&mut self, _work: &mut Work, _citizen: usize, _time: i64) -> bool {
223        false
224    }
225    /// Slot `+0x30`: a CompleteTask finished; true removes the work.
226    fn complete(&mut self, work: &mut Work, citizen: usize, param: i32) -> bool;
227    /// Slot `+0x38`: a CompleteTask was aborted.
228    fn fail(&mut self, work: &mut Work, citizen: usize, param: i32);
229    /// Job messages raised while the host ran a slot (aborted CompleteTasks of other works),
230    /// for the manager to deliver: `(work, citizen, param, completed)`.
231    fn take_events(&mut self) -> Vec<JobEvent> {
232        Vec::new()
233    }
234    /// Slot `+0x40`: clear every worker's action stack (their CompleteTask aborts call
235    /// [`WorkHost::fail`] on this work, which the host handles directly).
236    fn cancel(&mut self, work: &mut Work);
237    /// Works a slot `+0x20` asked the manager to offer in its place (`FUN_1400fdaf0` called
238    /// from inside a try, as ClearArea does with the piles in its footprint), in order. A try
239    /// answering [`NESTED`] has queued them.
240    fn take_offers(&mut self) -> Vec<i32> {
241        Vec::new()
242    }
243    /// The queued offers get a second try (`FUN_1400fdaf0(.., 1)`).
244    fn offers_again(&mut self) -> bool {
245        false
246    }
247    /// After a [`NESTED`] answer none of whose offers was taken: the rest of the try
248    /// (ConsumeProduce goes on to work a cycle).
249    fn resume(&mut self, _work: &mut Work, _citizen: usize) -> u8 {
250        1
251    }
252}
253
254/// A try's answer meaning "offer these other works for me" ([`WorkHost::take_offers`]).
255pub const NESTED: u8 = 3;
256
257/// A CompleteTask's report: `complete` (start) or failed (abort).
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub struct JobEvent {
260    /// The work.
261    pub work: i32,
262    /// The citizen.
263    pub citizen: usize,
264    /// The task's parameter.
265    pub param: i32,
266    /// Whether the task completed (else it was aborted).
267    pub complete: bool,
268}
269
270/// The manager.
271#[derive(Debug, Clone)]
272pub struct Jobs {
273    /// `+0x00`: last id (wraps from `0x7ffffffe` to 0); `+0x04`: next sequential priority.
274    next_id: i32,
275    next_priority: i32,
276    // +0x08
277    /// Pass counter (one per simulation iteration).
278    ///
279    /// Original: `FUN_14009b850`.
280    pub pass: i32,
281    /// `+0x10`: priority → id.
282    priorities: BTreeMap<i32, i32>,
283    // +0x30
284    /// Id → work.
285    pub works: BTreeMap<i32, Work>,
286    /// `+0x40..`: category 1..14 → ids.
287    categories: [BTreeSet<i32>; CATEGORIES],
288    /// `+0x120`: profession → ids.
289    professions: BTreeMap<usize, BTreeSet<i32>>,
290    // +0x130
291    /// Manager clock (ticks), advanced with the pass.
292    pub time: i64,
293}
294
295impl Default for Jobs {
296    fn default() -> Self {
297        Jobs {
298            next_id: 0,
299            next_priority: 0,
300            pass: 1,
301            priorities: BTreeMap::new(),
302            works: BTreeMap::new(),
303            categories: Default::default(),
304            professions: BTreeMap::new(),
305            time: 0,
306        }
307    }
308}
309
310/// `FUN_1400fca20`'s stock checks: `(slot, categories)`, tried when `count <= limit / 10`.
311const EMERGENCY_LISTS: [(usize, &[u32]); 9] = [
312    (5, &[1]),
313    (3, &[2, 4, 10]),
314    (4, &[3, 6, 4, 10, 12]),
315    (0, &[4, 10]),
316    (2, &[6, 12]),
317    (1, &[5, 11]),
318    (10, &[9]),
319    (7, &[7, 9]),
320    (6, &[8]),
321];
322
323impl Jobs {
324    /// One simulation iteration of the job manager.
325    ///
326    /// Original: `FUN_1400fe650`, and the step in `FUN_14009b850`.
327    pub fn step(&mut self, dt: i64) {
328        self.pass = self.pass.wrapping_add(1);
329        self.time = self.time.wrapping_add(dt);
330    }
331
332    /// Whether there is no work.
333    pub fn is_empty(&self) -> bool {
334        self.works.is_empty()
335    }
336
337    /// Register a work. With `keep`, the work gets that priority (moved down past any value already
338    /// taken), else the next one in sequence. Returns the work's id.
339    ///
340    /// Original: `FUN_1400fdd70(work, mask, keep, priority)`.
341    pub fn add(&mut self, mut work: Work, mask: u32, keep: Option<i32>) -> i32 {
342        self.next_id = self.next_id.wrapping_add(1);
343        let id = self.next_id;
344        if id == 0x7ffffffe {
345            self.next_id = 0;
346        }
347        let priority = match keep {
348            None => {
349                let p = self.next_priority;
350                self.next_priority = p.wrapping_add(1);
351                p
352            }
353            Some(mut p) => {
354                while self.priorities.contains_key(&p) {
355                    p -= 1;
356                }
357                p
358            }
359        };
360        work.id = id;
361        work.priority = priority;
362        self.priorities.entry(priority).or_insert(id);
363        for c in 1..=CATEGORIES {
364            if mask & (1 << c) != 0 {
365                self.categories[c - 1].insert(id);
366            }
367        }
368        self.professions
369            .entry(work.profession)
370            .or_default()
371            .insert(id);
372        self.works.insert(id, work);
373        id
374    }
375
376    // +0x130
377    /// A saved manager: its counters, then each work with the categories it was filed under
378    /// (1..14), and the professions its profession map holds. The priority index is rebuilt from
379    /// the works' own priorities, as the load pass does; the clock is not saved and restarts at 0.
380    ///
381    /// Original: `FUN_1400fe710`.
382    pub fn restore(
383        &mut self,
384        next_id: i32,
385        next_priority: i32,
386        pass: i32,
387        works: impl IntoIterator<Item = (Work, Vec<u32>)>,
388        professions: &[usize],
389    ) {
390        *self = Jobs {
391            next_id,
392            next_priority,
393            pass,
394            ..Default::default()
395        };
396        // The profession map keeps a profession whose works are all gone.
397        for &p in professions {
398            self.professions.entry(p).or_default();
399        }
400        for (work, categories) in works {
401            let id = work.id;
402            self.priorities.entry(work.priority).or_insert(id);
403            for c in categories {
404                if let Some(set) = (c as usize)
405                    .checked_sub(1)
406                    .and_then(|c| self.categories.get_mut(c))
407                {
408                    set.insert(id);
409                }
410            }
411            self.professions
412                .entry(work.profession)
413                .or_default()
414                .insert(id);
415            self.works.insert(id, work);
416        }
417    }
418
419    // +0x00; +0x04
420    /// The counters a save holds.
421    pub fn counters(&self) -> (i32, i32) {
422        (self.next_id, self.next_priority)
423    }
424
425    /// The works filed under category `c` (1..14), in id order.
426    pub fn category(&self, c: u32) -> Vec<i32> {
427        (c as usize)
428            .checked_sub(1)
429            .and_then(|c| self.categories.get(c))
430            .map(|set| set.iter().copied().collect())
431            .unwrap_or_default()
432    }
433
434    // +0x120
435    /// The works by profession, in profession then id order.
436    pub fn by_profession(&self) -> Vec<(usize, Vec<i32>)> {
437        self.professions
438            .iter()
439            .map(|(&p, ids)| (p, ids.iter().copied().collect()))
440            .collect()
441    }
442
443    /// Unregister, refresh, cancel every worker, delete.
444    ///
445    /// Original: `FUN_1400fe300`.
446    pub fn remove(&mut self, id: i32, host: &mut impl WorkHost) {
447        let Some(mut work) = self.works.remove(&id) else {
448            return;
449        };
450        for c in &mut self.categories {
451            c.remove(&id);
452        }
453        if self.priorities.get(&work.priority) == Some(&id) {
454            self.priorities.remove(&work.priority);
455        }
456        if let Some(set) = self.professions.get_mut(&work.profession) {
457            set.remove(&id);
458        }
459        host.refresh(&mut work);
460        host.cancel(&mut work);
461        self.drain(host);
462    }
463
464    /// Deliver the host's pending job events in order.
465    pub fn drain(&mut self, host: &mut impl WorkHost) {
466        loop {
467            let events = host.take_events();
468            if events.is_empty() {
469                return;
470            }
471            for e in events {
472                if e.complete {
473                    self.complete(e.work, e.citizen, e.param, host);
474                } else {
475                    self.fail(e.work, e.citizen, e.param, host);
476                }
477            }
478        }
479    }
480
481    /// Move a work to the front (`lowest priority − 1`).
482    ///
483    /// Original: `FUN_1400fefa0`.
484    pub fn prioritize(&mut self, id: i32) {
485        let Some(work) = self.works.get(&id) else {
486            return;
487        };
488        let top = self
489            .priorities
490            .keys()
491            .next()
492            .map_or(0, |&p| p.wrapping_sub(1));
493        let old = work.priority;
494        if self.priorities.get(&old) == Some(&id) {
495            self.priorities.remove(&old);
496        }
497        self.works.get_mut(&id).unwrap().priority = top;
498        self.priorities.entry(top).or_insert(id);
499    }
500
501    /// Move a work from one profession's list to another's.
502    ///
503    /// Original: `FUN_1400fd8d0`.
504    pub fn change_profession(&mut self, id: i32, from: usize, to: usize) {
505        if !self.works.contains_key(&id) {
506            return;
507        }
508        let Some(set) = self.professions.get_mut(&from) else {
509            self.professions.entry(from).or_default();
510            return;
511        };
512        if !set.remove(&id) {
513            return;
514        }
515        self.professions.entry(to).or_default().insert(id);
516        self.works.get_mut(&id).unwrap().profession = to;
517    }
518
519    /// A task finished (the start of `CompleteTask`): remove the work when its slot says it is
520    /// done.
521    ///
522    /// Original: `FUN_1400fdc30`.
523    pub fn complete(&mut self, id: i32, citizen: usize, param: i32, host: &mut impl WorkHost) {
524        let Some(mut work) = self.works.remove(&id) else {
525            return;
526        };
527        let done = host.complete(&mut work, citizen, param);
528        self.works.insert(id, work);
529        self.drain(host);
530        if done {
531            self.remove(id, host);
532        }
533    }
534
535    /// A task failed (`CompleteTask` cancelled).
536    ///
537    /// Original: `FUN_1400fdcd0`.
538    pub fn fail(&mut self, id: i32, citizen: usize, param: i32, host: &mut impl WorkHost) {
539        if let Some(mut work) = self.works.remove(&id) {
540            host.fail(&mut work, citizen, param);
541            self.works.insert(id, work);
542            self.drain(host);
543        }
544    }
545
546    /// Slot `+0x20` of a work taken out of the table, answering [`NESTED`] by offering the
547    /// queued works (`FUN_1400fdaf0` without a second try) until one is taken: then 2, else the
548    /// rest of the try (which may offer again). The work is back in the table afterwards.
549    fn try_one(&mut self, id: i32, mut work: Work, citizen: usize, host: &mut impl WorkHost) -> u8 {
550        let mut r = host.try_assign(&mut work, citizen, self.time);
551        self.works.insert(id, work);
552        while r == NESTED {
553            let again = host.offers_again();
554            for other in host.take_offers() {
555                if self.offer_one(other, citizen, again, host) {
556                    return 2;
557                }
558            }
559            let Some(mut work) = self.works.remove(&id) else {
560                return 1;
561            };
562            r = host.resume(&mut work, citizen);
563            self.works.insert(id, work);
564        }
565        r
566    }
567
568    /// Offer one work by id, with a second try when the first answers "not now" and `again` is set.
569    ///
570    /// Original: `FUN_1400fdaf0`.
571    pub fn offer_one(
572        &mut self,
573        id: i32,
574        citizen: usize,
575        again: bool,
576        host: &mut impl WorkHost,
577    ) -> bool {
578        let Some(mut work) = self.works.remove(&id) else {
579            return false;
580        };
581        let mut taken = false;
582        if work.failed_pass != self.pass {
583            host.refresh(&mut work);
584            match self.try_one(id, work, citizen, host) {
585                2 => taken = true,
586                1 => {
587                    if again {
588                        if let Some(mut work) = self.works.remove(&id) {
589                            host.refresh(&mut work);
590                            taken = host.try_assign_again(&mut work, citizen, self.time);
591                            self.works.insert(id, work);
592                        }
593                    }
594                    if !taken {
595                        if let Some(w) = self.works.get_mut(&id) {
596                            w.failed_pass = self.pass;
597                        }
598                    }
599                }
600                _ => {}
601            }
602        } else {
603            self.works.insert(id, work);
604        }
605        self.drain(host);
606        taken
607    }
608
609    /// Give a citizen work: activity 9.
610    ///
611    /// Original: `FUN_1400fcd00`.
612    pub fn assign_work(&mut self, citizen: usize, host: &mut impl WorkHost) {
613        if self.works.is_empty() {
614            return;
615        }
616        let laborer = host.laborer();
617        let profession = host.profession(citizen);
618        if profession != laborer {
619            if let Some(set) = self.professions.get(&profession).cloned() {
620                if self.try_list(citizen, &set, profession, false, false, host) {
621                    return;
622                }
623            }
624        }
625        if !self.assign_emergency(citizen, host) {
626            if let Some(set) = self.professions.get(&laborer).cloned() {
627                self.try_list(citizen, &set, laborer, false, false, host);
628            }
629        }
630    }
631
632    /// Give a citizen emergency work (activity 2), for example fighting a fire. Returns whether
633    /// there was some.
634    ///
635    /// Original: `FUN_1400fca20`.
636    pub fn assign_emergency(&mut self, citizen: usize, host: &mut impl WorkHost) -> bool {
637        let mut lists: Vec<&[u32]> = Vec::new();
638        let emergency = host.has_emergency(citizen);
639        if emergency {
640            lists.push(&[EMERGENCY]);
641        } else {
642            if host.profession(citizen) == host.builder() {
643                lists.push(&[CONSTRUCTION]);
644            }
645            for (slot, categories) in EMERGENCY_LISTS {
646                let (count, limit) = host.stock(slot);
647                if count <= limit / 10 {
648                    lists.push(categories);
649                }
650            }
651        }
652        let k = host.rng().rand_below(lists.len() as u32) as usize;
653        let Some(list) = lists.get(k).copied() else {
654            return false;
655        };
656        let profession = host.profession(citizen);
657        for &c in list {
658            let set = self.categories[c as usize - 1].clone();
659            if self.try_list(citizen, &set, profession, true, emergency, host) {
660                return true;
661            }
662        }
663        false
664    }
665
666    /// `FUN_1400fce80`: the citizen's workplace's works first (when it has the list's
667    /// profession), then, for professions that work anywhere (or in an emergency), all.
668    fn try_list(
669        &mut self,
670        citizen: usize,
671        set: &BTreeSet<i32>,
672        profession: usize,
673        by_distance: bool,
674        emergency: bool,
675        host: &mut impl WorkHost,
676    ) -> bool {
677        let workplace = if emergency {
678            None
679        } else {
680            host.workplace(citizen)
681        };
682        if let Some(wp) = workplace {
683            if profession == host.profession(citizen) {
684                let order = self.sort(citizen, set, Some(wp), by_distance, host);
685                if self.offer(citizen, &order, host) {
686                    return true;
687                }
688            }
689        }
690        if host.works_anywhere(profession) || emergency {
691            let order = self.sort(citizen, set, None, by_distance, host);
692            if self.offer(citizen, &order, host) {
693                return true;
694            }
695        }
696        false
697    }
698
699    /// `FUN_1400fd160` / `FUN_1400fd4d0`: the works of `set` (of entity `owner` when given),
700    /// ordered by path rank from the citizen's cell or by priority, then by id.
701    fn sort(
702        &self,
703        citizen: usize,
704        set: &BTreeSet<i32>,
705        owner: Option<usize>,
706        by_distance: bool,
707        host: &mut impl WorkHost,
708    ) -> Vec<i32> {
709        let from = host.cell(citizen);
710        let mut keyed: Vec<(i32, i32)> = set
711            .iter()
712            .filter_map(|id| self.works.get(id))
713            .filter(|w| owner.is_none_or(|o| w.entity == o))
714            .map(|w| {
715                let key = if by_distance {
716                    host.rank(from, w.cell)
717                } else {
718                    w.priority
719                };
720                (key, w.id)
721            })
722            .collect();
723        keyed.sort();
724        keyed.dedup();
725        keyed.into_iter().map(|(_, id)| id).collect()
726    }
727
728    /// `FUN_1400fd030`.
729    fn offer(&mut self, citizen: usize, order: &[i32], host: &mut impl WorkHost) -> bool {
730        for &id in order {
731            let Some(mut work) = self.works.remove(&id) else {
732                continue;
733            };
734            let mut taken = false;
735            if work.failed_pass != self.pass {
736                host.refresh(&mut work);
737                match self.try_one(id, work, citizen, host) {
738                    2 => taken = true,
739                    1 => {
740                        if let Some(w) = self.works.get_mut(&id) {
741                            w.failed_pass = self.pass;
742                        }
743                    }
744                    _ => {}
745                }
746            } else {
747                self.works.insert(id, work);
748            }
749            self.drain(host);
750            if taken {
751                return true;
752            }
753        }
754        false
755    }
756}
757
758#[cfg(test)]
759mod tests {
760    use super::*;
761
762    /// A host whose works answer from a table and record the order they were offered.
763    struct Host {
764        rng: Random,
765        answers: BTreeMap<i32, u8>,
766        offered: Vec<i32>,
767        profession: usize,
768        workplace: Option<usize>,
769        stock: [(i32, i32); 21],
770        cells: BTreeMap<i32, i32>,
771    }
772
773    impl WorkHost for Host {
774        fn profession(&self, _: usize) -> usize {
775            self.profession
776        }
777        fn workplace(&self, _: usize) -> Option<usize> {
778            self.workplace
779        }
780        fn cell(&self, _: usize) -> i32 {
781            0
782        }
783        fn has_emergency(&self, _: usize) -> bool {
784            false
785        }
786        fn works_anywhere(&self, p: usize) -> bool {
787            p < 2
788        }
789        fn rank(&mut self, _: i32, b: i32) -> i32 {
790            self.cells.get(&b).copied().unwrap_or(b)
791        }
792        fn stock(&self, slot: usize) -> (i32, i32) {
793            self.stock[slot]
794        }
795        fn laborer(&self) -> usize {
796            0
797        }
798        fn builder(&self) -> usize {
799            1
800        }
801        fn rng(&mut self) -> &mut Random {
802            &mut self.rng
803        }
804        fn refresh(&mut self, _: &mut Work) {}
805        fn try_assign(&mut self, work: &mut Work, _: usize, _: i64) -> u8 {
806            self.offered.push(work.id);
807            self.answers.get(&work.id).copied().unwrap_or(1)
808        }
809        fn complete(&mut self, _: &mut Work, _: usize, _: i32) -> bool {
810            true
811        }
812        fn fail(&mut self, _: &mut Work, _: usize, _: i32) {}
813        fn cancel(&mut self, _: &mut Work) {}
814    }
815
816    fn host() -> Host {
817        Host {
818            rng: Random::new(7),
819            answers: BTreeMap::new(),
820            offered: Vec::new(),
821            profession: 0,
822            workplace: None,
823            stock: [(100, 100); 21],
824            cells: BTreeMap::new(),
825        }
826    }
827
828    #[test]
829    fn ids_priorities_and_offer_order() {
830        let mut jobs = Jobs::default();
831        let mut h = host();
832        let a = jobs.add(Work::new(5, 10, 0, WorkKind::Test(0)), 1 << 13, None);
833        let b = jobs.add(Work::new(6, 20, 0, WorkKind::Test(0)), 1 << 13, Some(0));
834        assert_eq!((a, b), (1, 2));
835        // The kept priority 0 is taken by `a`, so `b` moves down to -1 and is offered first.
836        assert_eq!(jobs.works[&b].priority, -1);
837        jobs.assign_work(0, &mut h);
838        assert_eq!(h.offered, vec![b, a]);
839        // Both answered "not now": skipped for the rest of this pass.
840        h.offered.clear();
841        jobs.assign_work(0, &mut h);
842        assert!(h.offered.is_empty());
843        jobs.step(1);
844        h.answers.insert(a, 2);
845        jobs.assign_work(0, &mut h);
846        assert_eq!(h.offered, vec![b, a]);
847        jobs.remove(a, &mut h);
848        assert_eq!(jobs.works.len(), 1);
849    }
850
851    #[test]
852    fn builders_try_construction_by_distance() {
853        let mut jobs = Jobs::default();
854        let mut h = host();
855        h.profession = 1;
856        let near = jobs.add(Work::new(5, 30, 1, WorkKind::Test(0)), 1 << 13, None);
857        let far = jobs.add(Work::new(6, 3, 1, WorkKind::Test(0)), 1 << 13, None);
858        h.cells.insert(30, 50);
859        h.cells.insert(3, 40);
860        // One list (construction): Rand(1) is drawn, then the works by rank 40, 50.
861        let mut check = Random::new(7);
862        jobs.assign_emergency(0, &mut h);
863        check.rand_below(1);
864        assert_eq!(h.offered, vec![far, near]);
865        assert_eq!(h.rng.rand(), check.rand());
866    }
867
868    #[test]
869    fn no_list_draws_nothing() {
870        let mut jobs = Jobs::default();
871        let mut h = host();
872        jobs.add(Work::new(5, 30, 0, WorkKind::Test(0)), 1 << 10, None);
873        assert!(!jobs.assign_emergency(0, &mut h));
874        assert_eq!(h.rng.rand(), Random::new(7).rand());
875    }
876}