M src/bin/fcalc.rs => src/bin/fcalc.rs +2 -2
@@ 12,7 12,7 @@ lazy_static! {
fn main() {
println!("fcalc {} - Factorio Calculator", *VERSION);
- let product = ITEMS.lookup("Iron Plate").unwrap().as_product().unwrap();
+ let product = ITEMS.lookup("Iron Plate").unwrap();
let recipe = RECIPES.lookup(&product).unwrap();
- println!("To get {}:\n{}", product, recipe)
+ println!("{}", recipe)
}
M src/error.rs => src/error.rs +5 -3
@@ 5,12 5,14 @@ use thiserror::Error;
#[derive(Debug, Error)]
pub enum FcalcError {
- #[error("Could not read input data file")]
- FileError(#[from] io::Error),
+ #[error("Input data error")]
+ IOError(#[from] io::Error),
#[error("Bad product type")]
ProductTypeError,
#[error("Bad building type")]
- InvalidBuildingType
+ InvalidBuildingType,
+ #[error("Invalid item quanitty string")]
+ ItemQuantityError,
}
/// Export convenient Result type carrying this error.
M src/item.rs => src/item.rs +16 -57
@@ 1,9 1,6 @@
//! An item is something the player can hold. Some are raw, some are craftable.
-use crate::{
- recipe::{Recipe, RECIPES},
- FcalcError, Result,
-};
+use crate::recipe::{Recipe, RECIPES};
use lazy_static::lazy_static;
use serde::Deserialize;
use std::fmt;
@@ 12,53 9,24 @@ lazy_static! {
pub static ref ITEMS: Items = Items::new();
}
-#[derive(Clone, Debug, Deserialize)]
-pub enum Item {
- Raw(RawMaterial),
- Product(Product),
+/// An item in a recipe
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
+pub struct Item {
+ pub name: String,
+ pub raw: bool,
}
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<Product> {
- match self {
- Item::Product(p) => Ok(p.clone()),
- _ => Err(FcalcError::ProductTypeError),
+ if !self.raw {
+ RECIPES.lookup(self)
+ } else {
+ None
}
}
-
- pub fn as_raw(&self) -> Result<RawMaterial> {
- match self {
- Item::Raw(r) => Ok(r.clone()),
- _ => Err(FcalcError::ProductTypeError),
- }
- }
-}
-
-/// A base-level raw material, not craftable.
-#[derive(Clone, Debug, Deserialize)]
-pub struct RawMaterial {
- name: String,
}
-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 {
+impl fmt::Display for Item {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
@@ 67,8 35,7 @@ impl fmt::Display for Product {
/// The top-level Items struct, containing all supported items read from TOML.
#[derive(Debug, Deserialize)]
pub struct Items {
- raw_materials: Vec<RawMaterial>,
- products: Vec<Product>,
+ items: Vec<Item>,
}
impl Items {
@@ 78,18 45,11 @@ impl Items {
/// Find an item by name
pub fn lookup(&self, name: &str) -> Option<Item> {
- for r in &self.raw_materials {
- if r.name == name {
- return Some(Item::Raw(r.clone()));
+ for i in &self.items {
+ if i.name == name {
+ return Some(i.clone());
}
}
-
- for p in &self.products {
- if p.name == name {
- return Some(Item::Product(p.clone()));
- }
- }
-
None
}
}
@@ 107,7 67,6 @@ mod test {
use pretty_assertions::assert_eq;
#[test]
fn it_loads_all_items() {
- assert_eq!(ITEMS.raw_materials.len(), 2);
- assert_eq!(ITEMS.products.len(), 1)
+ assert_eq!(ITEMS.items.len(), 3);
}
}
M src/items.toml => src/items.toml +7 -4
@@ 1,10 1,13 @@
# All the items supported
-[[raw_materials]]
+[[items]]
name = "Coal"
+raw = true
-[[raw_materials]]
+[[items]]
name = "Iron Ore"
+raw = true
-[[products]]
-name = "Iron Plate">
\ No newline at end of file
+[[items]]
+name = "Iron Plate"
+raw = false<
\ No newline at end of file
M src/lib.rs => src/lib.rs +1 -1
@@ 10,4 10,4 @@ mod recipe;
pub use error::{FcalcError, Result};
pub use item::ITEMS;
-pub use recipe::RECIPES;>
\ No newline at end of file
+pub use recipe::RECIPES;
M src/recipe.rs => src/recipe.rs +141 -34
@@ 9,6 9,30 @@ lazy_static! {
pub static ref RECIPES: Recipes = Recipes::new();
}
+/// Each recipe is made in a type of building
+#[derive(Clone, Copy, Debug, PartialEq, Deserialize)]
+pub enum BuildingType {
+ Assembler,
+ Furnace,
+}
+
+impl FromStr for BuildingType {
+ type Err = FcalcError;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "Furnace" => Ok(BuildingType::Furnace),
+ "Assembler" => Ok(BuildingType::Assembler),
+ _ => Err(FcalcError::InvalidBuildingType),
+ }
+ }
+}
+
+impl fmt::Display for BuildingType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
/// A formula for building a product
#[derive(Clone, Debug, Deserialize)]
pub struct RawRecipe {
@@ 18,11 42,52 @@ pub struct RawRecipe {
building_type: String,
}
+/// An item with a quantity
+#[derive(Clone, Debug, PartialEq)]
+pub struct ItemQuantity {
+ item: Item,
+ quantity: u32,
+}
+
+impl ItemQuantity {
+ fn new(item: Item, quantity: u32) -> Self {
+ Self { item, quantity }
+ }
+}
+
+impl fmt::Display for ItemQuantity {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let plural = if self.quantity > 1 { "s" } else { "" };
+ write!(f, "{} {}{}", self.quantity, self.item, plural)
+ }
+}
+
+impl FromStr for ItemQuantity {
+ type Err = FcalcError;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let split = s.split(' ').collect::<Vec<&str>>();
+ if split.len() < 2 {
+ return Err(FcalcError::ItemQuantityError);
+ }
+ // Put the rest back together
+ let item_str = split[1..].iter().fold(String::new(), |mut acc, el| {
+ acc.push_str(el);
+ acc.push(' ');
+ acc
+ });
+ // Lop off last space
+ let item_str = &item_str[0..item_str.len() - 1];
+ let item = ITEMS.lookup(&item_str).expect("Unknown item");
+ let quantity = split[0].parse::<u32>().expect("u32");
+ Ok(Self::new(item, quantity))
+ }
+}
+
/// A formula for building a product
-#[derive(Clone, Debug, Deserialize)]
+#[derive(Clone, Debug, PartialEq)]
pub struct Recipe {
- pub inputs: Vec<Item>,
- pub output: Product,
+ pub inputs: Vec<ItemQuantity>,
+ pub output: ItemQuantity,
time: f64,
building_type: BuildingType,
}
@@ 32,50 97,43 @@ impl From<RawRecipe> for Recipe {
let inputs = raw
.inputs
.iter()
- .map(|el| ITEMS.lookup(el).expect("Could not find input product"))
+ .map(|el| ItemQuantity::from_str(el).unwrap())
.collect();
- let output = ITEMS
- .lookup(&raw.output)
- .expect("Could not find output product.")
- .as_product()
- .expect("Output product was a raw material");
+ let output = ItemQuantity::from_str(&raw.output).unwrap();
+ let building_type = BuildingType::from_str(&raw.building_type).unwrap();
Self {
inputs,
output,
time: raw.time,
- building_type: BuildingType::from_str(&raw.building_type).unwrap(),
+ building_type,
}
}
}
+/// This impl is used to produce new recipes.
+/// The passed ItemQuantity is the target output.
+impl From<ItemQuantity> for Recipe {
+ fn from(iq: ItemQuantity) -> Self {
+ unimplemented!()
+ }
+}
+
impl fmt::Display for Recipe {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let mut inputs = String::new();
+ for input in &self.inputs {
+ inputs.push_str(&input.to_string());
+ inputs.push_str(", ");
+ }
+ let inputs = &inputs[0..inputs.len() - 2];
write!(
f,
- "Inputs: {:?}, Time: {}, Building Type: {:?}",
- self.inputs, self.time, self.building_type
+ "{} => {} | {}, {} seconds",
+ inputs, self.output, self.building_type, self.time
)
}
}
-/// 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<Self, Self::Err> {
- 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 {
@@ 107,16 165,15 @@ impl Default for RawRecipes {
/// Recipe lookup
#[derive(Debug)]
pub struct Recipes {
- recipes: HashMap<Product, Recipe>,
+ recipes: HashMap<Item, Recipe>,
}
impl From<RawRecipes> 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);
+ recipes.insert(recipe.output.item.clone(), recipe);
}
Self { recipes }
}
@@ 128,7 185,7 @@ impl Recipes {
Self::from(raw)
}
- pub fn lookup(&self, product: &Product) -> Option<&Recipe> {
+ pub fn lookup(&self, product: &Item) -> Option<&Recipe> {
self.recipes.get(product)
}
}
@@ 141,4 198,54 @@ mod test {
fn it_loads_all_recipes() {
assert_eq!(RECIPES.recipes.len(), 1);
}
+ #[test]
+ fn itemquantity_from_str() {
+ assert_eq!(ItemQuantity::from_str("1 Iron Ore").unwrap(), ItemQuantity {
+ item: Item {
+ name: "Iron Ore".to_string(),
+ raw: true
+ },
+ quantity: 1
+ });
+ assert_eq!(ItemQuantity::from_str("35 Iron Plate").unwrap(), ItemQuantity {
+ item: Item {
+ name: "Iron Plate".to_string(),
+ raw: false
+ },
+ quantity: 35
+ });
+ }
+ #[test]
+ fn it_retrieves_defined_simple_recipe() {
+ let product = ITEMS.lookup("Iron Plate").unwrap();
+ let recipe = RECIPES.lookup(&product).unwrap();
+ let expected = Recipe {
+ inputs: vec![ItemQuantity::from_str("1 Iron Ore").unwrap()],
+ output: ItemQuantity::from_str("1 Iron Plate").unwrap(),
+ time: 3.2,
+ building_type: BuildingType::Furnace
+ };
+ assert_eq!(recipe, &expected);
+ }
+ #[test]
+ fn it_multiplies_simple_recipe() {
+ let product = ITEMS.lookup("Iron Plate").unwrap();
+ let target = ItemQuantity::from_str("3 Iron Plate").unwrap();
+ let recipe = Recipe::from(target);
+ let expected = Recipe {
+ inputs: vec![ItemQuantity::from_str("3 Iron Ore").unwrap()],
+ output: ItemQuantity::from_str("3 Iron Plate").unwrap(),
+ time: 3.2,
+ building_type: BuildingType::Furnace
+ };
+ assert_eq!(recipe, expected);
+ }
+ // #[test]
+ // fn it_expands_defined_compound_recipe() {
+ //
+ // }
+ // #[test]
+ // fn it_expands_custom_compound_recipe() {
+ //
+ // }
}
M src/recipes.toml => src/recipes.toml +2 -2
@@ 31,7 31,7 @@ building_type = "Assembler"
crafting_speed = 1.25
[[recipes]]
-inputs = ["Iron Ore"]
-output = "Iron Plate"
+inputs = ["1 Iron Ore"]
+output = "1 Iron Plate"
time = 3.2
building_type = "Furnace"=
\ No newline at end of file