1use banished_formats::game::{Merchant, TradeRow, ValueDescription};
11
12use crate::scheduler::ticks;
13use crate::town::Sim;
14use crate::work::construction::Callbacks;
15
16const PERIOD: f32 = 300.0;
18const STAY: f32 = 900.0;
20const ORDERED: f32 = 1.25;
22const ARRIVED: i32 = 0xd;
24
25#[derive(Debug, Clone, Default, PartialEq)]
27pub struct TradeTemplate {
28 pub boat: usize,
31 pub merchants: Vec<Merchant>,
33 pub rows: Vec<TradeRow>,
35 pub categories: Vec<u32>,
38 pub labels: Vec<(String, String)>,
42 pub sprites: Option<String>,
44 pub strings: Option<String>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct TradeGood {
52 pub template: usize,
54 pub material: Option<usize>,
56 pub value: ValueDescription,
58}
59
60#[derive(Debug, Clone, Default, PartialEq, Eq)]
64pub struct Order {
65 pub name: String,
67 pub goods: Vec<usize>,
69 pub state: i32,
71}
72
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
76pub struct Offer {
77 pub good: usize,
79 pub value: i32,
81 pub amount: i32,
83}
84
85#[derive(Debug, Clone, Default, PartialEq, Eq)]
91pub struct Trade {
92 pub mode: i32,
94 pub merchant: i32,
96 pub orders: Vec<Order>,
98 pub automatic: Vec<(u32, i32, i32)>,
100 pub offers: Vec<Offer>,
102 pub keep: std::collections::BTreeMap<usize, i32>,
104 pub working: bool,
106 pub held: bool,
108 pub timer: i64,
110 pub boat: Option<usize>,
112}
113
114impl Trade {
115 pub fn new(
120 t: &TradeTemplate,
121 rng: &mut crate::random::Random,
122 names: Option<&(
123 banished_formats::names::NameDatabase,
124 banished_formats::names::NameDatabase,
125 )>,
126 ) -> Trade {
127 let orders = t
128 .merchants
129 .iter()
130 .map(|_| {
131 let male = rng.rand_below(2) == 0;
132 let name = names.map_or(String::new(), |(m, f)| {
133 crate::people::names::citizen_name(if male { m } else { f }, rng)
134 });
135 Order {
136 name,
137 ..Default::default()
138 }
139 })
140 .collect();
141 Trade {
142 merchant: -1,
143 orders,
144 automatic: t
145 .categories
146 .iter()
147 .enumerate()
148 .map(|(i, &f)| (f, i as i32, 0))
149 .collect(),
150 ..Default::default()
151 }
152 }
153}
154
155impl Sim<'_, '_> {
156 pub(crate) fn trade_activate(&mut self, e: usize) {
159 let Some(site) = self.sites.get(&e) else {
160 return;
161 };
162 if site.trade.is_some() {
163 return;
164 }
165 let Some(d) = self.site_templates[site.template].trade.clone() else {
166 return;
167 };
168 self.trading_posts.push(e);
169 let trade = Trade::new(&d, self.rng, self.names);
170 self.sites.get_mut(&e).unwrap().trade = Some(trade);
171 }
172
173 fn trade_desc(&self, e: usize) -> Option<&TradeTemplate> {
174 self.site_templates[self.sites.get(&e)?.template]
175 .trade
176 .as_ref()
177 }
178
179 pub(crate) fn trade(&mut self, e: usize) -> Option<&mut Trade> {
180 self.sites.get_mut(&e)?.trade.as_mut()
181 }
182
183 fn trade_row(&self, d: &TradeTemplate) -> usize {
185 d.rows
186 .iter()
187 .position(|r| 0 <= self.population && self.population < r.population)
188 .unwrap_or(d.rows.len().saturating_sub(1))
189 }
190
191 pub(crate) fn trade_enter2(&mut self, jobs: &mut crate::work::jobs::Jobs, e: usize) {
195 let id = self.sites[&e].work_id;
196 if !jobs.works.contains_key(&id)
197 && self.workforce.workplaces.get(&e).is_some_and(|w| w.enabled)
198 {
199 self.set_trade_work(jobs, e);
200 }
201 let Some(d) = self.trade_desc(e).cloned() else {
202 return;
203 };
204 let row = d.rows[self.trade_row(&d)];
205 let f = self.rng.rand_float_tol();
206 let t = ticks((f * row.range + row.months) * PERIOD);
207 if let Some(tr) = self.trade(e) {
208 tr.timer = t;
209 }
210 }
211
212 pub(crate) fn trade_update2(&mut self, e: usize, dt: i64) {
215 let Some(tr) = self.trade(e) else { return };
216 tr.timer -= dt;
217 if 0 < tr.timer {
218 return;
219 }
220 let Some(boat_t) = self.trade_desc(e).map(|d| d.boat) else {
221 return;
222 };
223 let boat = self.add_boat(boat_t, e);
224 if let Some(tr) = self.trade(e) {
225 tr.boat = boat;
226 }
227 self.machine(e).request(0x12);
228 }
229
230 pub(crate) fn trade_leave(&mut self, e: usize) {
232 self.trading_posts.retain(|&p| p != e);
233 if let Some(boat) = self.trade(e).and_then(|t| t.boat) {
234 self.boat_orphan(boat);
235 let tr = self.trade(e).unwrap();
236 tr.offers.clear();
237 tr.merchant = -1;
238 tr.boat = None;
239 }
240 }
241
242 pub(crate) fn trade_enter13(&mut self, e: usize) {
245 self.merchant_goods(e);
246 if let Some(tr) = self.trade(e) {
247 tr.timer = ticks(STAY);
248 }
249 if self.trade(e).is_some_and(|t| t.mode == 1) {
250 self.trade_automatically(e);
251 }
252 }
253
254 pub(crate) fn trade_exit13(&mut self, e: usize) {
256 if self.trade(e).is_some_and(|t| t.mode == 2) {
257 self.trade_automatically(e);
258 }
259 if let Some(boat) = self.trade(e).and_then(|t| t.boat) {
260 self.boat_message(boat, 0);
261 }
262 let tr = self.trade(e).unwrap();
263 tr.boat = None;
264 tr.offers.clear();
265 tr.merchant = -1;
266 }
267
268 pub(crate) fn trade_update13(&mut self, e: usize, dt: i64) {
270 let Some(tr) = self.trade(e) else { return };
271 tr.timer -= dt;
272 if tr.timer < 1 && !tr.held {
273 self.machine(e).request(2);
274 }
275 }
276
277 pub fn trade_message(&mut self, e: usize, msg: i32) {
282 if self.trade(e).is_none() || self.machine(e).index < 0 {
283 return;
284 }
285 match (self.machine(e).state, msg) {
286 (0x12, crate::commerce::boat::DOCKED_MESSAGE) => {
287 self.site_events.push((ARRIVED, 2, e));
288 self.machine(e).request(0x13);
289 }
290 (0x12, crate::commerce::boat::GONE_MESSAGE) => {
291 if let Some(tr) = self.trade(e) {
292 tr.boat = None;
293 }
294 self.machine(e).request(2);
295 }
296 (0x13, 0xa) => self.machine(e).request(2),
297 _ => {}
298 }
299 }
300
301 fn merchant_goods(&mut self, e: usize) {
307 let Some(d) = self.trade_desc(e).cloned() else {
308 return;
309 };
310 let m = self.rng.rand_below(d.merchants.len() as u32) as usize;
311 let row = d.rows[self.trade_row(&d)];
312 let tr = self.trade(e).unwrap();
313 tr.offers.clear();
314 tr.merchant = m as i32;
315 let order = tr.orders.get(m).cloned().unwrap_or_default();
316 let ordered = order.state != 0 && !order.goods.is_empty();
317 let list = if ordered {
318 order.goods.clone()
319 } else {
320 self.merchant_list(&d.merchants[m])
321 };
322 if order.state != 2 {
323 if let Some(o) = self.trade(e).unwrap().orders.get_mut(m) {
324 o.goods.clear();
325 o.state = 0;
326 }
327 }
328 let lots = (d.merchants[m].lots / row.lot_divisor.max(1)).max(1);
329 for _ in 0..lots {
330 if list.is_empty() {
333 break;
334 }
335 let k = self.rng.rand_below(list.len() as u32) as usize;
336 let good = list[k];
337 let v = &self.trade_goods[good].value;
338 let amount = (v.lot / row.amount_divisor.max(1)).max(v.min_lot).max(1);
339 let mut value = v.value;
340 if ordered {
341 value = (value as f32 * ORDERED) as i32;
342 }
343 let value = value.max(1);
344 let tr = self.trade(e).unwrap();
345 match tr.offers.iter_mut().find(|o| o.good == good) {
346 Some(o) => o.amount += amount,
347 None => tr.offers.push(Offer {
348 good,
349 value,
350 amount,
351 }),
352 }
353 }
354 }
355
356 fn merchant_list(&self, m: &Merchant) -> Vec<usize> {
357 merchant_list(self.trade_goods, self.templates, self.materials, m)
358 }
359
360 pub fn toggle_trade_work(&mut self, jobs: &mut crate::work::jobs::Jobs, e: usize) {
366 let on = !self.workforce.workplaces.get(&e).is_some_and(|w| w.enabled);
367 if let Some(tr) = self.trade(e) {
368 tr.working = on;
369 }
370 if on {
371 self.set_trade_work(jobs, e);
372 } else {
373 self.remove_work(jobs, e);
374 }
375 self.enable_workplace(e, on);
376 }
377}
378
379pub fn sell_value(
385 goods: &[TradeGood],
386 materials: &[crate::work::stock::RawMaterial],
387 tr: &Trade,
388 d: Option<&TradeTemplate>,
389 material: usize,
390) -> i32 {
391 let Some(good) = goods.iter().find(|g| g.material == Some(material)) else {
392 return 0;
393 };
394 if tr.merchant < 0 {
395 return good.value.value;
396 }
397 let Some(m) = d.and_then(|d| d.merchants.get(tr.merchant as usize)) else {
398 return 0;
399 };
400 let flags = materials[material].flags;
401 if m.flags.0 & flags != 0 {
402 good.value.value
403 } else if m.flags.1 & flags != 0 {
404 (good.value.value as f32 * 0.75) as i32
405 } else {
406 0
407 }
408}
409
410pub fn merchant_list(
416 goods: &[TradeGood],
417 templates: &[crate::nature::seeding::Template],
418 materials: &[crate::work::stock::RawMaterial],
419 m: &Merchant,
420) -> Vec<usize> {
421 let mut list: Vec<usize> = m
422 .items
423 .iter()
424 .flatten()
425 .filter_map(|r| {
426 goods
427 .iter()
428 .position(|g| templates[g.template].reference.eq_ignore_ascii_case(r))
429 })
430 .collect();
431 for (i, g) in goods.iter().enumerate() {
432 if g.material
433 .is_some_and(|mat| materials[mat].flags & m.sells != 0)
434 {
435 list.push(i);
436 }
437 }
438 list
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
443pub enum TradeSetting {
444 Keep {
449 material: usize,
451 amount: i32,
453 },
454 Swap(usize, usize),
458 Automatic {
462 index: usize,
464 amount: i32,
466 },
467 Mode(i32),
472 OrderState(i32),
476 Order {
480 good: usize,
482 on: bool,
484 },
485 Held(bool),
490}
491
492#[derive(Debug, Clone, Default, PartialEq, Eq)]
497pub struct TradeWork {
498 pub haulers: std::collections::BTreeMap<usize, (usize, i32)>,
500}
501
502impl Sim<'_, '_> {
503 pub(crate) fn set_trade_work(&mut self, jobs: &mut crate::work::jobs::Jobs, e: usize) {
505 let work = crate::work::jobs::Work::new(
506 e,
507 self.rect_cell(e),
508 self.professions.laborer,
509 crate::work::jobs::WorkKind::Trade(TradeWork::default()),
510 );
511 self.set_work(jobs, e, work, 1, false);
512 }
513
514 pub fn sell_value(&self, e: usize, material: usize) -> i32 {
516 let Some(site) = self.sites.get(&e) else {
517 return 0;
518 };
519 let Some(tr) = site.trade.as_ref() else {
520 return 0;
521 };
522 sell_value(
523 self.trade_goods,
524 self.materials,
525 tr,
526 self.site_templates[site.template].trade.as_ref(),
527 material,
528 )
529 }
530
531 fn trade_risk(&mut self, e: usize) {
534 let Some(d) = self
535 .sites
536 .get(&e)
537 .and_then(|s| self.site_templates[s.template].trade_risk)
538 else {
539 return;
540 };
541 if self.visitors.0 < d.0 {
542 self.visitors.0 = d.0;
543 }
544 let time = ticks(d.1 as f32 * PERIOD);
545 if self.visitors.1 < time {
546 self.visitors.1 = time;
547 }
548 }
549
550 pub(crate) fn trade_automatically(&mut self, e: usize) {
556 self.trade_risk(e);
557 let storage = self.sites[&e].storage;
558 let capacity = self.storages[storage].capacity;
559 let mut used = self.storages[storage].used(self.materials);
560 let mut bought: std::collections::BTreeMap<usize, i32> = Default::default();
561 let requests = self
562 .trade(e)
563 .map(|t| t.automatic.clone())
564 .unwrap_or_default();
565 for (flags, _, want) in requests {
566 if want == 0 {
567 continue;
568 }
569 let wealth: i32 = self.storages[storage]
570 .items
571 .clone()
572 .iter()
573 .map(|i| self.sell_value(e, i.material) * (i.count - i.reserved))
574 .sum();
575 let offers = self.trade(e).unwrap().offers.clone();
576 let mut candidates = Vec::new();
577 let mut total = 0;
578 for (k, o) in offers.iter().enumerate() {
579 let good = &self.trade_goods[o.good];
580 if good
581 .material
582 .is_some_and(|m| self.materials[m].flags & flags != 0)
583 {
584 candidates.push(k);
585 total += o.amount;
586 }
587 }
588 let (mut spent, mut count, mut idx) = (0, 0, 0);
589 if !candidates.is_empty() {
590 while count < want && spent < wealth {
591 let k = candidates[idx];
592 let o = self.trade(e).unwrap().offers[k];
593 if 0 < o.amount {
594 let m = self.trade_goods[o.good].material.unwrap();
595 used += self.materials[m].size;
596 if capacity < used {
597 break;
598 }
599 spent += o.value;
600 *bought.entry(o.good).or_default() += 1;
601 self.trade(e).unwrap().offers[k].amount -= 1;
602 count += 1;
603 }
604 idx += 1;
605 if candidates.len() <= idx {
606 idx = 0;
607 }
608 if total <= count {
609 break;
610 }
611 }
612 }
613 let mut paid = 0;
615 let mut k = 0;
616 while k < self.storages[storage].items.len() {
617 let item = self.storages[storage].items[k].clone();
618 let v = self.sell_value(e, item.material);
619 let n = item.count - item.reserved;
620 let mut gone = 0;
621 while paid < spent && gone < n {
622 paid += v;
623 gone += 1;
624 *self.trade_value += v;
625 self.count_used(item.material, 1);
627 self.storages[storage].items[k].count -= 1;
628 used -= self.materials[item.material].size;
629 }
630 if self.storages[storage].items[k].count == 0 {
631 self.storages[storage].items.remove(k);
632 } else {
633 k += 1;
634 }
635 }
636 }
637 for (good, n) in bought {
638 if let Some(m) = self.trade_goods[good].material {
639 self.storages[storage].add(m, n);
640 self.discover(m);
641 }
642 }
643 }
644
645 pub(crate) fn discover(&mut self, material: usize) {
647 if !self.known_materials.contains(&material) {
648 self.known_materials.push(material);
649 }
650 }
651
652 pub fn exchange(&mut self, e: usize, sell: &[(usize, i32)], buy: &[(usize, i32)]) {
659 if self.trade(e).is_none_or(|t| t.merchant < 0) {
660 return;
661 }
662 self.trade_risk(e);
663 let storage = self.sites[&e].storage;
664 let mut value = 0;
665 for &(m, n) in sell {
666 if n == 0 {
667 continue;
668 }
669 value += self.sell_value(e, m) * n;
670 self.storages[storage].remove(m, n);
671 self.count_used(m, n);
672 }
673 *self.trade_value += value;
674 for &(good, n) in buy {
675 if n == 0 {
676 continue;
677 }
678 let tr = self.trade(e).unwrap();
680 if let Some(k) = tr.offers.iter().position(|o| o.good == good) {
681 tr.offers[k].amount -= n;
682 if tr.offers[k].amount == 0 {
683 tr.offers.remove(k);
684 }
685 }
686 let template = self.trade_goods[good].template;
687 if let Some(m) = self.trade_goods[good].material {
688 self.storages[storage].add(m, n);
689 self.discover(m);
690 } else {
691 if !self.known.contains(&template) {
692 self.known.push(template);
693 }
694 if self.wildlife.livestock_kinds.contains_key(&template) {
695 self.trade_livestock(e, template, n);
696 }
697 }
698 }
699 }
700
701 fn trade_livestock(&mut self, e: usize, template: usize, n: i32) {
703 let t = &self.templates[self.placed[e].template];
704 let local = |i: usize| t.create_points.get(i).copied().unwrap_or_default();
705 let (a, b) = (self.world_point(e, local(0)), self.world_point(e, local(1)));
706 let size = self.size as i32;
707 let half = (size >> 1) as f32;
708 let cell = |p: [f32; 3]| {
709 (
710 ((p[0] + half) as i32).clamp(0, size - 1),
711 ((p[1] + half) as i32).clamp(0, size - 1),
712 )
713 };
714 let (c0, c1) = (cell(a), cell(b));
715 let (x0, x1) = (c0.0.min(c1.0), c0.0.max(c1.0));
716 let (y0, y1) = (c0.1.min(c1.1), c0.1.max(c1.1));
717 let area = [
718 x0 as i16,
719 y0 as i16,
720 (x1 - x0 + 1) as i16,
721 (y1 - y0 + 1) as i16,
722 ];
723 self.with_wild(|w| w.trade_livestock(template, n, a, b, area));
724 }
725
726 pub fn trade_setting(&mut self, e: usize, setting: TradeSetting) {
731 let Some(tr) = self.trade(e) else { return };
732 match setting {
733 TradeSetting::Keep { material, amount } => {
734 tr.keep.insert(material, amount);
735 }
736 TradeSetting::Swap(a, b) => {
737 let n = tr.automatic.len();
738 if a < n && b < n {
739 tr.automatic.swap(a, b);
740 }
741 }
742 TradeSetting::Automatic { index, amount } => {
743 if let Some(x) = tr.automatic.get_mut(index) {
744 x.2 = amount;
745 }
746 }
747 TradeSetting::Mode(mode) => tr.mode = mode,
748 TradeSetting::OrderState(state) => {
749 let m = tr.merchant;
750 if let Some(o) = usize::try_from(m).ok().and_then(|m| tr.orders.get_mut(m)) {
751 o.state = state;
752 }
753 }
754 TradeSetting::Order { good, on } => {
756 let m = tr.merchant;
757 if let Some(o) = usize::try_from(m).ok().and_then(|m| tr.orders.get_mut(m)) {
758 let has = o.goods.contains(&good);
759 if !on || has {
760 o.goods.retain(|&g| g != good);
761 } else {
762 o.goods.push(good);
763 }
764 }
765 }
766 TradeSetting::Held(held) => tr.held = held,
767 }
768 }
769
770 pub(crate) fn trade_try(&mut self, work: &mut crate::work::jobs::Work, c: usize) -> u8 {
776 let e = work.entity;
777 let crate::work::jobs::WorkKind::Trade(tw) = &work.kind else {
778 return 0;
779 };
780 if self.work_max(e) <= tw.haulers.len() as i32 {
781 return 1;
782 }
783 if 0 < self.states[c].carried.used(self.materials) {
784 self.drop_off(c);
785 return 2;
786 }
787 let storage = self.sites[&e].storage;
788 let keep = self.trade(e).map(|t| t.keep.clone()).unwrap_or_default();
789 let (mut fetch, mut take) = (Vec::new(), Vec::new());
790 for m in 0..self.materials.len() {
791 let want = keep.get(&m).copied().unwrap_or(0);
792 let here = self.storages[storage].available(m);
793 let crate::work::jobs::WorkKind::Trade(tw) = &work.kind else {
794 return 0;
795 };
796 let moving: i32 = tw
797 .haulers
798 .values()
799 .filter(|(mat, _)| *mat == m)
800 .map(|(_, n)| n)
801 .sum();
802 let diff = want - moving - here;
803 if 0 < diff {
804 fetch.push((m, diff));
805 } else if diff < 0 {
806 take.push((m, -diff));
807 }
808 }
809 let here = self.citizen_cell(c);
810 let points = self.use_point_count(e);
811 for (m, n) in take {
812 let size = self.materials[m].size.max(1);
813 let carried = &self.states[c].carried;
814 let mut amount = n.min((carried.capacity - carried.used(self.materials)) / size);
815 let avail = self.storages[storage].available(m);
816 if avail == 0 {
817 continue;
818 }
819 amount = amount.min(avail);
820 let Some(found) = self.find_storage(m, amount, true, here) else {
821 continue;
822 };
823 let k = self.rng.rand_below(points as u32) as usize;
824 let source = self.use_point(e, k).0;
825 let target_entity = self.storage_sites[found.site].entity;
826 self.haul(
827 c,
828 work,
829 crate::town::Haul {
830 target_entity,
831 target_storage: found.storage,
832 target: found.target,
833 source_storage: storage,
834 source_entity: e,
835 source,
836 material: m,
837 amount,
838 param: 0,
839 partial: true,
840 complete_on_pickup: false,
841 tool: true,
842 items: Vec::new(),
843 },
844 );
845 self.took_job(c, work);
846 if let crate::work::jobs::WorkKind::Trade(tw) = &mut work.kind {
847 tw.haulers.insert(c, (m, -amount));
848 }
849 return 2;
850 }
851 for (m, n) in fetch {
852 let Some(found) = self.find_storage(m, 1, false, here) else {
853 continue;
854 };
855 let avail = self.storages[found.storage].available(m);
856 if avail == 0 {
857 continue;
858 }
859 let size = self.materials[m].size.max(1);
860 let carried = &self.states[c].carried;
861 let amount = n
862 .min(avail)
863 .min((carried.capacity - carried.used(self.materials)) / size);
864 if !self.storages[storage].fits(self.materials, m, amount) {
865 continue;
866 }
867 let k = self.rng.rand_below(points as u32) as usize;
868 let target = self.use_point(e, k).0;
869 let source_entity = self.storage_sites[found.site].entity;
870 self.haul(
871 c,
872 work,
873 crate::town::Haul {
874 target_entity: e,
875 target_storage: storage,
876 target,
877 source_storage: found.storage,
878 source_entity,
879 source: found.target,
880 material: m,
881 amount,
882 param: 0,
883 partial: false,
884 complete_on_pickup: false,
885 tool: true,
886 items: Vec::new(),
887 },
888 );
889 self.took_job(c, work);
890 if let crate::work::jobs::WorkKind::Trade(tw) = &mut work.kind {
891 tw.haulers.insert(c, (m, amount));
892 }
893 return 2;
894 }
895 1
896 }
897}