Skip to main content

banished_game/commerce/
trade.rs

1//! Trade: TradeComponent (the trading post) and its merchants. See docs/TRADE.md.
2//!
3//! Now and then (more often in bigger towns) a merchant's boat comes up the river; while it is
4//! docked the merchant's goods can be traded for the post's. A merchant brings what was ordered
5//! from it, else lots of the goods it deals in.
6//!
7//! Original: `FUN_14005cd90`, `FUN_14005cfb0`.
8// vtable 0x140160df0
9
10use banished_formats::game::{Merchant, TradeRow, ValueDescription};
11
12use crate::scheduler::ticks;
13use crate::town::Sim;
14use crate::work::construction::Callbacks;
15
16/// `DAT_140178934`.
17const PERIOD: f32 = 300.0;
18/// `DAT_140178944`: how long a merchant stays (s).
19const STAY: f32 = 900.0;
20/// `DAT_140178858`: ordered goods cost more.
21const ORDERED: f32 = 1.25;
22/// Event type 13: a merchant arrived.
23const ARRIVED: i32 = 0xd;
24
25/// A resolved TradeDescription.
26#[derive(Debug, Clone, Default, PartialEq)]
27pub struct TradeTemplate {
28    // +0x30
29    /// The boat template.
30    pub boat: usize,
31    /// The merchants.
32    pub merchants: Vec<Merchant>,
33    /// The rows of the trade window.
34    pub rows: Vec<TradeRow>,
35    // +0x38
36    /// The categories' flags.
37    pub categories: Vec<u32>,
38    // +0x20; +0x28
39    /// The categories' icon and name keys (the trade window's), in the description's sprite sheet
40    /// and string table.
41    pub labels: Vec<(String, String)>,
42    /// The sprite sheet of the icons.
43    pub sprites: Option<String>,
44    /// The string table of the names.
45    pub strings: Option<String>,
46}
47
48/// A good with a ValueDescription: its template, its raw material (none for seeds and
49/// livestock) and value.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct TradeGood {
52    /// The template.
53    pub template: usize,
54    /// The raw material; none for seeds and livestock.
55    pub material: Option<usize>,
56    /// The value.
57    pub value: ValueDescription,
58}
59
60// +0x30
61/// A merchant's order (TradeComponent, 0x20 bytes): the merchant's name, the goods ordered and its
62/// state (0 none, 1 once, 2 every time).
63#[derive(Debug, Clone, Default, PartialEq, Eq)]
64pub struct Order {
65    /// The merchant's name.
66    pub name: String,
67    /// The goods ordered.
68    pub goods: Vec<usize>,
69    /// The order's state: 0 none, 1 once, 2 every time.
70    pub state: i32,
71}
72
73// +0x50
74/// A good the merchant brought (0x10 bytes): the good, its value and how many.
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
76pub struct Offer {
77    /// The good.
78    pub good: usize,
79    /// Its value.
80    pub value: i32,
81    /// How many.
82    pub amount: i32,
83}
84
85// +0x28; +0x2c; +0x30; +0x40; +0x50; +0x60; +0x70; +0x71; +0x78; +0x80
86/// TradeComponent: when it trades by itself (0 never, 1 when a merchant arrives, 2 when it leaves),
87/// the merchant docked (−1), the orders by merchant, the automatic trades by category (flags,
88/// index, amount), the merchant's goods, the goods to keep at the post by raw material, TradeWork
89/// on, held (the trade window is open), the time to the next merchant or until it leaves, the boat.
90#[derive(Debug, Clone, Default, PartialEq, Eq)]
91pub struct Trade {
92    /// When it trades by itself: 0 never, 1 when a merchant arrives, 2 when it leaves.
93    pub mode: i32,
94    /// The merchant docked (−1 none).
95    pub merchant: i32,
96    /// The orders, by merchant.
97    pub orders: Vec<Order>,
98    /// The automatic trades by category: `(flags, index, amount)`.
99    pub automatic: Vec<(u32, i32, i32)>,
100    /// The merchant's goods.
101    pub offers: Vec<Offer>,
102    /// The goods to keep at the post, by raw material.
103    pub keep: std::collections::BTreeMap<usize, i32>,
104    /// Whether the post's hauling work is on.
105    pub working: bool,
106    /// Whether the trade window is open.
107    pub held: bool,
108    /// The time to the next merchant, or until it leaves.
109    pub timer: i64,
110    /// The boat, if any.
111    pub boat: Option<usize>,
112}
113
114impl Trade {
115    /// The activation: every merchant a name (`Rand(2)` picks the sex) and no order; every category
116    /// no automatic trade.
117    ///
118    /// Original: `FUN_14005cb80`.
119    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    /// TradeComponent's activation (slot `+0x30`, `FUN_14005cb30`): the post joins the town's
157    /// and its merchants get their names (`FUN_14005cb80`).
158    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    /// `FUN_14005be50`: the first row whose population the town is below (else the last).
184    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    /// State 2 enter (`FUN_14005b530`): with the post's workplace on and no work yet, its
192    /// TradeWork; the time to the next merchant (`FUN_14005bdb0`: the row's months ±
193    /// `RandFloatTol ×` its range, × 300 s).
194    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    /// State 2 update (`FUN_14005b3c0`): when the time is out, the merchant's boat sets out
213    /// and the post waits for it (0x12).
214    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    /// The transitions to 3 (`FUN_14005bac0`): the post leaves the town's; its boat sails off.
231    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    /// State 0x13 enter (`FUN_14005b160`): the merchant's goods (`FUN_14005bee0`); it stays
243    /// 900 s; trading by itself on arrival.
244    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    /// State 0x13 exit (`FUN_14005b030`): trading by itself on leaving; the boat casts off.
255    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    /// State 0x13 update (`FUN_14005b0e0`): the stay runs out, unless the trade window holds it.
269    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    /// The post's state messages: 9 in 0x12 the boat docked: event 13 at severity 2, → 0x13; 0xb in
278    /// 0x12 it went without docking: → 2; 0xa in 0x13 the merchant leaves now.
279    ///
280    /// Original: `FUN_14005b1b0`, `FUN_14005b320`, `FUN_14005bba0`.
281    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    /// `FUN_14005bee0`: a random merchant (`Rand(merchants)`); what was ordered from it, else
302    /// the goods it deals in (`FUN_140059fd0`); a single order is used up. Then `merchant lots
303    /// / the row's lot divisor` lots (at least one) of a random good: `max(lot / amount
304    /// divisor, smallest lot)` (at least one) of it at its value (× 1.25 when ordered, at least
305    /// one), added to its offer if it has one.
306    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            // Deviation: a merchant with nothing to sell brings nothing (the original reads
331            // an empty list).
332            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    // slot +0x70
361    /// The TradeWork switched (the post's work button): the workplace on with the work, or off
362    /// without it.
363    ///
364    /// Original: `FUN_14005c960`.
365    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
379// +0x14; +0x18
380/// What the merchant docked gives for one: with none, its value; else its value for what it buys,
381/// three quarters for what it takes, else nothing.
382///
383/// Original: `FUN_14005ae80`.
384pub 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
410// +0x10
411/// The merchant's own goods, then every good whose raw material has a flag it deals in, in the
412/// factory's order.
413///
414/// Original: `FUN_140059fd0`.
415pub 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/// What the trade window changes on the post's TradeComponent.
442#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
443pub enum TradeSetting {
444    // +0x60
445    /// Storage tab spinner `2000 + i`: what to keep at the post.
446    ///
447    /// Original: `FUN_140086f80`.
448    Keep {
449        /// The raw material.
450        material: usize,
451        /// The amount to keep.
452        amount: i32,
453    },
454    /// Purchase tab up `5000 + i` / down `6000 + i`: two automatic trades change places.
455    ///
456    /// Original: `FUN_1400871d0`.
457    Swap(usize, usize),
458    /// Purchase tab spinner `7000 + i`: the automatic trade's amount.
459    ///
460    /// Original: `FUN_140087120`.
461    Automatic {
462        /// The automatic trade.
463        index: usize,
464        /// The amount.
465        amount: i32,
466    },
467    // +0x28
468    /// `comboPurchase` (`0x4e22`): when it trades by itself.
469    ///
470    /// Original: `FUN_140087090`.
471    Mode(i32),
472    /// `comboOrder` (`0x4e23`): the docked merchant's order state.
473    ///
474    /// Original: `FUN_140087350`.
475    OrderState(i32),
476    /// Order tab check `8000 + i`: a good ordered from the docked merchant.
477    ///
478    /// Original: `FUN_1400872a0`.
479    Order {
480        /// The good.
481        good: usize,
482        /// Whether it is ordered.
483        on: bool,
484    },
485    // +0x71
486    /// The window is open or closed.
487    ///
488    /// Original: `FUN_140087f40`, `FUN_140087ed0`.
489    Held(bool),
490}
491
492// vtable 0x140167e30; +0x48
493/// TradeWork: each hauler and what it carries to (more) or from (fewer) the post.
494///
495/// Original: `FUN_1400fc370`.
496#[derive(Debug, Clone, Default, PartialEq, Eq)]
497pub struct TradeWork {
498    /// Each hauler and what it carries: `(raw material, count)`.
499    pub haulers: std::collections::BTreeMap<usize, (usize, i32)>,
500}
501
502impl Sim<'_, '_> {
503    /// `FUN_140134130(work, 1, 0, TradeWork)`.
504    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    /// [`sell_value`] at the post `e`.
515    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    /// `FUN_14005adf0`: trading brings the visitors' disease risk (TradeDescription `+0x68`
532    /// for `+0x6c` months).
533    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    /// `FUN_14005a430`: for every category with an amount set, the merchant's goods of that
551    /// category are bought one at a time in turn (each fitting the post's storage) up to the
552    /// amount, while what was bought is worth less than the post's goods; then the post's
553    /// goods pay, item by item, one at a time at the merchant's value (goods it has no use for
554    /// go for nothing). What was bought joins the post's storage.
555    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            // The post pays.
614            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                    // `FUN_14008e460`: each one traded away counts as used.
626                    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    /// `FUN_140097330`: the material joins the known list once.
646    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    /// TradeUI's trade button, while the merchant is docked: the visitors' disease risk; what is
653    /// sold leaves the post (used) and its value counts as traded away; what is bought leaves the
654    /// merchant's goods and arrives: raw materials in the post's storage (known), seeds known,
655    /// livestock known and at the post.
656    ///
657    /// Original: `FUN_1400868f0`, `FUN_14005b640`.
658    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            // `FUN_14005c400`: the offer shrinks, gone at none.
679            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    /// `FUN_14005b640`'s area: the cells between the post's first two create points.
702    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    /// TradeUI's settings: see [`TradeSetting`].
727    ///
728    /// Original: `FUN_140086f80`, `FUN_1400871d0`, `FUN_140087120`, `FUN_140087090`,
729    /// `FUN_140087350`, `FUN_1400872a0`.
730    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            // `FUN_140059df0`: on adds the good (once), off takes it out.
755            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    /// TradeWork's slot `+0x20` (`FUN_1400fb210`): below its most workers, a hauler carrying
771    /// something drops it off first. Then each raw material of the town's against what the
772    /// post keeps (`+0x60`), what it holds and what is on its way: too much is carried to a
773    /// storage that takes it (from a random use point); too little is fetched from a storage
774    /// that has it to a random use point, as much as the hauler carries and the post takes.
775    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}