1use std::hash::Hash;
30
31use crate::{
32 assets::{self, AssetExt},
33 comp::{Item, inventory::item},
34};
35use rand::prelude::*;
36use serde::{Deserialize, Serialize, de::DeserializeOwned};
37use tracing::warn;
38
39#[derive(Clone, Debug, PartialEq, Deserialize)]
40pub struct Lottery<T> {
41 items: Vec<(f32, T)>,
42 total: f32,
43}
44
45impl<T: DeserializeOwned + Send + Sync + 'static> assets::Asset for Lottery<T> {
46 type Loader = assets::LoadFrom<Vec<(f32, T)>, assets::RonLoader>;
47
48 const EXTENSION: &'static str = "ron";
49}
50
51impl<T> From<Vec<(f32, T)>> for Lottery<T> {
52 fn from(mut items: Vec<(f32, T)>) -> Lottery<T> {
53 let mut total = 0.0;
54
55 for (rate, _) in &mut items {
56 total += *rate;
57 *rate = total - *rate;
58 }
59
60 Self { items, total }
61 }
62}
63
64impl<T> Lottery<T> {
65 pub fn choose_seeded(&self, seed: u32) -> &T {
66 let x = ((seed % 65536) as f32 / 65536.0) * self.total;
67 &self.items[self
68 .items
69 .binary_search_by(|(y, _)| y.partial_cmp(&x).unwrap())
70 .unwrap_or_else(|i| i.saturating_sub(1))]
71 .1
72 }
73
74 pub fn choose(&self) -> &T { self.choose_seeded(thread_rng().gen()) }
75
76 pub fn iter(&self) -> impl Iterator<Item = &(f32, T)> { self.items.iter() }
77
78 pub fn total(&self) -> f32 { self.total }
79}
80
81pub fn distribute_many<T: Copy + Eq + Hash, I>(
83 participants: impl IntoIterator<Item = (f32, T)>,
84 rng: &mut impl Rng,
85 items: &[I],
86 mut get_amount: impl FnMut(&I) -> u32,
87 mut exec_item: impl FnMut(&I, T, u32),
88) {
89 struct Participant<T> {
90 weight: f32,
92 sorted_weight: f32,
93 data: T,
94 recieved_count: u32,
95 current_recieved_count: u32,
96 }
97
98 impl<T> Participant<T> {
99 fn give(&mut self, amount: u32) {
100 self.current_recieved_count += amount;
101 self.recieved_count += amount;
102 }
103 }
104
105 if items.is_empty() {
107 return;
108 }
109
110 let mut total_weight = 0.0;
111
112 let mut participants = participants
113 .into_iter()
114 .map(|(weight, participant)| Participant {
115 weight,
116 sorted_weight: {
117 total_weight += weight;
118 total_weight - weight
119 },
120 data: participant,
121 recieved_count: 0,
122 current_recieved_count: 0,
123 })
124 .collect::<Vec<_>>();
125
126 let total_item_amount = items.iter().map(&mut get_amount).sum::<u32>();
127
128 let mut current_total_weight = total_weight;
129
130 for item in items.iter() {
131 let amount = get_amount(item);
132 let mut distributed = 0;
133
134 let Some(mut give) = participants
135 .iter()
136 .map(|participant| {
137 (total_item_amount as f32 * participant.weight / total_weight).ceil() as u32
138 - participant.recieved_count
139 })
140 .min()
141 else {
142 tracing::error!("Tried to distribute items to no participants.");
143 return;
144 };
145
146 while distributed < amount {
147 let max_give = (amount / participants.len() as u32).clamp(1, amount - distributed);
150 give = give.clamp(1, max_give);
151 let x = rng.gen_range(0.0..=current_total_weight);
152
153 let index = participants
154 .binary_search_by(|item| item.sorted_weight.partial_cmp(&x).unwrap())
155 .unwrap_or_else(|i| i.saturating_sub(1));
156
157 let participant_count = participants.len();
158
159 let Some(winner) = participants.get_mut(index) else {
160 tracing::error!("Tried to distribute items to no participants.");
161 return;
162 };
163
164 winner.give(give);
165 distributed += give;
166
167 if participant_count > 1
169 && winner.recieved_count as f32 / total_item_amount as f32
170 >= winner.weight / total_weight
171 {
172 current_total_weight = index
173 .checked_sub(1)
174 .and_then(|i| Some(participants.get(i)?.sorted_weight))
175 .unwrap_or(0.0);
176 let winner = participants.swap_remove(index);
177 exec_item(item, winner.data, winner.current_recieved_count);
178
179 for participant in &mut participants[index..] {
181 current_total_weight += participant.weight;
182 participant.sorted_weight = current_total_weight - participant.weight;
183 }
184
185 give = participants
187 .iter()
188 .map(|participant| {
189 (total_item_amount as f32 * participant.weight / total_weight).ceil() as u32
190 - participant.recieved_count
191 })
192 .min()
193 .unwrap_or(0);
194 } else {
195 give = give.min(
196 (total_item_amount as f32 * winner.weight / total_weight).ceil() as u32
197 - winner.recieved_count,
198 );
199 }
200 }
201 for participant in participants.iter_mut() {
202 if participant.current_recieved_count != 0 {
203 exec_item(item, participant.data, participant.current_recieved_count);
204 participant.current_recieved_count = 0;
205 }
206 }
207 }
208}
209
210#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
211#[rustfmt::skip] pub enum LootSpec<T: AsRef<str>> {
213 Item(T),
215 LootTable(T),
217 Nothing,
219 ModularWeapon {
221 tool: item::tool::ToolKind,
222 material: item::Material,
223 hands: Option<item::tool::Hands>,
224 },
225 ModularWeaponPrimaryComponent {
228 tool: item::tool::ToolKind,
229 material: item::Material,
230 hands: Option<item::tool::Hands>,
231 },
232 MultiDrop(Box<LootSpec<T>>, u32, u32),
246 All(Vec<LootSpec<T>>),
270 Lottery(Vec<(f32, LootSpec<T>)>),
292}
293
294impl<T: AsRef<str>> LootSpec<T> {
295 fn to_items_inner(
296 &self,
297 rng: &mut rand::rngs::ThreadRng,
298 amount: u32,
299 items: &mut Vec<(u32, Item)>,
300 ) {
301 let convert_item = |item: &T| {
302 Item::new_from_asset(item.as_ref()).map_or_else(
303 |e| {
304 warn!(?e, "error while loading item: {}", item.as_ref());
305 None
306 },
307 Some,
308 )
309 };
310 let mut push_item = |mut item: Item, count: u32| {
311 let count = item.amount().saturating_mul(count);
312 item.set_amount(1).expect("1 is always a valid amount.");
313 let hash = item.item_hash();
314 match items.binary_search_by_key(&hash, |(_, item)| item.item_hash()) {
315 Ok(i) => {
316 let has_same_hash = |i: &usize| items[*i].1.item_hash() == hash;
323 if let Some(i) = (i..items.len())
324 .take_while(has_same_hash)
325 .chain((0..i).rev().take_while(has_same_hash))
326 .find(|i| items[*i].1 == item)
327 {
328 items[i].0 = items[i].0.saturating_add(count);
331 } else {
332 items.insert(i, (count, item));
333 }
334 },
335 Err(i) => items.insert(i, (count, item)),
336 }
337 };
338
339 match self {
340 Self::Item(item) => {
341 if let Some(item) = convert_item(item) {
342 push_item(item, amount);
343 }
344 },
345 Self::LootTable(table) => {
346 let loot_spec = Lottery::<LootSpec<String>>::load_expect(table.as_ref()).read();
347 for _ in 0..amount {
348 loot_spec.choose().to_items_inner(rng, 1, items)
349 }
350 },
351 Self::Lottery(table) => {
352 let lottery = Lottery::from(
353 table
354 .iter()
355 .map(|(weight, spec)| (*weight, spec))
356 .collect::<Vec<_>>(),
357 );
358
359 for _ in 0..amount {
360 lottery.choose().to_items_inner(rng, 1, items)
361 }
362 },
363 Self::Nothing => {},
364 Self::ModularWeapon {
365 tool,
366 material,
367 hands,
368 } => {
369 for _ in 0..amount {
370 match item::modular::random_weapon(*tool, *material, *hands, rng) {
371 Ok(item) => push_item(item, 1),
372 Err(e) => {
373 warn!(
374 ?e,
375 "error while creating modular weapon. Toolkind: {:?}, Material: \
376 {:?}, Hands: {:?}",
377 tool,
378 material,
379 hands,
380 );
381 },
382 }
383 }
384 },
385 Self::ModularWeaponPrimaryComponent {
386 tool,
387 material,
388 hands,
389 } => {
390 for _ in 0..amount {
391 match item::modular::random_weapon(*tool, *material, *hands, rng) {
392 Ok(item) => push_item(item, 1),
393 Err(e) => {
394 warn!(
395 ?e,
396 "error while creating modular weapon primary component. Toolkind: \
397 {:?}, Material: {:?}, Hands: {:?}",
398 tool,
399 material,
400 hands,
401 );
402 },
403 }
404 }
405 },
406 Self::MultiDrop(loot_spec, lower, upper) => {
407 let sub_amount = rng.gen_range(*lower..=*upper);
408 loot_spec.to_items_inner(rng, sub_amount.saturating_mul(amount), items);
411 },
412 Self::All(loot_specs) => {
413 for loot_spec in loot_specs {
414 loot_spec.to_items_inner(rng, amount, items);
415 }
416 },
417 }
418 }
419
420 pub fn to_items(&self) -> Option<Vec<(u32, Item)>> {
421 let mut items = Vec::new();
422 self.to_items_inner(&mut thread_rng(), 1, &mut items);
423
424 if !items.is_empty() {
425 items.sort_unstable_by_key(|(amount, _)| *amount);
426
427 Some(items)
428 } else {
429 None
430 }
431 }
432}
433
434impl Default for LootSpec<String> {
435 fn default() -> Self { Self::Nothing }
436}
437
438#[cfg(test)]
439pub mod tests {
440 use std::borrow::Borrow;
441
442 use super::*;
443 use crate::{assets, comp::Item};
444 use assets::AssetExt;
445
446 #[cfg(test)]
447 pub fn validate_loot_spec(item: &LootSpec<String>) {
448 let mut rng = thread_rng();
449 match item {
450 LootSpec::Item(item) => {
451 Item::new_from_asset_expect(item);
452 },
453 LootSpec::LootTable(loot_table) => {
454 let loot_table = Lottery::<LootSpec<String>>::load_expect(loot_table).read();
455 validate_table_contents(&loot_table);
456 },
457 LootSpec::Nothing => {},
458 LootSpec::ModularWeapon {
459 tool,
460 material,
461 hands,
462 } => {
463 item::modular::random_weapon(*tool, *material, *hands, &mut rng).unwrap_or_else(
464 |_| {
465 panic!(
466 "Failed to synthesize a modular {tool:?} made of {material:?} that \
467 had a hand restriction of {hands:?}."
468 )
469 },
470 );
471 },
472 LootSpec::ModularWeaponPrimaryComponent {
473 tool,
474 material,
475 hands,
476 } => {
477 item::modular::random_weapon_primary_component(*tool, *material, *hands, &mut rng)
478 .unwrap_or_else(|_| {
479 panic!(
480 "Failed to synthesize a modular weapon primary component: {tool:?} \
481 made of {material:?} that had a hand restriction of {hands:?}."
482 )
483 });
484 },
485 LootSpec::MultiDrop(loot_spec, lower, upper) => {
486 assert!(
487 upper >= lower,
488 "Upper quantity must be at least the value of lower quantity. Upper value: \
489 {}, low value: {}.",
490 upper,
491 lower
492 );
493 validate_loot_spec(loot_spec);
494 },
495 LootSpec::All(loot_specs) => {
496 for loot_spec in loot_specs {
497 validate_loot_spec(loot_spec);
498 }
499 },
500 LootSpec::Lottery(table) => {
501 let lottery = Lottery::from(
502 table
503 .iter()
504 .map(|(weight, spec)| (*weight, spec))
505 .collect::<Vec<_>>(),
506 );
507
508 validate_table_contents(&lottery);
509 },
510 }
511 }
512
513 fn validate_table_contents<T: Borrow<LootSpec<String>>>(table: &Lottery<T>) {
514 for (_, item) in table.iter() {
515 validate_loot_spec(item.borrow());
516 }
517 }
518
519 #[test]
520 fn test_loot_tables() {
521 let loot_tables = assets::load_rec_dir::<Lottery<LootSpec<String>>>("common.loot_tables")
522 .expect("load loot_tables");
523 for loot_table in loot_tables.read().ids() {
524 let loot_table = Lottery::<LootSpec<String>>::load_expect(loot_table);
525 validate_table_contents(&loot_table.read());
526 }
527 }
528
529 #[test]
530 fn test_distribute_many() {
531 let mut rng = thread_rng();
532
533 for _ in 0..10 {
535 distribute_many(
536 vec![(0.4f32, "a"), (0.4, "b"), (0.2, "c")],
537 &mut rng,
538 &[("item", 10)],
539 |(_, m)| *m,
540 |_item, winner, count| match winner {
541 "a" | "b" => assert_eq!(count, 4),
542 "c" => assert_eq!(count, 2),
543 _ => unreachable!(),
544 },
545 );
546 }
547 }
548}