veloren_common/comp/inventory/
recipe_book.rs

1use crate::{
2    comp::item::{Item, ItemKind},
3    recipe::{Recipe, RecipeBookManifest},
4};
5use hashbrown::HashSet;
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, Default, Serialize, Deserialize)]
9pub struct RecipeBook {
10    recipe_groups: Vec<Item>,
11    recipes: HashSet<String>,
12}
13
14impl RecipeBook {
15    pub(super) fn get<'a>(
16        &'a self,
17        recipe_key: &str,
18        rbm: &'a RecipeBookManifest,
19    ) -> Option<&'a Recipe> {
20        if self.recipes.iter().any(|r| r == recipe_key) {
21            rbm.get(recipe_key)
22        } else {
23            None
24        }
25    }
26
27    pub(super) fn len(&self) -> usize { self.recipes.len() }
28
29    pub(super) fn iter(&self) -> impl ExactSizeIterator<Item = &String> { self.recipes.iter() }
30
31    pub(super) fn iter_groups(&self) -> impl ExactSizeIterator<Item = &Item> {
32        self.recipe_groups.iter()
33    }
34
35    pub(super) fn get_available_iter<'a>(
36        &'a self,
37        rbm: &'a RecipeBookManifest,
38    ) -> impl Iterator<Item = (&'a String, &'a Recipe)> + 'a {
39        self.recipes
40            .iter()
41            .filter_map(|recipe: &String| rbm.get(recipe).map(|rbm_recipe| (recipe, rbm_recipe)))
42    }
43
44    pub(super) fn reset(&mut self) {
45        self.recipe_groups.clear();
46        self.recipes.clear();
47    }
48
49    /// Pushes a group of recipes to the recipe book. If group already exists
50    /// return the recipe group.
51    pub(super) fn push_group(&mut self, group: Item) -> Result<(), Item> {
52        if self
53            .recipe_groups
54            .iter()
55            .any(|rg| rg.item_definition_id() == group.item_definition_id())
56        {
57            Err(group)
58        } else {
59            self.recipe_groups.push(group);
60            self.update();
61            Ok(())
62        }
63    }
64
65    /// Syncs recipes hashset with recipe_groups vec
66    pub(super) fn update(&mut self) {
67        self.recipe_groups.iter().for_each(|group| {
68            if let ItemKind::RecipeGroup { recipes } = &*group.kind() {
69                self.recipes.extend(recipes.iter().map(String::from))
70            }
71        })
72    }
73
74    pub fn recipe_book_from_persistence(recipe_groups: Vec<Item>) -> Self {
75        let mut book = Self {
76            recipe_groups,
77            recipes: HashSet::new(),
78        };
79        book.update();
80        book
81    }
82
83    pub fn persistence_recipes_iter_with_index(&self) -> impl Iterator<Item = (usize, &Item)> {
84        self.recipe_groups.iter().enumerate()
85    }
86
87    pub(super) fn is_known(&self, recipe_key: &str) -> bool { self.recipes.contains(recipe_key) }
88}
89
90#[cfg(test)]
91mod tests {
92    use crate::{
93        comp::item::{Item, ItemKind},
94        recipe::{complete_recipe_book, default_component_recipe_book},
95    };
96    use hashbrown::HashSet;
97
98    fn load_recipe_items() -> Vec<Item> {
99        Item::new_from_asset_glob("common.items.recipes.*").expect("The directory should exist")
100    }
101
102    fn load_recipe_list() -> HashSet<String> {
103        let recipe_book = complete_recipe_book();
104        let component_recipe_book = default_component_recipe_book();
105
106        recipe_book
107            .read()
108            .keys()
109            .cloned()
110            .chain(
111                component_recipe_book
112                    .read()
113                    .iter()
114                    .map(|(_, cr)| &cr.recipe_book_key)
115                    .cloned(),
116            )
117            .collect::<HashSet<_>>()
118    }
119
120    fn valid_recipe(recipe: &str) -> bool {
121        let recipe_list = load_recipe_list();
122        recipe_list.contains(recipe)
123    }
124
125    /// Verify that all recipes in recipe items point to a valid recipe
126    #[test]
127    fn validate_recipes() {
128        let recipe_items = load_recipe_items();
129        for item in recipe_items {
130            let ItemKind::RecipeGroup { recipes } = &*item.kind() else {
131                panic!("Expected item to be of kind RecipeGroup")
132            };
133            assert!(recipes.iter().all(|r| valid_recipe(r)));
134        }
135    }
136
137    /// Verify that all recipes are contained in a recipe item
138    #[test]
139    fn recipes_reachable() {
140        let recipe_items = load_recipe_items();
141        let reachable_recipes = recipe_items
142            .iter()
143            .flat_map(|i| {
144                if let ItemKind::RecipeGroup { recipes } = &*i.kind() {
145                    recipes.to_vec()
146                } else {
147                    Vec::new()
148                }
149            })
150            .collect::<HashSet<_>>();
151
152        let recipe_list = load_recipe_list();
153
154        for recipe in recipe_list.iter() {
155            assert!(
156                reachable_recipes.contains(recipe),
157                "{recipe} was not found in a recipe item"
158            );
159        }
160    }
161}