1use std::collections::BTreeMap;
14
15use crate::town::{Haul, Sim};
16use crate::work::jobs::{Work, WorkKind};
17
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct MarketWork {
23 pub plans: BTreeMap<usize, Vec<(usize, i32)>>,
25}
26
27#[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#[derive(Debug, Clone, Copy, Default)]
44struct Tally {
45 have: i32,
46 town: i32,
47 inbound: i32,
48}
49
50impl Sim<'_, '_> {
51 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 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 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 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 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 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 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 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 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 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 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 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 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}