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    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
613            ItemDefinitionIdOwned::Simple(name)
614                if name.starts_with("common.items.crafting_ing.") =>
615            {
616                Good::Ingredients
617            },
618            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.mineral.") => {
619                Good::Ingredients
620            },
621            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.log.") => {
622                Good::Wood
623            },
624            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.flowers.") => {
625                Good::Ingredients
626            },
627            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.consumable.") => {
628                Good::Potions
629            },
630            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.food.") => {
631                Good::Food
632            },
633            ItemDefinitionIdOwned::Simple(name) if name.as_str() == Self::COIN_ITEM => Good::Coin,
634            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.recipes.") => {
635                Good::Recipe
636            },
637            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.glider.") => {
638                Good::Tools
639            },
640            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.utility.") => {
641                Good::default()
642            },
643            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.boss_drops.") => {
644                Good::Tools
645            },
646            ItemDefinitionIdOwned::Simple(name)
647                if name.starts_with("common.items.crafting_tools.") =>
648            {
649                Good::default()
650            },
651            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.lantern.") => {
652                Good::Tools
653            },
654            ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.keys.") => {
655                Good::Tools
656            },
657            ItemDefinitionIdOwned::Modular {
658                pseudo_base: _,
659                components: _,
660            } => Good::Tools,
661            ItemDefinitionIdOwned::Compound {
662                simple_base: _,
663                components: _,
664            } => Good::Ingredients,
665            _ => {
666                warn!("unknown loot item {:?}", name);
667                Good::default()
668            },
669        }
670    }
671
672    // look up price (inverse frequency) of an item
673    fn price_lookup(&self, requested_name: &ItemDefinitionIdOwned) -> Option<&MaterialUse> {
674        let canonical_name = self.equality_set.canonical(requested_name);
675        self.items
676            .0
677            .iter()
678            .find(|e| &e.name == canonical_name)
679            .map(|e| &e.price)
680    }
681
682    fn calculate_material_cost(&self, r: &RememberedRecipe) -> Option<MaterialUse> {
683        r.input
684            .iter()
685            .map(|(name, amount)| {
686                self.price_lookup(name).map(|x| {
687                    x.clone()
688                        * (if *amount > 0 {
689                            *amount as f32
690                        } else {
691                            Self::INVEST_FACTOR
692                        })
693                })
694            })
695            .try_fold(MaterialUse::default(), |acc, elem| Some(acc + elem?))
696    }
697
698    fn calculate_material_cost_sum(&self, r: &RememberedRecipe) -> Option<f32> {
699        self.calculate_material_cost(r)?
700            .iter()
701            .fold(None, |acc, elem| Some(acc.unwrap_or_default() + elem.0))
702    }
703
704    // re-look up prices and sort the vector by ascending material cost, return
705    // whether first cost is finite
706    fn sort_by_price(&self, recipes: &mut [RememberedRecipe]) -> bool {
707        for recipe in recipes.iter_mut() {
708            recipe.material_cost = self.calculate_material_cost_sum(recipe);
709        }
710        // put None to the end
711        recipes.sort_by(|a, b| {
712            if a.material_cost.is_some() {
713                if b.material_cost.is_some() {
714                    a.material_cost
715                        .partial_cmp(&b.material_cost)
716                        .unwrap_or(Ordering::Equal)
717                } else {
718                    Ordering::Less
719                }
720            } else if b.material_cost.is_some() {
721                Ordering::Greater
722            } else {
723                Ordering::Equal
724            }
725        });
726        if PRICING_DEBUG {
727            for i in recipes.iter() {
728                tracing::debug!("{:?}", *i);
729            }
730        }
731        //info!(? recipes);
732        recipes
733            .first()
734            .filter(|recipe| recipe.material_cost.is_some())
735            .is_some()
736    }
737
738    fn read() -> Self {
739        let mut result = Self::default();
740        let mut freq = FreqEntries::default();
741        let price_config =
742            Ron::<TradingPriceFile>::load_expect("common.trading.item_price_calculation").read();
743        result.equality_set = EqualitySet::load_expect("common.trading.item_price_equality")
744            .read()
745            .clone();
746        for table in &price_config.0.loot_tables {
747            if PRICING_DEBUG {
748                info!(?table);
749            }
750            let (frequency, can_sell, asset_path) = table;
751            let loot = ProbabilityFile::load_expect(asset_path);
752            for (p, item_asset, amount) in &loot.read().content {
753                let good = Self::good_from_item(item_asset);
754                let scaling = get_scaling(&price_config, good);
755                freq.add(
756                    &result.equality_set,
757                    item_asset,
758                    good,
759                    frequency * p * *amount * scaling,
760                    *can_sell,
761                );
762            }
763        }
764        freq.add(
765            &result.equality_set,
766            &ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
767            Good::Coin,
768            get_scaling(&price_config, Good::Coin),
769            true,
770        );
771        // convert frequency to price
772        result.items.0.extend(freq.0.iter().map(|elem| {
773            if elem.freq.0.is_empty() {
774                // likely equality
775                let canonical_name = result.equality_set.canonical(&elem.name);
776                let can_freq = freq.0.iter().find(|i| &i.name == canonical_name);
777                can_freq
778                    .map(|e| PriceEntry {
779                        name: elem.name.clone(),
780                        price: MaterialUse::from(e.freq.clone()),
781                        sell: elem.sell && e.sell,
782                        stackable: elem.stackable,
783                    })
784                    .unwrap_or(PriceEntry {
785                        name: elem.name.clone(),
786                        price: MaterialUse::from(elem.freq.clone()),
787                        sell: elem.sell,
788                        stackable: elem.stackable,
789                    })
790            } else {
791                PriceEntry {
792                    name: elem.name.clone(),
793                    price: MaterialUse::from(elem.freq.clone()),
794                    sell: elem.sell,
795                    stackable: elem.stackable,
796                }
797            }
798        }));
799        if PRICING_DEBUG {
800            for i in result.items.0.iter() {
801                tracing::debug!("before recipes {:?}", *i);
802            }
803        }
804
805        // Apply recipe book
806        let mut secondaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
807        let book = complete_recipe_book().read();
808        let mut ordered_recipes: Vec<RememberedRecipe> = Vec::new();
809        for (_, recipe) in book.iter() {
810            let (ref asset_path, amount) = recipe.output;
811            if let ItemKind::ModularComponent(
812                inventory::item::modular::ModularComponent::ToolSecondaryComponent {
813                    toolkind,
814                    stats: _,
815                    hand_restriction: _,
816                },
817            ) = asset_path.kind
818            {
819                secondaries
820                    .entry(toolkind)
821                    .or_default()
822                    .push(ItemDefinitionIdOwned::Simple(asset_path.id().into()));
823            }
824            ordered_recipes.push(RememberedRecipe {
825                output: ItemDefinitionIdOwned::Simple(asset_path.id().into()),
826                amount,
827                material_cost: None,
828                input: recipe
829                    .inputs
830                    .iter()
831                    .filter_map(|&(ref recipe_input, count, _)| {
832                        if let RecipeInput::Item(it) = recipe_input {
833                            // If item is not consumed in craft, ignore it
834                            if count == 0 {
835                                None
836                            } else {
837                                Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
838                            }
839                        } else {
840                            None
841                        }
842                    })
843                    .collect(),
844            });
845        }
846
847        // modular weapon recipes
848        let mut primaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
849        let comp_book = default_component_recipe_book().read();
850        for (key, recipe) in comp_book.iter() {
851            primaries
852                .entry(key.toolkind)
853                .or_default()
854                .push(recipe.itemdef_output());
855            ordered_recipes.push(RememberedRecipe {
856                output: recipe.itemdef_output(),
857                amount: 1,
858                material_cost: None,
859                input: recipe
860                    .inputs()
861                    .filter_map(|(ref recipe_input, count)| {
862                        if count == 0 {
863                            None
864                        } else {
865                            match recipe_input {
866                                RecipeInput::Item(it) => {
867                                    Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
868                                },
869                                RecipeInput::Tag(_) => todo!(),
870                                RecipeInput::TagSameItem(_) => todo!(),
871                                RecipeInput::ListSameItem(_) => todo!(),
872                            }
873                        }
874                    })
875                    .collect(),
876            });
877        }
878
879        // drain the larger map while iterating the shorter
880        for (kind, mut primary_vec) in primaries.drain() {
881            for primary in primary_vec.drain(0..) {
882                for secondary in secondaries[&kind].iter() {
883                    let input = vec![(primary.clone(), 1), (secondary.clone(), 1)];
884                    let components = vec![primary.clone(), secondary.clone()];
885                    let output = ItemDefinitionIdOwned::Modular {
886                        pseudo_base: ModularBase::Tool.pseudo_item_id().into(),
887                        components,
888                    };
889                    ordered_recipes.push(RememberedRecipe {
890                        output,
891                        amount: 1,
892                        material_cost: None,
893                        input,
894                    });
895                }
896            }
897        }
898        drop(secondaries);
899
900        // re-evaluate prices based on crafting tables
901        // (start with cheap ones to avoid changing material prices after evaluation)
902        let ability_map = &AbilityMap::load().read();
903        let msm = &MaterialStatManifest::load().read();
904        while result.sort_by_price(&mut ordered_recipes) {
905            ordered_recipes.retain(|recipe| {
906                if recipe.material_cost.is_some_and(|p| p < 1e-5) || recipe.amount == 0 {
907                    // don't handle recipes which have no raw materials
908                    false
909                } else if recipe.material_cost.is_some() {
910                    let actual_cost = result.calculate_material_cost(recipe);
911                    if let Some(usage) = actual_cost {
912                        let output_tradeable = recipe.input.iter().all(|(input, _)| {
913                            result
914                                .items
915                                .0
916                                .iter()
917                                .find(|item| item.name == *input)
918                                .is_some_and(|item| item.sell)
919                        });
920                        let stackable = Item::new_from_item_definition_id(
921                            recipe.output.as_ref(),
922                            ability_map,
923                            msm,
924                        )
925                        .is_ok_and(|i| i.is_stackable());
926                        let new_entry = PriceEntry {
927                            name: recipe.output.clone(),
928                            price: usage * (1.0 / (recipe.amount as f32 * Self::CRAFTING_FACTOR)),
929                            sell: output_tradeable,
930                            stackable,
931                        };
932                        if PRICING_DEBUG {
933                            tracing::trace!("Recipe {:?}", new_entry);
934                        }
935                        result.items.add_alternative(new_entry);
936                    } else {
937                        error!("Recipe {:?} incomplete confusion", recipe);
938                    }
939                    false
940                } else {
941                    // handle incomplete recipes later
942                    true
943                }
944            });
945            //info!(?ordered_recipes);
946        }
947        result
948    }
949
950    // TODO: optimize repeated use
951    fn random_items_impl(
952        &self,
953        stockmap: &mut HashMap<Good, f32>,
954        mut number: u32,
955        selling: bool,
956        always_coin: bool,
957        limit: u32,
958    ) -> Vec<(ItemDefinitionIdOwned, u32)> {
959        let mut candidates: Vec<&PriceEntry> = self
960            .items
961            .0
962            .iter()
963            .filter(|i| {
964                let excess = i
965                    .price
966                    .iter()
967                    .find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
968                excess.is_none()
969                    && (!selling || i.sell)
970                    && (!always_coin
971                        || i.name != ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()))
972            })
973            .collect();
974        let mut result = Vec::new();
975        if always_coin && number > 0 {
976            let amount = stockmap.get(&Good::Coin).copied().unwrap_or_default() as u32;
977            if amount > 0 {
978                result.push((
979                    ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
980                    amount,
981                ));
982                number -= 1;
983            }
984        }
985        for _ in 0..number {
986            candidates.retain(|i| {
987                let excess = i
988                    .price
989                    .iter()
990                    .find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
991                excess.is_none()
992            });
993            if candidates.is_empty() {
994                break;
995            }
996            let index = (rand::random::<f32>() * candidates.len() as f32).floor() as usize;
997            let result2 = candidates[index];
998            let amount: u32 = if result2.stackable {
999                let max_amount = result2
1000                    .price
1001                    .iter()
1002                    .map(|e| {
1003                        stockmap
1004                            .get_mut(&e.1)
1005                            .map(|stock| *stock / e.0.max(0.001))
1006                            .unwrap_or_default()
1007                    })
1008                    .fold(f32::INFINITY, f32::min)
1009                    .min(limit as f32);
1010                (rand::random::<f32>() * (max_amount - 1.0)).floor() as u32 + 1
1011            } else {
1012                1
1013            };
1014            for i in result2.price.iter() {
1015                stockmap.get_mut(&i.1).map(|v| *v -= i.0 * (amount as f32));
1016            }
1017            result.push((result2.name.clone(), amount));
1018            // avoid duplicates
1019            candidates.remove(index);
1020        }
1021        result
1022    }
1023
1024    fn get_materials_impl(&self, item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
1025        self.price_lookup(&item.to_owned()).cloned()
1026    }
1027
1028    #[must_use]
1029    pub fn random_items(
1030        stock: &mut HashMap<Good, f32>,
1031        number: u32,
1032        selling: bool,
1033        always_coin: bool,
1034        limit: u32,
1035    ) -> Vec<(ItemDefinitionIdOwned, u32)> {
1036        TRADE_PRICING.random_items_impl(stock, number, selling, always_coin, limit)
1037    }
1038
1039    #[must_use]
1040    pub fn get_materials(item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
1041        TRADE_PRICING.get_materials_impl(item)
1042    }
1043
1044    #[cfg(test)]
1045    fn instance() -> &'static Self { &TRADE_PRICING }
1046
1047    #[cfg(test)]
1048    fn print_sorted(&self) {
1049        use crate::comp::item::{DurabilityMultiplier, armor}; //, ItemKind, MaterialStatManifest};
1050
1051        println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
1052
1053        fn more_information(i: &Item, p: f32) -> (String, &'static str) {
1054            let msm = &MaterialStatManifest::load().read();
1055            let durability_multiplier = DurabilityMultiplier(1.0);
1056
1057            if let ItemKind::Armor(a) = &*i.kind() {
1058                (
1059                    match a.stats(msm, durability_multiplier).protection {
1060                        Some(armor::Protection::Invincible) => "Invincible".into(),
1061                        Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p),
1062                        None => "0.0".into(),
1063                    },
1064                    "prot/val",
1065                )
1066            } else if let ItemKind::Tool(t) = &*i.kind() {
1067                let stats = t.stats(durability_multiplier);
1068                (format!("{:.4}", stats.power * stats.speed * p), "dps/val")
1069            } else if let ItemKind::Consumable {
1070                kind: _,
1071                effects,
1072                container: _,
1073            } = &*i.kind()
1074            {
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}