veloren_common/comp/inventory/
recipe_book.rs1use 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 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 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 #[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 #[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}