use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RecipeRequirement {
pub item_id: u32,
#[serde(default = "default_count")]
pub count: u32,
#[serde(default)]
pub stay: bool,
}
const fn default_count() -> u32 {
1
}
impl RecipeRequirement {
#[must_use]
pub fn from_inf_value(s: &str) -> Self {
let parts: Vec<&str> = s.splitn(3, ',').collect();
let item_id = parts.first().and_then(|p| p.trim().parse().ok()).unwrap_or(0);
let count = parts
.get(1)
.and_then(|p| p.trim().parse().ok())
.unwrap_or(1);
let stay = parts.get(2).map_or(false, |p| p.trim() == "stay");
Self { item_id, count, stay }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RecipeResult {
pub item_id: u32,
#[serde(default = "default_count")]
pub count: u32,
}
impl RecipeResult {
#[must_use]
pub fn from_inf_value(s: &str) -> Self {
let parts: Vec<&str> = s.splitn(2, ',').collect();
let item_id = parts.first().and_then(|p| p.trim().parse().ok()).unwrap_or(0);
let count = parts
.get(1)
.and_then(|p| p.trim().parse().ok())
.unwrap_or(1);
Self { item_id, count }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RecipeDef {
pub serial_id: u32,
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub requirements: Vec<RecipeRequirement>,
#[serde(default)]
pub results: Vec<RecipeResult>,
#[serde(default)]
pub gen_name: Option<String>,
#[serde(default)]
pub script: Option<String>,
}
fn parse_requirements(fields: &HashMap<String, Vec<String>>) -> Vec<RecipeRequirement> {
fields
.get("req")
.map(|vals| vals.iter().map(|v| RecipeRequirement::from_inf_value(v)).collect())
.unwrap_or_default()
}
fn parse_results(fields: &HashMap<String, Vec<String>>) -> Vec<RecipeResult> {
fields
.get("gen")
.map(|vals| vals.iter().map(|v| RecipeResult::from_inf_value(v)).collect())
.unwrap_or_default()
}
fn first_str<'a>(fields: &'a HashMap<String, Vec<String>>, key: &str) -> Option<&'a str> {
fields.get(key)?.first().map(String::as_str)
}
fn first_string(fields: &HashMap<String, Vec<String>>, key: &str) -> Option<String> {
first_str(fields, key).map(ToOwned::to_owned)
}
fn first_u32(fields: &HashMap<String, Vec<String>>, key: &str) -> Option<u32> {
first_str(fields, key)?.parse().ok()
}
impl RecipeDef {
#[must_use]
pub fn from_combi_fields(
serial_id: u32,
fields: &HashMap<String, Vec<String>>,
) -> Option<Self> {
let results = parse_results(fields);
if results.is_empty() {
return None;
}
let id = first_string(fields, "id");
let requirements = parse_requirements(fields);
let gen_name = first_string(fields, "genname");
let script = None;
Some(Self {
serial_id,
id,
requirements,
results,
gen_name,
script,
})
}
#[must_use]
pub fn from_inf_fields(serial_id: u32, fields: &HashMap<String, Vec<String>>) -> Option<Self> {
Self::from_combi_fields(serial_id, fields)
}
#[must_use]
pub fn with_script(mut self, script: Option<String>) -> Self {
self.script = script;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_req_from_inf_minimal() {
let req = RecipeRequirement::from_inf_value("24");
assert_eq!(req.item_id, 24);
assert_eq!(req.count, 1);
assert!(!req.stay);
}
#[test]
fn test_req_from_inf_with_count() {
let req = RecipeRequirement::from_inf_value("24,5");
assert_eq!(req.item_id, 24);
assert_eq!(req.count, 5);
assert!(!req.stay);
}
#[test]
fn test_req_from_inf_with_stay() {
let req = RecipeRequirement::from_inf_value("24,1,stay");
assert_eq!(req.item_id, 24);
assert_eq!(req.count, 1);
assert!(req.stay);
}
#[test]
fn test_req_from_inf_with_count_and_stay() {
let req = RecipeRequirement::from_inf_value("42,3,stay");
assert_eq!(req.item_id, 42);
assert_eq!(req.count, 3);
assert!(req.stay);
}
#[test]
fn test_req_from_inf_empty() {
let req = RecipeRequirement::from_inf_value("");
assert_eq!(req.item_id, 0);
assert_eq!(req.count, 1);
assert!(!req.stay);
}
#[test]
fn test_gen_from_inf_minimal() {
let result = RecipeResult::from_inf_value("25");
assert_eq!(result.item_id, 25);
assert_eq!(result.count, 1);
}
#[test]
fn test_gen_from_inf_with_count() {
let result = RecipeResult::from_inf_value("25,10");
assert_eq!(result.item_id, 25);
assert_eq!(result.count, 10);
}
#[test]
fn test_gen_from_inf_empty() {
let result = RecipeResult::from_inf_value("");
assert_eq!(result.item_id, 0);
assert_eq!(result.count, 1);
}
#[test]
fn test_recipe_from_combi_fields_basic() {
let mut fields = HashMap::new();
fields.insert("id".into(), vec!["bendablebranch".into()]);
fields.insert("req".into(), vec!["24".into(), "38".into()]);
fields.insert("gen".into(), vec!["25".into()]);
let recipe = RecipeDef::from_combi_fields(1, &fields).unwrap();
assert_eq!(recipe.serial_id, 1);
assert_eq!(recipe.id.as_deref(), Some("bendablebranch"));
assert_eq!(recipe.requirements.len(), 2);
assert_eq!(recipe.requirements[0].item_id, 24);
assert_eq!(recipe.requirements[1].item_id, 38);
assert_eq!(recipe.results.len(), 1);
assert_eq!(recipe.results[0].item_id, 25);
}
#[test]
fn test_recipe_missing_gen_returns_none() {
let fields = HashMap::new();
assert!(RecipeDef::from_combi_fields(1, &fields).is_none());
}
#[test]
fn test_recipe_with_genname() {
let mut fields = HashMap::new();
fields.insert("gen".into(), vec!["82".into()]);
fields.insert("genname".into(), vec!["Dough".into()]);
let recipe = RecipeDef::from_combi_fields(2, &fields).unwrap();
assert_eq!(recipe.gen_name.as_deref(), Some("Dough"));
}
#[test]
fn test_recipe_with_script() {
let mut fields = HashMap::new();
fields.insert("gen".into(), vec!["43".into()]);
fields.insert("req".into(), vec!["42,9".into(), "23,1,stay".into()]);
let recipe = RecipeDef::from_combi_fields(3, &fields)
.unwrap()
.with_script(Some("play \"grind.wav\";".into()));
assert_eq!(
recipe.script.as_deref(),
Some("play \"grind.wav\";")
);
}
#[test]
fn test_recipe_serde_derives_compile() {
let recipe = RecipeDef {
serial_id: 1,
id: Some("test_recipe".into()),
requirements: vec![RecipeRequirement {
item_id: 10,
count: 2,
stay: false,
}],
results: vec![RecipeResult {
item_id: 20,
count: 1,
}],
gen_name: None,
script: None,
};
assert_eq!(recipe.serial_id, 1);
assert_eq!(recipe.results.len(), 1);
}
}