veloren_common/
recipe.rs

1use crate::{
2    assets::{
3        self, Asset, AssetCache, AssetExt, AssetHandle, BoxedError, CacheCombined, Ron,
4        SharedString,
5    },
6    comp::{
7        Inventory, Item,
8        inventory::slot::{InvSlotId, Slot},
9        item::{
10            ItemBase, ItemDef, ItemDefinitionId, ItemDefinitionIdOwned, ItemKind, ItemTag,
11            MaterialStatManifest, modular,
12            tool::{AbilityMap, ToolKind},
13        },
14    },
15    terrain::SpriteKind,
16};
17use hashbrown::HashMap;
18use serde::{Deserialize, Serialize};
19use std::{borrow::Cow, sync::Arc};
20
21#[derive(Clone, Debug, Serialize, Deserialize)]
22pub enum RecipeInput {
23    /// Only an item with a matching ItemDef can be used to satisfy this input
24    Item(Arc<ItemDef>),
25    /// Any items with this tag can be used to satisfy this input
26    Tag(ItemTag),
27    /// Similar to RecipeInput::Tag(_), but all items must be the same.
28    /// Specifically this means that a mix of different items with the tag
29    /// cannot be used.
30    /// TODO: Currently requires that all items must be in the same slot.
31    /// Eventually should be reworked so that items can be spread over multiple
32    /// slots.
33    TagSameItem(ItemTag),
34    /// List is similar to tag, but has items defined in centralized file
35    /// Similar to RecipeInput::TagSameItem(_), all items must be the same, they
36    /// cannot be a mix of different items defined in the list.
37    // Intent of using List over Tag is to make it harder for tag to be innocuously added to an
38    // item breaking a recipe
39    /// TODO: Currently requires that all items must be in the same slot.
40    /// Eventually should be reworked so that items can be spread over multiple
41    /// slots.
42    ListSameItem(Vec<Arc<ItemDef>>),
43}
44
45impl RecipeInput {
46    fn handle_requirement<'a, I: Iterator<Item = InvSlotId>>(
47        &'a self,
48        amount: u32,
49        slot_claims: &mut HashMap<InvSlotId, u32>,
50        unsatisfied_requirements: &mut Vec<(&'a RecipeInput, u32)>,
51        inv: &Inventory,
52        input_slots: I,
53    ) {
54        let mut required = amount;
55        // contains_any check used for recipes that have an input that is not consumed,
56        // e.g. craftsman hammer
57        // Goes through each slot and marks some amount from each slot as claimed
58        let contains_any = input_slots.into_iter().all(|slot| {
59            // Checks that the item in the slot can be used for the input
60            if let Some(item) = inv
61                .get(slot)
62                .filter(|item| item.matches_recipe_input(self, amount))
63            {
64                // Gets the number of items claimed from the slot, or sets to 0 if slot has
65                // not been claimed by another input yet
66                let claimed = slot_claims.entry(slot).or_insert(0);
67                let available = item.amount().saturating_sub(*claimed);
68                let provided = available.min(required);
69                required -= provided;
70                *claimed += provided;
71                true
72            } else {
73                false
74            }
75        });
76        // If there were not sufficient items to cover requirement between all provided
77        // slots, or if non-consumed item was not present, mark input as not satisfied
78        if required > 0 || !contains_any {
79            unsatisfied_requirements.push((self, required));
80        }
81    }
82}
83
84#[derive(Clone, Debug, Serialize, Deserialize)]
85pub struct Recipe {
86    pub output: (Arc<ItemDef>, u32),
87    /// Input required for recipe, amount of input needed, whether input should
88    /// be tracked as a modular component
89    pub inputs: Vec<(RecipeInput, u32, bool)>,
90    pub craft_sprite: Option<SpriteKind>,
91}
92
93impl Recipe {
94    /// Perform a recipe, returning a list of missing items on failure
95    pub fn craft_simple(
96        &self,
97        inv: &mut Inventory,
98        // Vec tying an input to a slot
99        slots: Vec<(u32, InvSlotId)>,
100        ability_map: &AbilityMap,
101        msm: &MaterialStatManifest,
102    ) -> Result<Vec<Item>, Vec<(&RecipeInput, u32)>> {
103        let mut slot_claims = HashMap::new();
104        let mut unsatisfied_requirements = Vec::new();
105        let mut component_slots = Vec::new();
106
107        // Checks each input against slots in the inventory. If the slots contain an
108        // item that fulfills the need of the input, marks some of the item as claimed
109        // up to quantity needed for the crafting input. If the item either
110        // cannot be used, or there is insufficient quantity, adds input and
111        // number of materials needed to unsatisfied requirements.
112        self.inputs.iter().enumerate().for_each(
113            |(i, &(ref input, ref amount, mut is_component))| {
114                let mut required = *amount;
115                // Check used for recipes that have an input that is not consumed, e.g.
116                // craftsman hammer
117                let mut contains_any = false;
118                // Gets all slots provided for this input by the frontend
119                let input_slots = slots
120                    .iter()
121                    .filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None });
122                // Goes through each slot and marks some amount from each slot as claimed
123                for slot in input_slots {
124                    // Checks that the item in the slot can be used for the input
125                    if let Some(item) = inv
126                        .get(*slot)
127                        .filter(|item| item.matches_recipe_input(input, *amount))
128                    {
129                        // Gets the number of items claimed from the slot, or sets to 0 if slot has
130                        // not been claimed by another input yet
131                        let claimed = slot_claims.entry(*slot).or_insert(0);
132                        let available = item.amount().saturating_sub(*claimed);
133                        let provided = available.min(required);
134                        required -= provided;
135                        *claimed += provided;
136                        // If input is a component and provided amount from this slot at least 1,
137                        // mark 1 piece as coming from that slot and set is_component to false to
138                        // indicate it has been claimed.
139                        if provided > 0 && is_component {
140                            component_slots.push(*slot);
141                            is_component = false;
142                        }
143                        contains_any = true;
144                    }
145                }
146                // If there were not sufficient items to cover requirement between all provided
147                // slots, or if non-consumed item was not present, mark input as not satisfied
148                if required > 0 || !contains_any {
149                    unsatisfied_requirements.push((input, required));
150                }
151            },
152        );
153
154        // If there are no unsatisfied requirements, create the items produced by the
155        // recipe in the necessary quantity and remove the items that the recipe
156        // consumes
157        if unsatisfied_requirements.is_empty() {
158            let mut components = Vec::new();
159            for slot in component_slots.iter() {
160                let component = inv
161                    .take(*slot, ability_map, msm)
162                    .expect("Expected item to exist in the inventory");
163                components.push(component);
164                let to_remove = slot_claims
165                    .get_mut(slot)
166                    .expect("If marked in component slots, should be in slot claims");
167                *to_remove -= 1;
168            }
169            for (slot, to_remove) in slot_claims.iter() {
170                for _ in 0..*to_remove {
171                    let _ = inv
172                        .take(*slot, ability_map, msm)
173                        .expect("Expected item to exist in the inventory");
174                }
175            }
176            let (item_def, quantity) = &self.output;
177
178            let crafted_item = Item::new_from_item_base(
179                ItemBase::Simple(Arc::clone(item_def)),
180                components,
181                ability_map,
182                msm,
183            );
184            let mut crafted_items = Vec::with_capacity(*quantity as usize);
185            for _ in 0..*quantity {
186                crafted_items.push(crafted_item.duplicate(ability_map, msm));
187            }
188            Ok(crafted_items)
189        } else {
190            Err(unsatisfied_requirements)
191        }
192    }
193
194    pub fn inputs(&self) -> impl ExactSizeIterator<Item = (&RecipeInput, u32, bool)> {
195        self.inputs
196            .iter()
197            .map(|(item_def, amount, is_mod_comp)| (item_def, *amount, *is_mod_comp))
198    }
199
200    /// Determine whether the inventory contains the ingredients for a recipe.
201    /// If it does, return a vec of inventory slots that contain the
202    /// ingredients needed, whose positions correspond to particular recipe
203    /// inputs. If items are missing, return the missing items, and how many
204    /// are missing.
205    pub fn inventory_contains_ingredients(
206        &self,
207        inv: &Inventory,
208        recipe_amount: u32,
209    ) -> Result<Vec<(u32, InvSlotId)>, Vec<(&RecipeInput, u32)>> {
210        inventory_contains_ingredients(
211            self.inputs()
212                .map(|(input, amount, _is_modular)| (input, amount)),
213            inv,
214            recipe_amount,
215        )
216    }
217
218    /// Calculates the maximum number of items craftable given the current
219    /// inventory state.
220    pub fn max_from_ingredients(&self, inv: &Inventory) -> u32 {
221        let mut max_recipes = None;
222
223        for (input, amount) in self
224            .inputs()
225            .map(|(input, amount, _is_modular)| (input, amount))
226        {
227            let needed = amount as f32;
228            let mut input_max = HashMap::<InvSlotId, u32>::new();
229
230            // Checks through every slot, filtering to only those that contain items that
231            // can satisfy the input.
232            for (inv_slot_id, slot) in inv.slots_with_id() {
233                if let Some(item) = slot
234                    .as_ref()
235                    .filter(|item| item.matches_recipe_input(input, amount))
236                {
237                    *input_max.entry(inv_slot_id).or_insert(0) += item.amount();
238                }
239            }
240
241            // Updates maximum craftable amount based on least recipe-proportional
242            // availability.
243            let max_item_proportion =
244                ((input_max.values().sum::<u32>() as f32) / needed).floor() as u32;
245            max_recipes = Some(match max_recipes {
246                None => max_item_proportion,
247                Some(max_recipes) if (max_item_proportion < max_recipes) => max_item_proportion,
248                Some(n) => n,
249            });
250        }
251
252        max_recipes.unwrap_or(0)
253    }
254}
255
256/// Determine whether the inventory contains the ingredients for a recipe.
257/// If it does, return a vec of inventory slots that contain the
258/// ingredients needed, whose positions correspond to particular recipe
259/// inputs. If items are missing, return the missing items, and how many
260/// are missing.
261// Note: Doc comment duplicated on two public functions that call this function
262fn inventory_contains_ingredients<'a, I: Iterator<Item = (&'a RecipeInput, u32)>>(
263    ingredients: I,
264    inv: &Inventory,
265    recipe_amount: u32,
266) -> Result<Vec<(u32, InvSlotId)>, Vec<(&'a RecipeInput, u32)>> {
267    // Hashmap tracking the quantity that needs to be removed from each slot (so
268    // that it doesn't think a slot can provide more items than it contains)
269    let mut slot_claims = HashMap::<InvSlotId, u32>::new();
270    // Important to be a vec and to remain separate from slot_claims as it must
271    // remain ordered, unlike the hashmap
272    let mut slots = Vec::<(u32, InvSlotId)>::new();
273    // The inputs to a recipe that have missing items, and the amount missing
274    let mut missing = Vec::<(&RecipeInput, u32)>::new();
275
276    for (i, (input, amount)) in ingredients.enumerate() {
277        let mut needed = amount * recipe_amount;
278        let mut contains_any = false;
279        // Checks through every slot, filtering to only those that contain items that
280        // can satisfy the input
281        for (inv_slot_id, slot) in inv.slots_with_id() {
282            if let Some(item) = slot
283                .as_ref()
284                .filter(|item| item.matches_recipe_input(input, amount))
285            {
286                let claim = slot_claims.entry(inv_slot_id).or_insert(0);
287                slots.push((i as u32, inv_slot_id));
288                let can_claim = (item.amount().saturating_sub(*claim)).min(needed);
289                *claim += can_claim;
290                needed -= can_claim;
291                contains_any = true;
292            }
293        }
294
295        if needed > 0 || !contains_any {
296            missing.push((input, needed));
297        }
298    }
299
300    if missing.is_empty() {
301        Ok(slots)
302    } else {
303        Err(missing)
304    }
305}
306
307pub enum SalvageError {
308    NotSalvageable,
309}
310
311pub fn try_salvage(
312    inv: &mut Inventory,
313    slot: InvSlotId,
314    ability_map: &AbilityMap,
315    msm: &MaterialStatManifest,
316) -> Result<Vec<Item>, SalvageError> {
317    if inv.get(slot).is_some_and(|item| item.is_salvageable()) {
318        let salvage_item = inv.get(slot).expect("Expected item to exist in inventory");
319        let salvage_output: Vec<_> = salvage_item
320            .salvage_output()
321            .flat_map(|(material, quantity)| {
322                std::iter::repeat_n(Item::new_from_asset_expect(material), quantity as usize)
323            })
324            .collect();
325        if salvage_output.is_empty() {
326            // If no output items, assume salvaging was a failure
327            // TODO: If we ever change salvaging to have a percent chance, remove the check
328            // of outputs being empty (requires assets to exist for rock and wood materials
329            // so that salvaging doesn't silently fail)
330            Err(SalvageError::NotSalvageable)
331        } else {
332            // Remove item that is being salvaged
333            let _ = inv
334                .take(slot, ability_map, msm)
335                .expect("Expected item to exist in inventory");
336            // Return the salvaging output
337            Ok(salvage_output)
338        }
339    } else {
340        Err(SalvageError::NotSalvageable)
341    }
342}
343
344pub enum ModularWeaponError {
345    InvalidSlot,
346    ComponentMismatch,
347    DifferentTools,
348    DifferentHands,
349}
350
351pub fn modular_weapon(
352    inv: &mut Inventory,
353    primary_component: InvSlotId,
354    secondary_component: InvSlotId,
355    ability_map: &AbilityMap,
356    msm: &MaterialStatManifest,
357) -> Result<Item, ModularWeaponError> {
358    use modular::ModularComponent;
359    // Closure to get inner modular component info from item in a given slot
360    fn unwrap_modular(inv: &Inventory, slot: InvSlotId) -> Option<Cow<'_, ModularComponent>> {
361        inv.get(slot).and_then(|item| match item.kind() {
362            Cow::Owned(ItemKind::ModularComponent(mod_comp)) => Some(Cow::Owned(mod_comp)),
363            Cow::Borrowed(ItemKind::ModularComponent(mod_comp)) => Some(Cow::Borrowed(mod_comp)),
364            _ => None,
365        })
366    }
367
368    // Checks if both components are compatible, and if so returns the toolkind to
369    // make weapon of
370    let compatiblity = if let (Some(primary_component), Some(secondary_component)) = (
371        unwrap_modular(inv, primary_component),
372        unwrap_modular(inv, secondary_component),
373    ) {
374        // Checks that damage and held component slots each contain a damage and held
375        // modular component respectively
376        if let (
377            ModularComponent::ToolPrimaryComponent {
378                toolkind: tool_a,
379                hand_restriction: hands_a,
380                ..
381            },
382            ModularComponent::ToolSecondaryComponent {
383                toolkind: tool_b,
384                hand_restriction: hands_b,
385                ..
386            },
387        ) = (&*primary_component, &*secondary_component)
388        {
389            // Checks that both components are of the same tool kind
390            if tool_a == tool_b {
391                // Checks that if both components have a hand restriction, they are the same
392                let hands_check = hands_a.zip(*hands_b).is_none_or(|(a, b)| a == b);
393                if hands_check {
394                    Ok(())
395                } else {
396                    Err(ModularWeaponError::DifferentHands)
397                }
398            } else {
399                Err(ModularWeaponError::DifferentTools)
400            }
401        } else {
402            Err(ModularWeaponError::ComponentMismatch)
403        }
404    } else {
405        Err(ModularWeaponError::InvalidSlot)
406    };
407
408    match compatiblity {
409        Ok(()) => {
410            // Remove components from inventory
411            let primary_component = inv
412                .take(primary_component, ability_map, msm)
413                .expect("Expected component to exist");
414            let secondary_component = inv
415                .take(secondary_component, ability_map, msm)
416                .expect("Expected component to exist");
417
418            // Create modular weapon
419            Ok(Item::new_from_item_base(
420                ItemBase::Modular(modular::ModularBase::Tool),
421                vec![primary_component, secondary_component],
422                ability_map,
423                msm,
424            ))
425        },
426        Err(err) => Err(err),
427    }
428}
429
430#[derive(Clone, Debug, Serialize, Deserialize)]
431pub struct RecipeBookManifest {
432    recipes: HashMap<String, Recipe>,
433}
434
435impl RecipeBookManifest {
436    pub fn load() -> AssetHandle<Self> { Self::load_expect("common.recipe_book_manifest") }
437
438    pub fn get(&self, recipe: &str) -> Option<&Recipe> { self.recipes.get(recipe) }
439
440    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&String, &Recipe)> { self.recipes.iter() }
441
442    pub fn keys(&self) -> impl ExactSizeIterator<Item = &String> { self.recipes.keys() }
443
444    pub fn get_available(&self, inv: &Inventory) -> Vec<(String, Recipe)> {
445        self.recipes
446            .iter()
447            .filter(|(_, recipe)| recipe.inventory_contains_ingredients(inv, 1).is_ok())
448            .map(|(name, recipe)| (name.clone(), recipe.clone()))
449            .collect()
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    #[test]
458    fn complete_recipe_book_valid_key_check() {
459        let recipe_book = complete_recipe_book().read();
460        let is_invalid_key =
461            |input: &str| input.chars().any(|c| c.is_uppercase() || c.is_whitespace());
462        assert!(!recipe_book.iter().any(|(k, _)| is_invalid_key(k)));
463    }
464}
465
466#[derive(Clone, Debug, Serialize, Deserialize)]
467pub enum RawRecipeInput {
468    Item(String),
469    Tag(ItemTag),
470    TagSameItem(ItemTag),
471    ListSameItem(String),
472}
473
474impl RawRecipeInput {
475    fn load_recipe_input(&self) -> Result<RecipeInput, assets::Error> {
476        let input = match self {
477            RawRecipeInput::Item(name) => RecipeInput::Item(Arc::<ItemDef>::load_cloned(name)?),
478            RawRecipeInput::Tag(tag) => RecipeInput::Tag(*tag),
479            RawRecipeInput::TagSameItem(tag) => RecipeInput::TagSameItem(*tag),
480            RawRecipeInput::ListSameItem(list) => {
481                let assets = Ron::<Vec<String>>::load_expect(list).read();
482                let items = assets
483                    .0
484                    .iter()
485                    .map(|asset| Arc::<ItemDef>::load_expect_cloned(asset))
486                    .collect();
487                RecipeInput::ListSameItem(items)
488            },
489        };
490        Ok(input)
491    }
492}
493
494#[derive(Clone, Deserialize)]
495pub(crate) struct RawRecipe {
496    pub(crate) output: (String, u32),
497    /// Input required for recipe, amount of input needed, whether input should
498    /// be tracked as a modular component
499    pub(crate) inputs: Vec<(RawRecipeInput, u32, bool)>,
500    pub(crate) craft_sprite: Option<SpriteKind>,
501}
502
503impl Asset for RecipeBookManifest {
504    fn load(cache: &AssetCache, specifier: &SharedString) -> Result<Self, BoxedError> {
505        fn load_item_def(spec: &(String, u32)) -> Result<(Arc<ItemDef>, u32), assets::Error> {
506            let def = Arc::<ItemDef>::load_cloned(&spec.0)?;
507            Ok((def, spec.1))
508        }
509
510        fn load_recipe_input(
511            (input, amount, is_mod_comp): &(RawRecipeInput, u32, bool),
512        ) -> Result<(RecipeInput, u32, bool), assets::Error> {
513            let def = input.load_recipe_input()?;
514            Ok((def, *amount, *is_mod_comp))
515        }
516
517        let raw = cache
518            .load_and_combine::<Ron<HashMap<String, RawRecipe>>>(specifier)?
519            .cloned()
520            .into_inner();
521
522        let recipes = raw
523            .iter()
524            .map(
525                |(
526                    name,
527                    RawRecipe {
528                        output,
529                        inputs,
530                        craft_sprite,
531                    },
532                )| {
533                    let inputs = inputs
534                        .iter()
535                        .map(load_recipe_input)
536                        .collect::<Result<Vec<_>, _>>()?;
537                    let output = load_item_def(output)?;
538                    Ok((name.clone(), Recipe {
539                        output,
540                        inputs,
541                        craft_sprite: *craft_sprite,
542                    }))
543                },
544            )
545            .collect::<Result<_, assets::Error>>()?;
546
547        Ok(RecipeBookManifest { recipes })
548    }
549}
550
551#[derive(Clone, Debug, Serialize, Deserialize)]
552pub struct ComponentRecipeBook {
553    recipes: HashMap<ComponentKey, ComponentRecipe>,
554}
555
556#[derive(Clone, Debug)]
557pub struct ReverseComponentRecipeBook {
558    recipes: HashMap<ItemDefinitionIdOwned, ComponentRecipe>,
559}
560
561impl ComponentRecipeBook {
562    pub fn get(&self, key: &ComponentKey) -> Option<&ComponentRecipe> { self.recipes.get(key) }
563
564    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&ComponentKey, &ComponentRecipe)> {
565        self.recipes.iter()
566    }
567}
568
569impl ReverseComponentRecipeBook {
570    pub fn get(&self, key: &ItemDefinitionIdOwned) -> Option<&ComponentRecipe> {
571        self.recipes.get(key)
572    }
573}
574
575#[derive(Clone, Debug, Serialize, Deserialize, Hash, Eq, PartialEq)]
576pub struct ComponentKey {
577    // Can't use ItemDef here because hash needed, item definition id used instead
578    // TODO: Make more general for other things that have component inputs that should be tracked
579    // after item creation
580    pub toolkind: ToolKind,
581    /// Refers to the item definition id of the material
582    pub material: String,
583    /// Refers to the item definition id of the modifier
584    pub modifier: Option<String>,
585}
586
587#[derive(Clone, Debug, Serialize, Deserialize)]
588pub struct ComponentRecipe {
589    pub recipe_book_key: String,
590    output: ComponentOutput,
591    material: (RecipeInput, u32),
592    modifier: Option<(RecipeInput, u32)>,
593    additional_inputs: Vec<(RecipeInput, u32)>,
594    pub craft_sprite: Option<SpriteKind>,
595}
596
597impl ComponentRecipe {
598    /// Craft an item that has components, returning a list of missing items on
599    /// failure
600    pub fn craft_component(
601        &self,
602        inv: &mut Inventory,
603        material_slot: InvSlotId,
604        modifier_slot: Option<InvSlotId>,
605        // Vec tying an input to a slot
606        slots: Vec<(u32, InvSlotId)>,
607        ability_map: &AbilityMap,
608        msm: &MaterialStatManifest,
609    ) -> Result<Vec<Item>, Vec<(&RecipeInput, u32)>> {
610        let mut slot_claims = HashMap::new();
611        let mut unsatisfied_requirements = Vec::new();
612
613        // Checks each input against slots in the inventory. If the slots contain an
614        // item that fulfills the need of the input, marks some of the item as claimed
615        // up to quantity needed for the crafting input. If the item either
616        // cannot be used, or there is insufficient quantity, adds input and
617        // number of materials needed to unsatisfied requirements.
618        self.material.0.handle_requirement(
619            self.material.1,
620            &mut slot_claims,
621            &mut unsatisfied_requirements,
622            inv,
623            core::iter::once(material_slot),
624        );
625        if let Some((modifier_input, modifier_amount)) = &self.modifier {
626            // TODO: Better way to get slot to use that ensures this requirement fails if no
627            // slot provided?
628            modifier_input.handle_requirement(
629                *modifier_amount,
630                &mut slot_claims,
631                &mut unsatisfied_requirements,
632                inv,
633                core::iter::once(modifier_slot.unwrap_or(InvSlotId::new(0, 0))),
634            );
635        }
636        self.additional_inputs
637            .iter()
638            .enumerate()
639            .for_each(|(i, (input, amount))| {
640                // Gets all slots provided for this input by the frontend
641                let input_slots = slots
642                    .iter()
643                    .filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None })
644                    .copied();
645                // Checks if requirement is met, and if not marks it as unsatisfied
646                input.handle_requirement(
647                    *amount,
648                    &mut slot_claims,
649                    &mut unsatisfied_requirements,
650                    inv,
651                    input_slots,
652                );
653            });
654
655        // If there are no unsatisfied requirements, create the items produced by the
656        // recipe in the necessary quantity and remove the items that the recipe
657        // consumes
658        if unsatisfied_requirements.is_empty() {
659            for (slot, to_remove) in slot_claims.iter() {
660                for _ in 0..*to_remove {
661                    let _ = inv
662                        .take(*slot, ability_map, msm)
663                        .expect("Expected item to exist in the inventory");
664                }
665            }
666
667            let crafted_item = self.item_output(ability_map, msm);
668
669            Ok(vec![crafted_item])
670        } else {
671            Err(unsatisfied_requirements)
672        }
673    }
674
675    /// Determine whether the inventory contains the additional ingredients for
676    /// a component recipe. If it does, return a vec of inventory slots that
677    /// contain the ingredients needed, whose positions correspond to particular
678    /// recipe are missing.
679    pub fn inventory_contains_additional_ingredients(
680        &self,
681        inv: &Inventory,
682    ) -> Result<Vec<(u32, InvSlotId)>, Vec<(&RecipeInput, u32)>> {
683        inventory_contains_ingredients(
684            self.additional_inputs
685                .iter()
686                .map(|(input, amount)| (input, *amount)),
687            inv,
688            1,
689        )
690    }
691
692    pub fn itemdef_output(&self) -> ItemDefinitionIdOwned {
693        match &self.output {
694            ComponentOutput::ItemComponents {
695                item: item_def,
696                components,
697            } => {
698                let components = components
699                    .iter()
700                    .map(|item_def| ItemDefinitionIdOwned::Simple(item_def.id().to_owned()))
701                    .collect::<Vec<_>>();
702                ItemDefinitionIdOwned::Compound {
703                    simple_base: item_def.id().to_owned(),
704                    components,
705                }
706            },
707        }
708    }
709
710    pub fn item_output(&self, ability_map: &AbilityMap, msm: &MaterialStatManifest) -> Item {
711        match &self.output {
712            ComponentOutput::ItemComponents {
713                item: item_def,
714                components,
715            } => {
716                let components = components
717                    .iter()
718                    .map(|item_def| {
719                        Item::new_from_item_base(
720                            ItemBase::Simple(Arc::clone(item_def)),
721                            Vec::new(),
722                            ability_map,
723                            msm,
724                        )
725                    })
726                    .collect::<Vec<_>>();
727                Item::new_from_item_base(
728                    ItemBase::Simple(Arc::clone(item_def)),
729                    components,
730                    ability_map,
731                    msm,
732                )
733            },
734        }
735    }
736
737    pub fn inputs(&self) -> impl ExactSizeIterator<Item = (&RecipeInput, u32)> {
738        self.into_iter().map(|(recipe, amount)| (recipe, *amount))
739    }
740}
741
742pub struct ComponentRecipeInputsIterator<'a> {
743    material: Option<&'a (RecipeInput, u32)>,
744    modifier: Option<&'a (RecipeInput, u32)>,
745    additional_inputs: std::slice::Iter<'a, (RecipeInput, u32)>,
746}
747
748impl<'a> Iterator for ComponentRecipeInputsIterator<'a> {
749    type Item = &'a (RecipeInput, u32);
750
751    fn next(&mut self) -> Option<&'a (RecipeInput, u32)> {
752        self.material
753            .take()
754            .or_else(|| self.modifier.take())
755            .or_else(|| self.additional_inputs.next())
756    }
757}
758
759impl<'a> IntoIterator for &'a ComponentRecipe {
760    type IntoIter = ComponentRecipeInputsIterator<'a>;
761    type Item = &'a (RecipeInput, u32);
762
763    fn into_iter(self) -> Self::IntoIter {
764        ComponentRecipeInputsIterator {
765            material: Some(&self.material),
766            modifier: self.modifier.as_ref(),
767            additional_inputs: self.additional_inputs.as_slice().iter(),
768        }
769    }
770}
771
772impl ExactSizeIterator for ComponentRecipeInputsIterator<'_> {
773    fn len(&self) -> usize {
774        self.material.is_some() as usize
775            + self.modifier.is_some() as usize
776            + self.additional_inputs.len()
777    }
778}
779
780#[derive(Clone, Deserialize)]
781struct RawComponentRecipe {
782    recipe_book_key: String,
783    output: RawComponentOutput,
784    /// String refers to an item definition id
785    material: (String, u32),
786    /// String refers to an item definition id
787    modifier: Option<(String, u32)>,
788    additional_inputs: Vec<(RawRecipeInput, u32)>,
789    craft_sprite: Option<SpriteKind>,
790}
791
792#[derive(Clone, Debug, Serialize, Deserialize)]
793enum ComponentOutput {
794    // TODO: Don't store list of components here in case we ever want components in future to have
795    // state to them (e.g. a component having sub-components)
796    ItemComponents {
797        item: Arc<ItemDef>,
798        components: Vec<Arc<ItemDef>>,
799    },
800}
801
802#[derive(Clone, Debug, Serialize, Deserialize)]
803enum RawComponentOutput {
804    /// Creates the primary component of a modular tool. Assumes that the
805    /// material used is the only component in the item.
806    ToolPrimaryComponent { toolkind: ToolKind, item: String },
807}
808
809impl Asset for ComponentRecipeBook {
810    fn load(cache: &AssetCache, specifier: &SharedString) -> Result<Self, BoxedError> {
811        fn create_recipe_key(raw_recipe: &RawComponentRecipe) -> ComponentKey {
812            match &raw_recipe.output {
813                RawComponentOutput::ToolPrimaryComponent { toolkind, item: _ } => {
814                    let material = String::from(&raw_recipe.material.0);
815                    let modifier = raw_recipe
816                        .modifier
817                        .as_ref()
818                        .map(|(modifier, _amount)| String::from(modifier));
819                    ComponentKey {
820                        toolkind: *toolkind,
821                        material,
822                        modifier,
823                    }
824                },
825            }
826        }
827
828        fn load_recipe(raw_recipe: &RawComponentRecipe) -> Result<ComponentRecipe, assets::Error> {
829            let output = match &raw_recipe.output {
830                RawComponentOutput::ToolPrimaryComponent { toolkind: _, item } => {
831                    let item = Arc::<ItemDef>::load_cloned(item)?;
832                    let components = vec![Arc::<ItemDef>::load_cloned(&raw_recipe.material.0)?];
833                    ComponentOutput::ItemComponents { item, components }
834                },
835            };
836            let material = (
837                RecipeInput::Item(Arc::<ItemDef>::load_cloned(&raw_recipe.material.0)?),
838                raw_recipe.material.1,
839            );
840            let modifier = if let Some((modifier, amount)) = &raw_recipe.modifier {
841                let modifier = Arc::<ItemDef>::load_cloned(modifier)?;
842                Some((RecipeInput::Item(modifier), *amount))
843            } else {
844                None
845            };
846            let additional_inputs = raw_recipe
847                .additional_inputs
848                .iter()
849                .map(|(input, amount)| input.load_recipe_input().map(|input| (input, *amount)))
850                .collect::<Result<Vec<_>, _>>()?;
851            let recipe_book_key = String::from(&raw_recipe.recipe_book_key);
852            Ok(ComponentRecipe {
853                recipe_book_key,
854                output,
855                material,
856                modifier,
857                additional_inputs,
858                craft_sprite: raw_recipe.craft_sprite,
859            })
860        }
861
862        let raw = cache
863            .load_and_combine::<Ron<Vec<RawComponentRecipe>>>(specifier)?
864            .cloned()
865            .into_inner();
866
867        let recipes = raw
868            .iter()
869            .map(|raw_recipe| {
870                load_recipe(raw_recipe).map(|recipe| (create_recipe_key(raw_recipe), recipe))
871            })
872            .collect::<Result<_, assets::Error>>()?;
873
874        Ok(ComponentRecipeBook { recipes })
875    }
876}
877
878#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Debug)]
879enum RepairKey {
880    ItemDefId(String),
881    ModularWeapon { material: String },
882}
883
884impl RepairKey {
885    fn from_item(item: &Item) -> Option<Self> {
886        match item.item_definition_id() {
887            ItemDefinitionId::Simple(item_id) => Some(Self::ItemDefId(String::from(item_id))),
888            ItemDefinitionId::Compound { .. } => None,
889            ItemDefinitionId::Modular { pseudo_base, .. } => match pseudo_base {
890                "veloren.core.pseudo_items.modular.tool" => {
891                    if let Some(ItemDefinitionId::Simple(material)) = item
892                        .components()
893                        .iter()
894                        .find(|comp| {
895                            matches!(
896                                &*comp.kind(),
897                                ItemKind::ModularComponent(
898                                    modular::ModularComponent::ToolPrimaryComponent { .. }
899                                )
900                            )
901                        })
902                        .and_then(|comp| {
903                            comp.components()
904                                .iter()
905                                .next()
906                                .map(|comp| comp.item_definition_id())
907                        })
908                    {
909                        let material = String::from(material);
910                        Some(Self::ModularWeapon { material })
911                    } else {
912                        None
913                    }
914                },
915                _ => None,
916            },
917        }
918    }
919}
920
921#[derive(Serialize, Deserialize, Clone)]
922struct RawRepairRecipe {
923    inputs: Vec<(RawRecipeInput, u32)>,
924}
925
926#[derive(Serialize, Deserialize, Clone)]
927struct RawRepairRecipeBook {
928    recipes: HashMap<RepairKey, RawRepairRecipe>,
929    fallback: RawRepairRecipe,
930}
931
932#[derive(Serialize, Deserialize, Clone, Debug)]
933pub struct RepairRecipe {
934    inputs: Vec<(RecipeInput, u32)>,
935}
936
937impl RepairRecipe {
938    /// Determine whether the inventory contains the ingredients for a repair.
939    /// If it does, return a vec of inventory slots that contain the
940    /// ingredients needed, whose positions correspond to particular repair
941    /// inputs. If items are missing, return the missing items, and how many
942    /// are missing.
943    pub fn inventory_contains_ingredients(
944        &self,
945        item: &Item,
946        inv: &Inventory,
947    ) -> Result<Vec<(u32, InvSlotId)>, Vec<(&RecipeInput, u32)>> {
948        inventory_contains_ingredients(self.inputs(item), inv, 1)
949    }
950
951    pub fn inputs(&self, item: &Item) -> impl Iterator<Item = (&RecipeInput, u32)> + use<'_> {
952        let item_durability = item.durability_lost().unwrap_or(0);
953        self.inputs
954            .iter()
955            .filter_map(move |(input, original_amount)| {
956                let amount = (original_amount * item_durability) / Item::MAX_DURABILITY;
957                // If original repair recipe consumed ingredients, but item not damaged enough
958                // to actually need to consume item, remove item as requirement.
959                if *original_amount > 0 && amount == 0 {
960                    None
961                } else {
962                    Some((input, amount))
963                }
964            })
965    }
966}
967
968#[derive(Serialize, Deserialize, Clone, Debug)]
969pub struct RepairRecipeBook {
970    recipes: HashMap<RepairKey, RepairRecipe>,
971    fallback: RepairRecipe,
972}
973
974impl RepairRecipeBook {
975    pub fn repair_recipe(&self, item: &Item) -> Option<&RepairRecipe> {
976        RepairKey::from_item(item)
977            .as_ref()
978            .and_then(|key| self.recipes.get(key))
979            .or_else(|| item.has_durability().then_some(&self.fallback))
980    }
981
982    pub fn repair_item(
983        &self,
984        inv: &mut Inventory,
985        item: Slot,
986        slots: Vec<(u32, InvSlotId)>,
987        ability_map: &AbilityMap,
988        msm: &MaterialStatManifest,
989    ) -> Result<(), Vec<(&RecipeInput, u32)>> {
990        let mut slot_claims = HashMap::new();
991        let mut unsatisfied_requirements = Vec::new();
992
993        if let Some(item) = match item {
994            Slot::Equip(slot) => inv.equipped(slot),
995            Slot::Inventory(slot) => inv.get(slot),
996            // Items in overflow slots cannot be repaired until item is moved to a real slot
997            Slot::Overflow(_) => None,
998        } && let Some(repair_recipe) = self.repair_recipe(item)
999        {
1000            repair_recipe
1001                .inputs(item)
1002                .enumerate()
1003                .for_each(|(i, (input, amount))| {
1004                    // Gets all slots provided for this input by the frontend
1005                    let input_slots = slots
1006                        .iter()
1007                        .filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None })
1008                        .copied();
1009                    // Checks if requirement is met, and if not marks it as unsatisfied
1010                    input.handle_requirement(
1011                        amount,
1012                        &mut slot_claims,
1013                        &mut unsatisfied_requirements,
1014                        inv,
1015                        input_slots,
1016                    );
1017                })
1018        }
1019
1020        if unsatisfied_requirements.is_empty() {
1021            for (slot, to_remove) in slot_claims.iter() {
1022                for _ in 0..*to_remove {
1023                    let _ = inv
1024                        .take(*slot, ability_map, msm)
1025                        .expect("Expected item to exist in the inventory");
1026                }
1027            }
1028
1029            inv.repair_item_at_slot(item, ability_map, msm);
1030
1031            Ok(())
1032        } else {
1033            Err(unsatisfied_requirements)
1034        }
1035    }
1036}
1037
1038impl Asset for RepairRecipeBook {
1039    fn load(cache: &AssetCache, specifier: &SharedString) -> Result<Self, BoxedError> {
1040        fn load_recipe_input(
1041            (input, amount): &(RawRecipeInput, u32),
1042        ) -> Result<(RecipeInput, u32), assets::Error> {
1043            let input = input.load_recipe_input()?;
1044            Ok((input, *amount))
1045        }
1046
1047        let raw = cache
1048            .load::<Ron<RawRepairRecipeBook>>(specifier)?
1049            .cloned()
1050            .into_inner();
1051
1052        let recipes = raw
1053            .recipes
1054            .iter()
1055            .map(|(key, RawRepairRecipe { inputs })| {
1056                let inputs = inputs
1057                    .iter()
1058                    .map(load_recipe_input)
1059                    .collect::<Result<Vec<_>, _>>()?;
1060                Ok((key.clone(), RepairRecipe { inputs }))
1061            })
1062            .collect::<Result<_, assets::Error>>()?;
1063
1064        let fallback = RepairRecipe {
1065            inputs: raw
1066                .fallback
1067                .inputs
1068                .iter()
1069                .map(load_recipe_input)
1070                .collect::<Result<Vec<_>, _>>()?,
1071        };
1072
1073        Ok(RepairRecipeBook { recipes, fallback })
1074    }
1075}
1076
1077pub fn complete_recipe_book() -> AssetHandle<RecipeBookManifest> {
1078    RecipeBookManifest::load_expect("common.recipe_book_manifest")
1079}
1080
1081pub fn default_component_recipe_book() -> AssetHandle<ComponentRecipeBook> {
1082    ComponentRecipeBook::load_expect("common.component_recipe_book")
1083}
1084
1085pub fn default_repair_recipe_book() -> AssetHandle<RepairRecipeBook> {
1086    RepairRecipeBook::load_expect("common.repair_recipe_book")
1087}
1088
1089impl Asset for ReverseComponentRecipeBook {
1090    fn load(cache: &AssetCache, specifier: &SharedString) -> Result<Self, BoxedError> {
1091        let forward = cache.load::<ComponentRecipeBook>(specifier)?.cloned();
1092        let mut recipes = HashMap::new();
1093        for (_, recipe) in forward.iter() {
1094            recipes.insert(recipe.itemdef_output(), recipe.clone());
1095        }
1096        Ok(ReverseComponentRecipeBook { recipes })
1097    }
1098}