1use 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
20pub 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#[derive(Debug, Clone, Copy)]
55pub(crate) struct ProduceResume {
56 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 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 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 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 pub(crate) fn create_cell(&self, e: usize, i: usize) -> i32 {
125 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 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 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 pub(crate) fn produce_clear_points(&mut self, e: usize) {
178 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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
746struct 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}