M src/bin/fcalc.rs => src/bin/fcalc.rs +4 -0
@@ 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)
}
M src/error.rs => src/error.rs +4 -0
@@ 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.
M src/item.rs => src/item.rs +66 -8
@@ 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<Product> {
+ match self {
+ Item::Product(p) => Ok(p.clone()),
+ _ => Err(FcalcError::ProductTypeError),
+ }
+ }
+
+ 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(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<Item> {
+ 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)
}
}
M src/lib.rs => src/lib.rs +2 -0
@@ 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
M src/recipe.rs => src/recipe.rs +142 -1
@@ 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<String>,
+ output: String,
+ time: f64,
+ building_type: String,
+}
+
+/// A formula for building a product
+#[derive(Clone, Debug, Deserialize)]
+pub struct Recipe {
+ pub inputs: Vec<Item>,
+ pub output: Product,
+ time: f64,
+ building_type: BuildingType,
+}
+
+impl From<RawRecipe> 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<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 {
+ building_type: BuildingType,
+ name: String,
+ crafting_speed: f64,
+}
+
+/// Top-level to read recipes.toml
+#[derive(Debug, Deserialize)]
+pub struct RawRecipes {
+ buildings: Vec<Building>,
+ recipes: Vec<RawRecipe>,
+}
+
+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<Product, 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);
+ }
+ 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);
+ }
+}
A src/recipes.toml => src/recipes.toml +37 -0
@@ 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