veloren_common/
lottery.rs

1// Example for calculating a drop rate:
2//
3// On every roll an f32 between 0 and 1 is created.
4// For every loot table a total range is created by the sum of the individual
5// ranges per item.
6//
7// This range is the sum of all single ranges defined per item in a table.
8//                                                   // Individual Range
9// (3, "common.items.food.cheese"),                  // 0.0..3.0
10// (3, "common.items.food.apple"),                   // 3.0..6.0
11// (3, "common.items.food.mushroom"),                // 6.0..9.0
12// (1, "common.items.food.coconut"),                 // 9.0..10.0
13// (0.05, "common.items.food.apple_mushroom_curry"), // 10.0..10.05
14// (0.10, "common.items.food.apple_stick"),          // 10.05..10.15
15// (0.10, "common.items.food.mushroom_stick"),       // 10.15..10.25
16//
17// The f32 is multiplied by the max. value needed to drop an item in this
18// particular table. X = max. value needed = 10.15
19//
20// Example roll
21// [Random Value 0..1] * X = Number inside the table's total range
22// 0.45777 * X = 4.65
23// 4.65 is in the range of 3.0..6.0 => Apple drops
24//
25// Example drop chance calculation
26// Cheese drop rate = 3/X = 29.6%
27// Coconut drop rate = 1/X = 9.85%
28
29use 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
81/// Try to distribute stacked items fairly between weighted participants.
82pub 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 / total
91        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    // Nothing to distribute, we can return early.
106    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            // Can't give more than amount, and don't give more than the average between all
148            // to keep things well distributed.
149            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 a participant has received enough, remove it.
168            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                // Keep participant weights correct so that we can binary search it.
180                for participant in &mut participants[index..] {
181                    current_total_weight += participant.weight;
182                    participant.sorted_weight = current_total_weight - participant.weight;
183                }
184
185                // Update max item give amount.
186                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)]
211pub enum LootSpec<T: AsRef<str>> {
212    /// Asset specifier
213    Item(T),
214    /// Loot table
215    LootTable(T),
216    /// No loot given
217    Nothing,
218    /// Modular weapon
219    ModularWeapon {
220        tool: item::tool::ToolKind,
221        material: item::Material,
222        hands: Option<item::tool::Hands>,
223    },
224    ModularWeaponPrimaryComponent {
225        tool: item::tool::ToolKind,
226        material: item::Material,
227        hands: Option<item::tool::Hands>,
228    },
229    /// Dropping lower-upper range items at random from the respective Category
230    /// (often a Loottable or Item(Coin)) LootSpec, lower range, upper range
231    MultiDrop(Box<LootSpec<T>>, u32, u32),
232    /// Each category is evaluated, often used to have guaranteed quest Item +
233    /// random reward
234    All(Vec<LootSpec<T>>),
235    /// Like a `LootTable` but inline
236    Lottery(Vec<(f32, LootSpec<T>)>),
237}
238
239impl<T: AsRef<str>> LootSpec<T> {
240    fn to_items_inner(
241        &self,
242        rng: &mut rand::rngs::ThreadRng,
243        amount: u32,
244        items: &mut Vec<(u32, Item)>,
245    ) {
246        let convert_item = |item: &T| {
247            Item::new_from_asset(item.as_ref()).map_or_else(
248                |e| {
249                    warn!(?e, "error while loading item: {}", item.as_ref());
250                    None
251                },
252                Some,
253            )
254        };
255        let mut push_item = |mut item: Item, count: u32| {
256            let count = item.amount().saturating_mul(count);
257            item.set_amount(1).expect("1 is always a valid amount.");
258            let hash = item.item_hash();
259            match items.binary_search_by_key(&hash, |(_, item)| item.item_hash()) {
260                Ok(i) => {
261                    // Since item hash can collide with other items, we search nearby items with the
262                    // same hash.
263                    // NOTE: The `ParitalEq` implementation for `Item` doesn't compare some data
264                    // like durability, or wether slots contain anything. Although since these are
265                    // Newly loaded items we don't care about comparing those for deduplication
266                    // here.
267                    let has_same_hash = |i: &usize| items[*i].1.item_hash() == hash;
268                    if let Some(i) = (i..items.len())
269                        .take_while(has_same_hash)
270                        .chain((0..i).rev().take_while(has_same_hash))
271                        .find(|i| items[*i].1 == item)
272                    {
273                        // We saturate at 4 billion items, could use u64 instead if this isn't
274                        // desirable.
275                        items[i].0 = items[i].0.saturating_add(count);
276                    } else {
277                        items.insert(i, (count, item));
278                    }
279                },
280                Err(i) => items.insert(i, (count, item)),
281            }
282        };
283
284        match self {
285            Self::Item(item) => {
286                if let Some(item) = convert_item(item) {
287                    push_item(item, amount);
288                }
289            },
290            Self::LootTable(table) => {
291                let loot_spec = Lottery::<LootSpec<String>>::load_expect(table.as_ref()).read();
292                for _ in 0..amount {
293                    loot_spec.choose().to_items_inner(rng, 1, items)
294                }
295            },
296            Self::Lottery(table) => {
297                let lottery = Lottery::from(
298                    table
299                        .iter()
300                        .map(|(weight, spec)| (*weight, spec))
301                        .collect::<Vec<_>>(),
302                );
303
304                for _ in 0..amount {
305                    lottery.choose().to_items_inner(rng, 1, items)
306                }
307            },
308            Self::Nothing => {},
309            Self::ModularWeapon {
310                tool,
311                material,
312                hands,
313            } => {
314                for _ in 0..amount {
315                    match item::modular::random_weapon(*tool, *material, *hands, rng) {
316                        Ok(item) => push_item(item, 1),
317                        Err(e) => {
318                            warn!(
319                                ?e,
320                                "error while creating modular weapon. Toolkind: {:?}, Material: \
321                                 {:?}, Hands: {:?}",
322                                tool,
323                                material,
324                                hands,
325                            );
326                        },
327                    }
328                }
329            },
330            Self::ModularWeaponPrimaryComponent {
331                tool,
332                material,
333                hands,
334            } => {
335                for _ in 0..amount {
336                    match item::modular::random_weapon(*tool, *material, *hands, rng) {
337                        Ok(item) => push_item(item, 1),
338                        Err(e) => {
339                            warn!(
340                                ?e,
341                                "error while creating modular weapon primary component. Toolkind: \
342                                 {:?}, Material: {:?}, Hands: {:?}",
343                                tool,
344                                material,
345                                hands,
346                            );
347                        },
348                    }
349                }
350            },
351            Self::MultiDrop(loot_spec, lower, upper) => {
352                let sub_amount = rng.gen_range(*lower..=*upper);
353                // We saturate at 4 billion items, could use u64 instead if this isn't
354                // desirable.
355                loot_spec.to_items_inner(rng, sub_amount.saturating_mul(amount), items);
356            },
357            Self::All(loot_specs) => {
358                for loot_spec in loot_specs {
359                    loot_spec.to_items_inner(rng, amount, items);
360                }
361            },
362        }
363    }
364
365    pub fn to_items(&self) -> Option<Vec<(u32, Item)>> {
366        let mut items = Vec::new();
367        self.to_items_inner(&mut thread_rng(), 1, &mut items);
368
369        if !items.is_empty() {
370            items.sort_unstable_by_key(|(amount, _)| *amount);
371
372            Some(items)
373        } else {
374            None
375        }
376    }
377}
378
379impl Default for LootSpec<String> {
380    fn default() -> Self { Self::Nothing }
381}
382
383#[cfg(test)]
384pub mod tests {
385    use std::borrow::Borrow;
386
387    use super::*;
388    use crate::{assets, comp::Item};
389    use assets::AssetExt;
390
391    #[cfg(test)]
392    pub fn validate_loot_spec(item: &LootSpec<String>) {
393        let mut rng = thread_rng();
394        match item {
395            LootSpec::Item(item) => {
396                Item::new_from_asset_expect(item);
397            },
398            LootSpec::LootTable(loot_table) => {
399                let loot_table = Lottery::<LootSpec<String>>::load_expect(loot_table).read();
400                validate_table_contents(&loot_table);
401            },
402            LootSpec::Nothing => {},
403            LootSpec::ModularWeapon {
404                tool,
405                material,
406                hands,
407            } => {
408                item::modular::random_weapon(*tool, *material, *hands, &mut rng).unwrap_or_else(
409                    |_| {
410                        panic!(
411                            "Failed to synthesize a modular {tool:?} made of {material:?} that \
412                             had a hand restriction of {hands:?}."
413                        )
414                    },
415                );
416            },
417            LootSpec::ModularWeaponPrimaryComponent {
418                tool,
419                material,
420                hands,
421            } => {
422                item::modular::random_weapon_primary_component(*tool, *material, *hands, &mut rng)
423                    .unwrap_or_else(|_| {
424                        panic!(
425                            "Failed to synthesize a modular weapon primary component: {tool:?} \
426                             made of {material:?} that had a hand restriction of {hands:?}."
427                        )
428                    });
429            },
430            LootSpec::MultiDrop(loot_spec, lower, upper) => {
431                assert!(
432                    upper >= lower,
433                    "Upper quantity must be at least the value of lower quantity. Upper value: \
434                     {}, low value: {}.",
435                    upper,
436                    lower
437                );
438                validate_loot_spec(loot_spec);
439            },
440            LootSpec::All(loot_specs) => {
441                for loot_spec in loot_specs {
442                    validate_loot_spec(loot_spec);
443                }
444            },
445            LootSpec::Lottery(table) => {
446                let lottery = Lottery::from(
447                    table
448                        .iter()
449                        .map(|(weight, spec)| (*weight, spec))
450                        .collect::<Vec<_>>(),
451                );
452
453                validate_table_contents(&lottery);
454            },
455        }
456    }
457
458    fn validate_table_contents<T: Borrow<LootSpec<String>>>(table: &Lottery<T>) {
459        for (_, item) in table.iter() {
460            validate_loot_spec(item.borrow());
461        }
462    }
463
464    #[test]
465    fn test_loot_tables() {
466        let loot_tables = assets::load_rec_dir::<Lottery<LootSpec<String>>>("common.loot_tables")
467            .expect("load loot_tables");
468        for loot_table in loot_tables.read().ids() {
469            let loot_table = Lottery::<LootSpec<String>>::load_expect(loot_table);
470            validate_table_contents(&loot_table.read());
471        }
472    }
473
474    #[test]
475    fn test_distribute_many() {
476        let mut rng = thread_rng();
477
478        // Known successful case
479        for _ in 0..10 {
480            distribute_many(
481                vec![(0.4f32, "a"), (0.4, "b"), (0.2, "c")],
482                &mut rng,
483                &[("item", 10)],
484                |(_, m)| *m,
485                |_item, winner, count| match winner {
486                    "a" | "b" => assert_eq!(count, 4),
487                    "c" => assert_eq!(count, 2),
488                    _ => unreachable!(),
489                },
490            );
491        }
492    }
493}