From 5ba32ecfb93e39f3b11e2abaa2ba6d5c7c848544 Mon Sep 17 00:00:00 2001 From: Ben Lovy Date: Sun, 3 Jan 2021 12:19:33 -0500 Subject: [PATCH] Recipe lookup --- src/bin/fcalc.rs | 4 ++ src/error.rs | 4 ++ src/item.rs | 74 +++++++++++++++++++++--- src/lib.rs | 2 + src/recipe.rs | 143 ++++++++++++++++++++++++++++++++++++++++++++++- src/recipes.toml | 37 ++++++++++++ 6 files changed, 255 insertions(+), 9 deletions(-) create mode 100644 src/recipes.toml diff --git a/src/bin/fcalc.rs b/src/bin/fcalc.rs index 2bb9bd8..4b4b2d6 100644 --- a/src/bin/fcalc.rs +++ b/src/bin/fcalc.rs @@ -2,6 +2,7 @@ #![cfg(feature = "bin")] +use fcalc::{ITEMS, RECIPES}; use lazy_static::lazy_static; use std::env::var; @@ -11,4 +12,7 @@ lazy_static! { fn main() { println!("fcalc {} - Factorio Calculator", *VERSION); + let product = ITEMS.lookup("Iron Plate").unwrap().as_product().unwrap(); + let recipe = RECIPES.lookup(&product).unwrap(); + println!("To get {}:\n{}", product, recipe) } diff --git a/src/error.rs b/src/error.rs index 26352fe..2b31426 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,6 +7,10 @@ use thiserror::Error; pub enum FcalcError { #[error("Could not read input data file")] FileError(#[from] io::Error), + #[error("Bad product type")] + ProductTypeError, + #[error("Bad building type")] + InvalidBuildingType } /// Export convenient Result type carrying this error. diff --git a/src/item.rs b/src/item.rs index 39270aa..239dfc0 100644 --- a/src/item.rs +++ b/src/item.rs @@ -1,27 +1,69 @@ //! An item is something the player can hold. Some are raw, some are craftable. +use crate::{ + recipe::{Recipe, RECIPES}, + FcalcError, Result, +}; use lazy_static::lazy_static; use serde::Deserialize; +use std::fmt; lazy_static! { pub static ref ITEMS: Items = Items::new(); } -// /// All items, raw or otherwise, implement this trait. -// // TODO is this even necessary? -// pub trait Item: Display {} +#[derive(Clone, Debug, Deserialize)] +pub enum Item { + Raw(RawMaterial), + Product(Product), +} + +impl Item { + pub fn recipe(&self) -> Option<&Recipe> { + match self { + Item::Raw(_) => None, + Item::Product(p) => RECIPES.lookup(&p), + } + } + + pub fn as_product(&self) -> Result { + match self { + Item::Product(p) => Ok(p.clone()), + _ => Err(FcalcError::ProductTypeError), + } + } + + pub fn as_raw(&self) -> Result { + match self { + Item::Raw(r) => Ok(r.clone()), + _ => Err(FcalcError::ProductTypeError), + } + } +} /// A base-level raw material, not craftable. -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct RawMaterial { name: String, } -#[derive(Debug, Deserialize)] +impl fmt::Display for RawMaterial { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)] pub struct Product { name: String, } +impl fmt::Display for Product { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} + /// The top-level Items struct, containing all supported items read from TOML. #[derive(Debug, Deserialize)] pub struct Items { @@ -33,6 +75,23 @@ impl Items { fn new() -> Self { Self::default() } + + /// Find an item by name + pub fn lookup(&self, name: &str) -> Option { + for r in &self.raw_materials { + if r.name == name { + return Some(Item::Raw(r.clone())); + } + } + + for p in &self.products { + if p.name == name { + return Some(Item::Product(p.clone())); + } + } + + None + } } impl Default for Items { @@ -48,8 +107,7 @@ mod test { use pretty_assertions::assert_eq; #[test] fn it_loads_all_items() { - let items = Items::new(); - assert_eq!(items.raw_materials.len(), 2); - assert_eq!(items.products.len(), 1); + assert_eq!(ITEMS.raw_materials.len(), 2); + assert_eq!(ITEMS.products.len(), 1) } } diff --git a/src/lib.rs b/src/lib.rs index 08006a3..66ec9f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,3 +9,5 @@ mod item; mod recipe; pub use error::{FcalcError, Result}; +pub use item::ITEMS; +pub use recipe::RECIPES; \ No newline at end of file diff --git a/src/recipe.rs b/src/recipe.rs index 4d949c4..a40513e 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -1,3 +1,144 @@ //! A recipe tracks the cost to produce an item. + +use crate::{item::*, FcalcError}; +use lazy_static::lazy_static; +use serde::Deserialize; +use std::{collections::HashMap, fmt, str::FromStr}; + +lazy_static! { + pub static ref RECIPES: Recipes = Recipes::new(); +} + +/// A formula for building a product +#[derive(Clone, Debug, Deserialize)] +pub struct RawRecipe { + inputs: Vec, + output: String, + time: f64, + building_type: String, +} + +/// A formula for building a product +#[derive(Clone, Debug, Deserialize)] +pub struct Recipe { + pub inputs: Vec, + pub output: Product, + time: f64, + building_type: BuildingType, +} + +impl From for Recipe { + fn from(raw: RawRecipe) -> Self { + let inputs = raw + .inputs + .iter() + .map(|el| ITEMS.lookup(el).expect("Could not find input product")) + .collect(); + let output = ITEMS + .lookup(&raw.output) + .expect("Could not find output product.") + .as_product() + .expect("Output product was a raw material"); + Self { + inputs, + output, + time: raw.time, + building_type: BuildingType::from_str(&raw.building_type).unwrap(), + } + } +} + +impl fmt::Display for Recipe { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Inputs: {:?}, Time: {}, Building Type: {:?}", + self.inputs, self.time, self.building_type + ) + } +} + +/// Each recipe is made in a type of building +#[derive(Clone, Copy, Debug, Deserialize)] +pub enum BuildingType { + Assembler, + Furnace, +} + +impl FromStr for BuildingType { + type Err = FcalcError; + fn from_str(s: &str) -> Result { + match s { + "Furnace" => Ok(BuildingType::Furnace), + "Assembler" => Ok(BuildingType::Assembler), + _ => Err(FcalcError::InvalidBuildingType), + } + } +} + +/// Each building type has several tiers +#[derive(Debug, Deserialize)] +pub struct Building { + building_type: BuildingType, + name: String, + crafting_speed: f64, +} + +/// Top-level to read recipes.toml +#[derive(Debug, Deserialize)] +pub struct RawRecipes { + buildings: Vec, + recipes: Vec, +} + +impl RawRecipes { + fn new() -> Self { + Self::default() + } +} + +impl Default for RawRecipes { + fn default() -> Self { + let input = include_str!("recipes.toml"); + toml::from_str(input).expect("Could not read recipes input file.") + } +} + +/// Recipe lookup #[derive(Debug)] -pub struct Recipe; +pub struct Recipes { + recipes: HashMap, +} + +impl From for Recipes { + fn from(raw: RawRecipes) -> Self { + let mut recipes = HashMap::new(); + for raw in &raw.recipes { + // TODO convert all Strings to Items in input and output + let recipe = Recipe::from(raw.clone()); + recipes.insert(recipe.output.clone(), recipe); + } + Self { recipes } + } +} + +impl Recipes { + fn new() -> Self { + let raw = RawRecipes::new(); + Self::from(raw) + } + + pub fn lookup(&self, product: &Product) -> Option<&Recipe> { + self.recipes.get(product) + } +} + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + #[test] + fn it_loads_all_recipes() { + assert_eq!(RECIPES.recipes.len(), 1); + } +} diff --git a/src/recipes.toml b/src/recipes.toml new file mode 100644 index 0000000..99666af --- /dev/null +++ b/src/recipes.toml @@ -0,0 +1,37 @@ +# All supported recipes + +[[buildings]] +name = "Stone Furnace" +building_type = "Furnace" +crafting_speed = 1 + +[[buildings]] +name = "Steel Furnace" +building_type = "Furnace" +crafting_speed = 2 + +[[buildings]] +name = "Electric Furnace" +building_type = "Furnace" +crafting_speed = 2 + +[[buildings]] +name = "Assembling Machine 1" +building_type = "Assembler" +crafting_speed = 0.5 + +[[buildings]] +name = "Assembling Machine 2" +building_type = "Assembler" +crafting_speed = 0.75 + +[[buildings]] +name = "Assembling Machine 3" +building_type = "Assembler" +crafting_speed = 1.25 + +[[recipes]] +inputs = ["Iron Ore"] +output = "Iron Plate" +time = 3.2 +building_type = "Furnace" \ No newline at end of file -- 2.38.5