Skip to main content

banished_game/work/
produce.rs

1//! Workplace production: ResourceProduceComponent's states and the `ConsumeProduce` work of
2//! ConsumeProduceComponent: the wood cutter, blacksmith, tailor, tavern, quarry and mine. See
3//! docs/JOBS.md "Production".
4//!
5//! A worker of the workplace's profession takes the work at a free "use" point. In order it hauls
6//! goods the current recipe does not use out of the building, fetches missing inputs, hauls the
7//! workplace's own output piles away while its "create" points are full, and otherwise works one
8//! cycle at its use point. Every `cycles` completed cycles the next cycle also consumes the inputs
9//! and leaves the output as a pile on a free create point.
10//!
11//! Original: `FUN_140130700`.
12// vtable 0x140167640
13
14use crate::behaviour::character::{Action, MoveTo};
15use crate::pathfind::WALKABLE;
16use crate::town::{Haul, Sim};
17use crate::work::construction::{ProduceWork, Recipe};
18use crate::work::jobs::{Jobs, Work, WorkKind};
19
20/// The stock-manager slot a raw material counts towards (production limits).
21///
22/// Original: `FUN_14008e460`.
23pub fn production_group(flags: u32) -> Option<usize> {
24    const ORDER: [(u32, usize); 21] = [
25        (10, 0),
26        (11, 1),
27        (12, 2),
28        (7, 3),
29        (8, 9),
30        (9, 4),
31        (5, 5),
32        (13, 6),
33        (14, 7),
34        (17, 8),
35        (16, 10),
36        (18, 11),
37        (19, 12),
38        (20, 13),
39        (21, 14),
40        (22, 15),
41        (23, 16),
42        (24, 17),
43        (25, 18),
44        (26, 19),
45        (27, 20),
46    ];
47    ORDER
48        .iter()
49        .find(|(bit, _)| flags >> bit & 1 != 0)
50        .map(|&(_, g)| g)
51}
52
53/// What the try decided before offering the output piles' pickups ([`Sim::produce_resume`]).
54#[derive(Debug, Clone, Copy)]
55pub(crate) struct ProduceResume {
56    /// The recipe selected at the start of the try (its output is the one produced).
57    selected: usize,
58    slot: usize,
59    point: Option<(usize, i32)>,
60    all_inputs: bool,
61}
62
63impl Sim<'_, '_> {
64    fn recipes(&self, e: usize) -> Option<&crate::work::construction::ProduceTemplate> {
65        let site = self.sites.get(&e)?;
66        self.site_templates[site.template].produce.as_ref()
67    }
68
69    /// `FUN_14011e940`: the selected recipe; selector −1 draws `RandFloat` over the weights.
70    pub(crate) fn produce_select(&mut self, e: usize) -> usize {
71        let Some(state) = self.sites.get(&e).and_then(|s| s.produce.as_ref()) else {
72            return 0;
73        };
74        if state.selector >= 0 {
75            return state.selector as usize;
76        }
77        if state.selector != -1 {
78            return 0;
79        }
80        let weights = state.weights.clone();
81        let total: f32 = weights.iter().sum();
82        let r = self.rng.rand_float();
83        let mut acc = 0.0;
84        for (i, w) in weights.iter().enumerate() {
85            acc += w;
86            if r * total < acc {
87                return i;
88            }
89        }
90        0
91    }
92
93    /// `FUN_1401282f0(interact, 0, i)`: use point `i` in the world and its heading: the
94    /// point's direction (`FromYawPitch(yaw, 0)` at load, `FUN_1401289d0`) turned by the
95    /// entity transform, then `Heading`. The entity's own points come first, then its
96    /// MultiObject parts' in turn, each turned by its own transform; past them all, the
97    /// entity's origin.
98    pub(crate) fn use_point(&self, e: usize, i: usize) -> ([f32; 3], f32) {
99        let mut i = i;
100        for x in self.interact_entities(e) {
101            let p = &self.placed[x];
102            let t = &self.templates[p.template];
103            if i < t.use_points.len() {
104                let yaw = t.use_yaws.get(i).copied().unwrap_or(0.0);
105                let model = p.model(t);
106                return (model.apply(t.use_points[i]), model.heading_of(yaw));
107            }
108            i -= t.use_points.len();
109        }
110        let p = &self.placed[e];
111        let model = p.model(&self.templates[p.template]);
112        (model.apply([0.0; 3]), model.heading_of(0.0))
113    }
114
115    /// `FUN_140128540(interact, 0)`: the use points of the entity and its MultiObject parts.
116    pub(crate) fn use_point_count(&self, e: usize) -> usize {
117        self.interact_entities(e)
118            .iter()
119            .map(|&x| self.templates[self.placed[x].template].use_points.len())
120            .sum()
121    }
122
123    /// The cell of create point `i` (`FUN_1401282f0(interact, 2, i)`).
124    pub(crate) fn create_cell(&self, e: usize, i: usize) -> i32 {
125        // The entity's own create points, then its MultiObject parts' (`FUN_1401282f0`).
126        let mut i = i;
127        for x in self.interact_entities(e) {
128            let points = &self.templates[self.placed[x].template].create_points;
129            if i < points.len() {
130                return self.cell_of(self.world_point(x, points[i]));
131            }
132            i -= points.len();
133        }
134        self.cell_of(self.world_point(e, [0.0; 3]))
135    }
136
137    /// ConsumeProduceComponent slot `+0x90` (`FUN_14011e1d0`) and the work's constructor
138    /// (`FUN_1400ee6d0`): one worker slot per use point, one state per create point.
139    pub(crate) fn produce_work(&self, e: usize) -> Work {
140        let t = &self.templates[self.placed[e].template];
141        let profession = self
142            .workforce
143            .workplaces
144            .get(&e)
145            .and_then(|w| w.profession)
146            .unwrap_or(self.professions.laborer);
147        Work::new(
148            e,
149            self.rect_cell(e),
150            profession,
151            WorkKind::Produce(ProduceWork {
152                workers: vec![None; t.use_points.len()],
153                points: vec![0; t.create_points.len()],
154                full: false,
155                cycles: 0,
156            }),
157        )
158    }
159
160    /// `FUN_1401307e0`: switch production off (`+0x48`) and go idle (0xe).
161    pub(crate) fn produce_disable(&mut self, e: usize) {
162        let Some(site) = self.sites.get_mut(&e) else {
163            return;
164        };
165        if let Some(p) = &mut site.produce {
166            p.disabled = true;
167        }
168        let m = &site.machine;
169        let state = if m.pending < 0 { m.state } else { m.pending };
170        if state != 0xe {
171            site.machine.request(0xe);
172        }
173    }
174
175    /// `FUN_140130460` (transition 2 → 0xd): every create point's cell becomes ground with no
176    /// object, ready for output piles.
177    pub(crate) fn produce_clear_points(&mut self, e: usize) {
178        // `FUN_140128540(.., 2)`: the entity's and its MultiObject parts' create points.
179        let n: usize = self
180            .interact_entities(e)
181            .iter()
182            .map(|&x| self.templates[self.placed[x].template].create_points.len())
183            .sum();
184        for i in 0..n {
185            let cell = self.create_cell(e, i) as usize;
186            self.cells.set_type(cell, crate::terrain::CellType(2));
187            self.cells.objects[cell] = None;
188        }
189    }
190
191    /// `FUN_1400a1210(map, cell, 1, owner, mask)`: the nearest cell, in rings of radius 0..=6
192    /// (rows, then columns), with no object, no occupant (or `owner`), not water, no road
193    /// (built or planned), whose type is in `mask`.
194    pub(crate) fn free_cell(&self, cell: i32, owner: Option<usize>, mask: u32) -> Option<i32> {
195        self.free_cell_where(cell, owner, |t, _| mask >> (t & 0x1f) & 1 != 0)
196    }
197
198    /// `FUN_1400a1210(map, cell, 0, none, _)` (a loose pile's cell, `FUN_1400ccd90`): as
199    /// [`Sim::free_cell`], on ground (type 2) with no occupant, no flags, and the terrain's
200    /// normal at the cell's middle within `0.97` of up.
201    pub(crate) fn free_flat_cell(&self, cell: i32) -> Option<i32> {
202        let n = self.size as i32;
203        self.free_cell_where(cell, None, |t, i| {
204            if t != 2 || self.cells.occupants[i].is_some() || self.cells.flags[i] != 0 {
205                return false;
206            }
207            let half = (n >> 1) as f32 - 0.5;
208            let (x, y) = ((i as i32 % n) as f32 - half, (i as i32 / n) as f32 - half);
209            let mut normal = [0.0; 3];
210            self.cells
211                .floors
212                .ground_normal(self.size, self.terrain, x, y, &mut normal);
213            0.97 <= normal[2]
214        })
215    }
216
217    fn free_cell_where(
218        &self,
219        cell: i32,
220        owner: Option<usize>,
221        ok: impl Fn(u32, usize) -> bool,
222    ) -> Option<i32> {
223        let n = self.size as i32;
224        let (cx, cy) = (cell % n, cell / n);
225        for r in 0..=6 {
226            for y in cy - r..=cy + r {
227                for x in cx - r..=cx + r {
228                    if (y - cy).abs() != r && (x - cx).abs() != r {
229                        continue;
230                    }
231                    if x < 0 || y < 0 || x >= n || y >= n {
232                        continue;
233                    }
234                    let i = (y * n + x) as usize;
235                    if self.cells.objects[i].is_some() {
236                        continue;
237                    }
238                    if let Some(o) = self.cells.occupants[i] {
239                        if Some(o) != owner {
240                            continue;
241                        }
242                    }
243                    let t = self.cells.types[i].0 as u32;
244                    // Types 0 and 1 (`1 << t & 3`), a road (`+0x2a`) or a planned one.
245                    if 1u32 << (t & 0x1f) & 3 != 0
246                        || self.cells.road_kind[i] != 0xff
247                        || self.pending_roads.contains_key(&(i as i32))
248                    {
249                        continue;
250                    }
251                    if ok(t, i) {
252                        return Some(y * n + x);
253                    }
254                }
255            }
256        }
257        None
258    }
259
260    /// `FUN_1400ecd10`: ConsumeProduce slot `+0x20`, with the status icons 2 (limited) and 3
261    /// (missing inputs).
262    pub(crate) fn produce_try(&mut self, work: &mut Work, c: usize, _time: i64) -> u8 {
263        let e = work.entity;
264        let Some(t) = self.recipes(e).cloned() else {
265            return 0;
266        };
267        let selected = self.produce_select(e);
268        let Some(recipe) = t.recipes.get(selected).cloned() else {
269            return 1;
270        };
271        let state = self.sites[&e].produce.clone().unwrap_or_default();
272        // The stock limit of the recipe's group (all groups while choosing at random).
273        let limited = |r: &Recipe, sim: &Sim| {
274            let (count, limit) = sim.stock[r.group.clamp(0, 20) as usize];
275            limit <= count
276        };
277        let is_limited = if state.selector < 0 {
278            t.recipes.iter().any(|r| limited(r, self))
279        } else {
280            limited(&recipe, self)
281        };
282        self.set_icon(e, crate::work::professions::icon::LIMIT, is_limited);
283        // `FUN_14011e870`: an exhausted workplace switches itself off.
284        let alive = t.lifetime < 0 || state.produced < t.lifetime;
285        if is_limited || !alive {
286            if !alive {
287                self.produce_disable(e);
288            }
289            self.set_icon(e, crate::work::professions::icon::NO_WORK, false);
290            return 1;
291        }
292        let WorkKind::Produce(pw) = &work.kind else {
293            return 0;
294        };
295        let slots = pw.workers.len() as u32;
296        let start = self.rng.rand_below(slots) as usize;
297        if slots < 1 {
298            return 1;
299        }
300        let Some(slot) = (0..slots as usize)
301            .map(|k| (start + k) % slots as usize)
302            .find(|&k| pw.workers[k].is_none())
303        else {
304            return 1;
305        };
306        let store = self.sites[&e].storage;
307        let citizen_cell = self.citizen_cell(c);
308        // Goods the recipe does not use leave the building.
309        let items: Vec<(usize, i32)> = self.storages[store]
310            .items
311            .iter()
312            .map(|i| (i.material, i.count - i.reserved))
313            .collect();
314        for (material, mut amount) in items {
315            if material == recipe.output_material
316                || recipe.inputs.iter().any(|&(m, _)| m == material)
317            {
318                continue;
319            }
320            let size = self.materials[material].size.max(1);
321            let carried = &self.states[c].carried;
322            let free = carried.capacity - carried.used(self.materials);
323            if free < amount * size {
324                amount = free / size;
325            }
326            if amount == 0 {
327                self.drop_off(c);
328                return 2;
329            }
330            let Some(found) = self.find_storage_excluding(material, amount, true, work.cell, e)
331            else {
332                continue;
333            };
334            if !self.reachable(citizen_cell, work.cell) {
335                return 1;
336            }
337            // A workplace that is itself a storage location (the tavern) hands the goods out
338            // at its drop point nearest the storage found (`FUN_140132aa0`), the others at
339            // their first use point.
340            let source = if self.templates[self.placed[e].template].storage_location {
341                let toward = self.cell_of(found.target);
342                match self.storage_point(e, toward) {
343                    Some(p) => p,
344                    None => continue,
345                }
346            } else {
347                self.use_point(e, 0).0
348            };
349            let target_entity = self.storage_sites[found.site].entity;
350            self.haul(
351                c,
352                work,
353                Haul {
354                    target_entity,
355                    target_storage: found.storage,
356                    target: found.target,
357                    source_storage: store,
358                    source_entity: e,
359                    source,
360                    material,
361                    amount,
362                    param: slot as i32,
363                    partial: false,
364                    complete_on_pickup: true,
365                    ..Default::default()
366                },
367            );
368            return self.produce_taken(work, c, slot);
369        }
370        // Missing inputs: fetch them (empty-handed), else drop off what is carried.
371        let mut all_inputs = true;
372        for &(material, need) in &recipe.inputs {
373            if self.storages[store].available(material) >= need {
374                continue;
375            }
376            all_inputs = false;
377            let Some(found) = self.find_storage_excluding(material, 1, false, citizen_cell, e)
378            else {
379                continue;
380            };
381            let source_storage = found.storage;
382            if self.storages[source_storage].available(material) == 0 {
383                continue;
384            }
385            let size = self.materials[material].size;
386            let carried = &self.states[c].carried;
387            let (capacity, used) = (carried.capacity, carried.used(self.materials));
388            if size <= capacity - used && used < 1 {
389                let (target, _) = self.use_point(e, slot);
390                let source_entity = self.storage_sites[found.site].entity;
391                self.haul(
392                    c,
393                    work,
394                    Haul {
395                        target_entity: e,
396                        target_storage: store,
397                        target,
398                        source_storage,
399                        source_entity,
400                        source: found.target,
401                        material,
402                        amount: -1,
403                        param: slot as i32,
404                        partial: true,
405                        complete_on_pickup: false,
406                        ..Default::default()
407                    },
408                );
409                return self.produce_taken(work, c, slot);
410            }
411            self.drop_off(c);
412            return 2;
413        }
414        // The create points: piles gone free their point; the first free one takes output.
415        let mut free = 0;
416        let mut point = None;
417        let n_points = match &work.kind {
418            WorkKind::Produce(pw) => pw.points.len(),
419            _ => 0,
420        };
421        for i in 0..n_points {
422            let cell = self.create_cell(e, i);
423            let empty = self.cells.objects[cell as usize].is_none();
424            let WorkKind::Produce(pw) = &mut work.kind else {
425                return 0;
426            };
427            if pw.points[i] == 2 && empty {
428                pw.points[i] = 0;
429            }
430            if pw.points[i] == 0 {
431                free += 1;
432                point.get_or_insert((i, cell));
433            }
434        }
435        let WorkKind::Produce(pw) = &mut work.kind else {
436            return 0;
437        };
438        if free == n_points {
439            pw.full = false;
440        }
441        let resume = ProduceResume {
442            selected,
443            slot,
444            point,
445            all_inputs,
446        };
447        if point.is_none() {
448            pw.full = true;
449        } else if !pw.full && all_inputs {
450            return self.produce_cycle(work, c, resume);
451        }
452        // Haul the workplace's own output away first (`FUN_1400fdaf0` on each create point's
453        // pile, then go on with the cycle).
454        for i in 0..n_points {
455            let cell = self.create_cell(e, i) as usize;
456            let Some(o) = self.cells.objects[cell] else {
457                continue;
458            };
459            if o == e {
460                continue;
461            }
462            if let Some(pile) = self.piles.get(&o) {
463                if pile.work >= 0 {
464                    self.offers.push(pile.work);
465                }
466            }
467        }
468        self.produce_resume = Some(resume);
469        crate::work::jobs::NESTED
470    }
471
472    /// The rest of `FUN_1400ecd10` once no output pile was taken.
473    pub(crate) fn produce_resume(&mut self, work: &mut Work, c: usize) -> u8 {
474        let Some(resume) = self.produce_resume.take() else {
475            return 1;
476        };
477        if resume.point.is_none() {
478            return 1;
479        }
480        self.produce_cycle(work, c, resume)
481    }
482
483    /// `LAB_1400edf97`: work a cycle at the use point when every input is present.
484    fn produce_cycle(&mut self, work: &mut Work, c: usize, resume: ProduceResume) -> u8 {
485        let e = work.entity;
486        let Some((point, out_cell)) = resume.point else {
487            return 1;
488        };
489        // `WorkUnavailable` while inputs are missing.
490        self.set_icon(
491            e,
492            crate::work::professions::icon::NO_WORK,
493            !resume.all_inputs,
494        );
495        if !resume.all_inputs {
496            return 1;
497        }
498        let (target, facing) = self.use_point(e, resume.slot);
499        if !self.reachable(self.citizen_cell(c), self.cell_of(target)) {
500            return 1;
501        }
502        let Some(t) = self.recipes(e).cloned() else {
503            return 0;
504        };
505        let WorkKind::Produce(pw) = &mut work.kind else {
506            return 0;
507        };
508        let produce = t.cycles <= pw.cycles;
509        if produce {
510            pw.cycles -= t.cycles;
511            if let Some(state) = self.sites.get_mut(&e).and_then(|s| s.produce.as_mut()) {
512                state.produced += 1;
513            }
514        }
515        let mut recipe = t.recipes.get(resume.selected).cloned().unwrap_or_default();
516        // `FUN_1400c4440` selects again for the inputs it consumes (a second draw when
517        // choosing at random).
518        recipe.inputs = if produce {
519            let again = self.produce_select(e);
520            t.recipes
521                .get(again)
522                .map(|r| r.inputs.clone())
523                .unwrap_or_default()
524        } else {
525            Vec::new()
526        };
527        let param = (if produce { 0x8000 } else { 0 })
528            | (((point as i32) | 0x100) << 8)
529            | resume.slot as i32;
530        self.produce_sequence(
531            c,
532            work,
533            ProduceSequence {
534                target,
535                facing,
536                recipe,
537                produce,
538                out_cell,
539                work_type: t.work_type,
540                duration: t.duration,
541                param,
542            },
543        );
544        if produce {
545            if let WorkKind::Produce(pw) = &mut work.kind {
546                pw.points[point] = 1;
547            }
548        }
549        self.produce_taken(work, c, resume.slot)
550    }
551
552    /// The citizen writes of every assignment, and the worker slot.
553    fn produce_taken(&mut self, work: &mut Work, c: usize, slot: usize) -> u8 {
554        self.took_job(c, work);
555        if let WorkKind::Produce(pw) = &mut work.kind {
556            pw.workers[slot] = Some(c);
557        }
558        2
559    }
560
561    /// `FUN_1400c4440`: walk to the use point (with the work's tool), consume the inputs,
562    /// work, then leave the output and report; a produced batch first sends the message to
563    /// the building's component of the description's type `+0x38` (the quarry's dig).
564    fn produce_sequence(&mut self, c: usize, work: &mut Work, s: ProduceSequence) {
565        self.clear_citizen(c, Some(work));
566        self.on_empty_think(c);
567        let store = self.sites[&work.entity].storage;
568        self.push(
569            c,
570            Action::CompleteTask {
571                work: work.id,
572                param: s.param,
573            },
574        );
575        if s.produce {
576            let batch = self.states[c].carried.capacity;
577            self.push(
578                c,
579                Action::CreateRawMaterial {
580                    template: s.recipe.output,
581                    cell: s.out_cell,
582                    batch,
583                    workplace: Some(work.entity),
584                    factor: 1.0,
585                },
586            );
587            if self.templates[self.placed[work.entity].template]
588                .quarry
589                .is_some()
590            {
591                self.push(
592                    c,
593                    Action::SendMessage(crate::behaviour::character::Message {
594                        component: crate::behaviour::character::Component::Quarry,
595                        message: 0,
596                        entity: Some(work.entity),
597                        a: 0,
598                        b: 0,
599                    }),
600                );
601            }
602        }
603        self.push(c, Action::ShowTool(-1));
604        let anim = if s.work_type < 0xb {
605            s.work_type as usize + 9
606        } else {
607            8
608        };
609        self.push(c, self.perform(c, s.facing, anim, s.duration, true));
610        if s.produce && !s.recipe.inputs.is_empty() {
611            self.push(
612                c,
613                Action::ConsumeStoredRawMaterial {
614                    storage: store,
615                    items: s.recipe.inputs.clone(),
616                    reserved: false,
617                },
618            );
619        }
620        let walk = match s.work_type {
621            0 | 1 | 4..=7 => 2,
622            2 | 3 => 3,
623            _ => 1,
624        };
625        let m = MoveTo::new(s.target, walk, 0, WALKABLE, 0, -1.0, self.rng);
626        self.push(c, Action::move_to(m));
627        self.push(c, Action::ShowTool(s.work_type as i32));
628    }
629
630    /// CreateRawMaterial's start (`FUN_1400cc010`): the output's yield (`+0x40`, or `+0x44`
631    /// for an educated worker) in piles of at most the worker's capacity, on the create
632    /// point's cell or, once it holds something, the nearest free ground (`FUN_1400a1210`,
633    /// mask 4), each turned `RandTol(2) × 90°`, each counted made for the workplace
634    /// (`FUN_14008e3c0`, `+0x51` always set), the pile given to its AutoPickupComponent.
635    #[allow(clippy::too_many_arguments)]
636    pub(crate) fn create_raw_material(
637        &mut self,
638        jobs: &mut Jobs,
639        c: usize,
640        template: usize,
641        cell: i32,
642        educated: bool,
643        factor: f32,
644        workplace: Option<usize>,
645    ) {
646        let (material, yields) = {
647            let pt = &self.templates[template];
648            (pt.pile_material, pt.pile_yield)
649        };
650        let Some(material) = material else { return };
651        let base = if educated { yields.1 } else { yields.0 };
652        let mut remaining = (base as f32 * factor) as i32;
653        let per = (self.states[c].carried.capacity / self.materials[material].size.max(1)).max(1);
654        let mut search = false;
655        while remaining > 0 {
656            let n = remaining.min(per);
657            if !search && self.cells.objects[cell as usize].is_some() {
658                search = true;
659            }
660            let target = if search {
661                self.free_cell(cell, None, 4)
662            } else {
663                Some(cell)
664            };
665            if let Some(at) = target {
666                let size = self.size as i32;
667                let yaw = self.rng.rand_tol(2) as f32 * 90.0;
668                let pile = self.spawn_pile(template, (at % size, at / size), n, yaw, None);
669                self.count_made(workplace, true, material, n);
670                // A workplace with an AutoPickupComponent (the hunter's lodge, orchards,
671                // pastures) takes the pile's work (`FUN_140045170`, once it is registered).
672                if let Some(wp) = workplace
673                    .filter(|wp| self.sites.get(wp).is_some_and(|s| s.auto_pickup.is_some()))
674                {
675                    if let Some(p) = self.piles.get_mut(&pile) {
676                        p.auto_pickup = Some(wp);
677                    }
678                }
679            }
680            remaining -= n;
681        }
682        self.activate_piles(jobs);
683    }
684
685    /// `FUN_1400eca00`: ConsumeProduce slot `+0x48`. Workers that lost the task free their
686    /// slot; then create points under way (1) that no remaining worker holds (by
687    /// `(param >> 4) & 0xf`, as the original reads it) are taken as holding output (2).
688    pub(crate) fn produce_refresh(&mut self, work: &mut Work) {
689        let id = work.id;
690        let WorkKind::Produce(pw) = &mut work.kind else {
691            return;
692        };
693        let mut lost = false;
694        let mut held = Vec::new();
695        for w in pw.workers.iter_mut() {
696            let Some(c) = *w else { continue };
697            match self.characters[c].stack.holds_task(id) {
698                None => {
699                    *w = None;
700                    lost = true;
701                }
702                Some(param) => held.push((param >> 4) & 0xf),
703            }
704        }
705        if lost {
706            for (i, p) in pw.points.iter_mut().enumerate() {
707                if !held.contains(&(i as i32)) && *p == 1 {
708                    *p = 2;
709                }
710            }
711        }
712    }
713
714    /// `FUN_1400ecc90` (complete) and `FUN_1400ecc50` (fail, without the cycle bit).
715    pub(crate) fn produce_complete(work: &mut Work, param: i32, failed: bool) {
716        let WorkKind::Produce(pw) = &mut work.kind else {
717            return;
718        };
719        if let Some(w) = pw.workers.get_mut((param & 0x7f) as usize) {
720            *w = None;
721        }
722        if param >> 15 & 1 != 0 {
723            if let Some(p) = pw.points.get_mut(((param >> 8) & 0x7f) as usize) {
724                *p = 2;
725            }
726        }
727        if !failed && param & 0x10000 != 0 {
728            pw.cycles += 1;
729        }
730    }
731
732    /// `FUN_1400ee400`: every worker's stack is cleared.
733    pub(crate) fn produce_cancel(&mut self, work: &mut Work) {
734        let WorkKind::Produce(pw) = &work.kind else {
735            return;
736        };
737        for c in pw.workers.clone().into_iter().flatten() {
738            self.clear_citizen(c, Some(work));
739        }
740        if let WorkKind::Produce(pw) = &mut work.kind {
741            pw.workers.iter_mut().for_each(|w| *w = None);
742        }
743    }
744}
745
746/// `FUN_1400c4440`'s arguments.
747struct ProduceSequence {
748    target: [f32; 3],
749    facing: f32,
750    recipe: Recipe,
751    produce: bool,
752    out_cell: i32,
753    work_type: u32,
754    duration: f32,
755    param: i32,
756}