1use crate::random::Random;
6
7#[derive(Debug, Clone, Default)]
9pub struct RawMaterial {
10 pub reference: String,
12 pub size: i32,
15 pub flags: u32,
18 pub tool_uses: i32,
21 pub preferred: bool,
24 pub pile_spacing: i32,
29 pub pile_amount: i32,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Item {
40 pub material: usize,
42 pub count: i32,
44 pub reserved: i32,
46}
47
48#[derive(Debug, Clone, Default)]
50pub struct Storage {
51 pub capacity: i32,
54 pub accepts: u32,
56 pub items: Vec<Item>,
58}
59
60impl Storage {
61 pub fn used(&self, materials: &[RawMaterial]) -> i32 {
65 self.items.iter().fold(0i32, |n, e| {
66 n.wrapping_add(materials[e.material].size.wrapping_mul(e.count))
67 })
68 }
69
70 pub fn fits(&self, materials: &[RawMaterial], material: usize, count: i32) -> bool {
74 self.accepts & materials[material].flags != 0
75 && materials[material]
76 .size
77 .wrapping_mul(count)
78 .wrapping_add(self.used(materials))
79 <= self.capacity
80 }
81
82 pub fn entry(&self, material: usize) -> Option<&Item> {
84 self.items.iter().find(|e| e.material == material)
85 }
86
87 pub fn available(&self, material: usize) -> i32 {
91 self.entry(material)
92 .map_or(0, |e| e.count.wrapping_sub(e.reserved))
93 }
94
95 pub fn add(&mut self, material: usize, count: i32) {
99 if let Some(e) = self.items.iter_mut().find(|e| e.material == material) {
100 e.count = e.count.wrapping_add(count);
101 } else {
102 self.items.push(Item {
103 material,
104 count,
105 reserved: 0,
106 });
107 }
108 }
109
110 pub fn remove(&mut self, material: usize, count: i32) {
114 if let Some(i) = self.items.iter().position(|e| e.material == material) {
115 self.items[i].count = self.items[i].count.wrapping_sub(count);
116 if self.items[i].count == 0 {
117 self.items.remove(i);
118 }
119 }
120 }
121
122 pub fn reserve(&mut self, material: usize, count: i32) {
126 if let Some(e) = self.items.iter_mut().find(|e| e.material == material) {
127 e.reserved = e.reserved.wrapping_add(count);
128 }
129 }
130
131 pub fn unreserve(&mut self, material: usize, count: i32) {
135 if let Some(e) = self.items.iter_mut().find(|e| e.material == material) {
136 e.reserved = e.reserved.wrapping_sub(count).max(0);
137 }
138 }
139
140 pub fn find_item(
145 &self,
146 materials: &[RawMaterial],
147 category: u32,
148 rng: &mut Random,
149 ) -> Option<usize> {
150 let bit = *[5, 6, 9, 13, 14].get(category as usize)?;
151 let eligible = |e: &&Item| {
152 materials[e.material].flags & (1 << bit) != 0 && e.count.wrapping_sub(e.reserved) > 0
153 };
154 let count = self.items.iter().filter(eligible).count();
155 if count == 0 {
156 return None;
157 }
158 let preferred = self
159 .items
160 .iter()
161 .filter(eligible)
162 .enumerate()
163 .filter(|(_, e)| materials[e.material].preferred)
164 .map(|(i, _)| i)
165 .last();
166 let index = preferred.unwrap_or_else(|| rng.rand_below(count as u32) as usize);
167 self.items
168 .iter()
169 .filter(eligible)
170 .nth(index)
171 .map(|e| e.material)
172 }
173}
174
175#[derive(Debug, Clone)]
178pub struct StorageSite {
179 pub entity: usize,
181 pub storage: usize,
183 pub location_flags: u32,
186 pub radius: Option<i32>,
189}
190
191#[derive(Debug, Clone, PartialEq)]
193pub struct StorageMatch {
194 pub site: usize,
196 pub storage: usize,
198 pub material: usize,
200 pub target: [f32; 3],
202}
203
204pub trait StorageSearchMap {
208 fn entrance(&mut self, entity: usize, rng: &mut Random) -> i32;
210 fn rank(&mut self, a: i32, b: i32) -> i32;
212 fn market_position(
214 &mut self,
215 entity: usize,
216 from: i32,
217 rng: &mut Random,
218 ) -> Option<(i32, [f32; 3])>;
219 fn storage_position(&mut self, entity: usize, from: i32) -> Option<(i32, [f32; 3])>;
221}
222
223#[allow(clippy::too_many_arguments)]
228pub fn find_storage(
229 sites: &[StorageSite],
230 storages: &[Storage],
231 materials: &[RawMaterial],
232 category: u32,
233 cell: i32,
234 home: i32,
235 map: &mut impl StorageSearchMap,
236 rng: &mut Random,
237) -> Option<StorageMatch> {
238 let (_, found) = scan_storages(
239 sites,
240 storages,
241 materials,
242 category,
243 cell,
244 Some(home),
245 i32::MAX,
246 map,
247 rng,
248 );
249 found
250}
251
252#[allow(clippy::too_many_arguments)]
258pub fn scan_storages(
259 sites: &[StorageSite],
260 storages: &[Storage],
261 materials: &[RawMaterial],
262 category: u32,
263 cell: i32,
264 home: Option<i32>,
265 best: i32,
266 map: &mut impl StorageSearchMap,
267 rng: &mut Random,
268) -> (i32, Option<StorageMatch>) {
269 let mut best = best;
270 let mut found = None;
271 let mut covered = false;
272 for (site, s) in sites.iter().enumerate().filter(|(_, s)| s.radius.is_some()) {
273 let entrance = map.entrance(s.entity, rng);
274 let in_radius = home.is_some_and(|h| h >= 0 && map.rank(entrance, h) <= s.radius.unwrap());
275 if map.rank(entrance, cell) < best {
276 if let Some(material) = storages[s.storage].find_item(materials, category, rng) {
277 covered |= in_radius;
278 if let Some((rank, target)) = map.market_position(s.entity, cell, rng) {
279 if rank >= 0 && rank < best {
280 best = rank;
281 found = Some(StorageMatch {
282 site,
283 storage: s.storage,
284 material,
285 target,
286 });
287 }
288 }
289 }
290 }
291 }
292 if covered && found.is_some() {
293 return (best, found);
294 }
295 for (site, s) in sites.iter().enumerate().filter(|(_, s)| s.radius.is_none()) {
296 let entrance = map.entrance(s.entity, rng);
297 if map.rank(entrance, cell) < best {
298 let inventory = &storages[s.storage];
299 if let Some(material) = inventory.find_item(materials, category, rng) {
300 let flags = if s.location_flags == 0 {
301 inventory.accepts
302 } else {
303 s.location_flags
304 };
305 if materials[material].flags & flags != 0 {
306 if let Some((rank, target)) = map.storage_position(s.entity, cell) {
307 if rank >= 0 && rank < best {
308 best = rank;
309 found = Some(StorageMatch {
310 site,
311 storage: s.storage,
312 material,
313 target,
314 });
315 }
316 }
317 }
318 }
319 }
320 }
321 (best, found)
322}
323
324#[allow(clippy::too_many_arguments)]
332pub fn find_material_storage(
333 sites: &[StorageSite],
334 storages: &[Storage],
335 materials: &[RawMaterial],
336 material: usize,
337 count: i32,
338 put: bool,
339 cell: i32,
340 exclude: Option<usize>,
341 map: &mut impl StorageSearchMap,
342 rng: &mut Random,
343) -> Option<StorageMatch> {
344 let mut best = i32::MAX;
345 let mut found = None;
346 for market in [false, true] {
347 for (site, s) in sites.iter().enumerate() {
348 if s.radius.is_some() != market || Some(s.entity) == exclude {
349 continue;
350 }
351 let inventory = &storages[s.storage];
352 if !market {
353 let flags = if s.location_flags == 0 {
354 inventory.accepts
355 } else {
356 s.location_flags
357 };
358 if materials[material].flags & flags == 0 {
359 continue;
360 }
361 }
362 let ok = if put {
363 inventory.fits(materials, material, count)
364 } else {
365 inventory.available(material).wrapping_sub(count) >= 0
366 };
367 if !ok {
368 continue;
369 }
370 let entrance = map.entrance(s.entity, rng);
371 let rank = map.rank(entrance, cell);
372 if rank < best {
373 let point = if market {
374 map.market_position(s.entity, cell, rng)
375 } else {
376 map.storage_position(s.entity, cell)
377 };
378 if let Some((r, target)) = point {
379 if r >= 0 && r < best {
380 best = rank;
381 found = Some(StorageMatch {
382 site,
383 storage: s.storage,
384 material,
385 target,
386 });
387 }
388 }
389 }
390 }
391 }
392 found
393}
394
395pub fn consume_stored(storage: &mut Storage, items: &[(usize, i32)], reserved: bool) -> bool {
401 if items.is_empty() {
403 return false;
404 }
405 for &(material, count) in items {
406 if reserved {
407 storage.unreserve(material, count);
408 } else if storage.available(material) < count {
409 return false;
410 }
411 storage.remove(material, count);
412 }
413 true
414}
415
416pub fn pickup_stored(
423 source: &mut Storage,
424 carried: &mut Storage,
425 materials: &[RawMaterial],
426 items: &[(usize, i32)],
427 partial: bool,
428) -> bool {
429 for &(material, requested) in items {
430 let mut count = if requested > 0 {
431 requested
432 } else {
433 (carried.capacity - carried.used(materials)) / materials[material].size
434 };
435 let available = source.entry(material).map_or(0, |e| e.count);
436 if partial {
437 count = count.min(available);
438 if count == 0 {
439 return false;
440 }
441 } else if available < count {
442 return false;
443 }
444 source.unreserve(material, count);
445 source.remove(material, count);
446 carried.add(material, count);
447 }
448 true
449}
450
451pub fn distribute(
456 storages: &mut [Storage],
457 materials: &[RawMaterial],
458 candidates: &[usize],
459 mask: u32,
460 cap: i32,
461) {
462 let list: Vec<usize> = candidates
463 .iter()
464 .copied()
465 .filter(|&c| materials[c].flags & mask != 0)
466 .collect();
467 if list.is_empty() || cap <= 0 {
468 return;
469 }
470 let mut given = 0;
471 loop {
472 let mut progressed = false;
473 for s in storages.iter_mut() {
474 if given < cap {
475 let m = list[given as usize % list.len()];
476 if s.fits(materials, m, 1) {
477 s.add(m, 1);
478 given += 1;
479 progressed = true;
480 }
481 }
482 }
483 if !progressed || cap <= given {
484 break;
485 }
486 }
487}
488
489pub const STOCK_SLOTS: usize = 21;
493
494pub fn stock_slot(flags: u32) -> Option<usize> {
499 if flags & 0x20 != 0 {
500 return Some(5);
501 }
502 if flags & 0x80 != 0 {
503 return Some(3);
504 }
505 const BITS: [(u32, usize); 18] = [
506 (8, 9),
507 (9, 4),
508 (10, 0),
509 (11, 1),
510 (12, 2),
511 (13, 6),
512 (14, 7),
513 (17, 8),
514 (16, 10),
515 (18, 11),
516 (19, 12),
517 (20, 13),
518 (21, 14),
519 (22, 15),
520 (23, 16),
521 (24, 17),
522 (25, 18),
523 (26, 19),
524 ];
525 for (bit, slot) in BITS {
526 if flags >> bit & 1 != 0 {
527 return Some(slot);
528 }
529 }
530 if flags >> 27 & 1 != 0 {
531 return Some(20);
532 }
533 None
534}
535
536pub fn stock_table(storages: &[Storage], materials: &[RawMaterial]) -> [i32; STOCK_SLOTS] {
538 let mut table = [0; STOCK_SLOTS];
539 for s in storages {
540 for e in &s.items {
541 let (m, n) = (e.material, e.count);
542 if let Some(slot) = stock_slot(materials[m].flags) {
543 table[slot] += n;
544 }
545 }
546 }
547 table
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 fn material(size: i32, flags: u32) -> RawMaterial {
555 RawMaterial {
556 reference: String::new(),
557 size,
558 flags,
559 ..Default::default()
560 }
561 }
562
563 #[test]
564 fn slots() {
565 assert_eq!(stock_slot(0x400), Some(0)); assert_eq!(stock_slot(0x800), Some(1)); assert_eq!(stock_slot(0x1000), Some(2)); assert_eq!(stock_slot(0xc0), Some(3)); assert_eq!(stock_slot(0x200), Some(4)); assert_eq!(stock_slot(0x22), Some(5)); assert_eq!(stock_slot(0x4000), Some(7)); }
573
574 #[test]
575 fn round_robin_respects_capacity_and_flags() {
576 let materials = vec![material(11, 0x400), material(1, 0x22)];
577 let mut storages = vec![
578 Storage {
579 capacity: 6000,
580 accepts: 0x16220,
581 items: vec![],
582 },
583 Storage {
584 capacity: 50,
585 accepts: 0x1c40,
586 items: vec![],
587 },
588 ];
589 distribute(&mut storages, &materials, &[0, 1], 0x400, 120);
590 distribute(&mut storages, &materials, &[0, 1], 0x20, 1800);
591 let table = stock_table(&storages, &materials);
592 assert_eq!(table[0], 4); assert_eq!(table[5], 1800);
594 }
595}