veloren_common/comp/inventory/
trade_pricing.rs

1use crate::{
2    assets::{
3        Asset, AssetCache, AssetExt, AssetReadGuard, BoxedError, FileAsset, Ron, SharedString,
4        load_ron,
5    },
6    comp::{
7        inventory,
8        item::{
9            Item, ItemDefinitionId, ItemDefinitionIdOwned, ItemKind, MaterialStatManifest,
10            ModularBase,
11        },
12        tool::AbilityMap,
13    },
14    lottery::LootSpec,
15    recipe::{RecipeInput, complete_recipe_book, default_component_recipe_book},
16    trade::Good,
17};
18use hashbrown::HashMap;
19use lazy_static::lazy_static;
20use serde::Deserialize;
21use std::{borrow::Cow, cmp::Ordering};
22use tracing::{error, info, warn};
23
24use super::item::{Material, ToolKind};
25
26const PRICING_DEBUG: bool = false;
27
28#[derive(Default, Debug)]
29pub struct TradePricing {
30    items: PriceEntries,
31    equality_set: EqualitySet,
32}
33
34// combination logic:
35// price is the inverse of frequency
36// you can use either equivalent A or B => add frequency
37// you need both equivalent A and B => add price
38
39/// Material equivalent for an item (price)
40#[derive(Default, Debug, Clone)]
41pub struct MaterialUse(Vec<(f32, Good)>);
42
43impl std::ops::Mul<f32> for MaterialUse {
44    type Output = Self;
45
46    fn mul(self, rhs: f32) -> Self::Output {
47        Self(self.0.iter().map(|v| (v.0 * rhs, v.1)).collect())
48    }
49}
50
51// used by the add variants
52fn vector_add_eq(result: &mut Vec<(f32, Good)>, rhs: &[(f32, Good)]) {
53    for (amount, good) in rhs {
54        if result
55            .iter_mut()
56            .find(|(_amount2, good2)| *good == *good2)
57            .map(|elem| elem.0 += *amount)
58            .is_none()
59        {
60            result.push((*amount, *good));
61        }
62    }
63}
64
65impl std::ops::Add for MaterialUse {
66    type Output = Self;
67
68    fn add(self, rhs: Self) -> Self::Output {
69        let mut result = self;
70        vector_add_eq(&mut result.0, &rhs.0);
71        result
72    }
73}
74
75impl std::ops::AddAssign for MaterialUse {
76    fn add_assign(&mut self, rhs: Self) { vector_add_eq(&mut self.0, &rhs.0); }
77}
78
79impl std::iter::Sum<MaterialUse> for MaterialUse {
80    fn sum<I>(iter: I) -> Self
81    where
82        I: Iterator<Item = Self>,
83    {
84        let mut ret = Self::default();
85        for i in iter {
86            ret += i;
87        }
88        ret
89    }
90}
91
92impl std::ops::Deref for MaterialUse {
93    type Target = [(f32, Good)];
94
95    fn deref(&self) -> &Self::Target { self.0.deref() }
96}
97
98/// Frequency
99#[derive(Default, Debug, Clone)]
100pub struct MaterialFrequency(Vec<(f32, Good)>);
101
102// to compute price from frequency:
103// price[i] = 1/frequency[i] * 1/sum(frequency) * 1/sum(1/frequency)
104// scaling individual components so that ratio is inverted and the sum of all
105// inverted elements is equivalent to inverse of the original sum
106fn vector_invert(result: &mut [(f32, Good)]) {
107    let mut oldsum: f32 = 0.0;
108    let mut newsum: f32 = 0.0;
109    for (value, _good) in result.iter_mut() {
110        oldsum += *value;
111        *value = 1.0 / *value;
112        newsum += *value;
113    }
114    let scale = 1.0 / (oldsum * newsum);
115    for (value, _good) in result.iter_mut() {
116        *value *= scale;
117    }
118}
119
120impl From<MaterialUse> for MaterialFrequency {
121    fn from(u: MaterialUse) -> Self {
122        let mut result = Self(u.0);
123        vector_invert(&mut result.0);
124        result
125    }
126}
127
128// identical computation
129impl From<MaterialFrequency> for MaterialUse {
130    fn from(u: MaterialFrequency) -> Self {
131        let mut result = Self(u.0);
132        vector_invert(&mut result.0);
133        result
134    }
135}
136
137impl std::ops::Add for MaterialFrequency {
138    type Output = Self;
139
140    fn add(self, rhs: Self) -> Self::Output {
141        let mut result = self;
142        vector_add_eq(&mut result.0, &rhs.0);
143        result
144    }
145}
146
147impl std::ops::AddAssign for MaterialFrequency {
148    fn add_assign(&mut self, rhs: Self) { vector_add_eq(&mut self.0, &rhs.0); }
149}
150
151#[derive(Debug)]
152struct PriceEntry {
153    name: ItemDefinitionIdOwned,
154    price: MaterialUse,
155    // sellable by merchants
156    sell: bool,
157    stackable: bool,
158}
159#[derive(Debug)]
160struct FreqEntry {
161    name: ItemDefinitionIdOwned,
162    freq: MaterialFrequency,
163    sell: bool,
164    stackable: bool,
165}
166
167#[derive(Default, Debug)]
168struct PriceEntries(Vec<PriceEntry>);
169#[derive(Default, Debug)]
170struct FreqEntries(Vec<FreqEntry>);
171
172impl PriceEntries {
173    fn add_alternative(&mut self, b: PriceEntry) {
174        // alternatives are added in frequency (gets more frequent)
175        let already = self.0.iter_mut().find(|i| i.name == b.name);
176        if let Some(entry) = already {
177            let entry_freq: MaterialFrequency = std::mem::take(&mut entry.price).into();
178            let b_freq: MaterialFrequency = b.price.into();
179            let result = entry_freq + b_freq;
180            entry.price = result.into();
181        } else {
182            self.0.push(b);
183        }
184    }
185}
186
187impl FreqEntries {
188    fn add(
189        &mut self,
190        eqset: &EqualitySet,
191        item_name: &ItemDefinitionIdOwned,
192        good: Good,
193        probability: f32,
194        can_sell: bool,
195    ) {
196        let canonical_itemname = eqset.canonical(item_name);
197        let old = self
198            .0
199            .iter_mut()
200            .find(|elem| elem.name == *canonical_itemname);
201        let new_freq = MaterialFrequency(vec![(probability, good)]);
202        // Increase probability if already in entries, or add new entry
203        if let Some(FreqEntry {
204            name: asset,
205            freq: old_probability,
206            sell: old_can_sell,
207            stackable: _,
208        }) = old
209        {
210            if PRICING_DEBUG {
211                info!("Update {:?} {:?}+{:?}", asset, old_probability, probability);
212            }
213            if !can_sell && *old_can_sell {
214                *old_can_sell = false;
215            }
216            *old_probability += new_freq;
217        } else {
218            let stackable = Item::new_from_item_definition_id(
219                canonical_itemname.as_ref(),
220                &AbilityMap::load().read(),
221                &MaterialStatManifest::load().read(),
222            )
223            .is_ok_and(|i| i.is_stackable());
224            let new_mat_prob: FreqEntry = FreqEntry {
225                name: canonical_itemname.to_owned(),
226                freq: new_freq,
227                sell: can_sell,
228                stackable,
229            };
230            if PRICING_DEBUG {
231                info!("New {:?}", new_mat_prob);
232            }
233            self.0.push(new_mat_prob);
234        }
235
236        // Add the non-canonical item so that it'll show up in merchant inventories
237        // It will have infinity as its price, but it's fine,
238        // because we determine all prices based on canonical value
239        if canonical_itemname != item_name && !self.0.iter().any(|elem| elem.name == *item_name) {
240            self.0.push(FreqEntry {
241                name: item_name.to_owned(),
242                freq: Default::default(),
243                sell: can_sell,
244                stackable: false,
245            });
246        }
247    }
248}
249
250lazy_static! {
251    static ref TRADE_PRICING: TradePricing = TradePricing::read();
252}
253
254#[derive(Clone)]
255/// A collection of items with probabilty, created
256/// hierarchically from `LootSpec`s
257/// (probability, item id, average amount)
258///
259/// This collection is NOT normalized (the sum of probabilities may not equal to
260/// one, as maltiple items can drop in one roll)
261pub struct ProbabilityFile {
262    pub content: Vec<(f32, ItemDefinitionIdOwned, f32)>,
263}
264
265impl FileAsset for ProbabilityFile {
266    const EXTENSION: &'static str = "ron";
267
268    fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> {
269        load_ron::<Vec<(f32, LootSpec<String>)>>(&bytes).map(Vec::into)
270    }
271}
272
273type ComponentPool =
274    HashMap<(ToolKind, String), Vec<(ItemDefinitionIdOwned, Option<inventory::item::Hands>)>>;
275
276lazy_static! {
277    static ref PRIMARY_COMPONENT_POOL: ComponentPool = {
278        let mut component_pool = HashMap::new();
279
280        // Load recipe book (done to check that material is valid for a particular component)
281        use crate::recipe::ComponentKey;
282        let recipes = default_component_recipe_book().read();
283
284        recipes
285            .iter()
286            .for_each(|(ComponentKey { toolkind, material, .. }, recipe)| {
287                let component = recipe.itemdef_output();
288                let hand_restriction = None; // once there exists a hand restriction add the logic here - for a slight price correction
289                let entry: &mut Vec<_> = component_pool.entry((*toolkind, String::from(material))).or_default();
290                entry.push((component, hand_restriction));
291            });
292
293        component_pool
294    };
295
296    static ref SECONDARY_COMPONENT_POOL: ComponentPool = {
297        let mut component_pool = HashMap::new();
298
299        // Load recipe book (done to check that material is valid for a particular component)
300        //use crate::recipe::ComponentKey;
301        let recipes = complete_recipe_book().read();
302
303        recipes
304            .iter()
305            .for_each(|(_, recipe)| {
306                let (ref asset_path, _) = recipe.output;
307                if let ItemKind::ModularComponent(
308                    crate::comp::inventory::item::modular::ModularComponent::ToolSecondaryComponent {
309                        toolkind,
310                        stats: _,
311                        hand_restriction,
312                    },
313                ) = asset_path.kind
314                {
315                    let component = ItemDefinitionIdOwned::Simple(asset_path.id().into());
316                    let entry: &mut Vec<_> = component_pool.entry((toolkind, String::new())).or_default();
317                    entry.push((component, hand_restriction));
318                }});
319
320        component_pool
321    };
322}
323
324// expand this loot specification towards individual item descriptions
325// partial duplicate of random_weapon_primary_component
326// returning an Iterator is difficult due to the branch and it is always used as
327// a vec afterwards
328pub fn expand_primary_component(
329    tool: ToolKind,
330    material: Material,
331    hand_restriction: Option<inventory::item::Hands>,
332) -> Vec<ItemDefinitionIdOwned> {
333    if let Some(material_id) = material.asset_identifier() {
334        PRIMARY_COMPONENT_POOL
335            .get(&(tool, material_id.to_owned()))
336            .into_iter()
337            .flatten()
338            .filter(move |(_comp, hand)| match (hand_restriction, *hand) {
339                (Some(restriction), Some(hand)) => restriction == hand,
340                (None, _) | (_, None) => true,
341            })
342            .map(|e| e.0.clone())
343            .collect()
344    } else {
345        Vec::new()
346    }
347}
348
349pub fn expand_secondary_component(
350    tool: ToolKind,
351    _material: Material,
352    hand_restriction: Option<inventory::item::Hands>,
353) -> impl Iterator<Item = ItemDefinitionIdOwned> {
354    SECONDARY_COMPONENT_POOL
355        .get(&(tool, String::new()))
356        .into_iter()
357        .flatten()
358        .filter(move |(_comp, hand)| match (hand_restriction, *hand) {
359            (Some(restriction), Some(hand)) => restriction == hand,
360            (None, _) | (_, None) => true,
361        })
362        .map(|e| e.0.clone())
363}
364
365impl From<Vec<(f32, LootSpec<String>)>> for ProbabilityFile {
366    fn from(content: Vec<(f32, LootSpec<String>)>) -> Self {
367        let rescale = if content.is_empty() {
368            1.0
369        } else {
370            1.0 / content.iter().map(|e| e.0).sum::<f32>()
371        };
372        fn get_content(
373            rescale: f32,
374            p0: f32,
375            loot: LootSpec<String>,
376        ) -> Vec<(f32, ItemDefinitionIdOwned, f32)> {
377            match loot {
378                LootSpec::Item(asset) => {
379                    vec![(p0 * rescale, ItemDefinitionIdOwned::Simple(asset), 1.0)]
380                },
381                LootSpec::LootTable(table_asset) => {
382                    let unscaled = &ProbabilityFile::load_expect(&table_asset).read().content;
383                    let scale = p0 * rescale;
384                    unscaled
385                        .iter()
386                        .map(|(p1, asset, amount)| (*p1 * scale, asset.clone(), *amount))
387                        .collect::<Vec<_>>()
388                },
389                LootSpec::Lottery(table) => {
390                    let unscaled = ProbabilityFile::from(table);
391                    let scale = p0 * rescale;
392                    unscaled
393                        .content
394                        .into_iter()
395                        .map(|(p1, asset, amount)| (p1 * scale, asset, amount))
396                        .collect::<Vec<_>>()
397                },
398                LootSpec::ModularWeapon {
399                    tool,
400                    material,
401                    hands,
402                } => {
403                    let mut primary = expand_primary_component(tool, material, hands);
404                    let secondary: Vec<ItemDefinitionIdOwned> =
405                        expand_secondary_component(tool, material, hands).collect();
406                    let freq = if primary.is_empty() || secondary.is_empty() {
407                        0.0
408                    } else {
409                        p0 * rescale / ((primary.len() * secondary.len()) as f32)
410                    };
411                    let res: Vec<(f32, ItemDefinitionIdOwned, f32)> = primary
412                        .drain(0..)
413                        .flat_map(|p| {
414                            secondary.iter().map(move |s| {
415                                let components = vec![p.clone(), s.clone()];
416                                (
417                                    freq,
418                                    ItemDefinitionIdOwned::Modular {
419                                        pseudo_base: ModularBase::Tool.pseudo_item_id().into(),
420                                        components,
421                                    },
422                                    1.0f32,
423                                )
424                            })
425                        })
426                        .collect();
427                    res
428                },
429                LootSpec::ModularWeaponPrimaryComponent {
430                    tool,
431                    material,
432                    hands,
433                } => {
434                    let mut res = expand_primary_component(tool, material, hands);
435                    let freq = if res.is_empty() {
436                        0.0
437                    } else {
438                        p0 * rescale / (res.len() as f32)
439                    };
440                    let res: Vec<(f32, ItemDefinitionIdOwned, f32)> =
441                        res.drain(0..).map(|e| (freq, e, 1.0f32)).collect();
442                    res
443                },
444                LootSpec::Nothing => Vec::new(),
445                LootSpec::MultiDrop(loot, a, b) => {
446                    let average_count = (a + b) as f32 * 0.5;
447                    let mut content = get_content(rescale, p0, *loot);
448                    for (_, _, count) in content.iter_mut() {
449                        *count *= average_count;
450                    }
451                    content
452                },
453                LootSpec::All(loot_specs) => loot_specs
454                    .into_iter()
455                    .flat_map(|loot| get_content(rescale, p0, loot))
456                    .collect(),
457            }
458        }
459        Self {
460            content: content
461                .into_iter()
462                .flat_map(|(p0, loot)| get_content(rescale, p0, loot))
463                .collect(),
464        }
465    }
466}
467
468#[derive(Debug, Deserialize)]
469struct TradingPriceFile {
470    /// Tuple format: (frequency, can_sell, asset_path)
471    pub loot_tables: Vec<(f32, bool, String)>,
472    /// the amount of Good equivalent to the most common item
473    pub good_scaling: Vec<(Good, f32)>,
474}
475
476#[derive(Clone, Debug, Default)]
477struct EqualitySet {
478    // which item should this item's occurrences be counted towards
479    equivalence_class: HashMap<ItemDefinitionIdOwned, ItemDefinitionIdOwned>,
480}
481
482impl EqualitySet {
483    fn canonical<'a>(&'a self, item_name: &'a ItemDefinitionIdOwned) -> &'a ItemDefinitionIdOwned {
484        // TODO: use hashbrown Equivalent trait to avoid needing owned item def here
485
486        (self
487            .equivalence_class
488            .get(item_name)
489            .map_or(item_name, |i| i)) as _
490    }
491}
492
493impl Asset for EqualitySet {
494    fn load(cache: &AssetCache, id: &SharedString) -> Result<Self, BoxedError> {
495        #[derive(Debug, Deserialize)]
496        enum EqualitySpec {
497            LootTable(String),
498            Set(Vec<String>),
499        }
500
501        let mut eqset = Self {
502            equivalence_class: HashMap::new(),
503        };
504
505        let manifest = &cache.load::<Ron<Vec<EqualitySpec>>>(id)?.read().0;
506        for set in manifest {
507            let items: Vec<ItemDefinitionIdOwned> = match set {
508                EqualitySpec::LootTable(table) => {
509                    let acc = &ProbabilityFile::load_expect(table).read().content;
510
511                    acc.iter().map(|(_p, item, _)| item).cloned().collect()
512                },
513                EqualitySpec::Set(xs) => xs
514                    .iter()
515                    .map(|s| ItemDefinitionIdOwned::Simple(s.clone()))
516                    .collect(),
517            };
518            let mut iter = items.iter();
519            if let Some(first) = iter.next() {
520                eqset.equivalence_class.insert(first.clone(), first.clone());
521                for item in iter {
522                    eqset.equivalence_class.insert(item.clone(), first.clone());
523                }
524            }
525        }
526        Ok(eqset)
527    }
528}
529
530#[derive(Debug)]
531struct RememberedRecipe {
532    output: ItemDefinitionIdOwned,
533    amount: u32,
534    material_cost: Option<f32>,
535    input: Vec<(ItemDefinitionIdOwned, u32)>,
536}
537
538fn get_scaling(contents: &AssetReadGuard<Ron<TradingPriceFile>>, good: Good) -> f32 {
539    contents
540        .0
541        .good_scaling
542        .iter()
543        .find(|(good_kind, _)| *good_kind == good)
544        .map_or(1.0, |(_, scaling)| *scaling)
545}
546
547#[cfg(test)]
548impl PartialOrd for ItemDefinitionIdOwned {
549    fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
550}
551
552#[cfg(test)]
553impl Ord for ItemDefinitionIdOwned {
554    fn cmp(&self, other: &Self) -> Ordering {
555        match self {
556            ItemDefinitionIdOwned::Simple(na) => match other {
557                ItemDefinitionIdOwned::Simple(nb) => na.cmp(nb),
558                _ => Ordering::Less,
559            },
560            ItemDefinitionIdOwned::Modular {
561                pseudo_base,
562                components,
563            } => match other {
564                ItemDefinitionIdOwned::Simple(_) => Ordering::Greater,
565                ItemDefinitionIdOwned::Modular {
566                    pseudo_base: pseudo_base2,
567                    components: components2,
568                } => pseudo_base
569                    .cmp(pseudo_base2)
570                    .then_with(|| components.cmp(components2)),
571                _ => Ordering::Less,
572            },
573            ItemDefinitionIdOwned::Compound {
574                simple_base,
575                components,
576            } => match other {
577                ItemDefinitionIdOwned::Compound {
578                    simple_base: simple_base2,
579                    components: components2,
580                } => simple_base
581                    .cmp(simple_base2)
582                    .then_with(|| components.cmp(components2)),
583                _ => Ordering::Greater,
584            },
585        }
586    }
587}
588
589impl TradePricing {
590    const COIN_ITEM: &'static str = "common.items.utility.coins";
591    const CRAFTING_FACTOR: f32 = 0.95;
592    // increase price a bit compared to sum of ingredients
593    const INVEST_FACTOR: f32 = 0.33;
594
595    pub fn good_from_item(name: &ItemDefinitionIdOwned) -> Good {
596        match name {
597            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.armor.") => {
598                Good::Armor
599            },
600
601            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.weapons.") => {
602                Good::Tools
603            },
604            ItemDefinitionIdOwned::Simple(name)
605                if name.starts_with("common.items.modular.weapon.") =>
606            {
607                Good::Tools
608            },
609            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.tool.") => {
610                Good::Tools
611            },
612            ItemDefinitionIdOwned::Simple(name)
613                if name.starts_with("common.items.crafting_ing.") =>
614            {
615                Good::Ingredients
616            },
617            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.mineral.") => {
618                Good::Ingredients
619            },
620            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.log.") => {
621                Good::Wood
622            },
623            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.flowers.") => {
624                Good::Ingredients
625            },
626            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.consumable.") => {
627                Good::Potions
628            },
629            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.charms.") => {
630                Good::Potions
631            },
632            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.food.") => {
633                Good::Food
634            },
635            ItemDefinitionIdOwned::Simple(name) if name.as_str() == Self::COIN_ITEM => Good::Coin,
636            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.recipes.") => {
637                Good::Recipe
638            },
639            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.glider.") => {
640                Good::Tools
641            },
642            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.utility.") => {
643                Good::default()
644            },
645            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.boss_drops.") => {
646                Good::Tools
647            },
648            ItemDefinitionIdOwned::Simple(name)
649                if name.starts_with("common.items.crafting_tools.") =>
650            {
651                Good::default()
652            },
653            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.lantern.") => {
654                Good::Tools
655            },
656            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.keys.") => {
657                Good::Tools
658            },
659            ItemDefinitionIdOwned::Modular {
660                pseudo_base: _,
661                components: _,
662            } => Good::Tools,
663            ItemDefinitionIdOwned::Compound {
664                simple_base: _,
665                components: _,
666            } => Good::Tools,
667            _ => {
668                warn!("unknown loot item {:?}", name);
669                Good::default()
670            },
671        }
672    }
673
674    // look up price (inverse frequency) of an item
675    fn price_lookup(&self, requested_name: &ItemDefinitionIdOwned) -> Option<&MaterialUse> {
676        let canonical_name = self.equality_set.canonical(requested_name);
677        self.items
678            .0
679            .iter()
680            .find(|e| &e.name == canonical_name)
681            .map(|e| &e.price)
682    }
683
684    fn calculate_material_cost(&self, r: &RememberedRecipe) -> Option<MaterialUse> {
685        r.input
686            .iter()
687            .map(|(name, amount)| {
688                self.price_lookup(name).map(|x| {
689                    x.clone()
690                        * (if *amount > 0 {
691                            *amount as f32
692                        } else {
693                            Self::INVEST_FACTOR
694                        })
695                })
696            })
697            .try_fold(MaterialUse::default(), |acc, elem| Some(acc + elem?))
698    }
699
700    fn calculate_material_cost_sum(&self, r: &RememberedRecipe) -> Option<f32> {
701        self.calculate_material_cost(r)?
702            .iter()
703            .fold(None, |acc, elem| Some(acc.unwrap_or_default() + elem.0))
704    }
705
706    // re-look up prices and sort the vector by ascending material cost, return
707    // whether first cost is finite
708    fn sort_by_price(&self, recipes: &mut [RememberedRecipe]) -> bool {
709        for recipe in recipes.iter_mut() {
710            recipe.material_cost = self.calculate_material_cost_sum(recipe);
711        }
712        // put None to the end
713        recipes.sort_by(|a, b| {
714            if a.material_cost.is_some() {
715                if b.material_cost.is_some() {
716                    a.material_cost
717                        .partial_cmp(&b.material_cost)
718                        .unwrap_or(Ordering::Equal)
719                } else {
720                    Ordering::Less
721                }
722            } else if b.material_cost.is_some() {
723                Ordering::Greater
724            } else {
725                Ordering::Equal
726            }
727        });
728        if PRICING_DEBUG {
729            for i in recipes.iter() {
730                tracing::debug!("{:?}", *i);
731            }
732        }
733        //info!(? recipes);
734        recipes
735            .first()
736            .filter(|recipe| recipe.material_cost.is_some())
737            .is_some()
738    }
739
740    fn read() -> Self {
741        let mut result = Self::default();
742        let mut freq = FreqEntries::default();
743        let price_config =
744            Ron::<TradingPriceFile>::load_expect("common.trading.item_price_calculation").read();
745        result.equality_set = EqualitySet::load_expect("common.trading.item_price_equality")
746            .read()
747            .clone();
748        for table in &price_config.0.loot_tables {
749            if PRICING_DEBUG {
750                info!(?table);
751            }
752            let (frequency, can_sell, asset_path) = table;
753            let loot = ProbabilityFile::load_expect(asset_path);
754            for (p, item_asset, amount) in &loot.read().content {
755                let good = Self::good_from_item(item_asset);
756                let scaling = get_scaling(&price_config, good);
757                freq.add(
758                    &result.equality_set,
759                    item_asset,
760                    good,
761                    frequency * p * *amount * scaling,
762                    *can_sell,
763                );
764            }
765        }
766        freq.add(
767            &result.equality_set,
768            &ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
769            Good::Coin,
770            get_scaling(&price_config, Good::Coin),
771            true,
772        );
773        // convert frequency to price
774        result.items.0.extend(freq.0.iter().map(|elem| {
775            if elem.freq.0.is_empty() {
776                // likely equality
777                let canonical_name = result.equality_set.canonical(&elem.name);
778                let can_freq = freq.0.iter().find(|i| &i.name == canonical_name);
779                can_freq
780                    .map(|e| PriceEntry {
781                        name: elem.name.clone(),
782                        price: MaterialUse::from(e.freq.clone()),
783                        sell: elem.sell && e.sell,
784                        stackable: elem.stackable,
785                    })
786                    .unwrap_or(PriceEntry {
787                        name: elem.name.clone(),
788                        price: MaterialUse::from(elem.freq.clone()),
789                        sell: elem.sell,
790                        stackable: elem.stackable,
791                    })
792            } else {
793                PriceEntry {
794                    name: elem.name.clone(),
795                    price: MaterialUse::from(elem.freq.clone()),
796                    sell: elem.sell,
797                    stackable: elem.stackable,
798                }
799            }
800        }));
801        if PRICING_DEBUG {
802            for i in result.items.0.iter() {
803                tracing::debug!("before recipes {:?}", *i);
804            }
805        }
806
807        // Apply recipe book
808        let mut secondaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
809        let book = complete_recipe_book().read();
810        let mut ordered_recipes: Vec<RememberedRecipe> = Vec::new();
811        for (_, recipe) in book.iter() {
812            let (ref asset_path, amount) = recipe.output;
813            if let ItemKind::ModularComponent(
814                inventory::item::modular::ModularComponent::ToolSecondaryComponent {
815                    toolkind,
816                    stats: _,
817                    hand_restriction: _,
818                },
819            ) = asset_path.kind
820            {
821                secondaries
822                    .entry(toolkind)
823                    .or_default()
824                    .push(ItemDefinitionIdOwned::Simple(asset_path.id().into()));
825            }
826            ordered_recipes.push(RememberedRecipe {
827                output: ItemDefinitionIdOwned::Simple(asset_path.id().into()),
828                amount,
829                material_cost: None,
830                input: recipe
831                    .inputs
832                    .iter()
833                    .filter_map(|&(ref recipe_input, count, _)| {
834                        if let RecipeInput::Item(it) = recipe_input {
835                            // If item is not consumed in craft, ignore it
836                            if count == 0 {
837                                None
838                            } else {
839                                Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
840                            }
841                        } else {
842                            None
843                        }
844                    })
845                    .collect(),
846            });
847        }
848
849        // modular weapon recipes
850        let mut primaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
851        let comp_book = default_component_recipe_book().read();
852        for (key, recipe) in comp_book.iter() {
853            primaries
854                .entry(key.toolkind)
855                .or_default()
856                .push(recipe.itemdef_output());
857            ordered_recipes.push(RememberedRecipe {
858                output: recipe.itemdef_output(),
859                amount: 1,
860                material_cost: None,
861                input: recipe
862                    .inputs()
863                    .filter_map(|(ref recipe_input, count)| {
864                        if count == 0 {
865                            None
866                        } else {
867                            match recipe_input {
868                                RecipeInput::Item(it) => {
869                                    Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
870                                },
871                                RecipeInput::Tag(_) => todo!(),
872                                RecipeInput::TagSameItem(_) => todo!(),
873                                RecipeInput::ListSameItem(_) => todo!(),
874                            }
875                        }
876                    })
877                    .collect(),
878            });
879        }
880
881        // drain the larger map while iterating the shorter
882        for (kind, mut primary_vec) in primaries.drain() {
883            for primary in primary_vec.drain(0..) {
884                for secondary in secondaries[&kind].iter() {
885                    let input = vec![(primary.clone(), 1), (secondary.clone(), 1)];
886                    let components = vec![primary.clone(), secondary.clone()];
887                    let output = ItemDefinitionIdOwned::Modular {
888                        pseudo_base: ModularBase::Tool.pseudo_item_id().into(),
889                        components,
890                    };
891                    ordered_recipes.push(RememberedRecipe {
892                        output,
893                        amount: 1,
894                        material_cost: None,
895                        input,
896                    });
897                }
898            }
899        }
900        drop(secondaries);
901
902        // re-evaluate prices based on crafting tables
903        // (start with cheap ones to avoid changing material prices after evaluation)
904        let ability_map = &AbilityMap::load().read();
905        let msm = &MaterialStatManifest::load().read();
906        while result.sort_by_price(&mut ordered_recipes) {
907            ordered_recipes.retain(|recipe| {
908                if recipe.material_cost.is_some_and(|p| p < 1e-5) || recipe.amount == 0 {
909                    // don't handle recipes which have no raw materials
910                    false
911                } else if recipe.material_cost.is_some() {
912                    let actual_cost = result.calculate_material_cost(recipe);
913                    if let Some(usage) = actual_cost {
914                        let output_tradeable = recipe.input.iter().all(|(input, _)| {
915                            result
916                                .items
917                                .0
918                                .iter()
919                                .find(|item| item.name == *input)
920                                .is_some_and(|item| item.sell)
921                        });
922                        let stackable = Item::new_from_item_definition_id(
923                            recipe.output.as_ref(),
924                            ability_map,
925                            msm,
926                        )
927                        .is_ok_and(|i| i.is_stackable());
928                        let new_entry = PriceEntry {
929                            name: recipe.output.clone(),
930                            price: usage * (1.0 / (recipe.amount as f32 * Self::CRAFTING_FACTOR)),
931                            sell: output_tradeable,
932                            stackable,
933                        };
934                        if PRICING_DEBUG {
935                            tracing::trace!("Recipe {:?}", new_entry);
936                        }
937                        result.items.add_alternative(new_entry);
938                    } else {
939                        error!("Recipe {:?} incomplete confusion", recipe);
940                    }
941                    false
942                } else {
943                    // handle incomplete recipes later
944                    true
945                }
946            });
947            //info!(?ordered_recipes);
948        }
949        result
950    }
951
952    // TODO: optimize repeated use
953    fn random_items_impl(
954        &self,
955        stockmap: &mut HashMap<Good, f32>,
956        mut number: u32,
957        selling: bool,
958        always_coin: bool,
959        limit: u32,
960        mut permitted: impl FnMut(Good) -> bool,
961    ) -> Vec<(ItemDefinitionIdOwned, u32)> {
962        // 1. Pre-filter from all possible items to keep ones we want.
963        // * Sellable
964        // * Fit under good limit (would probably always be true here)
965        // * Permitted by good disriminant
966        let mut candidates: Vec<&PriceEntry> = self
967            .items
968            .0
969            .iter()
970            .filter(|i| {
971                let excess = i
972                    .price
973                    .iter()
974                    .find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
975                permitted(TradePricing::good_from_item(&i.name))
976                    && excess.is_none()
977                    && (!selling || i.sell)
978                    && (!always_coin
979                        || i.name != ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()))
980            })
981            .collect();
982        // 2. Start putting items
983        let mut result = Vec::new();
984        // 3. Put the coin stack
985        if always_coin && number > 0 {
986            let amount = stockmap.get(&Good::Coin).copied().unwrap_or_default() as u32;
987            if amount > 0 {
988                result.push((
989                    ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
990                    amount,
991                ));
992                number -= 1;
993            }
994        }
995        // 4. At each step, roll an item (and its amount), then discard
996        // corresponding ingredients that the item is made of
997        //
998        // If all we have no more resources (in at least one category),
999        // break out of the loop and return.
1000        for _ in 0..number {
1001            candidates.retain(|i| {
1002                let excess = i
1003                    .price
1004                    .iter()
1005                    .find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
1006                excess.is_none()
1007            });
1008            if candidates.is_empty() {
1009                break;
1010            }
1011            let index = (rand::random::<f32>() * candidates.len() as f32).floor() as usize;
1012            let result2 = candidates[index];
1013            let amount: u32 = if result2.stackable {
1014                let max_amount = result2
1015                    .price
1016                    .iter()
1017                    .map(|e| {
1018                        stockmap
1019                            .get_mut(&e.1)
1020                            .map(|stock| *stock / e.0.max(0.001))
1021                            .unwrap_or_default()
1022                    })
1023                    .fold(f32::INFINITY, f32::min)
1024                    .min(limit as f32);
1025                (rand::random::<f32>() * (max_amount - 1.0)).floor() as u32 + 1
1026            } else {
1027                1
1028            };
1029            for i in result2.price.iter() {
1030                stockmap.get_mut(&i.1).map(|v| *v -= i.0 * (amount as f32));
1031            }
1032            result.push((result2.name.clone(), amount));
1033            // avoid duplicates
1034            candidates.remove(index);
1035        }
1036        result
1037    }
1038
1039    fn get_materials_impl(&self, item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
1040        self.price_lookup(&item.to_owned()).cloned()
1041    }
1042
1043    #[must_use]
1044    pub fn random_items(
1045        stock: &mut HashMap<Good, f32>,
1046        number: u32,
1047        selling: bool,
1048        always_coin: bool,
1049        limit: u32,
1050        permitted: impl FnMut(Good) -> bool,
1051    ) -> Vec<(ItemDefinitionIdOwned, u32)> {
1052        TRADE_PRICING.random_items_impl(stock, number, selling, always_coin, limit, permitted)
1053    }
1054
1055    #[must_use]
1056    pub fn get_materials(item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
1057        TRADE_PRICING.get_materials_impl(item)
1058    }
1059
1060    #[cfg(test)]
1061    fn instance() -> &'static Self { &TRADE_PRICING }
1062
1063    #[cfg(test)]
1064    fn print_sorted(&self) {
1065        use crate::comp::item::{DurabilityMultiplier, armor}; //, ItemKind, MaterialStatManifest};
1066
1067        println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
1068
1069        fn more_information(i: &Item, p: f32) -> (String, &'static str) {
1070            let msm = &MaterialStatManifest::load().read();
1071            let durability_multiplier = DurabilityMultiplier(1.0);
1072
1073            if let ItemKind::Armor(a) = &*i.kind() {
1074                (
1075                    match a.stats(msm, durability_multiplier).protection {
1076                        Some(armor::Protection::Invincible) => "Invincible".into(),
1077                        Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p),
1078                        None => "0.0".into(),
1079                    },
1080                    "prot/val",
1081                )
1082            } else if let ItemKind::Tool(t) = &*i.kind() {
1083                let stats = t.stats(durability_multiplier);
1084                (format!("{:.4}", stats.power * stats.speed * p), "dps/val")
1085            } else if let ItemKind::Consumable {
1086                kind: _,
1087                effects,
1088                container: _,
1089            } = &*i.kind()
1090            {
1091                (
1092                    effects
1093                        .effects()
1094                        .iter()
1095                        .map(|e| {
1096                            if let crate::effect::Effect::Buff(b) = e {
1097                                format!("{:.2}", b.data.strength * p)
1098                            } else {
1099                                format!("{:?}", e)
1100                            }
1101                        })
1102                        .collect::<Vec<String>>()
1103                        .join(" "),
1104                    "str/val",
1105                )
1106            } else {
1107                (Default::default(), "")
1108            }
1109        }
1110        let mut sorted: Vec<(f32, &PriceEntry)> = self
1111            .items
1112            .0
1113            .iter()
1114            .map(|e| (e.price.iter().map(|i| i.0.to_owned()).sum(), e))
1115            .collect();
1116        sorted.sort_by(|(p, e), (p2, e2)| {
1117            p2.partial_cmp(p)
1118                .unwrap_or(Ordering::Equal)
1119                .then(e.name.cmp(&e2.name))
1120        });
1121
1122        for (
1123            pricesum,
1124            PriceEntry {
1125                name: item_id,
1126                price: mat_use,
1127                sell: can_sell,
1128                stackable: _,
1129            },
1130        ) in sorted.iter()
1131        {
1132            Item::new_from_item_definition_id(
1133                item_id.as_ref(),
1134                &AbilityMap::load().read(),
1135                &MaterialStatManifest::load().read(),
1136            )
1137            .ok()
1138            .map(|it| {
1139                //let price = mat_use.iter().map(|(amount, _good)| *amount).sum::<f32>();
1140                let prob = 1.0 / pricesum;
1141                let (info, unit) = more_information(&it, prob);
1142                let materials = mat_use
1143                    .iter()
1144                    .fold(String::new(), |agg, i| agg + &format!("{:?}.", i.1));
1145                println!(
1146                    "{:?}, {}, {:>4.2}, {}, {:?}, {}, {},",
1147                    &item_id,
1148                    if *can_sell { "yes" } else { "no" },
1149                    pricesum,
1150                    materials,
1151                    it.quality(),
1152                    info,
1153                    unit,
1154                );
1155            });
1156        }
1157    }
1158}
1159
1160/// hierarchically combine and scale this loot table
1161#[must_use]
1162pub fn expand_loot_table(loot_table: &str) -> Vec<(f32, ItemDefinitionIdOwned, f32)> {
1163    ProbabilityFile::from(vec![(1.0, LootSpec::LootTable(loot_table.into()))]).content
1164}
1165
1166// if you want to take a look at the calculated values run:
1167// cd common && cargo test trade_pricing -- --nocapture
1168#[cfg(test)]
1169mod tests {
1170    use crate::{comp::inventory::trade_pricing::TradePricing, trade::Good};
1171    use tracing::{Level, info};
1172    use tracing_subscriber::{FmtSubscriber, filter::EnvFilter};
1173
1174    fn init() {
1175        FmtSubscriber::builder()
1176            .with_max_level(Level::ERROR)
1177            .with_env_filter(EnvFilter::from_default_env())
1178            .try_init()
1179            .unwrap_or(());
1180    }
1181
1182    #[test]
1183    fn test_prices1() {
1184        init();
1185        info!("init");
1186
1187        TradePricing::instance().print_sorted();
1188    }
1189
1190    #[test]
1191    fn test_prices2() {
1192        init();
1193        info!("init");
1194
1195        let mut stock: hashbrown::HashMap<Good, f32> = [
1196            (Good::Ingredients, 50.0),
1197            (Good::Tools, 10.0),
1198            (Good::Armor, 10.0),
1199            //(Good::Ores, 20.0),
1200        ]
1201        .iter()
1202        .copied()
1203        .collect();
1204
1205        let loadout = TradePricing::random_items(&mut stock, 20, false, false, 999, |_| true);
1206        for i in loadout.iter() {
1207            info!("Random item {:?}*{}", i.0, i.1);
1208        }
1209    }
1210}