veloren_common/
recipe.rs

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