veloren_common/comp/inventory/
trade_pricing.rs

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