openstranded_common_crafting/
recipe_def.rs1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct RecipeRequirement {
21 pub item_id: u32,
23
24 #[serde(default = "default_count")]
26 pub count: u32,
27
28 #[serde(default)]
30 pub stay: bool,
31}
32
33const fn default_count() -> u32 {
34 1
35}
36
37impl RecipeRequirement {
38 #[must_use]
42 pub fn from_inf_value(s: &str) -> Self {
43 let parts: Vec<&str> = s.splitn(3, ',').collect();
44 let item_id = parts.first().and_then(|p| p.trim().parse().ok()).unwrap_or(0);
45 let count = parts
46 .get(1)
47 .and_then(|p| p.trim().parse().ok())
48 .unwrap_or(1);
49 let stay = parts.get(2).map_or(false, |p| p.trim() == "stay");
50 Self { item_id, count, stay }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub struct RecipeResult {
69 pub item_id: u32,
71
72 #[serde(default = "default_count")]
74 pub count: u32,
75}
76
77impl RecipeResult {
78 #[must_use]
82 pub fn from_inf_value(s: &str) -> Self {
83 let parts: Vec<&str> = s.splitn(2, ',').collect();
84 let item_id = parts.first().and_then(|p| p.trim().parse().ok()).unwrap_or(0);
85 let count = parts
86 .get(1)
87 .and_then(|p| p.trim().parse().ok())
88 .unwrap_or(1);
89 Self { item_id, count }
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct RecipeDef {
114 pub serial_id: u32,
116
117 #[serde(default)]
119 pub id: Option<String>,
120
121 #[serde(default)]
123 pub requirements: Vec<RecipeRequirement>,
124
125 #[serde(default)]
127 pub results: Vec<RecipeResult>,
128
129 #[serde(default)]
131 pub gen_name: Option<String>,
132
133 #[serde(default)]
135 pub script: Option<String>,
136}
137
138fn parse_requirements(fields: &HashMap<String, Vec<String>>) -> Vec<RecipeRequirement> {
141 fields
142 .get("req")
143 .map(|vals| vals.iter().map(|v| RecipeRequirement::from_inf_value(v)).collect())
144 .unwrap_or_default()
145}
146
147fn parse_results(fields: &HashMap<String, Vec<String>>) -> Vec<RecipeResult> {
148 fields
149 .get("gen")
150 .map(|vals| vals.iter().map(|v| RecipeResult::from_inf_value(v)).collect())
151 .unwrap_or_default()
152}
153
154fn first_str<'a>(fields: &'a HashMap<String, Vec<String>>, key: &str) -> Option<&'a str> {
157 fields.get(key)?.first().map(String::as_str)
158}
159
160fn first_string(fields: &HashMap<String, Vec<String>>, key: &str) -> Option<String> {
161 first_str(fields, key).map(ToOwned::to_owned)
162}
163
164fn first_u32(fields: &HashMap<String, Vec<String>>, key: &str) -> Option<u32> {
165 first_str(fields, key)?.parse().ok()
166}
167
168impl RecipeDef {
169 #[must_use]
178 pub fn from_combi_fields(
179 serial_id: u32,
180 fields: &HashMap<String, Vec<String>>,
181 ) -> Option<Self> {
182 let results = parse_results(fields);
184 if results.is_empty() {
185 return None;
186 }
187
188 let id = first_string(fields, "id");
189 let requirements = parse_requirements(fields);
190 let gen_name = first_string(fields, "genname");
191
192 let script = None;
196
197 Some(Self {
198 serial_id,
199 id,
200 requirements,
201 results,
202 gen_name,
203 script,
204 })
205 }
206
207 #[must_use]
217 pub fn from_inf_fields(serial_id: u32, fields: &HashMap<String, Vec<String>>) -> Option<Self> {
218 Self::from_combi_fields(serial_id, fields)
219 }
220
221 #[must_use]
223 pub fn with_script(mut self, script: Option<String>) -> Self {
224 self.script = script;
225 self
226 }
227}
228
229#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
238 fn test_req_from_inf_minimal() {
239 let req = RecipeRequirement::from_inf_value("24");
240 assert_eq!(req.item_id, 24);
241 assert_eq!(req.count, 1);
242 assert!(!req.stay);
243 }
244
245 #[test]
246 fn test_req_from_inf_with_count() {
247 let req = RecipeRequirement::from_inf_value("24,5");
248 assert_eq!(req.item_id, 24);
249 assert_eq!(req.count, 5);
250 assert!(!req.stay);
251 }
252
253 #[test]
254 fn test_req_from_inf_with_stay() {
255 let req = RecipeRequirement::from_inf_value("24,1,stay");
256 assert_eq!(req.item_id, 24);
257 assert_eq!(req.count, 1);
258 assert!(req.stay);
259 }
260
261 #[test]
262 fn test_req_from_inf_with_count_and_stay() {
263 let req = RecipeRequirement::from_inf_value("42,3,stay");
264 assert_eq!(req.item_id, 42);
265 assert_eq!(req.count, 3);
266 assert!(req.stay);
267 }
268
269 #[test]
270 fn test_req_from_inf_empty() {
271 let req = RecipeRequirement::from_inf_value("");
272 assert_eq!(req.item_id, 0);
273 assert_eq!(req.count, 1);
274 assert!(!req.stay);
275 }
276
277 #[test]
280 fn test_gen_from_inf_minimal() {
281 let result = RecipeResult::from_inf_value("25");
282 assert_eq!(result.item_id, 25);
283 assert_eq!(result.count, 1);
284 }
285
286 #[test]
287 fn test_gen_from_inf_with_count() {
288 let result = RecipeResult::from_inf_value("25,10");
289 assert_eq!(result.item_id, 25);
290 assert_eq!(result.count, 10);
291 }
292
293 #[test]
294 fn test_gen_from_inf_empty() {
295 let result = RecipeResult::from_inf_value("");
296 assert_eq!(result.item_id, 0);
297 assert_eq!(result.count, 1);
298 }
299
300 #[test]
303 fn test_recipe_from_combi_fields_basic() {
304 let mut fields = HashMap::new();
305 fields.insert("id".into(), vec!["bendablebranch".into()]);
306 fields.insert("req".into(), vec!["24".into(), "38".into()]);
307 fields.insert("gen".into(), vec!["25".into()]);
308
309 let recipe = RecipeDef::from_combi_fields(1, &fields).unwrap();
310 assert_eq!(recipe.serial_id, 1);
311 assert_eq!(recipe.id.as_deref(), Some("bendablebranch"));
312 assert_eq!(recipe.requirements.len(), 2);
313 assert_eq!(recipe.requirements[0].item_id, 24);
314 assert_eq!(recipe.requirements[1].item_id, 38);
315 assert_eq!(recipe.results.len(), 1);
316 assert_eq!(recipe.results[0].item_id, 25);
317 }
318
319 #[test]
320 fn test_recipe_missing_gen_returns_none() {
321 let fields = HashMap::new();
322 assert!(RecipeDef::from_combi_fields(1, &fields).is_none());
323 }
324
325 #[test]
326 fn test_recipe_with_genname() {
327 let mut fields = HashMap::new();
328 fields.insert("gen".into(), vec!["82".into()]);
329 fields.insert("genname".into(), vec!["Dough".into()]);
330
331 let recipe = RecipeDef::from_combi_fields(2, &fields).unwrap();
332 assert_eq!(recipe.gen_name.as_deref(), Some("Dough"));
333 }
334
335 #[test]
336 fn test_recipe_with_script() {
337 let mut fields = HashMap::new();
338 fields.insert("gen".into(), vec!["43".into()]);
339 fields.insert("req".into(), vec!["42,9".into(), "23,1,stay".into()]);
340
341 let recipe = RecipeDef::from_combi_fields(3, &fields)
342 .unwrap()
343 .with_script(Some("play \"grind.wav\";".into()));
344 assert_eq!(
345 recipe.script.as_deref(),
346 Some("play \"grind.wav\";")
347 );
348 }
349
350 #[test]
351 fn test_recipe_serde_derives_compile() {
352 let recipe = RecipeDef {
355 serial_id: 1,
356 id: Some("test_recipe".into()),
357 requirements: vec![RecipeRequirement {
358 item_id: 10,
359 count: 2,
360 stay: false,
361 }],
362 results: vec![RecipeResult {
363 item_id: 20,
364 count: 1,
365 }],
366 gen_name: None,
367 script: None,
368 };
369 assert_eq!(recipe.serial_id, 1);
370 assert_eq!(recipe.results.len(), 1);
371 }
372}