diff --git a/Cargo.lock b/Cargo.lock index 3a07ba6..454ce47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -225,6 +225,31 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" +[[package]] +name = "bon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a636f83af97c6946f3f5cf5c268ec02375bf5efd371110292dfd57961f57a509" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7eaf1bfaa5b8d512abfd36d0c432591fef139d3de9ee54f1f839ea109d70d33" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "built" version = "0.7.4" @@ -448,6 +473,41 @@ dependencies = [ "memchr", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "either" version = "1.13.0" @@ -491,6 +551,7 @@ name = "factorio_blueprint" version = "0.1.0" dependencies = [ "base64", + "bon", "clap 4.5.17", "criterion", "flate2", @@ -609,6 +670,12 @@ dependencies = [ "libc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "image" version = "0.25.2" @@ -1029,10 +1096,20 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.86" +name = "prettyplease" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1282,6 +1359,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "rusty-fork" version = "0.3.0" @@ -1450,9 +1533,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "2.0.77" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ea2dcc9..30f3466 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ harness = false [dependencies] base64 = "0.21.5" +bon = "3.0.2" clap = { version = "4.4.8", features = ["derive"] } flate2 = "1.0.28" image = "0.25.2" diff --git a/examples/train_blueprint.rs b/examples/train_blueprint.rs new file mode 100644 index 0000000..406b374 --- /dev/null +++ b/examples/train_blueprint.rs @@ -0,0 +1,14 @@ +use factorio_blueprint::blueprint::{train::generate_train, BlueprintString}; + +fn main() { + let b = generate_train(2, 4, true, true); + + let b = BlueprintString::Blueprint(b); + + println!("{}", serde_json::to_string_pretty(&b).unwrap()); + + println!( + "{}", + factorio_blueprint::blueprint::encode(&serde_json::to_string(&b).unwrap()) + ); +} diff --git a/src/bin/decode_blueprint.rs b/src/bin/decode_blueprint.rs new file mode 100644 index 0000000..83dfd8f --- /dev/null +++ b/src/bin/decode_blueprint.rs @@ -0,0 +1,29 @@ +use base64::prelude::*; +use clap::Parser; +use factorio_blueprint::blueprint::{decode, encode}; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +struct Args { + filename: PathBuf, + #[arg(short, long)] + encode: bool, +} + +fn main() { + let args = Args::parse(); + + let s = std::fs::read_to_string(args.filename).unwrap(); + + if args.encode { + let j: serde_json::Value = serde_json::from_str(&s).unwrap(); + let d = encode(&serde_json::to_string(&j).unwrap()); + println!("{}", d); + } else { + let d = decode(&s); + + let j: serde_json::Value = serde_json::from_str(&d).unwrap(); + + println!("{}", serde_json::to_string_pretty(&j).unwrap()); + } +} diff --git a/src/blueprint/mod.rs b/src/blueprint/mod.rs index 06a7d57..ff5a237 100644 --- a/src/blueprint/mod.rs +++ b/src/blueprint/mod.rs @@ -1,4 +1,5 @@ use base64::engine::general_purpose::STANDARD; +use base64::prelude::BASE64_STANDARD; use base64::Engine; use std::io::Cursor; use std::io::Read; @@ -7,11 +8,13 @@ pub mod structs; pub use structs::*; +pub mod train; + pub fn decode(s: &str) -> String { - let raw = s.as_bytes(); + let raw = s.trim().as_bytes(); assert!(raw[0] == b'0'); - let u = STANDARD.decode(&raw[1..]).unwrap(); + let u = BASE64_STANDARD.decode(&raw[1..]).unwrap(); let mut o = String::new(); diff --git a/src/blueprint/structs.rs b/src/blueprint/structs.rs index 66947f4..eb2271b 100644 --- a/src/blueprint/structs.rs +++ b/src/blueprint/structs.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use bon::Builder; use serde::Deserialize; use serde::Serialize; @@ -28,63 +29,164 @@ pub struct BlueprintBookEntry { index: u32, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Builder)] pub struct Blueprint { + #[builder(skip = "blueprint".to_owned())] item: String, - label: Option, + label: String, + #[serde(skip_serializing_if = "Option::is_none")] label_color: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[builder(default)] entities: Vec, - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[builder(default)] tiles: Vec, - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[builder(default)] icons: Vec, - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[builder(default)] schedules: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[builder(default)] + stock_connections: Vec, + #[serde(skip_serializing_if = "Option::is_none")] description: Option, + #[serde(skip_serializing_if = "Option::is_none")] snap_to_grid: Option, + #[serde(skip_serializing_if = "Option::is_none")] absolute_snapping: Option, + #[serde(skip_serializing_if = "Option::is_none")] position_relative_to_grid: Option, version: u64, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Builder)] +pub struct BlueprintStockConnection { + #[builder(start_fn)] + stock: u32, + #[serde(skip_serializing_if = "Option::is_none")] + front: Option, + #[serde(skip_serializing_if = "Option::is_none")] + back: Option, +} + +#[derive(Serialize, Deserialize, Debug, Builder)] pub struct BlueprintEntity { - entity_number: u64, + #[builder(start_fn)] name: String, + #[builder(start_fn)] + entity_number: u32, + #[builder(start_fn)] position: BlueprintPosition, + #[serde(skip_serializing_if = "Option::is_none")] direciton: Option, + #[serde(skip_serializing_if = "Option::is_none")] + orientation: Option, + #[serde(skip_serializing_if = "Option::is_none")] connections: Option, + #[serde(skip_serializing_if = "Option::is_none")] neighbours: Option>, + #[serde(skip_serializing_if = "Option::is_none")] control_behaviour: Option<()>, - items: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + items: Option>, + #[serde(skip_serializing_if = "Option::is_none")] recipe: Option, + #[serde(skip_serializing_if = "Option::is_none")] bar: Option, + #[serde(skip_serializing_if = "Option::is_none")] inventory: Option, + #[serde(skip_serializing_if = "Option::is_none")] infinity_settings: Option<()>, - #[serde(rename = "type")] + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] underground_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] input_priority: Option, + #[serde(skip_serializing_if = "Option::is_none")] output_priority: Option, + #[serde(skip_serializing_if = "Option::is_none")] filter: Option, + #[serde(skip_serializing_if = "Option::is_none")] filters: Option>, + #[serde(skip_serializing_if = "Option::is_none")] filter_mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] override_stack_size: Option, + #[serde(skip_serializing_if = "Option::is_none")] drop_position: Option, + #[serde(skip_serializing_if = "Option::is_none")] pickup_position: Option, + #[serde(skip_serializing_if = "Option::is_none")] request_filters: Option>, + #[serde(skip_serializing_if = "Option::is_none")] request_from_buffers: Option, + #[serde(skip_serializing_if = "Option::is_none")] parameters: Option, + #[serde(skip_serializing_if = "Option::is_none")] alert_parameters: Option, + #[serde(skip_serializing_if = "Option::is_none")] auto_launch: Option, + #[serde(skip_serializing_if = "Option::is_none")] variation: Option, + #[serde(skip_serializing_if = "Option::is_none")] color: Option, + #[serde(skip_serializing_if = "Option::is_none")] station: Option, + #[serde(skip_serializing_if = "Option::is_none")] manuel_trains_limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] switch_state: Option, + #[serde(skip_serializing_if = "Option::is_none")] tags: Option, } +#[derive(Serialize, Deserialize, Debug)] +pub struct BlueprintItemRequest { + id: BlueprintItemID, + items: BlueprintItem, +} + +impl BlueprintItemRequest { + pub fn new(id: BlueprintItemID, items: Vec) -> Self { + Self { + id, + items: BlueprintItem { + in_inventory: items, + }, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Builder)] +pub struct BlueprintItemID { + #[builder(start_fn)] + name: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct BlueprintItem { + in_inventory: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct BlueprintInventoryLocation { + count: u32, + inventory: u32, + stack: u32, +} + +impl BlueprintInventoryLocation { + pub fn new(count: u32, inventory: u32, stack: u32) -> Self { + Self { + count, + inventory, + stack, + } + } +} + #[derive(Serialize, Deserialize, Debug)] pub struct BlueprintInventory { filters: Vec, @@ -154,6 +256,12 @@ pub struct BlueprintPosition { y: f64, } +impl BlueprintPosition { + pub fn new(x: f64, y: f64) -> Self { + BlueprintPosition { x, y } + } +} + #[derive(Serialize, Deserialize, Debug)] pub struct BlueprintIcon { index: u32, diff --git a/src/blueprint/train.rs b/src/blueprint/train.rs new file mode 100644 index 0000000..266a4ca --- /dev/null +++ b/src/blueprint/train.rs @@ -0,0 +1,82 @@ +use super::{ + Blueprint, BlueprintEntity, BlueprintInventoryLocation, BlueprintItemID, BlueprintItemRequest, + BlueprintPosition, BlueprintStockConnection, +}; + +pub fn generate_train(locomotives: u32, wagons: u32, rails: bool, fluid: bool) -> Blueprint { + let mut e = Vec::new(); + + for i in 0..locomotives { + e.push( + BlueprintEntity::builder( + "locomotive".to_owned(), + i + 1, + BlueprintPosition::new(1.0, 3.5 + 7.0 * i as f64), + ) + .items(vec![BlueprintItemRequest::new( + BlueprintItemID::builder("rocket-fuel".to_owned()).build(), + vec![BlueprintInventoryLocation::new(5, 1, 0)], + )]) + .build(), + ); + } + + for i in 0..wagons { + e.push( + BlueprintEntity::builder( + if fluid { + "fluid-wagon".to_owned() + } else { + "cargo-wagon".to_owned() + }, + locomotives + i + 1, + BlueprintPosition::new(1.0, 3.5 + 7.0 * (locomotives + i) as f64), + ) + .build(), + ); + } + + if rails { + for i in 0..((locomotives + wagons) * 7).div_ceil(2) { + e.push( + BlueprintEntity::builder( + "straight-rail".to_owned(), + locomotives + wagons + i + 1, + BlueprintPosition::new(1.0, 1.0 + 2.0 * i as f64), + ) + .build(), + ); + } + } + + let mut stock_connections = Vec::new(); + + if locomotives + wagons > 1 { + stock_connections.push(BlueprintStockConnection::builder(1).back(2).build()); + for i in 2..=(locomotives + wagons - 1) { + stock_connections.push( + BlueprintStockConnection::builder(i) + .front(i - 1) + .back(i + 1) + .build(), + ); + } + + stock_connections.push( + BlueprintStockConnection::builder(locomotives + wagons) + .front(locomotives + wagons - 1) + .build(), + ); + } + + Blueprint::builder() + .label("train".to_string()) + .entities(e) + .stock_connections(stock_connections) + .version(562949954797573) + .snap_to_grid(BlueprintPosition::new( + 4.0, + 2.0 * ((locomotives + wagons) * 7).div_ceil(2) as f64, + )) + .build() +}