Skip to main content

banished_game/commerce/
market.rs

1//! Markets: the market building, its vendors' work, and the market's locations. See
2//! docs/MARKETS.md.
3//!
4//! Vendors keep the market stocked by category, each up to its share of the market's room. A
5//! category stocked beyond twice its share is carried away first. Otherwise a vendor picks the town
6//! storage most worth the trip (the goods most wanted, by distance), and brings what it can carry
7//! of the categories still under twice their share.
8//!
9//! Original: `MarketComponent` (a `ResourceProduceComponent`; its work from slot `FUN_14004f3a0`),
10//! `MarketWork`, `MarketLocationComponent`; `FUN_14004fa70`.
11// vtable 0x14015fd60; vtable 0x140167b40; +0x90
12
13use std::collections::BTreeMap;
14
15use crate::town::{Haul, Sim};
16use crate::work::jobs::{Work, WorkKind};
17
18// +0x48
19/// MarketWork: each vendor's plan (material, count) on its way to the market; empty for a vendor
20/// carrying goods away.
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct MarketWork {
23    /// Each vendor's plan: `(raw material, count)`.
24    pub plans: BTreeMap<usize, Vec<(usize, i32)>>,
25}
26
27/// A category of the try's table (0x20 bytes): its flags, the units the market holds, the
28/// units on their way, the units in the town's storages, the fill, the fill over its share,
29/// the units in the storage weighed, and the room left.
30#[derive(Debug, Clone, Copy, Default)]
31struct Category {
32    flags: u32,
33    have: i32,
34    inbound: i32,
35    town: i32,
36    fill: f32,
37    ratio: f32,
38    here: i32,
39    room: i32,
40}
41
42/// The try's per-material tally (`+8` in the market, `+0xc` in the town, `+0x10` on its way).
43#[derive(Debug, Clone, Copy, Default)]
44struct Tally {
45    have: i32,
46    town: i32,
47    inbound: i32,
48}
49
50impl Sim<'_, '_> {
51    /// Slot `+0x90` (`FUN_14004f3a0`).
52    pub(crate) fn market_work(&self, e: usize) -> Work {
53        let profession = self
54            .workforce
55            .workplaces
56            .get(&e)
57            .and_then(|w| w.profession)
58            .unwrap_or(self.professions.laborer);
59        Work::new(
60            e,
61            self.rect_cell(e),
62            profession,
63            WorkKind::Market(MarketWork::default()),
64        )
65    }
66
67    /// A building that produces (ConsumeProduceComponent): vendors pass its storage over.
68    fn produces(&self, e: usize) -> bool {
69        self.sites
70            .get(&e)
71            .is_some_and(|s| self.site_templates[s.template].produce.is_some())
72    }
73
74    /// A random use point of the market (`Rand` over its InteractComponent's points).
75    fn market_point(&mut self, e: usize) -> [f32; 3] {
76        let count = self.use_point_count(e);
77        let k = self.rng.rand_below(count as u32) as usize;
78        self.use_point(e, k).0
79    }
80
81    /// MarketWork's slot `+0x20` (`FUN_1400f5450`).
82    pub(crate) fn market_try(&mut self, work: &mut Work, c: usize) -> u8 {
83        let e = work.entity;
84        let WorkKind::Market(mw) = &work.kind else {
85            return 0;
86        };
87        if self.work_max(e) <= mw.plans.len() as i32 {
88            return 1;
89        }
90        if 0 < self.states[c].carried.used(self.materials) {
91            self.drop_off(c);
92            return 2;
93        }
94        let Some(d) = self
95            .sites
96            .get(&e)
97            .and_then(|s| self.site_templates[s.template].market.clone())
98        else {
99            return 0;
100        };
101        let storage = self.sites[&e].storage;
102        let here = self.citizen_cell(c);
103        // `FUN_14011ba10`: the room the vendor has.
104        let carried = &self.states[c].carried;
105        let mut carry = carried.capacity - carried.used(self.materials);
106        let cap = self.storages[storage].capacity as f32;
107        if d.threshold <= self.storages[storage].used(self.materials) as f32 / cap {
108            return 1;
109        }
110        let mut cats: Vec<Category> = d
111            .quotas
112            .iter()
113            .map(|&(flags, _)| Category {
114                flags,
115                ..Default::default()
116            })
117            .collect();
118        let mut tally: BTreeMap<usize, Tally> = BTreeMap::new();
119        let flags_of = |sim: &Self, m: usize| sim.materials[m].flags;
120        // What the market holds (the per-material tally adds `size Ă— count²`, as the original).
121        for item in self.storages[storage].items.clone() {
122            let avail = item.count - item.reserved;
123            let units = self.materials[item.material].size * avail;
124            tally.entry(item.material).or_default().have += avail * units;
125            let flags = flags_of(self, item.material);
126            for cat in cats.iter_mut().filter(|k| k.flags & flags != 0) {
127                cat.have += units;
128            }
129        }
130        // What is on its way.
131        if let WorkKind::Market(mw) = &work.kind {
132            for plan in mw.plans.values() {
133                for &(m, n) in plan {
134                    tally.entry(m).or_default().inbound += n;
135                    let units = self.materials[m].size * n;
136                    let flags = flags_of(self, m);
137                    for cat in cats.iter_mut().filter(|k| k.flags & flags != 0) {
138                        cat.inbound += units;
139                    }
140                }
141            }
142        }
143        // What the town's storages hold (not the workshops').
144        for site in self.storage_sites.iter().filter(|s| s.radius.is_none()) {
145            if self.produces(site.entity) {
146                continue;
147            }
148            for item in &self.storages[site.storage].items {
149                let units = (item.count - item.reserved) * self.materials[item.material].size;
150                tally.entry(item.material).or_default().town += units;
151                let flags = self.materials[item.material].flags;
152                for cat in cats.iter_mut().filter(|k| k.flags & flags != 0) {
153                    cat.town += units;
154                }
155            }
156        }
157        let inv = 1.0 / cap;
158        for (cat, &(_, share)) in cats.iter_mut().zip(&d.quotas) {
159            cat.fill = (cat.inbound + cat.have) as f32 * inv;
160            cat.ratio = cat.fill / share;
161            cat.room = (cap * share) as i32 - cat.inbound - cat.have;
162        }
163        // A category with more than twice its share: the goods above 1.25 times it go to
164        // another storage.
165        for (i, cat) in cats.iter().enumerate() {
166            let share = d.quotas[i].1;
167            if share * 2.0 >= cat.have as f32 * inv {
168                continue;
169            }
170            let (have, flags) = (cat.have, cat.flags);
171            let keep = (share * 1.25 * cap) as i32;
172            for item in self.storages[storage].items.clone() {
173                let m = item.material;
174                if self.materials[m].flags & flags == 0 {
175                    continue;
176                }
177                let size = self.materials[m].size.max(1);
178                let excess = (have - keep) / size;
179                let mut amount = carry / size;
180                if item.count - item.reserved < amount {
181                    amount = item.count - item.reserved;
182                }
183                if excess < amount {
184                    amount = excess;
185                }
186                if amount <= 0 {
187                    continue;
188                }
189                let Some(found) = self.find_storage_excluding(m, amount, true, here, e) else {
190                    continue;
191                };
192                let source = self.market_point(e);
193                let target_entity = self.storage_sites[found.site].entity;
194                self.haul(
195                    c,
196                    work,
197                    Haul {
198                        target_entity,
199                        target_storage: found.storage,
200                        target: found.target,
201                        source_storage: storage,
202                        source_entity: e,
203                        source,
204                        material: m,
205                        amount,
206                        param: 0,
207                        partial: true,
208                        complete_on_pickup: false,
209                        tool: true,
210                        items: Vec::new(),
211                    },
212                );
213                self.took_job(c, work);
214                if let WorkKind::Market(mw) = &mut work.kind {
215                    mw.plans.insert(c, Vec::new());
216                }
217                return 2;
218            }
219        }
220        // The least stocked categories first (`FUN_14002bfe0`).
221        crate::behaviour::ai::crt_qsort_by(&mut cats, |a, b| {
222            if a.ratio > b.ratio {
223                1
224            } else if a.ratio < b.ratio {
225                -1
226            } else {
227                0
228            }
229        });
230        // The storage most worth the trip: the goods each category lacks, the first counting
231        // most, over the path rank (or `n +` distance).
232        let mut best = None;
233        let mut best_score = 0.0f32;
234        let sites: Vec<(usize, usize, usize)> = self
235            .storage_sites
236            .iter()
237            .enumerate()
238            .filter(|(_, s)| s.radius.is_none())
239            .map(|(k, s)| (k, s.entity, s.storage))
240            .collect();
241        for (k, entity, s) in sites {
242            let entrance = self.entrance_cell(entity);
243            if self.produces(entity) || entrance < 0 || !self.reachable(here, entrance) {
244                continue;
245            }
246            for cat in cats.iter_mut() {
247                cat.here = 0;
248            }
249            for item in &self.storages[s].items {
250                let units = (item.count - item.reserved) * self.materials[item.material].size;
251                let flags = self.materials[item.material].flags;
252                for cat in cats.iter_mut().filter(|c| c.flags & flags != 0) {
253                    cat.here += units;
254                }
255            }
256            let rank = self.rank_or_distance(entrance, here);
257            let w = if 0 < rank { 1.0 / rank as f32 } else { 1.0 };
258            let mut score = 0.0f32;
259            for (i, cat) in cats.iter().enumerate() {
260                score += (1.0 - cat.ratio) * cat.here.min(carry) as f32 * (1.0 / (i as f32 + 1.0));
261            }
262            if best_score < score * w {
263                best = Some(k);
264                best_score = score * w;
265            }
266        }
267        let Some(k) = best else { return 1 };
268        let (src_entity, src_storage) =
269            (self.storage_sites[k].entity, self.storage_sites[k].storage);
270        // Its goods of a category still under twice its share (the share read at the sorted
271        // category's index, as the original): the market's least stocked first.
272        let mut cands: Vec<(usize, i32, i32, usize)> = Vec::new();
273        for item in self.storages[src_storage].items.clone() {
274            let flags = self.materials[item.material].flags;
275            if let Some(i) = cats
276                .iter()
277                .enumerate()
278                .position(|(i, cat)| cat.flags & flags != 0 && cat.fill < d.quotas[i].1 * 2.0)
279            {
280                let t = tally.get(&item.material).copied().unwrap_or_default();
281                cands.push((
282                    item.material,
283                    t.inbound + t.have,
284                    item.count - item.reserved,
285                    i,
286                ));
287            }
288        }
289        crate::behaviour::ai::crt_qsort_by(&mut cands, |a, b| a.1 - b.1);
290        let target = self.market_point(e);
291        // The storage's drop point nearest the vendor (`FUN_140132aa0`).
292        let source = self
293            .storage_point(src_entity, here)
294            .unwrap_or_else(|| self.ground_at(self.rect_cell(src_entity)));
295        let mut plan = Vec::new();
296        for (m, _, avail, i) in cands {
297            let size = self.materials[m].size.max(1);
298            let mut amount = carry / size;
299            if avail < amount {
300                amount = avail;
301            }
302            if cats[i].room / size < amount {
303                amount = cats[i].room / size;
304            }
305            if 0 < amount {
306                plan.push((m, amount));
307                carry -= amount * size;
308                cats[i].room -= amount * size;
309            }
310        }
311        if plan.is_empty() {
312            return 1;
313        }
314        self.haul(
315            c,
316            work,
317            Haul {
318                target_entity: e,
319                target_storage: storage,
320                target,
321                source_storage: src_storage,
322                source_entity: src_entity,
323                source,
324                material: usize::MAX,
325                amount: -1,
326                param: 0,
327                partial: true,
328                complete_on_pickup: false,
329                tool: true,
330                items: plan.clone(),
331            },
332        );
333        self.took_job(c, work);
334        if let WorkKind::Market(mw) = &mut work.kind {
335            mw.plans.insert(c, plan);
336        }
337        2
338    }
339}